├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── actions ├── changeController.js ├── deis.js └── index.js ├── components ├── ButtonWarning.js ├── ConfigVars.js ├── HorizontalPanel.js ├── LogsTable.js └── ScaleSlider.js ├── containers ├── .eslintrc ├── App.js ├── Apps │ ├── List.js │ ├── Show │ │ ├── Access.js │ │ ├── Builds.js │ │ ├── Config.js │ │ ├── Domains.js │ │ ├── Logs.js │ │ ├── Overview.js │ │ ├── OverviewScale.js │ │ ├── Releases.js │ │ └── index.js │ └── index.js ├── Auth │ ├── AboutModal.js │ ├── Controller.js │ ├── Login.js │ ├── Register.js │ └── index.js ├── Dash │ ├── Users │ │ ├── UserItem.js │ │ └── index.js │ └── index.js ├── DevTools.js ├── Profile │ ├── Keys.js │ ├── Password.js │ └── index.js ├── Root.dev.js ├── Root.electron.js ├── Root.js └── Root.prod.js ├── electron ├── .gitignore ├── icons.icns ├── index.html ├── main.js └── package.json ├── index.html ├── index.js ├── middleware └── local-storage.js ├── package.json ├── reducers └── index.js ├── routes.js ├── server.js ├── static ├── animation.gif ├── deis-dash-logo-lg.png ├── deis-dash-logo-md copy.png ├── deis-dash-logo-md.png ├── deis-logo.png ├── favicon.ico └── gplaypattern │ ├── gplaypattern.png │ ├── gplaypattern_@2X.png │ ├── pattern-bright.png │ └── readme.txt ├── store ├── configureStore.dev.js ├── configureStore.electron.js ├── configureStore.js └── configureStore.prod.js ├── styles ├── _apps.scss ├── _apps_show.scss ├── _auth.scss ├── _config.scss ├── _header.scss ├── _hpanel.scss ├── _logs.scss ├── _rc-slider.scss ├── _users.scss ├── bootstrap │ ├── _config.scss │ ├── _variables.orig.scss │ ├── _variables.scss │ └── bootstrap.scss └── main.scss ├── utils └── asset-path.js ├── webpack.build.config.js ├── webpack.config.js └── webpack.electron.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-object-rest-spread"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // Use this file as a starting point for your project's .eslintrc. 2 | // Copy this file, and add rule overrides as needed. 3 | { 4 | "extends": "airbnb", 5 | "parser": "babel-eslint", 6 | "rules": { 7 | "no-param-reassign": [2, {"props": false}], 8 | "semi": [2, "never"], 9 | "react/prop-types": [0] 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | *.sqlite3 31 | *.sqlite3-journal 32 | .tmp 33 | 34 | # webpack build 35 | .envrc 36 | dist/ 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Olivier Lalonde 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deis Dash 2 | 3 | Deis Dash is a web based UI for the [Deis PaaS](http://deis.io/). 4 | 5 | [Try it now!](http://www.deisdash.com) 6 | 7 | ![screenshots](./static/animation.gif) 8 | 9 | Features: 10 | 11 | - Login and register 12 | - Change password and manage Git ssh keys 13 | - List users, grant admin, delete user, register user (admin only) 14 | - List and create new apps 15 | - Scale app 16 | - Destroy app 17 | - Edit app configuration 18 | - List app builds 19 | - Add and remove app domain names 20 | - Add and remove app collaborators 21 | - Display and filter app logs 22 | 23 | Roadmap: 24 | 25 | - Support pagination 26 | - Support for tags, releases, certs, limits 27 | 28 | ## Install 29 | 30 | ```bash 31 | # Clone this repository 32 | git clone https://github.com/olalonde/deisdash.git 33 | cd deisdash 34 | # Create Deis app 35 | deis create dash 36 | # Configure app 37 | ## tells npm to install devDependencies (needed for build) 38 | deis config:set NPM_CONFIG_PRODUCTION=false 39 | ## tells our server to use the built files 40 | deis config:set NODE_ENV=production 41 | # Deploy app 42 | git push deis 43 | # Open Deis Dash in web browser 44 | deis open 45 | ``` 46 | 47 | It was mainly tested on deis v1.12.2. If you find bugs with your version 48 | of Deis, please report them through Github issues. 49 | 50 | ## Configure 51 | 52 | Optional configuration: 53 | 54 | To set the default deis controller (by default, deisdash tries to guess 55 | it based on domain). 56 | 57 | ``` 58 | deis config:set \ 59 | DEFAULT_CONTROLLER=https://deis.yourdomain.com \ 60 | CONTROLLER_LOCKED=true 61 | ``` 62 | 63 | Some of those configurations will only take effect when the app is 64 | rebuilt through a git push. 65 | 66 | ## Development 67 | 68 | Deis Dash is a single page app written with React, Redux and Webpack. If 69 | you want to contribute code, try to lint your code with eslint. 70 | 71 | To start contributing, make sure you have Node >v5 installed. 72 | 73 | ``` 74 | npm install 75 | # start development server 76 | npm run start 77 | ``` 78 | 79 | ## TODO 80 | 81 | - Package as docker image with automated docker build? 82 | 83 | ## License 84 | 85 | Copyright 2016 Olivier Lalonde 86 | 87 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 88 | 89 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 90 | -------------------------------------------------------------------------------- /actions/changeController.js: -------------------------------------------------------------------------------- 1 | import { controllerInfo } from './deis' 2 | 3 | export default (controller) => (dispatch) => { 4 | dispatch({ type: 'CHANGE_CONTROLLER', controller }) 5 | 6 | // check if controller is valid... 7 | dispatch(controllerInfo(controller)) 8 | } 9 | -------------------------------------------------------------------------------- /actions/deis.js: -------------------------------------------------------------------------------- 1 | // Generic redux friendly client for Deis API 2 | 3 | // Relies on the deis-api middleware 4 | import 'isomorphic-fetch' 5 | import { resolve } from 'url' 6 | 7 | // const DEFAULT_DEIS_VERSION = 'v1' 8 | const compileType = (path, method) => { 9 | const parts = path.split('/').filter(_ => _ !== '') 10 | const str = [method, ...parts].join('_').toUpperCase() 11 | return str 12 | } 13 | 14 | const buildOpts = (opts, method, token) => { 15 | const authHeaders = token ? { 16 | Authorization: `token ${token}`, 17 | } : {} 18 | return { 19 | method, 20 | ...opts, 21 | headers: { 22 | ...authHeaders, 23 | 'Content-Type': 'application/json', 24 | ...opts.headers, 25 | }, 26 | body: (() => { 27 | if (typeof opts.body === 'object') { 28 | return JSON.stringify(opts.body) 29 | } 30 | return opts.body 31 | })(), 32 | } 33 | } 34 | 35 | const buildURL = (path, controller, versionStr, noTrailingSlash = false) => { 36 | // TODO: improve this 37 | const version = Number(versionStr) >= 2 ? 2 : 1 38 | // normalize path (remove leading slash, add trailing slash) 39 | const p1 = path.split('/').filter(part => part !== '').join('/') 40 | const p2 = p1 === '/' || p1 === '' ? '' : `${p1}/` 41 | const p3 = noTrailingSlash ? p2.slice(0, -1) : p2 42 | const url = resolve(controller, `v${version}/${p3}`) 43 | return url 44 | } 45 | 46 | const mapState = (s) => { 47 | const version = s.controllerInfo ? s.controllerInfo.version : 1 48 | return { 49 | controller: s.controller, 50 | version, 51 | token: s.user && s.user.token, 52 | } 53 | } 54 | 55 | const defaultMapResponseToAction = (response, json, baseAction) => { 56 | if (response.status >= 200 && response.status < 300) { 57 | return { 58 | ...baseAction, 59 | payload: json, 60 | success: true, 61 | } 62 | } 63 | return { 64 | ...baseAction, 65 | payload: json, 66 | metadata: { 67 | statusCode: response.status, 68 | }, 69 | error: true, 70 | } 71 | } 72 | 73 | const mapMethodToVerb = (method) => { 74 | if (method === 'del') return 'delete' 75 | return method 76 | } 77 | 78 | const client = ['get', 'put', 'post', 'del', 'head', 'options'].reduce((obj, method) => { 79 | obj[method] = (path, opts = {}, { 80 | action = {}, 81 | mapResponse = defaultMapResponseToAction, 82 | raw = false, 83 | } = {}) => (dispatch, getState) => { 84 | const baseAction = { type: compileType(path, method), ...action } 85 | dispatch({ ...baseAction, payload: opts.body, pending: true }) 86 | 87 | // TODO: noTrailingSlash not so elegant... lets remove that and make sure we use the 88 | // right paths everywhere 89 | 90 | const noTrailingSlash = opts.noTrailingSlash 91 | const { controller, version, token } = mapState(getState()) 92 | const verb = mapMethodToVerb(method) 93 | 94 | const args = [ 95 | buildURL(path, controller, version, noTrailingSlash), 96 | buildOpts(opts, verb, token), 97 | ] 98 | return fetch(...args).catch((err) => { 99 | // Unexpected error... network error? 100 | err.isFetchError = true 101 | throw err 102 | }).then((response) => { 103 | // Try to parse response as JSON 104 | // and ignore parsing error 105 | if (raw) { 106 | return response.text().then((text) => [response, text]) 107 | } 108 | return response.json().catch(() => null).then((json) => [response, json]) 109 | }).then(([response, json]) => ( 110 | dispatch(mapResponse(response, json, baseAction)) 111 | )).catch((err) => { 112 | if (err.isFetchError) { 113 | return dispatch({ ...baseAction, payload: err.message ? err.message : err, error: true }) 114 | } 115 | throw err 116 | }) 117 | } 118 | 119 | return obj 120 | }, {}) 121 | 122 | export const controllerInfo = () => ( 123 | client.options('/', {}, { 124 | action: { type: 'CONTROLLER_INFO' }, 125 | mapResponse: (response, json, baseAction) => { 126 | const version = response.headers.get('X_DEIS_API_VERSION') ? 127 | response.headers.get('X_DEIS_API_VERSION') : response.headers.get('DEIS_API_VERSION') 128 | if (version) { 129 | return { 130 | ...baseAction, 131 | payload: { version }, 132 | success: true, 133 | } 134 | } 135 | return { 136 | ...baseAction, 137 | payload: { statusCode: response.status }, 138 | error: true, 139 | } 140 | }, 141 | }) 142 | ) 143 | 144 | export const listApps = () => ( 145 | client.get('/apps') 146 | ) 147 | 148 | export const listUsers = () => ( 149 | client.get('/users') 150 | ) 151 | 152 | export const login = (username, password) => ( 153 | client.post('/auth/login', { body: { username, password } }, { 154 | mapResponse: (response, json, baseAction) => { 155 | if (json && json.token) { 156 | return { 157 | ...baseAction, 158 | payload: { username, token: json.token }, 159 | success: true, 160 | } 161 | } 162 | return { 163 | ...baseAction, 164 | payload: json, 165 | error: true, 166 | } 167 | }, 168 | }) 169 | ) 170 | 171 | // We need to use this hack until /whoami endpoint is implemented 172 | // https://github.com/deis/deis/issues/4792 173 | // Basically, we try an "admin" only query and see if we are authorized 174 | export const getUserIsAdmin = () => ( 175 | client.get(`/admin/perms/`, {}, { action: { type: 'GET_USER_IS_ADMIN' } }) 176 | ) 177 | 178 | export const grantAdmin = (username) => ( 179 | client.post(`/admin/perms/`, { body: { username } }, { 180 | action: { type: 'GRANT_ADMIN', metadata: { username } }, 181 | }) 182 | ) 183 | 184 | export const revokeAdmin = (username) => ( 185 | client.del(`/admin/perms/${username}`, {}, { 186 | action: { type: 'REVOKE_ADMIN', metadata: { username } }, 187 | }) 188 | ) 189 | 190 | export const getUsers = () => client.get(`/users`) 191 | 192 | export const delUser = (username) => { 193 | // disable self-deletion for now 194 | if (!username) return null 195 | 196 | return client.del(`/auth/cancel/`, { 197 | body: { username }, 198 | }, { 199 | action: { 200 | metadata: { username }, 201 | }, 202 | }) 203 | } 204 | 205 | export const register = (email, username, password) => (dispatch, getState) => ( 206 | client.post('/auth/register', { 207 | body: { email, username, password }, 208 | })(dispatch, getState).then((action) => { 209 | if (action.success) { 210 | // Automatically login after registration 211 | return dispatch(login(username, password)) 212 | } 213 | return action 214 | }) 215 | ) 216 | 217 | export const changePassword = (oldPassword, newPassword) => ( 218 | client.post('/auth/passwd', { 219 | body: { 220 | password: oldPassword, 221 | new_password: newPassword, 222 | }, 223 | }) 224 | ) 225 | 226 | export const getKeys = () => ( 227 | client.get(`/keys`) 228 | ) 229 | 230 | export const delKey = (id) => ( 231 | client.del(`/keys/${id}`, { noTrailingSlash: true }, { 232 | action: { 233 | type: 'DEL_KEY', 234 | metadata: { id }, 235 | }, 236 | }) 237 | ) 238 | 239 | export const addKey = (key, _id) => { 240 | let id = _id 241 | // take last word of key file as ID 242 | if (!id) { 243 | id = key.trim().split(' ').pop() 244 | } 245 | return client.post(`/keys`, { body: { id, public: key } }) 246 | } 247 | 248 | export const destroyApp = (appID) => (dispatch, getState) => ( 249 | client.del(`/apps/${appID}`, {}, { 250 | action: { type: 'DELETE_APP' }, 251 | })(dispatch, getState).then(() => { 252 | // refresh app list 253 | dispatch(listApps()) 254 | }) 255 | ) 256 | 257 | export const createApp = (appID) => (dispatch, getState) => ( 258 | client.post(`/apps`, { 259 | body: { id: appID }, 260 | }, { 261 | action: { type: 'POST_APP' }, 262 | })(dispatch, getState).then(() => { 263 | // refresh app list 264 | dispatch(listApps()) 265 | }) 266 | ) 267 | 268 | export const createUser = (username, password, email) => (dispatch, getState) => ( 269 | client.post(`/auth/register`, { 270 | body: { username, password, email }, 271 | }, { 272 | action: { type: 'POST_USER' }, 273 | })(dispatch, getState).then(() => { 274 | // refresh user list 275 | dispatch(listUsers()) 276 | }) 277 | ) 278 | 279 | export const appOverview = (appID) => ( 280 | client.get(`/apps/${appID}`, {}, { action: { type: 'GET_APP_OVERVIEW' } }) 281 | ) 282 | 283 | export const appConfig = (appID) => ( 284 | client.get(`/apps/${appID}/config`, {}, { action: { type: 'GET_APP_CONFIG' } }) 285 | ) 286 | 287 | export const addAppConfig = (appID) => (key, val) => ( 288 | client.post(`/apps/${appID}/config`, { 289 | body: { 290 | values: { [key]: val }, 291 | }, 292 | }, { action: { type: 'ADD_APP_CONFIG' } }) 293 | ) 294 | 295 | export const delAppConfig = (appID) => (key) => ( 296 | client.post(`/apps/${appID}/config`, { 297 | body: { 298 | values: { [key]: null }, 299 | }, 300 | }, { action: { type: 'DEL_APP_CONFIG' } }) 301 | ) 302 | 303 | export const appLogs = (appID) => ( 304 | client.get(`/apps/${appID}/logs`, {}, { action: { type: 'GET_APP_LOGS' }, raw: true }) 305 | ) 306 | 307 | export const appScale = (appID) => (structure) => ( 308 | client.post(`/apps/${appID}/scale/`, { 309 | body: structure, 310 | }, { action: { type: 'POST_APP_SCALE' } }) 311 | ) 312 | export const appBuilds = (appID) => ( 313 | client.get(`/apps/${appID}/builds`, {}, { action: { type: 'GET_APP_BUILDS' } }) 314 | ) 315 | export const appReleases = (appID) => ( 316 | client.get(`/apps/${appID}/releases`, {}, { action: { type: 'GET_APP_RELEASES' } }) 317 | ) 318 | export const appDomainsAdd = (appID) => (domain) => ( 319 | client.post(`/apps/${appID}/domains/`, { 320 | body: { domain }, 321 | }, { action: { type: 'ADD_APP_DOMAIN' } }) 322 | ) 323 | export const appDomainsDel = (appID) => (domain) => ( 324 | client.del(`/apps/${appID}/domains/${domain}`, {}, { 325 | action: { type: 'DEL_APP_DOMAIN', metadata: { domain } }, 326 | }) 327 | ) 328 | export const appDomains = (appID) => ( 329 | client.get(`/apps/${appID}/domains`, {}, { action: { type: 'GET_APP_DOMAINS' } }) 330 | ) 331 | export const appPerms = (appID) => ( 332 | client.get(`/apps/${appID}/perms`, {}, { action: { type: 'GET_APP_PERMS' } }) 333 | ) 334 | export const appPermsCreate = (appID) => (username) => ( 335 | client.post(`/apps/${appID}/perms/`, { 336 | body: { username }, 337 | }, { action: { type: 'POST_APP_PERMS', metadata: { username } } }) 338 | ) 339 | 340 | export const appPermsDel = (appID) => (username) => ( 341 | client.del(`/apps/${appID}/perms/${username}`, {}, { 342 | action: { type: 'DEL_APP_PERMS', metadata: { username } }, 343 | }) 344 | ) 345 | 346 | export const logout = () => ({ type: 'LOGOUT' }) 347 | 348 | export default client 349 | -------------------------------------------------------------------------------- /actions/index.js: -------------------------------------------------------------------------------- 1 | export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE' 2 | 3 | export function resetErrorMessage() { 4 | return { 5 | type: RESET_ERROR_MESSAGE, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /components/ButtonWarning.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Modal } from 'react-bootstrap' 3 | 4 | class ButtonWarning extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | this.click = this.click.bind(this) 8 | this.close = this.close.bind(this) 9 | this.state = { 10 | showModal: false, 11 | } 12 | } 13 | 14 | click() { 15 | this.setState({ showModal: true }) 16 | } 17 | 18 | close() { 19 | this.setState({ showModal: false }) 20 | } 21 | 22 | render() { 23 | const { children, message, title, onConfirm, confirmText } = this.props 24 | return ( 25 |
26 | 27 | 28 | {title} 29 | 30 | 31 | {message} 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 44 |
45 | ) 46 | } 47 | } 48 | 49 | export default ButtonWarning 50 | -------------------------------------------------------------------------------- /components/ConfigVars.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class ConfigVar extends React.Component { 4 | 5 | constructor(props) { 6 | super(props) 7 | this.onClickEdit = this.onClickEdit.bind(this) 8 | this.onClickDelete = this.onClickDelete.bind(this) 9 | this.onClickSave = this.onClickSave.bind(this) 10 | this.onClickCancel = this.onClickCancel.bind(this) 11 | this.onChange = this.onChange.bind(this) 12 | 13 | this.state = { 14 | editing: false, 15 | deletePending: false, 16 | savePending: false, 17 | newVal: this.props.val, 18 | } 19 | } 20 | 21 | onClickEdit() { 22 | this.setState({ 23 | editing: true, 24 | }) 25 | } 26 | 27 | onClickCancel() { 28 | this.setState({ 29 | editing: false, 30 | newVal: this.props.val, 31 | }) 32 | } 33 | 34 | onClickDelete() { 35 | this.setState({ deletePending: true }) 36 | this.props.onDelete(this.props.k) 37 | } 38 | 39 | onClickSave() { 40 | this.setState({ 41 | editing: false, 42 | savePending: true, 43 | }) 44 | this.props.onUpdate(this.props.k, this.state.newVal).then(() => { 45 | this.setState({ 46 | savePending: false, 47 | }) 48 | }) 49 | } 50 | 51 | onChange(e) { 52 | this.setState({ 53 | newVal: e.target.value, 54 | }) 55 | } 56 | 57 | render() { 58 | const { 59 | k, 60 | rowClassName = '', 61 | } = this.props 62 | 63 | const val = this.state.editing || this.state.savePending ? this.state.newVal : this.props.val 64 | 65 | let inputDisabled = true 66 | let buttons = [] 67 | if (this.state.editing) { 68 | inputDisabled = false 69 | buttons = [ 70 | , 73 | , 76 | ] 77 | } else if (this.state.savePending) { 78 | buttons = [ 79 | , 85 | ] 86 | } else if (this.state.deletePending) { 87 | buttons = [ 88 | , 92 | ] 93 | } else { 94 | buttons = [ 95 | , 98 | , 101 | ] 102 | } 103 | 104 | return ( 105 |
106 |
107 | 108 |
109 |
110 | 116 |
117 |
118 | {buttons} 119 |
120 |
121 | ) 122 | } 123 | } 124 | 125 | export default class ConfigVars extends React.Component { 126 | constructor(props) { 127 | super(props) 128 | this.state = { 129 | k: '', 130 | val: '', 131 | saving: false, 132 | } 133 | this.onClickAdd = this.onClickAdd.bind(this) 134 | } 135 | 136 | onClickAdd() { 137 | this.setState({ saving: true }) 138 | this.props.onCreate(this.state.k, this.state.val).then(() => { 139 | this.setState({ k: '', val: '', saving: false }) 140 | }) 141 | // todo: catch error 142 | } 143 | 144 | onChange(name) { 145 | return (e) => { 146 | this.setState({ [name]: e.target.value }) 147 | } 148 | } 149 | 150 | render() { 151 | const { config, rowClassName } = this.props 152 | const listItems = Object.keys(config).map((k) => { 153 | const val = config[k] 154 | return ( 155 | 162 | ) 163 | }) 164 | 165 | const disabled = this.state.saving 166 | 167 | const addButton = !this.state.saving 168 | ? 173 | : 183 | 184 | return ( 185 |
186 | {listItems} 187 | 188 | {/* add config row */} 189 |
190 |
191 | 197 |
198 |
199 | 205 |
206 |
207 | {addButton} 208 |
209 |
210 | 211 |
212 | ) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /components/HorizontalPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | 4 | const HorizontalPanel = (props) => { 5 | const { title, first } = props 6 | return ( 7 |
8 |
9 |
10 |
{title}
11 |
12 |
13 | {props.children} 14 |
15 |
16 |
17 | ) 18 | } 19 | 20 | export default HorizontalPanel 21 | -------------------------------------------------------------------------------- /components/LogsTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Table, Input } from 'react-bootstrap' 3 | import { debounce } from 'lodash' 4 | import moment from 'moment' 5 | 6 | const parseProcess = (str) => { 7 | const matches = str.match(/\[(.*)\]/) 8 | return matches[1] || str 9 | } 10 | 11 | const parseProcessV2 = (str) => ( 12 | str 13 | ) 14 | 15 | const parseLine = line => { 16 | const firstSpace = line.indexOf(' ') 17 | const secondSpace = line.indexOf(' ', firstSpace + 1) 18 | const raw = line 19 | const date = line.slice(0, firstSpace).trim() 20 | const process = parseProcess(line.slice(firstSpace, secondSpace).trim()) 21 | const text = line.slice(secondSpace, line.length) 22 | 23 | return { date, process, text, raw } 24 | } 25 | 26 | const parseLineV2 = line => { 27 | const firstSpace = line.indexOf(' ') 28 | const raw = line 29 | const process = parseProcessV2(line.slice(0, firstSpace).trim()) 30 | const text = line.slice(firstSpace + 4, line.length) 31 | 32 | return { process, text, raw } 33 | } 34 | 35 | // date, app, process 36 | const parseLogs = (logs) => { 37 | const splitLogs = logs.split('\n') 38 | /* eslint-disable no-console */ 39 | // detect v2 log format 40 | if (splitLogs.length === 1 && logs.substr(0, 2) === 'b\'' && logs[logs.length - 1] === '\'') { 41 | console.log('v2 style log') 42 | return logs.substr(2, logs.length - 3).split('\\n') 43 | .map((line) => line.trim()) 44 | .filter((line) => line !== '') 45 | .map(parseLineV2) 46 | } 47 | return splitLogs 48 | .map((line) => line.trim()) 49 | .filter((line) => line !== '') 50 | .map(parseLine) 51 | } 52 | 53 | // Merge all rows together, then extract keys 54 | /* 55 | const getColumns = (logs) => ( 56 | Object.keys(logs.reduce((cols, row) => ({ ...cols, ...row }), {})) 57 | ) 58 | */ 59 | 60 | 61 | export default class LogsTable extends React.Component { 62 | constructor(props) { 63 | super(props) 64 | 65 | this.onFilter = this.onFilter.bind(this) 66 | this.togglePrettyDate = this.togglePrettyDate.bind(this) 67 | 68 | const rawLogs = props.logs 69 | 70 | this.state = { 71 | logs: parseLogs(rawLogs), 72 | filter: { 73 | process: '', 74 | text: '', 75 | }, 76 | pendingFilter: { 77 | process: '', 78 | text: '', 79 | }, 80 | prettyDate: false, 81 | } 82 | } 83 | 84 | onFilter(colName) { 85 | const debouncedFn = debounce((value) => { 86 | this.setState({ 87 | filter: { 88 | ...this.state.filter, 89 | [colName]: value, 90 | }, 91 | }) 92 | }, 300) 93 | return (e) => { 94 | // https://github.com/facebook/react/issues/2850 95 | this.setState({ 96 | pendingFilter: { 97 | ...this.state.pendingFilter, 98 | [colName]: e.target.value, 99 | }, 100 | }) 101 | debouncedFn(e.target.value) 102 | } 103 | } 104 | 105 | togglePrettyDate() { 106 | this.setState({ 107 | prettyDate: !this.state.prettyDate, 108 | }) 109 | } 110 | 111 | render() { 112 | // TODO: move this to reducer? 113 | // const columns = getColumns(logs) 114 | const { logs, filter } = this.state 115 | 116 | let filteredLogs = logs 117 | 118 | if (filter.process) { 119 | filteredLogs = filteredLogs.filter((row) => ( 120 | row.process && row.process.indexOf(filter.process) >= 0 121 | )) 122 | } 123 | 124 | if (filter.text) { 125 | filteredLogs = filteredLogs.filter((row) => ( 126 | row.text && row.text.indexOf(filter.text) >= 0 127 | )) 128 | } 129 | 130 | const trs = filteredLogs.map((row, idx) => { 131 | const date = this.state.prettyDate 132 | ? moment.utc(row.date).fromNow() 133 | : row.date 134 | return ( 135 | 136 | {date} 137 | {row.process} 138 | {row.text} 139 | 140 | ) 141 | }) 142 | 143 | return ( 144 |
145 |
146 |
147 | 153 |
154 |
155 | 161 |
162 |
163 | 168 |
169 |
170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | {trs} 180 | 181 |
DateProcessMessage
182 |
183 | ) 184 | } 185 | } 186 | 187 | 188 | /* 189 | export default ({ logs }) => { 190 | // TODO: move parsing to reducer? 191 | 192 | const parsedLogs = parseLogs(logs) 193 | 194 | const tableOpts = { 195 | rowsHeight: 50, 196 | rowsCount: parseLogs.length, 197 | width: 5000, 198 | height: 5000, 199 | headerHeight: 50, 200 | } 201 | 202 | const columns = [ 203 | Date} 205 | cell={Column 1 static content} 206 | width={2000} 207 | /> 208 | ] 209 | 210 | return ( 211 | 217 | Basic content} 219 | width={200} 220 | /> 221 |
222 | ) 223 | } 224 | */ 225 | -------------------------------------------------------------------------------- /components/ScaleSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Slider from 'rc-slider' 3 | 4 | export default class ScaleSlider extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | this.onSliderChange = this.onSliderChange.bind(this) 8 | this.state = { 9 | value: props.value, 10 | } 11 | } 12 | 13 | componentWillReceiveProps(props) { 14 | this.setState({ value: props.value }) 15 | } 16 | 17 | onSliderChange(value) { 18 | this.setState({ value }) 19 | this.props.onSliderChange(value) 20 | } 21 | 22 | render() { 23 | return ( 24 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /containers/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "react/prop-types": 0, 4 | "react/jsx-no-bind": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { push } from 'react-router-redux' 4 | import { resetErrorMessage } from '../actions' 5 | 6 | class App extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.handleDismissClick = this.handleDismissClick.bind(this) 10 | } 11 | 12 | handleDismissClick(e) { 13 | this.props.resetErrorMessage() 14 | e.preventDefault() 15 | } 16 | 17 | renderErrorMessage() { 18 | const { errorMessage } = this.props 19 | if (!errorMessage) { 20 | return null 21 | } 22 | 23 | return ( 24 |
25 | {errorMessage} 26 | {' '} 27 | ( 28 | Dismiss 29 | ) 30 |
31 | ) 32 | } 33 | 34 | render() { 35 | const { children } = this.props 36 | return ( 37 |
38 | {this.renderErrorMessage()} 39 | {children} 40 |
41 | ) 42 | } 43 | } 44 | 45 | App.propTypes = { 46 | // Injected by React Redux 47 | errorMessage: PropTypes.string, 48 | resetErrorMessage: PropTypes.func.isRequired, 49 | push: PropTypes.func.isRequired, 50 | // Injected by React Router 51 | children: PropTypes.node, 52 | } 53 | 54 | function mapStateToProps(state) { 55 | return { 56 | errorMessage: state.errorMessage, 57 | } 58 | } 59 | 60 | export default connect(mapStateToProps, { 61 | resetErrorMessage, 62 | push, 63 | })(App) 64 | -------------------------------------------------------------------------------- /containers/Apps/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router' 4 | import { createApp } from '../../actions/deis' 5 | 6 | class CreateApp extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | 10 | this.create = this.create.bind(this) 11 | this.change = this.change.bind(this) 12 | this.state = { 13 | id: '', 14 | } 15 | } 16 | 17 | change(e) { 18 | this.setState({ 19 | id: e.target.value, 20 | }) 21 | } 22 | 23 | create() { 24 | this.setState({ id: '' }) 25 | this.props.createApp(this.state.id) 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 |
32 | 38 |
39 | 40 |
41 | ) 42 | } 43 | 44 | } 45 | 46 | class List extends React.Component { 47 | constructor(props) { 48 | super(props) 49 | this.createApp = this.createApp.bind(this) 50 | } 51 | 52 | createApp(id) { 53 | const { dispatch } = this.props 54 | dispatch(createApp(id)) 55 | } 56 | 57 | render() { 58 | const { apps } = this.props 59 | 60 | const list = apps.map((app) => { 61 | const name = app.id 62 | const processes = Object.keys(app.structure).map((key) => { 63 | const psName = key 64 | const psNum = app.structure[key] 65 | return {`${psName}=${psNum}`} 66 | }) 67 | return ( 68 |
69 | {name} 70 |
{processes}
71 |
72 | ) 73 | }) 74 | 75 | return ( 76 |
77 |
78 |
{list}
79 | 80 |
81 |
82 | ) 83 | } 84 | } 85 | 86 | export default connect(s => ({ 87 | apps: s.apps, 88 | }))(List) 89 | -------------------------------------------------------------------------------- /containers/Apps/Show/Access.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { connect } from 'react-redux' 4 | 5 | class PermItem extends React.Component { 6 | render() { 7 | const { username, onRemove } = this.props 8 | return ( 9 |
10 |
11 | 16 |
17 |
18 | {`${username} `} 19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | class Access extends React.Component { 26 | componentWillMount() { 27 | const appID = this.props.params.appID 28 | 29 | this.onChangeUsername = this.onChangeUsername.bind(this) 30 | this.onGrantAccess = this.onGrantAccess.bind(this) 31 | this.onRevokeAccess = this.onRevokeAccess.bind(this) 32 | 33 | this.state = { 34 | username: '', 35 | userNotFound: false, 36 | delUserError: false, 37 | } 38 | 39 | this.loadData(appID) 40 | } 41 | 42 | componentWillReceiveProps(nextProps) { 43 | const appID = this.props.params.appID 44 | const nextAppID = nextProps.params.appID 45 | if (appID !== nextAppID) { 46 | // @todo use should update? 47 | this.loadData(nextAppID) 48 | } 49 | } 50 | 51 | onChangeUsername(e) { 52 | this.setState({ username: e.target.value }) 53 | } 54 | 55 | onRevokeAccess(username) { 56 | const { dispatch } = this.props 57 | const appID = this.props.params.appID 58 | this.setState({ delUserError: false }) 59 | dispatch(deis.appPermsDel(appID)(username)).then(({ error, payload }) => { 60 | if (error) { 61 | this.setState({ delUserError: payload.detail }) 62 | } 63 | }) 64 | } 65 | 66 | onGrantAccess() { 67 | const { dispatch } = this.props 68 | const appID = this.props.params.appID 69 | const username = this.state.username 70 | this.setState({ 71 | username: '', 72 | userNotFound: false, 73 | grantAccessError: false, 74 | }) 75 | dispatch(deis.appPermsCreate(appID)(username)).then(({ error, payload }) => { 76 | if (error) { 77 | if (payload.detail) { 78 | const message = payload.detail 79 | this.setState({ grantAccessError: message }) 80 | } else { 81 | this.setState({ grantAccessError: 'Error granting access.' }) 82 | } 83 | } 84 | }) 85 | } 86 | 87 | loadData(appID) { 88 | const { dispatch } = this.props 89 | dispatch(deis.appPerms(appID)) 90 | } 91 | 92 | render() { 93 | const appID = this.props.params.appID 94 | const { users } = this.props 95 | if (!users) return
96 | const perms = users.map((username) => { 97 | const onRemove = () => (this.onRevokeAccess(username)) 98 | return ( 99 | 100 | ) 101 | }) 102 | 103 | return ( 104 |
105 | { this.state.delUserError 106 | ?

{this.state.delUserError}

107 | : null } 108 | { perms.length ? 109 |

The following users have access to {appID}:

110 | :

You are the only one with access to {appID}.

111 | } 112 | { perms.length 113 | ?
{perms}
114 | : null 115 | } 116 |
117 | { this.state.grantAccessError 118 | ? ( 119 |

120 | { this.state.grantAccessError } 121 |

122 | ) 123 | : null 124 | } 125 |
126 |
127 | 134 |
135 |
136 | 140 |
141 |
142 |
143 | ) 144 | } 145 | } 146 | 147 | export default connect(s => { 148 | if (s.activeApp && s.activeApp.perms) { 149 | return { 150 | users: s.activeApp.perms.users, 151 | } 152 | } 153 | return {} 154 | })(Access) 155 | -------------------------------------------------------------------------------- /containers/Apps/Show/Builds.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | import * as deis from '../../../actions/deis' 4 | import { connect } from 'react-redux' 5 | 6 | class Builds extends React.Component { 7 | componentWillMount() { 8 | const appID = this.props.params.appID 9 | this.loadData(appID) 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | const appID = this.props.params.appID 14 | const nextAppID = nextProps.params.appID 15 | if (appID !== nextAppID) { 16 | // @todo use should update? 17 | this.loadData(nextAppID) 18 | } 19 | } 20 | 21 | loadData(appID) { 22 | const { dispatch } = this.props 23 | dispatch(deis.appBuilds(appID)) 24 | } 25 | 26 | render() { 27 | // only display last 5 28 | if (!(this.props.data && this.props.data.results)) { 29 | return
30 | } 31 | 32 | const { results } = this.props.data 33 | results.reverse() 34 | 35 | const builds = [] 36 | for (let i = 0; i < results.length; i++) { 37 | const build = results[results.length - 1 - i] 38 | if (!build) break 39 | builds.push({ 40 | sha: build.sha, 41 | created: moment.utc(build.created).fromNow(), 42 | uuid: build.uuid, 43 | }) 44 | } 45 | 46 | return ( 47 |
48 |
49 |
Build SHA
50 |
Date
51 |
52 | { 53 | builds.map((build) => ( 54 |
55 |
{build.sha}
56 |
{build.created}
57 |
58 | )) 59 | } 60 |
61 | ) 62 | } 63 | } 64 | 65 | export default connect(s => ({ 66 | data: s.activeApp.builds, 67 | }))(Builds) 68 | -------------------------------------------------------------------------------- /containers/Apps/Show/Config.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { connect } from 'react-redux' 4 | 5 | import ConfigVars from '../../../components/ConfigVars' 6 | 7 | class Configuration extends React.Component { 8 | componentWillMount() { 9 | const appID = this.props.params.appID 10 | this.loadData(appID) 11 | this.onUpdate = this.onUpdate.bind(this) 12 | this.onCreate = this.onCreate.bind(this) 13 | this.onDelete = this.onDelete.bind(this) 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | const appID = this.props.params.appID 18 | const nextAppID = nextProps.params.appID 19 | if (appID !== nextAppID) { 20 | // @todo use should update? 21 | this.loadData(nextAppID) 22 | } 23 | } 24 | 25 | onUpdate(k, val) { 26 | // TODO... 27 | const { dispatch, params: { appID } } = this.props 28 | return dispatch(deis.addAppConfig(appID)(k, val)) 29 | } 30 | 31 | onDelete(k) { 32 | const { dispatch, params: { appID } } = this.props 33 | return dispatch(deis.delAppConfig(appID)(k)) 34 | } 35 | 36 | onCreate(k, val) { 37 | const { dispatch, params: { appID } } = this.props 38 | return dispatch(deis.addAppConfig(appID)(k, val)) 39 | } 40 | 41 | loadData(appID) { 42 | const { dispatch } = this.props 43 | dispatch(deis.appConfig(appID)) 44 | } 45 | 46 | render() { 47 | if (!this.props.data) return
48 | const config = this.props.data.values 49 | 50 | // Sort the ENV Vars by name 51 | const orderedConfig = {}; 52 | Object.keys(config).sort().forEach(function(key) { 53 | orderedConfig[key] = config[key]; 54 | }); 55 | 56 | return ( 57 | 64 | ) 65 | } 66 | } 67 | 68 | export default connect(s => ({ 69 | data: s.activeApp.config, 70 | }))(Configuration) 71 | -------------------------------------------------------------------------------- /containers/Apps/Show/Domains.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { connect } from 'react-redux' 4 | 5 | class Domains extends React.Component { 6 | 7 | constructor(props) { 8 | super(props) 9 | 10 | this.onChangeNewDomain = this.onChangeNewDomain.bind(this) 11 | this.onAddNewDomain = this.onAddNewDomain.bind(this) 12 | this.onDeleteDomain = this.onDeleteDomain.bind(this) 13 | 14 | this.state = { 15 | newDomain: '', 16 | } 17 | } 18 | 19 | componentWillMount() { 20 | const appID = this.props.params.appID 21 | this.loadData(appID) 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | const appID = this.props.params.appID 26 | const nextAppID = nextProps.params.appID 27 | if (appID !== nextAppID) { 28 | // @todo use should update? 29 | this.loadData(nextAppID) 30 | } 31 | } 32 | 33 | onChangeNewDomain(e) { 34 | this.setState({ newDomain: e.target.value }) 35 | } 36 | 37 | // TODO: handle errors 38 | onAddNewDomain() { 39 | const { dispatch } = this.props 40 | const appID = this.props.params.appID 41 | const newDomain = this.state.newDomain 42 | this.setState({ newDomain: '' }) 43 | dispatch(deis.appDomainsAdd(appID)(newDomain)).then(({ error, payload }) => { 44 | console.log(payload) 45 | console.log(error) 46 | }) 47 | } 48 | 49 | // TODO: handle errors 50 | onDeleteDomain(domain) { 51 | const { dispatch } = this.props 52 | const appID = this.props.params.appID 53 | dispatch(deis.appDomainsDel(appID)(domain)).then(({ error, payload }) => { 54 | console.log(payload) 55 | console.log(error) 56 | }) 57 | } 58 | 59 | loadData(appID) { 60 | const { dispatch } = this.props 61 | dispatch(deis.appDomains(appID)) 62 | } 63 | 64 | render() { 65 | if (!this.props.data) return
66 | const domains = this.props.data.results.map((domainInfo) => { 67 | const { domain } = domainInfo 68 | const onDelete = () => this.onDeleteDomain(domain) 69 | return ( 70 |
71 |
72 | 77 |
78 |
79 | {domain} 80 |
81 |
82 | ) 83 | }) 84 | 85 | return ( 86 |
87 |
88 | {domains} 89 |
90 |
91 |
92 |
93 | 100 |
101 |
102 | 106 |
107 |
108 |
109 | ) 110 | } 111 | } 112 | 113 | export default connect(s => { 114 | if (s.activeApp && s.activeApp.domains) { 115 | return { 116 | data: s.activeApp.domains, 117 | } 118 | } 119 | return {} 120 | })(Domains) 121 | -------------------------------------------------------------------------------- /containers/Apps/Show/Logs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { connect } from 'react-redux' 4 | import LogsTable from '../../../components/LogsTable' 5 | 6 | class Logs extends React.Component { 7 | componentWillMount() { 8 | const appID = this.props.params.appID 9 | this.loadData(appID) 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | const appID = this.props.params.appID 14 | const nextAppID = nextProps.params.appID 15 | if (appID !== nextAppID) { 16 | // @todo use should update? 17 | this.loadData(nextAppID) 18 | } 19 | } 20 | 21 | loadData(appID) { 22 | const { dispatch } = this.props 23 | dispatch(deis.appLogs(appID)) 24 | } 25 | 26 | render() { 27 | if (!this.props.data) return
Loading latest logs...
28 | 29 | const logs = this.props.data 30 | 31 | return ( 32 |
33 | 34 |
35 | ) 36 | } 37 | } 38 | 39 | export default connect(s => ({ 40 | data: s.activeApp.logs, 41 | }))(Logs) 42 | -------------------------------------------------------------------------------- /containers/Apps/Show/Overview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { connect } from 'react-redux' 4 | import ButtonWarning from '../../../components/ButtonWarning' 5 | import OverviewScale from './OverviewScale' 6 | import HorizontalPanel from '../../../components/HorizontalPanel' 7 | import { routeActions } from 'react-router-redux' 8 | 9 | const KeyVal = (props) => { 10 | const { k, type = '' } = props 11 | let val = props.val 12 | if (!val) return 13 | 14 | if (type === 'url') { 15 | // todo: dont add http:// here... 16 | val = {val} 17 | } else if (type === 'scale') { 18 | val = Object.keys(val).map((key) => ( 19 | {key}={val[key]} 20 | )) 21 | } else if (type === 'domains') { 22 | const domains = val 23 | val = domains.results.map(({ domain }) => ( 24 | {domain} 25 | )) 26 | } 27 | 28 | return ( 29 |
30 | 31 |
32 |

{val}

33 |
34 |
35 | ) 36 | } 37 | 38 | class Overview extends React.Component { 39 | 40 | constructor(props) { 41 | super(props) 42 | this.onDestroyApp = this.onDestroyApp.bind(this) 43 | this.state = { 44 | destroying: false, 45 | } 46 | } 47 | 48 | componentWillMount() { 49 | const appID = this.props.params.appID 50 | this.loadData(appID) 51 | } 52 | 53 | componentWillReceiveProps(nextProps) { 54 | const appID = this.props.params.appID 55 | const nextAppID = nextProps.params.appID 56 | if (appID !== nextAppID) { 57 | // @todo use should update? 58 | this.loadData(nextAppID) 59 | } 60 | } 61 | 62 | onDestroyApp() { 63 | const { dispatch, data: { id } } = this.props 64 | dispatch(deis.destroyApp(id)) 65 | dispatch(routeActions.push('/dash/apps')) 66 | } 67 | 68 | loadData(appID) { 69 | const { dispatch } = this.props 70 | dispatch(deis.appOverview(appID)) 71 | } 72 | 73 | render() { 74 | if (!this.props.data) return
75 | 76 | const { 77 | id, 78 | owner, 79 | url, 80 | structure, 81 | } = this.props.data 82 | const appID = this.props.params.appID 83 | 84 | // TODO: get structure even when all processes are scaled to 0 85 | 86 | const warningTitle = ( 87 | 88 | Warning! 89 | 90 | ) 91 | const warningMessage = ( 92 |
93 |

94 | This operation is permanent and irrevocable. You are about to destroy: 95 |

96 |

97 | {id} 98 |

99 |
100 | ) 101 | 102 | return ( 103 |
104 | 105 |
106 | 107 | 108 | 109 | 110 | 111 |
112 | 113 | 114 | 115 | 116 | 121 | 122 | Destroy app 123 | 124 | 125 |
126 | ) 127 | } 128 | } 129 | 130 | export default connect(s => { 131 | if (s.activeApp && s.activeApp.overview) { 132 | return { 133 | data: s.activeApp.overview, 134 | } 135 | } 136 | return {} 137 | })(Overview) 138 | -------------------------------------------------------------------------------- /containers/Apps/Show/OverviewScale.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { connect } from 'react-redux' 4 | import ScaleSlider from '../../../components/ScaleSlider' 5 | import classnames from 'classnames' 6 | 7 | // FIXME: getting weird react error when changing app page after a scale 8 | // parentComponent must be a valid React Component 9 | // Most of this code should probably be re-written 10 | class OverviewScale extends React.Component { 11 | 12 | constructor(props) { 13 | super(props) 14 | this.onSliderChange = this.onSliderChange.bind(this) 15 | this.onScale = this.onScale.bind(this) 16 | // Keep track of new app structure 17 | this.state = { 18 | structure: {}, 19 | oldStructure: props.structure, 20 | } 21 | } 22 | 23 | componentWillReceiveProps(props) { 24 | this.setState({ oldStructure: props.structure }) 25 | } 26 | 27 | onSliderChange(name, value) { 28 | // only keep track of scale values that are different from props 29 | const structure = this.state.oldStructure 30 | if (value === structure[name]) { 31 | // hmm this is really ugly 32 | this.setState({ 33 | structure: { 34 | ...this.state.structure, 35 | [name]: undefined, 36 | }, 37 | }) 38 | } else { 39 | this.setState({ 40 | structure: { 41 | ...this.state.structure, 42 | [name]: value, 43 | }, 44 | }) 45 | } 46 | } 47 | 48 | onScale() { 49 | const { dispatch, appID } = this.props 50 | const newStructure = this.state.structure 51 | // remove "undefined" values... really hackyish... 52 | Object.keys(newStructure).forEach((key) => { 53 | if (newStructure[key] === undefined) { 54 | delete newStructure[key] 55 | } 56 | }) 57 | this.setState({ scalePending: true }) 58 | dispatch(deis.appScale(appID)(newStructure)).then(({ success }) => { 59 | if (success) { 60 | this.setState({ 61 | scalePending: false, 62 | structure: {}, 63 | oldStructure: { 64 | ...this.state.oldStructure, 65 | ...newStructure, 66 | }, 67 | }) 68 | } 69 | }) 70 | } 71 | 72 | render() { 73 | const structure = this.state.oldStructure 74 | 75 | const sliders = Object.keys(structure).map((processName) => { 76 | const processScale = this.state.structure[processName] !== undefined 77 | ? this.state.structure[processName] 78 | : structure[processName] 79 | const onSliderChange = value => this.onSliderChange(processName, value) 80 | return ( 81 |
82 |
83 | {processName} 84 |
85 |
86 | 87 |
88 |
89 | ) 90 | }) 91 | 92 | const valuesChanged = Object.keys(this.state.structure) 93 | .filter((k) => this.state.structure[k] !== undefined).length 94 | 95 | const scaleButton = this.state.scalePending 96 | ? 97 | 101 | : 102 | 103 | 104 | return ( 105 |
106 |
107 | {sliders} 108 |
109 |
110 | {scaleButton} 111 |
112 |
113 | ) 114 | } 115 | } 116 | 117 | export default connect()(OverviewScale) 118 | -------------------------------------------------------------------------------- /containers/Apps/Show/Releases.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | import * as deis from '../../../actions/deis' 4 | import { connect } from 'react-redux' 5 | 6 | class Releases extends React.Component { 7 | componentWillMount() { 8 | const appID = this.props.params.appID 9 | this.loadData(appID) 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | const appID = this.props.params.appID 14 | const nextAppID = nextProps.params.appID 15 | if (appID !== nextAppID) { 16 | // @todo use should update? 17 | this.loadData(nextAppID) 18 | } 19 | } 20 | 21 | loadData(appID) { 22 | const { dispatch } = this.props 23 | dispatch(deis.appReleases(appID)) 24 | } 25 | 26 | render() { 27 | // only display last 5 28 | if (!(this.props.data && this.props.data.results)) { 29 | return
30 | } 31 | 32 | const { results } = this.props.data 33 | results.reverse() 34 | 35 | const releases = [] 36 | for (let i = 0; i < results.length; i++) { 37 | const build = results[results.length - 1 - i] 38 | if (!build) break 39 | releases.push({ 40 | version: build.version, 41 | owner: build.owner, 42 | summary: build.summary, 43 | created: moment.utc(build.created).fromNow(), 44 | uuid: build.uuid, 45 | }) 46 | } 47 | 48 | return ( 49 |
50 |
51 |
Version
52 |
User
53 |
Date
54 |
Summary
55 |
56 | { 57 | releases.map((build) => ( 58 |
59 |
v{build.version}
60 |
{build.owner}
61 |
{build.created}
62 |
{build.summary}
63 |
64 | )) 65 | } 66 |
67 | ) 68 | } 69 | } 70 | 71 | export default connect(s => ({ 72 | data: s.activeApp.releases, 73 | }))(Releases) 74 | -------------------------------------------------------------------------------- /containers/Apps/Show/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { Nav, NavItem } from 'react-bootstrap' 5 | import { LinkContainer } from 'react-router-bootstrap' 6 | 7 | import Overview from './Overview' 8 | import Logs from './Logs' 9 | import Config from './Config' 10 | import Builds from './Builds' 11 | import Releases from './Releases' 12 | import Domains from './Domains' 13 | import Access from './Access' 14 | 15 | const Icon = ({ glyph }) => ( 16 | 17 | 18 | 19 | ) 20 | class Show extends React.Component { 21 | constructor(props) { 22 | super(props) 23 | } 24 | 25 | render() { 26 | const { appID } = this.props.params 27 | 28 | const icons = { 29 | overview: , 30 | config: , 31 | builds: , 32 | releases: , 33 | domains: , 34 | access: , 35 | logs: , 36 | } 37 | 38 | return ( 39 |
40 |
41 | 64 |
65 |
66 | {this.props.children} 67 |
68 |
69 | ) 70 | } 71 | } 72 | 73 | Show.Overview = Overview 74 | Show.Logs = Logs 75 | Show.Builds = Builds 76 | Show.Releases = Releases 77 | Show.Config = Config 78 | Show.Domains = Domains 79 | Show.Access = Access 80 | 81 | /** 82 | * TODO: dont use activeApp, keep all retrieved app in an object 83 | * and just refresh the data when needed 84 | */ 85 | export default connect()(Show) 86 | -------------------------------------------------------------------------------- /containers/Apps/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Nav, NavItem } from 'react-bootstrap' 4 | import { listApps } from '../../actions/deis' 5 | import { LinkContainer } from 'react-router-bootstrap' 6 | import Show from './Show' 7 | import List from './List' 8 | 9 | class Apps extends Component { 10 | constructor(props) { 11 | super(props) 12 | } 13 | 14 | componentWillMount() { 15 | // this.props.dispatch(); 16 | this.props.dispatch(listApps()) 17 | } 18 | 19 | render() { 20 | // return apps... 21 | const { apps, children } = this.props 22 | 23 | // Sort the apps by name 24 | apps.sort(function(a,b) {return (a.id > b.id) ? 1 : ((b.id > a.id) ? -1 : 0);} ); 25 | 26 | const items = apps.map((app) => ( 27 | 28 | {app.id} 29 | 30 | )) 31 | 32 | return ( 33 |
34 |
35 | 38 |
39 |
40 | {children} 41 |
42 |
43 | ) 44 | } 45 | } 46 | 47 | Apps.Show = Show 48 | Apps.List = List 49 | 50 | export default connect((s) => ({ 51 | apps: s.apps, 52 | }))(Apps) 53 | -------------------------------------------------------------------------------- /containers/Auth/AboutModal.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react' 3 | import { Modal, Button } from 'react-bootstrap' 4 | import animationGif from '../../static/animation.gif' 5 | 6 | export default (props) => ( 7 | 8 | 9 | About 10 | 11 | 12 |

