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);