dome: front-end: add WelcomePage (board selection page)
Dome now displays a board selection page at first. This page also offers
the function to add new boards.
BUG=b/30999924
TEST=Manually tested
Change-Id: I133047e29fbf5cddc16bc395294cb7a770dcaf73
Reviewed-on: https://chromium-review.googlesource.com/374222
Commit-Ready: Mao Huang <littlecvr@chromium.org>
Tested-by: Mao Huang <littlecvr@chromium.org>
Reviewed-by: Ting Shen <phoenixshen@chromium.org>
diff --git a/py/dome/backend/urls.py b/py/dome/backend/urls.py
index 344e571..a10ba7a 100644
--- a/py/dome/backend/urls.py
+++ b/py/dome/backend/urls.py
@@ -31,7 +31,7 @@
urlpatterns = [
- url(r'^%s/$' % BOARD_URL_ARG,
+ url(r'^$',
TemplateView.as_view(template_name='index.html')),
url(r'^boards/$',
views.BoardCollectionView.as_view()),
diff --git a/py/dome/frontend/actions/bundles.js b/py/dome/frontend/actions/bundles.js
index 8845f9d..533ffac 100644
--- a/py/dome/frontend/actions/bundles.js
+++ b/py/dome/frontend/actions/bundles.js
@@ -5,11 +5,14 @@
import 'babel-polyfill';
import fetch from 'isomorphic-fetch';
-import {BOARD, API_URL} from '../common';
import ActionTypes from '../constants/ActionTypes';
import FormNames from '../constants/FormNames';
import UploadingTaskStates from '../constants/UploadingTaskStates';
+function _apiURL(getState) {
+ return `/boards/${getState().getIn(['dome', 'currentBoard'])}`;
+}
+
function _checkHTTPStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
@@ -23,7 +26,7 @@
function _createAndStartUploadingTask(dispatch, getState, taskDescription,
method, url, formData) {
var uploadingTasks = getState().getIn(['bundles', 'uploadingTasks']);
- var taskIDs = uploadingTasks.keySeq().toArray().map(x => parseInt(x));
+ var taskIDs = uploadingTasks.keySeq().toArray().map(parseInt);
var taskID = 1;
if (taskIDs.length > 0) {
@@ -32,7 +35,7 @@
dispatch(createUploadingTask(taskID, taskDescription));
- return fetch(`${API_URL}/${url}/`, {
+ return fetch(`${_apiURL(getState)}/${url}/`, {
method: method,
body: formData
}).then(_checkHTTPStatus).then(function() {
@@ -60,7 +63,7 @@
// annouce that we're currenty fetching
dispatch(requestBundles());
- fetch(`${API_URL}/bundles.json`).then(response => {
+ fetch(`${_apiURL(getState)}/bundles.json`).then(response => {
response.json().then(json => {
dispatch(receiveBundles(json));
}, error => {
diff --git a/py/dome/frontend/actions/dome.js b/py/dome/frontend/actions/dome.js
index c97a081..7253678 100644
--- a/py/dome/frontend/actions/dome.js
+++ b/py/dome/frontend/actions/dome.js
@@ -4,12 +4,40 @@
import ActionTypes from '../constants/ActionTypes';
+const receiveBoards = boards => ({
+ type: ActionTypes.RECEIVE_BOARDS,
+ boards
+});
+
+// TODO(littlecvr): similar to fetchBundles, refactor code if possible
+const fetchBoards = () => (dispatch, getState) => {
+ fetch('/boards.json').then(response => {
+ response.json().then(json => {
+ dispatch(receiveBoards(json));
+ }, error => {
+ // TODO(littlecvr): better error handling
+ console.log('error parsing board list response');
+ console.log(error);
+ });
+ }, error => {
+ // TODO(littlecvr): better error handling
+ console.log('error fetching board list');
+ console.log(error);
+ });
+};
+
+const switchBoard = nextBoard => (dispatch, getState) => dispatch({
+ type: ActionTypes.SWITCH_BOARD,
+ prevBoard: getState().getIn(['dome', 'board']),
+ nextBoard
+});
+
const switchApp = nextApp => (dispatch, getState) => dispatch({
type: ActionTypes.SWITCH_APP,
- prevApp: getState().getIn(['dome', 'currentApp']),
+ prevApp: getState().getIn(['dome', 'app']),
nextApp
});
export default {
- switchApp
+ fetchBoards, switchBoard, switchApp
};
diff --git a/py/dome/frontend/common.js b/py/dome/frontend/common.js
deleted file mode 100644
index 08dfe51..0000000
--- a/py/dome/frontend/common.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2016 The Chromium OS Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// TODO(littlecvr): should be able to let user select board instead of typing
-// the URL directly.
-/**
- * Parse and return the board name from current URL.
- *
- * For example, if the URL is 'http://localhost:8080/totoro/' then the function
- * returns 'totoro'.
- */
-function getCurrentBoard() {
- var board = null;
- var currentURL = location.pathname;
- var groups = new RegExp('/([^/]+)/').exec(currentURL);
- if (groups.length > 1) {
- board = groups[1];
- }
- return board;
-}
-
-export const BOARD = getCurrentBoard();
-export const API_URL = '/boards/' + BOARD;
diff --git a/py/dome/frontend/components/AppPage.js b/py/dome/frontend/components/AppPage.js
new file mode 100644
index 0000000..57467e9
--- /dev/null
+++ b/py/dome/frontend/components/AppPage.js
@@ -0,0 +1,81 @@
+// Copyright 2016 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {connect} from 'react-redux';
+import Drawer from 'material-ui/Drawer';
+import Immutable from 'immutable';
+import MenuItem from 'material-ui/MenuItem';
+import RaisedButton from 'material-ui/RaisedButton';
+import React from 'react';
+
+import AppNames from '../constants/AppNames';
+import BundlesApp from './BundlesApp';
+import DomeActions from '../actions/dome';
+import FixedAppBar from './FixedAppBar';
+import SettingsApp from './SettingsApp';
+
+const AppPage = React.createClass({
+ toggleAppMenu() {
+ this.setState({appMenuOpened: !this.state.appMenuOpened});
+ },
+
+ handleClick(nextApp) {
+ // close the drawer
+ this.setState({appMenuOpened: false});
+ this.props.switchApp(nextApp);
+ },
+
+ getInitialState() {
+ return {
+ appMenuOpened: false
+ };
+ },
+
+ render() {
+ var app = null;
+ if (this.props.app == AppNames.BUNDLES_APP) {
+ app = <BundlesApp />;
+ } else if (this.props.app == AppNames.SETTINGS_APP) {
+ app = <SettingsApp />;
+ } else {
+ console.log(`Unknown app ${this.props.app}`);
+ }
+
+ return (
+ <div>
+ <FixedAppBar
+ title="Dome"
+ onLeftIconButtonTouchTap={this.toggleAppMenu}
+ />
+ <Drawer
+ docked={false}
+ open={this.state.appMenuOpened}
+ onRequestChange={open => this.setState({appMenuOpened: open})}
+ >
+ <MenuItem onTouchTap={() => this.handleClick(AppNames.BUNDLES_APP)}>
+ Bundles
+ </MenuItem>
+ <MenuItem onTouchTap={() => this.handleClick(AppNames.SETTINGS_APP)}>
+ Settings
+ </MenuItem>
+ </Drawer>
+ {app}
+ </div>
+ );
+ }
+});
+
+function mapStateToProps(state) {
+ return {
+ app: state.getIn(['dome', 'currentApp'])
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ switchApp: nextApp => dispatch(DomeActions.switchApp(nextApp))
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AppPage);
diff --git a/py/dome/frontend/components/DomeApp.js b/py/dome/frontend/components/DomeApp.js
index ec7b76b..8a6c3ca 100644
--- a/py/dome/frontend/components/DomeApp.js
+++ b/py/dome/frontend/components/DomeApp.js
@@ -3,79 +3,22 @@
// found in the LICENSE file.
import {connect} from 'react-redux';
-import Drawer from 'material-ui/Drawer';
-import Immutable from 'immutable';
-import MenuItem from 'material-ui/MenuItem';
-import RaisedButton from 'material-ui/RaisedButton';
import React from 'react';
-import AppNames from '../constants/AppNames';
-import BundlesApp from './BundlesApp';
-import DomeActions from '../actions/dome';
-import FixedAppBar from './FixedAppBar';
-import SettingsApp from './SettingsApp';
+import WelcomePage from './WelcomePage';
+import AppPage from './AppPage';
-const DomeApp = React.createClass({
- toggleAppMenu() {
- this.setState({appMenuOpened: !this.state.appMenuOpened});
- },
-
- handleClick(nextApp) {
- // close the drawer
- this.setState({appMenuOpened: false});
- this.props.switchApp(nextApp);
- },
-
- getInitialState() {
- return {
- appMenuOpened: false
- };
- },
-
- render() {
- var currentApp = null;
- if (this.props.currentApp == AppNames.BUNDLES_APP) {
- currentApp = <BundlesApp />;
- } else if (this.props.currentApp == AppNames.SETTINGS_APP) {
- currentApp = <SettingsApp />;
- } else {
- console.log(`Unknown app ${this.props.currentApp}`);
- }
-
- return (
- <div>
- <FixedAppBar
- title="Dome"
- onLeftIconButtonTouchTap={this.toggleAppMenu}
- />
- <Drawer
- docked={false}
- open={this.state.appMenuOpened}
- onRequestChange={open => this.setState({appMenuOpened: open})}
- >
- <MenuItem onTouchTap={() => this.handleClick(AppNames.BUNDLES_APP)}>
- Bundles
- </MenuItem>
- <MenuItem onTouchTap={() => this.handleClick(AppNames.SETTINGS_APP)}>
- Settings
- </MenuItem>
- </Drawer>
- {currentApp}
- </div>
- );
- }
-});
+const DomeApp = props => (
+ <div>
+ {props.board === '' && <WelcomePage />}
+ {props.board !== '' && <AppPage />}
+ </div>
+);
function mapStateToProps(state) {
return {
- currentApp: state.getIn(['dome', 'currentApp'])
+ board: state.getIn(['dome', 'currentBoard'])
};
}
-function mapDispatchToProps(dispatch) {
- return {
- switchApp: nextApp => dispatch(DomeActions.switchApp(nextApp))
- };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(DomeApp);
+export default connect(mapStateToProps, null)(DomeApp);
diff --git a/py/dome/frontend/components/UpdatingResourceForm.js b/py/dome/frontend/components/UpdatingResourceForm.js
index 172cc5c..1ec4496 100644
--- a/py/dome/frontend/components/UpdatingResourceForm.js
+++ b/py/dome/frontend/components/UpdatingResourceForm.js
@@ -9,7 +9,6 @@
import React from 'react';
import TextField from 'material-ui/TextField';
-import {BOARD} from '../common';
import Actions from '../actions/bundles';
import FormNames from '../constants/FormNames';
@@ -54,7 +53,7 @@
var formData = new FormData();
// TODO: implement this
- formData.append('board', BOARD);
+ formData.append('board', this.props.board);
formData.append('is_inplace_update', this.state.isInPlaceUpdate);
formData.append('src_bundle_name', this.props.bundleName);
formData.append('dst_bundle_name', this.state.nameInputValue);
@@ -137,6 +136,7 @@
function mapStateToProps(state, ownProps) {
return {
+ board: state.getIn(['dome', 'currentBoard']),
show: state.getIn([
'bundles', 'formVisibility', FormNames.UPDATING_RESOURCE_FORM], false),
bundleName: state.getIn([
diff --git a/py/dome/frontend/components/UploadingBundleForm.js b/py/dome/frontend/components/UploadingBundleForm.js
index 0044897..caf56df 100644
--- a/py/dome/frontend/components/UploadingBundleForm.js
+++ b/py/dome/frontend/components/UploadingBundleForm.js
@@ -8,7 +8,6 @@
import React from 'react';
import TextField from 'material-ui/TextField';
-import {BOARD} from '../common';
import Actions from '../actions/bundles';
import FormNames from '../constants/FormNames';
@@ -31,7 +30,7 @@
}
var formData = new FormData();
- formData.append('board', BOARD);
+ formData.append('board', this.props.board);
formData.append('name', this.state.nameInputValue);
formData.append('note', this.state.noteInputValue);
formData.append('bundle_file', this.fileInput.files[0]);
@@ -104,6 +103,7 @@
function mapStateToProps(state, ownProps) {
return {
+ board: state.getIn(['dome', 'currentBoard']),
show: state.getIn([
'bundles', 'formVisibility', FormNames.UPLOADING_BUNDLE_FORM], false)
};
diff --git a/py/dome/frontend/components/WelcomePage.js b/py/dome/frontend/components/WelcomePage.js
new file mode 100644
index 0000000..96cc274
--- /dev/null
+++ b/py/dome/frontend/components/WelcomePage.js
@@ -0,0 +1,123 @@
+// Copyright 2016 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {connect} from 'react-redux';
+import MenuItem from 'material-ui/MenuItem';
+import Paper from 'material-ui/Paper';
+import RaisedButton from 'material-ui/RaisedButton';
+import React from 'react';
+import SelectField from 'material-ui/SelectField';
+import TextField from 'material-ui/TextField';
+
+import DomeActions from '../actions/dome';
+
+var WelcomePage = React.createClass({
+ handleSelectChange(evt, index, value) {
+ if (value === '') {
+ return;
+ }
+ this.props.switchBoard(value);
+ },
+
+ setShowAddBoardForm(show, evt) {
+ evt.preventDefault();
+ this.setState({showAddBoardForm: show});
+ },
+
+ getInitialState() {
+ return {
+ showAddBoardForm: false
+ };
+ },
+
+ componentDidMount() {
+ this.props.fetchBoards();
+ },
+
+ render: function() {
+ const style = {margin: 24};
+ return (
+ <Paper style={{
+ maxWidth: 400, height: '100%',
+ margin: 'auto', padding: 20,
+ textAlign: 'center'
+ }}>
+ {/* TODO(littlecvr): make a logo! */}
+ <h1 style={{textAlign: 'center'}}>Dome</h1>
+
+ <div style={style}>
+ <SelectField
+ style={{textAlign: 'initial'}}
+ fullWidth={true}
+ floatingLabelText="SELECT A BOARD"
+ onChange={this.handleSelectChange}
+ >
+ {this.props.boards.map(board => {
+ var name = board.get('name');
+ return <MenuItem key={name} value={name} primaryText={name} />;
+ })}
+ </SelectField>
+ </div>
+
+ <div style={style}>OR</div>
+
+ <form style={style}>
+ <TextField
+ name="name"
+ fullWidth={true}
+ floatingLabelText="New board name"
+ />
+ {!this.state.showAddBoardForm && <RaisedButton
+ label="CREATE A NEW BOARD"
+ primary={true}
+ fullWidth={true}
+ // TODO(littlecvr): implement this
+ onTouchTap={() => alert('not implemented yet')}
+ />}
+ {!this.state.showAddBoardForm && <div style={style}>
+ If you had manually set up the Umpire Docker container, you can
+ {' '}
+ <a href="#" onClick={e => this.setShowAddBoardForm(true, e)}>
+ add an existing board
+ </a>.
+ </div>}
+ {this.state.showAddBoardForm && <TextField
+ name="url"
+ fullWidth={true}
+ floatingLabelText="URL to Umpire RPC server"
+ hintText="http://localhost:8080/"
+ />}
+ {this.state.showAddBoardForm && <RaisedButton
+ label="ADD AN EXISTING BOARD"
+ primary={true}
+ fullWidth={true}
+ // TODO(littlecvr): implement this
+ onTouchTap={() => alert('not implemented yet')}
+ />}
+ {this.state.showAddBoardForm && <div style={style}>
+ If you had not set up the Umpire Docker container, you should {' '}
+ <a href="#" onClick={e => this.setShowAddBoardForm(false, e)}>
+ create a new board
+ </a>.
+ </div>}
+ </form>
+ </Paper>
+ );
+ }
+});
+
+function mapStateToProps(state) {
+ return {
+ boards: state.getIn(['dome', 'boards'])
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ fetchBoards: () => dispatch(DomeActions.fetchBoards()),
+ switchBoard: nextBoard => dispatch(DomeActions.switchBoard(nextBoard))
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(WelcomePage);
diff --git a/py/dome/frontend/constants/ActionTypes.js b/py/dome/frontend/constants/ActionTypes.js
index 74f628e..24eb2cd 100644
--- a/py/dome/frontend/constants/ActionTypes.js
+++ b/py/dome/frontend/constants/ActionTypes.js
@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+export const SWITCH_BOARD = 'SWITCH_BOARD';
export const SWITCH_APP = 'SWITCH_APP';
+export const RECEIVE_BOARDS = 'RECEIVE_BOARDS';
// form actions
export const OPEN_FORM = 'OPEN_FORM';
@@ -18,7 +20,7 @@
export const RECEIVE_BUNDLES = 'RECEIVE_BUNDLES';
export default {
- SWITCH_APP,
+ SWITCH_BOARD, SWITCH_APP, RECEIVE_BOARDS,
OPEN_FORM, CLOSE_FORM,
CREATE_UPLOADING_TASK, CHANGE_UPLOADING_TASK_STATE, REMOVE_UPLOADING_TASK,
REQUEST_BUNDLES, RECEIVE_BUNDLES
diff --git a/py/dome/frontend/reducers/dome.js b/py/dome/frontend/reducers/dome.js
index 8b7dc68..9b144f4 100644
--- a/py/dome/frontend/reducers/dome.js
+++ b/py/dome/frontend/reducers/dome.js
@@ -8,11 +8,19 @@
import AppNames from '../constants/AppNames';
const INITIAL_STATE = Immutable.fromJS({
+ boards: [],
+ currentBoard: '',
currentApp: AppNames.BUNDLES_APP // default app is bundle manager
});
export default function domeReducer(state = INITIAL_STATE, action) {
switch (action.type) {
+ case ActionTypes.RECEIVE_BOARDS:
+ return state.set('boards', Immutable.fromJS(action.boards));
+
+ case ActionTypes.SWITCH_BOARD:
+ return state.set('currentBoard', action.nextBoard);
+
case ActionTypes.SWITCH_APP:
return state.set('currentApp', action.nextApp);