13 | deisdash animation 18 |

19 |

20 | Deis Dash is an unofficial web based UI for the Deis PaaS (Platform as a Service). 21 | It is meant to complement the official Deis 22 | {` `}command line interface. 23 |

24 |

25 | It is developed and maintained by Olivier Lalonde. 26 |

27 |

28 | Github: olalonde/deisdash
29 | Open Source License: Apache License, Version 2.0
30 |

31 |

32 | Please report bugs and feature requests on the Github issue tracker. 33 |

34 |
35 |
36 | 37 | 38 | 39 |
40 | ) 41 | -------------------------------------------------------------------------------- /containers/Auth/Controller.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import changeController from '../../actions/changeController' 4 | import { debounce } from 'lodash' 5 | 6 | class Controller extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.onChangeController = this.onChangeController.bind(this) 10 | const updateController = this.updateController.bind(this) 11 | this.updateController = debounce(updateController, 500) 12 | this.state = { 13 | controller: props.controller, 14 | } 15 | } 16 | 17 | componentWillMount() { 18 | this.props.dispatch(changeController(this.props.controller)) 19 | } 20 | 21 | onChangeController(e) { 22 | let controller = e.target.value 23 | if (controller === '') { 24 | controller = 'https://' 25 | } else if (controller.length < 4) { 26 | controller = 'http' 27 | } 28 | this.setState({ controller }) 29 | this.updateController(controller) 30 | } 31 | 32 | updateController(controller) { 33 | this.props.dispatch(changeController(controller)) 34 | } 35 | 36 | render() { 37 | const { controllerInfo } = this.props 38 | 39 | // const controllerClass = controller.match(/^https?:\/\//) ? '' : 'has-error' 40 | let controllerClass = '' 41 | let status = 'validating controller...' 42 | 43 | if (controllerInfo) { 44 | const { isValid } = controllerInfo 45 | controllerClass = isValid ? 'has-success' : 'has-error' 46 | if (controllerInfo.version) { 47 | status = `version ${controllerInfo.version}` 48 | } else { 49 | status = `invalid or unreachable` 50 | } 51 | } 52 | 53 | return ( 54 |
55 |

Deis Controller {status}

56 |
57 | 62 |
63 |
64 | ) 65 | } 66 | } 67 | 68 | export default connect(s => ({ 69 | controller: s.controller, 70 | controllerInfo: s.controllerInfo, 71 | }))(Controller) 72 | -------------------------------------------------------------------------------- /containers/Auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import * as deis from '../../actions/deis' 4 | 5 | class Login extends Component { 6 | constructor(props) { 7 | super(props) 8 | // boilerplate binding 9 | this.onForgotPassword = this.onForgotPassword.bind(this) 10 | this.onLogin = this.onLogin.bind(this) 11 | 12 | this.state = { 13 | forgotPasswordClicked: false, 14 | } 15 | } 16 | 17 | onForgotPassword() { 18 | this.setState({ forgotPasswordClicked: true }) 19 | } 20 | 21 | onLogin(e) { 22 | e.preventDefault() 23 | const { dispatch } = this.props 24 | const { username, password } = this.state 25 | dispatch(deis.login(username, password)) 26 | } 27 | 28 | handleChange(name) { 29 | return (e) => { 30 | this.setState({ [name]: e.target.value }) 31 | } 32 | } 33 | 34 | render() { 35 | const forgotPassword = this.state.forgotPasswordClicked 36 | ? 37 |

38 |
Just kidding... Deis does not support that yet! 39 |

40 | : 41 | 42 | 43 | const { error } = this.props 44 | 45 | return ( 46 |
47 | { error &&

Invalid username or password.

} 48 |
49 | 54 |
55 |
56 | 61 |
62 |
63 | 67 | { forgotPassword } 68 |
69 |
70 | ) 71 | } 72 | } 73 | 74 | export default connect((s) => ({ 75 | error: s.ui.login.error, 76 | }))(Login) 77 | -------------------------------------------------------------------------------- /containers/Auth/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import * as deis from '../../actions/deis' 4 | import { routeActions } from 'react-router-redux' 5 | import classnames from 'classnames' 6 | 7 | class Register extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | email: '', 12 | username: '', 13 | password: '', 14 | } 15 | this.onRegister = this.onRegister.bind(this) 16 | } 17 | 18 | onRegister(e) { 19 | e.preventDefault() 20 | const { dispatch } = this.props 21 | const { email, username, password } = this.state 22 | dispatch(deis.register(email, username, password)).then((action) => { 23 | // @todo: the register action also dispatches log in 24 | // put redirects in middleware? duplicated in login 25 | if (action.success) { 26 | dispatch(routeActions.push('/dash')) 27 | } 28 | }) 29 | } 30 | 31 | render() { 32 | const { error, disabled } = this.props 33 | let helpBlocks = {} 34 | if (error) { 35 | Object.keys(error).forEach((field) => { 36 | const messages = error[field].map((message) => ( 37 | {message} 38 | )) 39 | helpBlocks[field] = messages 40 | }) 41 | } 42 | 43 | // helper to generate classes with errors 44 | const c = (field) => classnames({ 45 | 'has-error': helpBlocks[field], 46 | 'form-group': true, 47 | }) 48 | 49 | return ( 50 |
51 | { disabled && ( 52 |

53 | Registration is currently disabled. 54 | See the Deis documentation. 55 |

56 | )} 57 |
58 | 59 | this.setState({ email: e.target.value})} 62 | type="email" className="form-control" placeholder="Email" /> 63 | {helpBlocks.email} 64 |
65 |
66 | 67 | this.setState({ username: e.target.value})} 70 | type="text" className="form-control" placeholder="Username" /> 71 | {helpBlocks.username} 72 |
73 |
74 | 75 | this.setState({ password: e.target.value})} 78 | type="password" className="form-control" placeholder="Password" /> 79 | {helpBlocks.password} 80 |
81 | 83 |
84 | ) 85 | } 86 | 87 | } 88 | 89 | export default connect(s => ({ 90 | error: s.ui.register.error, 91 | disabled: s.ui.register.disabled, 92 | }))(Register) 93 | -------------------------------------------------------------------------------- /containers/Auth/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import { routeActions } from 'react-router-redux' 5 | import { Link } from 'react-router' 6 | import classnames from 'classnames' 7 | 8 | import Controller from './Controller' 9 | import Register from './Register' 10 | import Login from './Login' 11 | import AboutModal from './AboutModal' 12 | 13 | // import logoImg from '../../static/deis-logo.png' 14 | import animationGif from '../../static/animation.gif' 15 | import deisDashLogo from '../../static/deis-dash-logo-md.png' 16 | 17 | const modals = ['about'] 18 | 19 | class Auth extends Component { 20 | constructor(props) { 21 | super(props) 22 | this.closeModal = this.closeModal.bind(this) 23 | } 24 | 25 | closeModal() { 26 | this.props.dispatch(routeActions.push('/')) 27 | } 28 | 29 | maybeRedirect(props) { 30 | const { dispatch, user, route } = props 31 | // user is logged in 32 | if (user && user.token && !modals.includes(route.path)) { 33 | dispatch(routeActions.push('/dash')) 34 | } 35 | } 36 | 37 | componentWillMount() { 38 | this.maybeRedirect(this.props) 39 | } 40 | 41 | componentWillReceiveProps(nextProps) { 42 | this.maybeRedirect(nextProps) 43 | } 44 | 45 | render() { 46 | const { controllerInfo, route, version } = this.props 47 | console.log(this.props) 48 | // put isElectron in redux state? 49 | const isElectron = process.env.ELECTRON 50 | const validController = controllerInfo && controllerInfo.isValid 51 | const extraClass = validController ? '' : 'hide' 52 | const className = `col-md-6 ${extraClass}` 53 | 54 | const showAbout = route.path === 'about' 55 | 56 | return ( 57 |
58 | 59 |
60 |
61 | {/* 62 | TODO... 63 | Mac App (preview!) 64 | */} 65 | About 66 |
67 |
68 | deis logo 69 | Deis Dash 70 | {version} 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | deisdash animation 85 |
86 |
87 |
88 |
89 |

Login

90 | 91 |
92 |
93 |
94 |
95 |

Register

96 | 97 |
98 |
99 |
100 |
101 |
102 |
103 | ) 104 | } 105 | } 106 | 107 | export default connect(s => ({ 108 | user: s.user, 109 | controllerInfo: s.controllerInfo, 110 | version: s.version, 111 | }))(Auth) 112 | -------------------------------------------------------------------------------- /containers/Dash/Users/UserItem.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import React from 'react' 3 | import ButtonWarning from '../../../components/ButtonWarning' 4 | 5 | const boolToStr = (someBool) => { 6 | if (someBool === true) { 7 | return 'Yes' 8 | } else if (someBool === false) { 9 | return 'No' 10 | } 11 | return 'N/A' 12 | } 13 | 14 | class UserItem extends React.Component { 15 | constructor(props) { 16 | super(props) 17 | this.onDestroy = this.onDestroy.bind(this) 18 | this.grantAdmin = this.grantAdmin.bind(this) 19 | this.revokeAdmin = this.revokeAdmin.bind(this) 20 | } 21 | 22 | onDestroy() { 23 | return this.props.onDestroyUser(this.props.user.username) 24 | } 25 | 26 | grantAdmin() { 27 | return this.props.grantAdmin(this.props.user.username) 28 | } 29 | 30 | revokeAdmin() { 31 | return this.props.revokeAdmin(this.props.user.username) 32 | } 33 | 34 | render() { 35 | // self represents logged in user 36 | const { user, self } = this.props 37 | 38 | const { 39 | date_joined, 40 | email, 41 | first_name, 42 | // groups, 43 | is_active, 44 | is_staff, 45 | is_superuser, 46 | last_login, 47 | last_name, 48 | // user_permissions, 49 | username, 50 | } = user 51 | 52 | const isSelf = self.username === user.username 53 | 54 | const name = `${first_name} ${last_name}`.trim() 55 | 56 | const warningTitle = ( 57 | 58 | Warning! 59 | 60 | ) 61 | const warningMessage = ( 62 |
63 |

64 | This operation is permanent and irrevocable. You are about to destroy the user: 65 |

66 |

67 | {username} 68 |

69 |
70 | ) 71 | 72 | const delButton = ( 73 | 80 | 81 | 82 | ) 83 | 84 | const grantAdminButton = ( 85 | 88 | ) 89 | 90 | const revokeAdminButton = ( 91 | 94 | ) 95 | 96 | const adminButton = is_superuser ? revokeAdminButton : grantAdminButton 97 | 98 | return ( 99 | 100 | { username } 101 | { email } 102 | { name } 103 | { boolToStr(is_active) } 104 | { boolToStr(is_staff) } 105 | { boolToStr(is_superuser) } 106 | { date_joined } 107 | { last_login } 108 | 109 | { isSelf ? null : delButton } 110 | 111 | 112 | { isSelf ? null : adminButton } 113 | 114 | 115 | ) 116 | } 117 | } 118 | 119 | export default UserItem 120 | -------------------------------------------------------------------------------- /containers/Dash/Users/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as deis from '../../../actions/deis' 3 | import { routeActions } from 'react-router-redux' 4 | import { connect } from 'react-redux' 5 | import UserItem from './UserItem' 6 | import { Table } from 'react-bootstrap' 7 | 8 | class CreateUser extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | 12 | this.create = this.create.bind(this) 13 | this.change = this.change.bind(this) 14 | this.state = { 15 | username: '', 16 | password: '', 17 | email: '', 18 | } 19 | } 20 | 21 | change(type, field) { 22 | const nextState = {} 23 | nextState[type] = field.target.value 24 | this.setState(nextState) 25 | } 26 | 27 | create() { 28 | this.props.createUser(this.state.username, this.state.password, this.state.email) 29 | this.setState({ username: '', password: '', email: '' }) 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 |
36 | 42 | 48 | 54 |
55 | 56 |
57 | ) 58 | } 59 | } 60 | 61 | class Users extends React.Component { 62 | constructor(props) { 63 | super(props) 64 | this.onDestroyUser = this.onDestroyUser.bind(this) 65 | this.grantAdmin = this.grantAdmin.bind(this) 66 | this.revokeAdmin = this.revokeAdmin.bind(this) 67 | this.createUser = this.createUser.bind(this) 68 | } 69 | // TODO: also do this in willreceiveprops 70 | componentWillMount() { 71 | const { dispatch } = this.props 72 | dispatch(deis.getUsers()).then(({ error }) => { 73 | if (error) { 74 | this.props.dispatch(routeActions.push('/dash')) 75 | } 76 | }) 77 | } 78 | 79 | onDestroyUser(username) { 80 | const { dispatch } = this.props 81 | dispatch(deis.delUser(username)) 82 | } 83 | 84 | createUser(username, password, email) { 85 | const { dispatch } = this.props 86 | dispatch(deis.createUser(username, password, email)) 87 | } 88 | 89 | grantAdmin(username) { 90 | const { dispatch } = this.props 91 | dispatch(deis.grantAdmin(username)) 92 | } 93 | 94 | revokeAdmin(username) { 95 | const { dispatch } = this.props 96 | dispatch(deis.revokeAdmin(username)) 97 | } 98 | 99 | render() { 100 | const { users } = this.props 101 | 102 | if (!users) return
103 | 104 | // Sort by username 105 | users.sort(function( a,b) {return (a.username > b.username) ? 1 : ((b.username > a.username) ? -1 : 0);} ); 106 | 107 | const userItems = users.map((user) => ( 108 | 116 | )) 117 | 118 | return ( 119 |
120 |

Users

121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | {userItems} 138 | 139 |
UsernameEmailNameActive?Staff?Superuser?Date joinedLast login
140 | 141 |
142 | ) 143 | } 144 | } 145 | 146 | export default connect(s => ({ 147 | users: s.users && s.users.results, 148 | user: s.user, 149 | }))(Users) 150 | -------------------------------------------------------------------------------- /containers/Dash/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LinkContainer } from 'react-router-bootstrap' 3 | import { connect } from 'react-redux' 4 | import { parse } from 'url' 5 | import { routeActions } from 'react-router-redux' 6 | import * as deis from '../../actions/deis' 7 | import { 8 | Nav, 9 | Navbar, 10 | NavItem, 11 | NavDropdown, 12 | MenuItem, 13 | } from 'react-bootstrap' 14 | import deisDashLogo from '../../static/deis-dash-logo-md.png' 15 | 16 | const mapStateToProps = s => ({ 17 | user: s.user, 18 | domain: parse(s.controller).host, 19 | }) 20 | 21 | /* eslint-disable space-before-keywords */ 22 | const Header = connect(mapStateToProps, null, null, {pure: false})(class extends React.Component { 23 | constructor(props) { 24 | super(props) 25 | this.onLogout = this.onLogout.bind(this) 26 | } 27 | 28 | componentWillMount() { 29 | const { dispatch } = this.props 30 | dispatch(deis.getUserIsAdmin()) 31 | } 32 | 33 | onLogout(e) { 34 | e.preventDefault() 35 | this.props.dispatch(deis.logout()) 36 | this.props.dispatch(routeActions.push('/')) 37 | } 38 | 39 | render() { 40 | const { user, domain } = this.props 41 | const userNavEle = ( 42 |
43 | {user.username}{`@${domain}`} 44 |
45 | ) 46 | return ( 47 | 48 | 49 | 50 | deis dash 51 | 52 | 53 | 54 | 55 | 65 | 84 | 85 | 86 | ) 87 | } 88 | }) 89 | 90 | 91 | class Dash extends React.Component { 92 | // redirect user if not logged in 93 | // TODO: also do this in willreceiveprops 94 | componentWillMount() { 95 | // TODO: do this in the router? 96 | if (!this.props.user) { 97 | this.props.dispatch(routeActions.push('/')) 98 | } 99 | } 100 | 101 | render() { 102 | if (!this.props.user) return
103 | const { children } = this.props 104 | return ( 105 |
106 |
107 |
108 | {children} 109 |
110 |
111 | ) 112 | } 113 | } 114 | 115 | export default connect(s => ({ 116 | user: s.user, 117 | }))(Dash) 118 | -------------------------------------------------------------------------------- /containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /containers/Profile/Keys.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import HorizontalPanel from '../../components/HorizontalPanel' 4 | import * as deis from '../../actions/deis' 5 | 6 | const Key = ({ uuid, id, val, onDelete, deletePending }) => ( 7 |
8 |
{val}
9 | 14 |
15 | ) 16 | 17 | class Keys extends React.Component { 18 | constructor(props) { 19 | super(props) 20 | this.onDelete = this.onDelete.bind(this) 21 | this.onChange = this.onChange.bind(this) 22 | this.addKey = this.addKey.bind(this) 23 | 24 | this.state = { 25 | newKey: '', 26 | pending: false, 27 | error: false, 28 | } 29 | } 30 | 31 | componentWillMount() { 32 | this.loadData() 33 | } 34 | 35 | onChange(e) { 36 | this.setState({ 37 | newKey: e.target.value, 38 | }) 39 | } 40 | 41 | onDelete(id) { 42 | const { dispatch } = this.props 43 | dispatch(deis.delKey(id)) 44 | } 45 | 46 | addKey() { 47 | const { dispatch } = this.props 48 | const key = this.state.newKey 49 | this.setState({ newKey: '', error: false, pending: true }) 50 | dispatch(deis.addKey(key)).then((action) => { 51 | this.setState({ pending: false }) 52 | if (action.error) { 53 | this.setState({ error: action.payload }) 54 | } 55 | }) 56 | } 57 | 58 | loadData() { 59 | const { dispatch } = this.props 60 | dispatch(deis.getKeys()) 61 | } 62 | 63 | render() { 64 | const { keys } = this.props 65 | let body 66 | if (!keys) { 67 | body =
Loading...
68 | } else { 69 | const keyItems = keys.map((key) => ( 70 | 71 | )) 72 | 73 | body = ( 74 |
75 | {keyItems} 76 | { this.state.error && ( 77 |

78 | Could not add key. 79 | Are you sure it is a valid{` `} 80 | ssh public key? 81 |

82 | )} 83 |
84 |