Просмотр исходного кода

Merge branch 'master' into edit_endpoint

* master:
  Adds EditProfile component
  - Adds to webpack CORIOLIS_URL environment variable to be used for API url - Adds openId federation url - Fixes quotas on config
  Adds last execution for replicas
  - Adds timeout message to instances, hides other form elements until it loads - Adds props to LoadingIcon, adds message and padding - Renames Vms to instances - Fixes LoginPage for smaller screens
  - Fixes timestamp - Adds timestamp helper method
  - Decouples migrations into migration and replica - Added new views: ReplicaView, ReplicaList, ReplicaDetail - Added new actions in MigrationActions - Cleans up views, uses MainList to replica and migrations
  - Updates EndpointList - Renames ConnectionsList to EndpointList - Fixes Loading on MainList, ProjectList - Fixes Project Dropdown
  Adjusts ProjectList to new MainList filtering
  Adds multiple filter capability to MainList and FilteredTable components
  Refactors ProjectList to use MainList as abstract layer
  New MainList component - provides abstractisation for main lists throughout the application
George Vrancianu 8 лет назад
Родитель
Сommit
880f1e8067
55 измененных файлов с 2463 добавлено и 1587 удалено
  1. 1 0
      package.json
  2. 62 41
      src/actions/MigrationActions/MigrationActions.js
  3. 2 1
      src/components/CloudConnectionDetail/CloudConnectionDetail.js
  4. 10 8
      src/components/CloudConnectionsView/CloudConnectionsView.js
  5. 0 337
      src/components/ConnectionsList/ConnectionsList.js
  6. 0 6
      src/components/ConnectionsList/package.json
  7. 178 0
      src/components/EditProfile/EditProfile.js
  8. 164 0
      src/components/EditProfile/EditProfile.scss
  9. 6 0
      src/components/EditProfile/package.json
  10. 187 0
      src/components/EndpointList/EndpointList.js
  11. 4 4
      src/components/EndpointList/EndpointList.scss
  12. 6 0
      src/components/EndpointList/package.json
  13. 9 7
      src/components/FilteredTable/FilteredTable.js
  14. 5 0
      src/components/Helper/Helper.js
  15. 19 8
      src/components/LoadingIcon/LoadingIcon.js
  16. 5 0
      src/components/LoadingIcon/LoadingIcon.scss
  17. 8 7
      src/components/LoginPage/LoginPage.js
  18. 3 3
      src/components/LoginPage/LoginPage.scss
  19. 206 168
      src/components/MainList/MainList.js
  20. 36 36
      src/components/MainList/MainList.scss
  21. 1 1
      src/components/MigrationDetail/MigrationDetail.js
  22. 91 353
      src/components/MigrationList/MigrationList.js
  23. 5 5
      src/components/MigrationList/MigrationList.scss
  24. 0 6
      src/components/MigrationSchedule/package.json
  25. 23 64
      src/components/MigrationView/MigrationView.js
  26. 19 1
      src/components/MigrationWizard/MigrationWizard.js
  27. 2 2
      src/components/MigrationWizard/MigrationWizard.scss
  28. 71 255
      src/components/ProjectList/ProjectList.js
  29. 1 21
      src/components/ProjectList/ProjectList.scss
  30. 11 9
      src/components/ProjectsDropdown/ProjectsDropdown.js
  31. 189 0
      src/components/ReplicaDetail/ReplicaDetail.js
  32. 78 0
      src/components/ReplicaDetail/ReplicaDetail.scss
  33. 6 0
      src/components/ReplicaDetail/package.json
  34. 91 52
      src/components/ReplicaExecutions/ReplicaExecutions.js
  35. 196 0
      src/components/ReplicaList/ReplicaList.js
  36. 177 0
      src/components/ReplicaList/ReplicaList.scss
  37. 6 0
      src/components/ReplicaList/package.json
  38. 7 7
      src/components/ReplicaSchedule/ReplicaSchedule.js
  39. 0 0
      src/components/ReplicaSchedule/ReplicaSchedule.scss
  40. 6 0
      src/components/ReplicaSchedule/package.json
  41. 184 0
      src/components/ReplicaView/ReplicaView.js
  42. 112 0
      src/components/ReplicaView/ReplicaView.scss
  43. 6 0
      src/components/ReplicaView/package.json
  44. 3 2
      src/components/SearchBox/SearchBox.js
  45. 9 6
      src/components/Tasks/Tasks.js
  46. 32 38
      src/components/WizardVms/WizardVms.js
  47. 2 0
      src/components/WizardVms/WizardVms.scss
  48. 34 33
      src/config.sample.js
  49. 16 14
      src/routes.js
  50. 1 0
      src/stores/ConnectionsStore/ConnectionsStore.js
  51. 112 60
      src/stores/MigrationStore/MigrationStore.js
  52. 44 25
      src/stores/NotificationsStore/NotificationsStore.js
  53. 6 5
      src/stores/UserStore/UserStore.js
  54. 4 0
      src/stores/WizardStore/WizardStore.js
  55. 7 2
      tools/webpack.config.js

+ 1 - 0
package.json

@@ -11,6 +11,7 @@
     "body-parser": "1.15.0",
     "classnames": "2.2.3",
     "cookie-parser": "1.4.1",
+    "estraverse-fb": "^1.3.2",
     "eventemitter3": "1.1.1",
     "express": "4.13.4",
     "express-jwt": "3.3.0",

+ 62 - 41
src/actions/MigrationActions/MigrationActions.js

@@ -21,35 +21,31 @@ import NotificationActions from '../NotificationActions'
 import {servicesUrl, securityGroups} from '../../config';
 
 let MigrationActions = Reflux.createActions({
-  'loadMigrations': { children: ['completed', 'failed'], shouldEmit: () => {} },
-  'loadMigration': { children: ['completed', 'failed'] }, // TODO: Reload migration action
-  'addMigration': { children: ['completed', 'failed'] },
-  'deleteMigration': { children: ['completed', 'failed'] },
-  'executeReplica': { children: ['completed', 'failed'] },
-  'cancelMigration': { children: ['completed', 'failed'] },
-  'getReplicaExecutions': { children: ['completed', 'failed'] },
-  'getReplicaExecutionDetail': { children: ['completed', 'failed'] },
-  'createMigrationFromReplica': { children: ['completed', 'failed'] },
-  'deleteReplicaExecution': { children: ['completed', 'failed'] },
-  'getMigration': {},
-  'setMigration': {},
-  'setMigrationProperty': {}
+  loadMigrations: { children: ['completed', 'failed'], shouldEmit: () => {} },
+  loadReplicas: { children: ['completed', 'failed'], shouldEmit: () => {} },
+  loadMigration: { children: ['completed', 'failed'] }, // TODO: Reload migration action
+  addMigration: { children: ['completed', 'failed'] },
+  deleteMigration: { children: ['completed', 'failed'] },
+  deleteReplica: { children: ['completed', 'failed'] },
+  executeReplica: { children: ['completed', 'failed'] },
+  cancelMigration: { children: ['completed', 'failed'] },
+  getReplicaExecutions: { children: ['completed', 'failed'] },
+  getReplicaExecutionDetail: { children: ['completed', 'failed'] },
+  createMigrationFromReplica: { children: ['completed', 'failed'] },
+  deleteReplicaExecution: { children: ['completed', 'failed'] },
+  getMigration: {},
+  setMigration: {},
+  setReplica: {},
+  setMigrationProperty: {}
 })
 
 MigrationActions.loadMigrations.listen(() => {
   let projectId = Reflux.GlobalState.userStore.currentUser.project.id
 
   Api.sendAjaxRequest({
-      url: `${servicesUrl.coriolis}/${projectId}/migrations/detail`,
-      method: "GET"
-    })
-    .then(MigrationActions.loadMigrations.completed, MigrationActions.loadMigrations.failed)
-    .catch(MigrationActions.loadMigrations.failed);
-
-  Api.sendAjaxRequest({
-      url: `${servicesUrl.coriolis}/${projectId}/replicas/detail`,
-      method: "GET"
-    })
+    url: `${servicesUrl.coriolis}/${projectId}/migrations/detail`,
+    method: "GET"
+  })
     .then(MigrationActions.loadMigrations.completed, MigrationActions.loadMigrations.failed)
     .catch(MigrationActions.loadMigrations.failed);
 })
@@ -59,9 +55,25 @@ MigrationActions.loadMigrations.shouldEmit = () => {
   if (typeof projectId === "undefined") {
     return false
   }
-  if (Reflux.GlobalState.migrationStore.queryInProgress) {
+  return true
+}
+
+MigrationActions.loadReplicas.listen(() => {
+  let projectId = Reflux.GlobalState.userStore.currentUser.project.id
+  Api.sendAjaxRequest({
+    url: `${servicesUrl.coriolis}/${projectId}/replicas/detail`,
+    method: "GET"
+  })
+    .then(MigrationActions.loadReplicas.completed, MigrationActions.loadReplicas.failed)
+    .catch(MigrationActions.loadReplicas.failed);
+})
+
+MigrationActions.loadReplicas.shouldEmit = () => {
+  let projectId = Reflux.GlobalState.userStore.currentUser.project.id
+  if (typeof projectId === "undefined") {
     return false
   }
+
   return true
 }
 
@@ -82,14 +94,23 @@ MigrationActions.loadMigration.shouldEmit = () => {
 }
 
 MigrationActions.deleteMigration.listen((migration) => {
-  let migrationType = migration.type === 'replica' ? 'replicas' : 'migrations'
   let projectId = Reflux.GlobalState.userStore.currentUser.project.id
   Api.sendAjaxRequest({
-      url: `${servicesUrl.coriolis}/${projectId}/${migrationType}/${migration.id}`,
-      method: "DELETE"
-    })
-    .then(MigrationActions.deleteMigration.completed(migration), MigrationActions.deleteMigration.failed)
-    .catch(MigrationActions.deleteMigration.failed);
+    url: `${servicesUrl.coriolis}/${projectId}/migrations/${migration.id}`,
+    method: "DELETE"
+  })
+  .then(MigrationActions.deleteMigration.completed(migration), MigrationActions.deleteMigration.failed)
+  .catch(MigrationActions.deleteMigration.failed);
+})
+
+MigrationActions.deleteReplica.listen((replica) => {
+  let projectId = Reflux.GlobalState.userStore.currentUser.project.id
+  Api.sendAjaxRequest({
+    url: `${servicesUrl.coriolis}/${projectId}/replicas/${replica.id}`,
+    method: "DELETE"
+  })
+    .then(MigrationActions.deleteReplica.completed(replica), MigrationActions.deleteReplica.failed)
+    .catch(MigrationActions.deleteReplica.failed);
 })
 
 MigrationActions.executeReplica.listen((replica, callback = null) => {
@@ -103,10 +124,10 @@ MigrationActions.executeReplica.listen((replica, callback = null) => {
     }
 
     Api.sendAjaxRequest({
-        url: `${servicesUrl.coriolis}/${projectId}/replicas/${replica.id}/executions`,
-        method: "POST",
-        data: payload
-      })
+      url: `${servicesUrl.coriolis}/${projectId}/replicas/${replica.id}/executions`,
+      method: "POST",
+      data: payload
+    })
       .then((response) => {
         MigrationActions.executeReplica.completed(replica, response)
         if (callback) {
@@ -189,7 +210,7 @@ MigrationActions.deleteReplicaExecution.listen((replica, executionId, callback =
     method: "DELETE"
   })
     .then((response) => {
-      MigrationActions.deleteReplicaExecution.completed(replica, executionId, response)
+      MigrationActions.deleteReplicaExecution.completed(replica, executionId)
       if (callback) {
         callback(replica, executionId, response)
       }
@@ -225,12 +246,12 @@ MigrationActions.addMigration.listen((migration) => {
   destinationEnv["network_map"] = network_map
 
   payload[migration.migrationType] = {
-    "origin_endpoint_id": migration.sourceCloud.credential.id,
-    "destination_endpoint_id": migration.targetCloud.credential.id,
-    "destination_environment": destinationEnv,
-    "instances": instances,
-    "notes": migration.notes,
-    "security_groups": securityGroups
+    origin_endpoint_id: migration.sourceCloud.credential.id,
+    destination_endpoint_id: migration.targetCloud.credential.id,
+    destination_environment: destinationEnv,
+    instances: instances,
+    notes: migration.notes,
+    security_groups: securityGroups
   }
 
   let migrationType = migration.migrationType === 'replica' ? 'replicas' : 'migrations'

+ 2 - 1
src/components/CloudConnectionDetail/CloudConnectionDetail.js

@@ -89,6 +89,7 @@ class CloudConnectionDetail extends Component {
 
   render() {
     let item = this.props.connection
+    let createdAt = Helper.getTimeObject(item.created_at)
     if (item) {
       return (
         <div className={s.root}>
@@ -122,7 +123,7 @@ class CloudConnectionDetail extends Component {
                 Created
               </div>
               <div className={s.value}>
-                <Moment format="MM/DD/YYYY HH:MM" date={item.created} />
+                <Moment format="MM/DD/YYYY HH:mm" date={createdAt} />
               </div>
             </div>
           </div>

+ 10 - 8
src/components/CloudConnectionsView/CloudConnectionsView.js

@@ -63,18 +63,18 @@ class CloudConnectionsView extends Component {
     }
   }
 
-  componentWillReceiveProps(newProps, oldProps) {
+  componentDidMount() {
+    this.context.onSetTitle(this.title);
+  }
+
+  componentWillReceiveProps(newProps) {
     if (newProps.connections) {
-      let connection = newProps.connections.filter((connection => connection.id == this.props.connectionId))[0]
+      let connection = newProps.connections.filter((item => item.id === this.props.connectionId))[0]
 
       this.setState({ connection: connection })
     }
   }
 
-  componentDidMount() {
-    this.context.onSetTitle(this.title);
-  }
-
   onConnectionsActionsChange(option) {
     switch (option.value) {
       case "delete":
@@ -183,7 +183,9 @@ class CloudConnectionsView extends Component {
                   <button onClick={(e) => this.validateConnection(e)}>Validate Endpoint</button>
                 </div>
                 <div className={s.rightSide}>
-                  <button onClick={(e) => this.deleteConnection(e)} className="wire" style={{float: "right"}}>Delete</button>
+                  <button onClick={(e) => this.deleteConnection(e)} className="wire" style={{ float: "right" }}>
+                    Delete
+                  </button>
                 </div>
               </div>
             </div>
@@ -221,7 +223,7 @@ class CloudConnectionsView extends Component {
     } else {
       return (
         <div className={s.root}>
-          <Header title={title} linkUrl="/cloud-endpoints"/>
+          <Header title={title} linkUrl="/cloud-endpoints" />
           <div className={s.container}>
             <LoadingIcon />
           </div>

+ 0 - 337
src/components/ConnectionsList/ConnectionsList.js

@@ -1,337 +0,0 @@
-/*
-Copyright (C) 2017  Cloudbase Solutions SRL
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-import React, { PropTypes } from 'react';
-import Reflux from 'reflux';
-import withStyles from 'isomorphic-style-loader/lib/withStyles';
-import Location from '../../core/Location';
-import Dropdown from '../NewDropdown';
-import SearchBox from '../SearchBox';
-import Moment from 'react-moment';
-import s from './ConnectionsList.scss';
-import AddCloudConnection from '../AddCloudConnection';
-import Modal from 'react-modal';
-import ConnectionsStore from '../../stores/ConnectionsStore';
-import ConnectionsActions from '../../actions/ConnectionsActions';
-import TextTruncate from 'react-text-truncate';
-import UserIcon from '../UserIcon';
-import FilteredTable from '../FilteredTable';
-import EndpointUsage from '../EndpointUsage';
-import NotificationIcon from '../NotificationIcon';
-import ConfirmationDialog from '../ConfirmationDialog'
-import ProjectsDropdown from '../ProjectsDropdown';
-
-
-const title = 'Cloud Endpoints';
-const connectionTypes = [
-  { label: "All", type: "all" },
-  { label: "Oracle Cloud", type: "opc" },
-  { label: "Oracle VM Server", type: "oracle_vm" },
-  { label: "Openstack", type: "openstack" },
-  { label: "VMware", type: "vmware_vsphere" }
-]
-const connectionActions = [
-  { label: "Delete", value: "delete" }
-]
-
-
-class ConnectionsList extends Reflux.Component {
-  constructor(props) {
-    super(props)
-    this.store = ConnectionsStore
-
-    this.state = {
-      showModal: false,
-      queryText: '',
-      filterType: 'all',
-      selectedAll: false,
-      searchMin: true,
-      connections: null,
-      confirmationDialog: {
-        visible: false,
-        message: "Are you sure?",
-        onConfirm: null,
-        onCancel: null
-      }
-    }
-  }
-
-  static contextTypes = {
-    onSetTitle: PropTypes.func.isRequired,
-  };
-
-  componentWillMount() {
-    super.componentWillMount.call(this)
-
-    this.context.onSetTitle(title);
-    if (this.state.connections == null) {
-      ConnectionsActions.loadConnections()
-    }
-  }
-
-  connectionsSelected() {
-    let count = this.connectionsSelectedCount(),
-        total = 0
-    if (this.state.connections) {
-      total = this.state.connections.length
-    }
-
-    return `${count} of ${total} connection(s) selected`;
-  }
-
-  connectionsSelectedCount() {
-    let count = 0
-    if (this.state.connections) {
-      this.state.connections.forEach((item) => {
-        if (item.selected) count++
-      })
-    }
-    return count
-  }
-
-  connectionDetail(e, item) {
-    Location.push('/cloud-endpoints/' + item.id + "/")
-  }
-
-  checkItem(e, itemRef) {
-    let items = this.state.connections
-    items.forEach((item) => {
-      if (item == itemRef) {
-        item.selected = !item.selected
-      }
-    })
-    this.setState({ connections: items, selectedAll: false  })
-  }
-
-  checkAll() {
-    let items = this.state.connections
-    let selectedAll = this.state.selectedAll
-
-    items.forEach((item) => {
-      item.selected = !selectedAll
-    })
-
-    this.setState({ connections: items, selectedAll: !selectedAll })
-  }
-
-  filterFn(item, queryText, filterType) {
-    return (
-      item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
-      (filterType == "all" || filterType == item.type)
-    )
-  }
-
-  searchItem(queryText) {
-    this.setState({ queryText: queryText })
-  }
-
-  filterType(e, type) {
-    this.setState({ filterType: type })
-  }
-
-  closeModal() {
-    this.setState({ showModal: false })
-  }
-
-  bulkActions(action) {
-    switch (action.value) {
-      case "delete":
-        this.setState({
-          confirmationDialog: {
-            visible: true,
-            onConfirm: () => {
-              this.setState({ confirmationDialog: { visible: false }})
-              let selectedConnections = this.state.connections.filter((connection) => connection.selected)
-              selectedConnections.forEach(connection => {
-                ConnectionsActions.deleteConnection(connection)
-              })
-            },
-            onCancel: () => {
-              this.setState({ confirmationDialog: { visible: false }})
-            }
-          }
-        })
-
-        break;
-    }
-  }
-
-  renderSearch(items) {
-    let output = null
-    if (items && items.length) {
-      output = items.map((item, index) => (
-        <div className={"item " + (item.selected ? " selected" : "")} key={"vm_" + index}>
-          <div className="checkbox-container">
-            <input
-              id={"vm_check_" + index}
-              type="checkbox"
-              checked={item.selected}
-              onChange={(e) => this.checkItem(e, item)}
-              className="checkbox-normal"
-            />
-            <label htmlFor={"vm_check_" + index}></label>
-          </div>
-          <span className="cell cell-icon" onClick={(e) => this.connectionDetail(e, item)}>
-            <div className={"icon endpoint"}></div>
-            <span className="details">
-              {/*{item.name ? item.name : "N/A"}*/}
-              <TextTruncate line={1} truncateText="..." text={item.name} />
-              <span className={s.description}>{item.description == "" ? "N/A" : item.description}</span>
-            </span>
-          </span>
-          <span className="cell">
-              <div className={s.cloudImage + " icon small-cloud " + item.type}></div>
-            </span>
-          <span className={"cell " + s.composite}>
-              <span className={s.label}>Created</span>
-              <span className={s.value}>
-                <Moment fromNow ago date={item.created_at}/> ago
-              </span>
-            </span>
-          <span className={"cell " + s.composite}>
-              <span className={s.label}>Usage</span>
-              <span className={s.value}>
-                <EndpointUsage connectionId={item.id} />
-              </span>
-            </span>
-        </div>
-      ), this)
-    }
-    return output
-  }
-  
-  currentInstance(migration) {
-    let instance = "N/A"
-    migration.vms.forEach((item) => {
-      if (item.selected) {
-        instance = item.name
-      }
-    })
-    return instance
-  }
-
-  showNewConnectionModal() {
-    this.setState({ showModal: true })
-  }
-
-  render() {
-    let itemStates = connectionTypes.map((state, index) => (
-        <a
-          className={this.state.filterType == state.type || (this.state.filterType == null && state.type == "all") ?
-            "selected" : ""}
-          onClick={(e) => this.filterType(e, state.type)} key={"status_" + index}
-        >{state.label}</a>
-      ), this)
-
-    let modalStyle = {
-      content: {
-        padding: "0px",
-        borderRadius: "4px",
-        bottom: "auto",
-        width: "576px",
-        height: "auto",
-        left: "50%",
-        top: "70px",
-        marginLeft: "-288px"
-      }
-    }
-
-    return (
-      <div className={s.root}>
-        <div className={s.container}>
-          <div className={s.pageHeader}>
-            <div className={s.top}>
-              <h1>{title}</h1>
-              <div className={s.topActions}>
-                <ProjectsDropdown />
-                <button onClick={(e) => this.showNewConnectionModal(e)}>New</button>
-                <UserIcon />
-                <NotificationIcon />
-              </div>
-            </div>
-            <div className="filters">
-              <div className="checkbox-container">
-                <input
-                  id={"vm_check_all"}
-                  type="checkbox"
-                  checked={this.state.selectedAll}
-                  onChange={(e) => this.checkAll()}
-                  className="checkbox-normal"
-                />
-                <label htmlFor={"vm_check_all"}></label>
-              </div>
-              <div className="category-filter">
-                {itemStates}
-              </div>
-              <div className="name-filter">
-                <SearchBox
-                  placeholder="Search"
-                  value={this.state.queryText}
-                  onChange={(e) => this.searchItem(e)}
-                  minimize={true} // eslint-disable-line react/jsx-boolean-value
-                  onClick={(e) => this.toggleSearch(e)}
-                  className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
-                />
-              </div>
-              <div className={s.bulkActions + (this.connectionsSelectedCount() === 0 ? " invisible": "")}>
-                <div className={s.connectionsCount}>
-                  {this.connectionsSelected()}
-                </div>
-                <Dropdown
-                  options={connectionActions}
-                  placeholder="More Actions"
-                  onChange={(e) => this.bulkActions(e)}
-                />
-              </div>
-            </div>
-          </div>
-          <div className={s.pageContent}>
-            <FilteredTable
-              items={this.state.connections}
-              filterFn={this.filterFn}
-              queryText={this.state.queryText}
-              filterType={this.state.filterType}
-              renderSearch={(e) => this.renderSearch(e)}
-            ></FilteredTable>
-          </div>
-          <div className={s.pageFooter}>
-
-          </div>
-        </div>
-        <Modal
-          isOpen={this.state.showModal}
-          contentLabel="Add new cloud connection"
-          style={modalStyle}
-        >
-          <AddCloudConnection
-            closeHandle={(e) => this.closeModal(e)}
-            addHandle={(e) => this.closeModal(e)}
-          />
-        </Modal>
-        <ConfirmationDialog
-          visible={this.state.confirmationDialog.visible}
-          message={this.state.confirmationDialog.message}
-          onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
-          onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
-        />
-      </div>
-    );
-  }
-
-}
-
-export default withStyles(ConnectionsList, s);

+ 0 - 6
src/components/ConnectionsList/package.json

@@ -1,6 +0,0 @@
-{
-  "name": "ConnectionsList",
-  "version": "0.0.0",
-  "private": true,
-  "main": "./ConnectionsList.js"
-}

+ 178 - 0
src/components/EditProfile/EditProfile.js

@@ -0,0 +1,178 @@
+/*
+ Copyright (C) 2017  Cloudbase Solutions SRL
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { Component, PropTypes } from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './EditProfile.scss';
+import Dropdown from '../NewDropdown';
+import NotificationActions from '../../actions/NotificationActions';
+import UserActions from '../../actions/UserActions';
+
+const title = 'Edit Profile';
+
+class EditProfile extends Component {
+
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired,
+  }
+  static propTypes = {
+    user: PropTypes.object,
+    type: PropTypes.string,
+    closeHandle: PropTypes.func
+  }
+  static defaultProps = {
+    user: null,
+    type: "edit"
+  }
+
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      firstName: props.user.firstName,
+      lastName: props.user.lastName,
+      email: props.user.email,
+      primaryProject: null,
+      requiredFields: [],
+      formSubmitted: false
+    }
+  }
+
+  componentWillMount() {
+    this.context.onSetTitle(title);
+  }
+
+  componentDidMount() {
+
+  }
+
+  handleChangeFirstName(e) {
+    this.setState({ firstName: e.target.value })
+  }
+
+  handleChangeLastName(e) {
+    this.setState({ lastName: e.target.value })
+  }
+
+  handleChangeEmail(e) {
+    this.setState({ email: e.target.value })
+  }
+
+  handleSave() {
+    UserActions.setUserInfo(this.props.user.id, {
+      extra: {
+        firstName: this.state.firstName,
+        lastName: this.state.lastName
+      },
+      email: this.state.email
+    })
+    let valid = true
+
+    for (let i in this.state.currentCloudData) {
+      if (this.state.requiredFields.indexOf(i) > -1 && !this.state.currentCloudData[i]) {
+        valid = false
+      }
+    }
+    if (this.state.connectionName.trim().length == 0) {
+      valid = false
+    }
+    if (!valid) {
+      NotificationActions.notify("Please fill all required fields", "error")
+      this.setState({ formSubmitted: true })
+    } else {
+      // TODO: Save action here
+    }
+  }
+
+  isValid(field) {
+    if (field.required && this.state.formSubmitted) {
+      if (this.state.currentCloudData[field.name].length == 0) {
+        return false
+      } else {
+        return true
+      }
+    } else {
+      return true
+    }
+  }
+
+  handleCancel() {
+    this.props.closeHandle();
+  }
+
+  render() {
+    let projectOptions = []
+    if (this.props.user) {
+      projectOptions = this.props.user.projects.map(project => ({ label: project.name, id: project.id }))
+    }
+    console.log(projectOptions)
+    return (
+      <div className={s.root}>
+        <div className={s.header}>
+          <h3>{title}</h3>
+        </div>
+        <div className={s.container}>
+          <div className={s.fields}>
+            <div className="form-group">
+              <label>First Name</label>
+              <input
+                type="text"
+                placeholder="First Name"
+                onChange={(e) => this.handleChangeFirstName(e)}
+                value={this.state.firstName}
+              />
+            </div>
+            <div className="form-group">
+              <label>Last Name</label>
+              <input
+                type="text"
+                placeholder="Last Name"
+                onChange={(e) => this.handleChangeLastName(e)}
+                value={this.state.lastName}
+              />
+            </div>
+            <div className="form-group">
+              <label>Email</label>
+              <input
+                type="text"
+                placeholder="Email"
+                onChange={(e) => this.handleChangeEmail(e)}
+                value={this.state.email}
+              />
+            </div>
+            <div className="form-group">
+              <label>Main Project</label>
+              <Dropdown
+                options={projectOptions}
+                placeholder="Switch Project"
+                onChange={(e) => this.switchProject(e)}
+                value={this.state.primaryProject}
+              />
+            </div>
+          </div>
+          <div className={s.buttons}>
+            <button className={s.leftBtn + " gray"} onClick={(e) => this.handleCancel(e)}>Cancel</button>
+            <button className={s.rightBtn} onClick={(e) => this.handleSave(e)}>Save</button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withStyles(EditProfile, s);

+ 164 - 0
src/components/EditProfile/EditProfile.scss

@@ -0,0 +1,164 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  :global(.Dropdown-control) {
+    text-align: left;
+  }
+  .buttons {
+    padding-top: 16px;
+    .leftBtn {
+      float: left;
+    }
+    .rightBtn {
+      float: right;
+    }
+    &:after {
+
+    }
+    .centerBtn {
+      margin: 0 auto;
+      width: 256px;
+      display: block;
+    }
+  }
+}
+.header {
+  h3 {
+    /* Add Cloud Endpoint: */
+    font-weight: $weight-light;
+    font-size: 24px;
+    color: $gray-darker;
+    padding: 10px;
+    background-color: $gray-light;
+    text-align: center;
+    margin: 0;
+    border-radius: 4px 4px 0 0;
+  }
+}
+.container {
+  margin: 0 auto;
+  padding: 48px 32px 32px;
+  position: relative;
+  :global(.form-group) {
+    margin-bottom: 16px;
+    &:global(.switch-radio) {
+      width: 100%;
+    }
+  }
+  input[type="text"], input[type="password"] {
+    width: 100%;
+    box-sizing: border-box;
+  }
+  textarea {
+    width: 100%;
+    height: 96px;
+    box-sizing: border-box;
+  }
+  .fields {
+    :global(.form-group):not(:global(.switch-radio)) {
+      width: 50%;
+      float: left;
+      box-sizing: border-box;
+      label {
+        text-align: left;
+        text-transform: uppercase;
+        font-size: 9px;
+        font-weight: 500;
+        color: $black;
+        display: block;
+        margin-bottom: 6px;
+      }
+      &:nth-child(2n) {
+        padding-left: 16px;
+      }
+      &:nth-child(2n+1) {
+        padding-right: 16px;
+      }
+    }
+    &:after {
+      clear: both;
+      display: block;
+      content: " ";
+      height: 0;
+    }
+    .radioOption {
+      margin-bottom: 16px;
+      label {
+        margin-left: 16px;
+      }
+
+    }
+    &.larger {
+      margin-top: 0px;
+    }
+  }
+  &:after {
+    clear: both;
+    display: block;
+    content: " ";
+    height: 0;
+  }
+  .error {
+    input {
+      border-color: $red;
+    }
+    :global(.Dropdown-control) {
+      border-color: $red;
+    }
+  }
+}
+.cloudList {
+  height: 324px;
+  overflow-y: scroll;
+  padding: 8px;
+  .cloudImage {
+    width: 128px;
+    height: 85.5px;
+    background-size: contain;
+  }
+  .cloudContainer {
+    width: 33%;
+    float: left;
+    height: 110px;
+    &:nth-child(3n) {
+      text-align: right;
+    }
+    &:nth-child(3n+2) {
+      text-align: center;
+    }
+    :global(.icon) {
+      transition: transform $animation-swift-out;
+      &:hover {
+        cursor: pointer;
+        transform: scale(1.05);
+      }
+    }
+  }
+  &:after {
+    clear: both;
+    display: block;
+    content: " ";
+    height: 0;
+  }
+}
+.cloudImage {
+  text-align: center;
+  height: 164px;
+}

+ 6 - 0
src/components/EditProfile/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "EditProfile",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./EditProfile.js"
+}

+ 187 - 0
src/components/EndpointList/EndpointList.js

@@ -0,0 +1,187 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { PropTypes } from 'react';
+import Reflux from 'reflux';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import Location from '../../core/Location';
+import Moment from 'react-moment';
+import s from './EndpointList.scss';
+import AddCloudConnection from '../AddCloudConnection';
+import Modal from 'react-modal';
+import ConnectionsStore from '../../stores/ConnectionsStore';
+import ConnectionsActions from '../../actions/ConnectionsActions';
+import TextTruncate from 'react-text-truncate';
+import UserIcon from '../UserIcon';
+import EndpointUsage from '../EndpointUsage';
+import NotificationIcon from '../NotificationIcon';
+import ProjectsDropdown from '../ProjectsDropdown';
+import MainList from '../MainList';
+import Helper from '../Helper';
+
+
+const title = 'Cloud Endpoints';
+const connectionActions = {
+  delete_action: {
+    label: "Delete",
+    action: (item) => {
+      ConnectionsActions.deleteConnection(item)
+    },
+    confirm: true
+  }
+}
+
+const filters = [
+  {
+    field: "type",
+    options: [
+      { value: null, label: "All" },
+      { value: "opc", label: "Oracle Cloud" },
+      { value: "oracle_vm", label: "Oracle VM Server" },
+      { value: "openstack", label: "Openstack" },
+      { value: "vmware_vsphere", label: "VMware" }
+    ]
+  }
+]
+
+class EndpointList extends Reflux.Component {
+
+  constructor(props) {
+    super(props)
+    this.store = ConnectionsStore
+
+    this.state = {
+      showModal: false,
+      connections: null
+    }
+
+    this.renderItem = this.renderItem.bind(this)
+  }
+
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired,
+  };
+
+  componentWillMount() {
+    super.componentWillMount.call(this)
+
+    this.context.onSetTitle(title);
+    if (this.state.connections == null) {
+      ConnectionsActions.loadConnections()
+    }
+  }
+
+  connectionDetail(e, item) {
+    Location.push('/cloud-endpoints/' + item.id + "/")
+  }
+
+  refresh() {
+    ConnectionsActions.loadConnections()
+  }
+
+  showNewConnectionModal() {
+    this.setState({ showModal: true })
+  }
+
+  closeModal() {
+    this.setState({ showModal: false })
+  }
+
+  renderItem(item) {
+    let createdAt = Helper.getTimeObject(item.created_at)
+    return (
+      <div className={"item " + (item.selected ? " selected" : "")} key={"vm_" + item.id}>
+        <span className="cell cell-icon" onClick={(e) => this.connectionDetail(e, item)}>
+          <div className={"icon endpoint"}></div>
+          <span className="details">
+            <TextTruncate line={1} truncateText="..." text={item.name} />
+            <span className={s.description}>{item.description == "" ? "N/A" : item.description}</span>
+          </span>
+        </span>
+        <span className="cell">
+          <div className={s.cloudImage + " icon small-cloud " + item.type}></div>
+        </span>
+        <span className={"cell " + s.composite}>
+          <span className={s.label}>Created</span>
+          <span className={s.value}>
+            <Moment fromNow ago date={createdAt} /> ago
+          </span>
+        </span>
+        <span className={"cell " + s.composite}>
+          <span className={s.label}>Usage</span>
+          <span className={s.value}>
+            <EndpointUsage connectionId={item.id} />
+          </span>
+        </span>
+      </div>
+    )
+  }
+
+  render() {
+    let modalStyle = {
+      content: {
+        padding: "0px",
+        borderRadius: "4px",
+        bottom: "auto",
+        width: "576px",
+        height: "auto",
+        left: "50%",
+        top: "70px",
+        marginLeft: "-288px"
+      }
+    }
+
+    return (
+      <div className={s.root}>
+        <div className={s.container}>
+          <div className={s.pageHeader}>
+            <div className={s.top}>
+              <h1>{title}</h1>
+              <div className={s.topActions}>
+                <ProjectsDropdown />
+                <button onClick={(e) => this.showNewConnectionModal(e)}>New</button>
+                <UserIcon />
+                <NotificationIcon />
+              </div>
+            </div>
+          </div>
+          <MainList
+            items={this.state.connections}
+            actions={connectionActions}
+            itemName="connection"
+            renderItem={this.renderItem}
+            filters={filters}
+            refresh={this.refresh}
+          />
+        </div>
+        <Modal
+          isOpen={this.state.showModal}
+          contentLabel="Add new cloud connection"
+          style={modalStyle}
+        >
+          <AddCloudConnection
+            closeHandle={(e) => this.closeModal(e)}
+            addHandle={(e) => this.closeModal(e)}
+          />
+        </Modal>
+      </div>
+    );
+  }
+
+}
+
+export default withStyles(EndpointList, s);

+ 4 - 4
src/components/ConnectionsList/ConnectionsList.scss → src/components/EndpointList/EndpointList.scss

@@ -112,17 +112,17 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   :global(.items-list) {
     :global(.item) {
       position: relative;
-      span:global(.cell):nth-child(2) {
+      span:global(.cell):nth-child(1) {
         flex: 3;
         cursor: pointer;
       }
-      span:global(.cell):nth-child(3) {
+      span:global(.cell):nth-child(2) {
         flex: 2;
       }
-      span:global(.cell):nth-child(4) {
+      span:global(.cell):nth-child(3) {
         flex: 2;
       }
-      span:global(.cell):nth-child(5) {
+      span:global(.cell):nth-child(4) {
         flex: 3;
       }
       :global(.checkbox-container) {

+ 6 - 0
src/components/EndpointList/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "EndpointList",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./EndpointList.js"
+}

+ 9 - 7
src/components/FilteredTable/FilteredTable.js

@@ -24,17 +24,19 @@ import LoadingIcon from "../LoadingIcon/LoadingIcon";
 class FilteredTable extends Component {
 
   static defaultProps = {
-    items: [],
+    items: null,
     filterFn: null,
-    renderSearchItem: null
+    renderSearchItem: null,
+    customClassName: null
   }
 
   static propTypes = {
     items: PropTypes.array,
     filterFn: PropTypes.func,
     queryText: PropTypes.string,
-    filterType: PropTypes.string,
-    renderSearch: PropTypes.func
+    filters: PropTypes.array,
+    renderSearch: PropTypes.func,
+    customClassName: PropTypes.string
   }
 
   constructor(props) {
@@ -52,7 +54,7 @@ class FilteredTable extends Component {
 
   componentWillReceiveProps(newProps, oldProps) {
     if (newProps.items) {
-      this.setState({filteredData: newProps.items}, () => {
+      this.setState({ filteredData: newProps.items }, () => {
         this.searchItem()
       })
     }
@@ -62,7 +64,7 @@ class FilteredTable extends Component {
     let queryResult = []
     if (this.props.items.length) {
       this.props.items.forEach((item) => {
-        if (this.props.filterFn(item, this.props.queryText, this.props.filterType, this.props.filterStatus)) {
+        if (this.props.filterFn(item, this.props.queryText, this.props.filters)) {
           queryResult.push(item)
         }
       }, this)
@@ -74,7 +76,7 @@ class FilteredTable extends Component {
   }
 
   render() {
-    let output = <LoadingIcon/>
+    let output = <LoadingIcon />
     if (this.state.filteredData) {
       if (this.state.filteredData.length) {
         output = (<div className="items-list">{this.props.renderSearch(this.state.filteredData)}</div>)

+ 5 - 0
src/components/Helper/Helper.js

@@ -25,6 +25,11 @@ class Helper extends Component {
   static generateMigrationName(type) {
     return type + "_" + moment().format("MMDDYY-HHmmss")
   }
+  static getTimeObject(rawDate) {
+    let offset = (new Date().getTimezoneOffset()) / 60 * -1;
+
+    return moment(rawDate).add(offset, 'hours')
+  }
 }
 
 export default Helper;

+ 19 - 8
src/components/LoadingIcon/LoadingIcon.js

@@ -46,28 +46,39 @@ class LoadingIcon extends Component {
   static propTypes = {
     width: PropTypes.number,
     height: PropTypes.number,
-    animate: PropTypes.bool
+    animate: PropTypes.bool,
+    padding: PropTypes.number,
+    text: PropTypes.string
   }
 
   static defaultProps = {
     width: 137,
     height: 122,
-    animate: true
+    animate: true,
+    padding: 32,
+    text: "Loading..."
   }
 
   render() {
-    let wrapperStyle = {
+    let loaderStyle = {
       width: this.props.width,
       height: this.props.height,
-      margin: "0 auto"
+      margin: "0 auto",
+    }
+    let wrapperStyle = {
+      paddingTop: this.props.padding,
+      paddingBottom: this.props.padding
     }
-    return <div className={s.root}>
+
+    return (<div className={s.root} style={wrapperStyle}>
       <div
         className={!this.props.animate && "noAnimation"}
-        style={wrapperStyle}
+        style={loaderStyle}
         dangerouslySetInnerHTML={{__html: svgIcon}}
-      ></div>
-    </div>
+      >
+      </div>
+      {this.props.text && (<div className={s.text}>{this.props.text}</div>)}
+    </div>)
   }
 
 }

+ 5 - 0
src/components/LoadingIcon/LoadingIcon.scss

@@ -76,4 +76,9 @@ $base-opacity:          0.3;
       }
     }
   }
+  .text {
+    font-size: 18px;
+    text-align: center;
+    margin-top: 16px;
+  }
 }

+ 8 - 7
src/components/LoginPage/LoginPage.js

@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
 /*
 Copyright (C) 2017  Cloudbase Solutions SRL
 
@@ -123,21 +124,21 @@ class LoginPage extends Reflux.Component {
   }
 
   render() {
-    let loginButtonsTemplate = loginButtons.map(btn => {
-      return (<div className="form-group" key={"loginbtn_" + btn.id}>
+    let loginButtonsTemplate = loginButtons.map(btn => (
+      <div className="form-group" key={"loginbtn_" + btn.id}>
         <a className={s.loginBtn + " " + btn.id} href={btn.url}>
           <span className={s.icon}></span> Sign in with {btn.name}
         </a>
-      </div>)
-    })
+      </div>
+    ))
 
     return (
       <div className={s.root}>
         <div className={s.container + " " + (loginButtons.length <= 2 ? s.oneColumn : "")}>
           <div className={s.logo}>
             <div className={s.large}>
-              <LoadingIcon width={224} height={200} animate={this.state.loadingState} />
-              <div className={s.coriolisText} dangerouslySetInnerHTML={{__html: coriolisTextSvg}}></div>
+              <LoadingIcon width={224} height={200} animate={this.state.loadingState} padding={16} text=""/>
+              <div className={s.coriolisText} dangerouslySetInnerHTML={{ __html: coriolisTextSvg }}></div>
             </div>
             <div className={s.small}>
               <div className={"logo coriolis-white"}></div>
@@ -184,7 +185,7 @@ class LoginPage extends Reflux.Component {
         </div>
         <div className={s.footerLogo}>
           <div className={s.text}>Coriolis® is a service offered by</div>
-          <div className={s.coriolisLogo} dangerouslySetInnerHTML={{__html: footerLogoSvg}}></div>
+          <div className={s.coriolisLogo} dangerouslySetInnerHTML={{ __html: footerLogoSvg }}></div>
         </div>
       </div>
     );

+ 3 - 3
src/components/LoginPage/LoginPage.scss

@@ -180,7 +180,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
       }
     }
     .loginSeparator {
-      margin: 8px 0 8px;
+      margin: 16px 0 16px;
       .line {
         width: 16px;
       }
@@ -189,12 +189,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 }
 
 
-@media screen and (max-height: 860px) and (min-height: 760px) {
+@media screen and (max-height: 880px) and (min-height: 760px) {
   .container {
     padding-top: 2vh;
   }
   .loginContainer {
-    margin-top: 16px;
+    margin-top: 32px;
   }
   .root .footerLogo {
     bottom: 16px;

+ 206 - 168
src/components/MainList/MainList.js

@@ -1,218 +1,260 @@
 /*
-Copyright (C) 2017  Cloudbase Solutions SRL
+ Copyright (C) 2017  Cloudbase Solutions SRL
 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU Affero General Public License for more details.
 
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
+ You should have received a copy of the GNU Affero General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
 
-import React, { PropTypes } from 'react';
-import Reflux from 'reflux';
+import React, { Component, PropTypes } from 'react';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
-import Location from '../../core/Location';
 import Dropdown from '../NewDropdown';
 import SearchBox from '../SearchBox';
-import Config from '../Config';
 import Moment from 'react-moment';
-import s from './ConnectionsList.scss';
-import AddCloudConnection from '../AddCloudConnection';
-import Modal from 'react-modal';
-import ConnectionsStore from '../../stores/ConnectionsStore';
-import ConnectionsActions from '../../actions/ConnectionsActions';
+import s from './MainList.scss';
+import FilteredTable from '../FilteredTable';
 import TextTruncate from 'react-text-truncate';
-import UserIcon from '../UserIcon';
-import FilteredTable from '../FilteredTable'
+import ConfirmationDialog from '../ConfirmationDialog'
 
 
-const title = 'Cloud Connections';
-const connectionTypes = [
-  { label: "All", type: "all" },
-  { label: "Amazon", type: "amazon" },
-  { label: "Azure", type: "azure" },
-  { label: "Openstack", type: "openstack" },
-  { label: "VMware", type: "vmware_vsphere" }
-]
+class MainList extends Component {
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired
+  }
+
+  static propTypes = {
+    itemName: PropTypes.string,
+    items: PropTypes.array,
+    renderItem: PropTypes.func,
+    filters: PropTypes.array,
+    actions: PropTypes.object,
+    refresh: PropTypes.any,
+    detailAction: PropTypes.func
+  }
+
+  static defaultProps = {
+    itemName: "items",
+    items: null,
+    filters: [],
+    actions: null,
+    refresh: false
+  }
 
-class MainList extends Reflux.Component {
   constructor(props) {
     super(props)
-    this.store = ConnectionsStore
 
     this.state = {
-      showModal: false,
       queryText: '',
-      filterType: 'all',
+      items: this.props.items,
+      filters: [],
       searchMin: true,
-      connections: null
+      selectedAll: false,
+      confirmationDialog: {
+        visible: false,
+        message: "Are you sure?",
+        onConfirm: null,
+        onCancel: null
+      }
+    }
+    this.listActions = []
+    for (let i in this.props.actions) {
+      this.listActions.push({ label: props.actions[i].label, value: i })
     }
-  }
 
-  static contextTypes = {
-    onSetTitle: PropTypes.func.isRequired,
-  };
+    this.filter = this.filter.bind(this)
+  }
 
   componentWillMount() {
-    super.componentWillMount.call(this)
+    this.componentWillReceiveProps(this.props)
+  }
 
-    this.context.onSetTitle(title);
-    if (this.state.connections == null) {
-      ConnectionsActions.loadConnections()
-    }
+  componentWillReceiveProps(newProps) {
+    let stateFilters = this.state.filters
+    this.props.filters.forEach(filter => {
+      if (!stateFilters[filter.field]) {
+        stateFilters[filter.field] = null
+      }
+    })
+    this.setState({ items: newProps.items, filters: stateFilters })
   }
 
-  connectionsSelected() {
-    let count = 0,
-        total = 0
-    if (this.state.connections) {
-      this.state.connections.forEach((item) => {
-        if (item.selected) count++
+  selectedCount() {
+    let count = 0
+    if (this.state.items) {
+      this.state.items.forEach((item) => {
+        if (item.selected) {
+          count++
+        }
       })
-      total = this.state.connections.length
+    }
+    return count
+  }
+
+  itemsSelected() {
+    let count = 0
+    let total = 0
+    if (this.state.items) {
+      count = this.selectedCount()
+      total = this.state.items.length
     }
 
-    return `${count} of ${total} connection(s) selected`;
+    return `${count} of ${total} ${this.props.itemName}(s) selected`;
   }
 
-  connectionDetail(e, item) {
-    Location.push('/cloud-endpoints/' + item.id + "/")
+  itemDetail(e, item) {
+    if (typeof this.props.detailAction == "function") {
+      this.props.detailAction(item)
+    }
   }
 
   checkItem(e, itemRef) {
-    let items = this.state.connections
+    let items = this.state.items
     items.forEach((item) => {
       if (item == itemRef) {
         item.selected = !item.selected
       }
     })
-    this.setState({ connections: items })
+    this.setState({ items: items, selectedAll: false })
   }
 
-  filterFn(item, queryText, filterType) {
-    return (
-      item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
-      (filterType == "all" || filterType == item.type)
-    )
-  }
+  checkAll() {
+    let items = this.state.items
+    let selectedAll = this.state.selectedAll
 
-  searchItem(queryText) {
-    this.setState({ queryText: queryText.target.value })
+    items.forEach((item) => {
+      item.selected = !selectedAll
+    })
+
+    this.setState({ items: items, selectedAll: !selectedAll })
   }
 
-  filterType(e, type) {
-    this.setState({ filterType: type })
+  searchItem(queryText) {
+    if (queryText.target) {
+      this.setState({ queryText: queryText.target.value })
+    } else {
+      this.setState({ queryText: queryText })
+    }
   }
 
-  closeModal() {
-    this.setState({ showModal: false })
+  filter(filter, option) {
+    let stateFilters = this.state.filters
+    stateFilters[filter.field] = option.value
+    this.setState({ filters: stateFilters })
+    this.searchItem(this.state.queryText)
   }
 
-  bulkActions(action) {
-    switch (action.value) {
-      case "delete":
-        let selectedConnections = this.state.connections.filter((connection) => connection.selected)
-        selectedConnections.forEach(connection => {
-          ConnectionsActions.deleteConnection(connection)
-        })
-        break;
+  filterFn(item, queryText, filters) {
+    let valid = true
+    if (item.name.toLowerCase().indexOf(queryText.toLowerCase()) == -1) {
+      valid = false
+    }
+    for (let field in filters) {
+      if (item[field] != filters[field] && filters[field] != null) {
+        valid = false
+      }
     }
+
+    return valid
   }
 
   renderSearch(items) {
-    let output = null
-    if (items && items.length) {
-      output = items.map((item, index) => (
-        <div className="item" key={"vm_" + index}>
-          <div className="checkbox-container">
-            <input
-              id={"vm_check_" + index}
-              type="checkbox"
-              checked={item.selected}
-              onChange={(e) => this.checkItem(e, item)}
-              className="checkbox-normal"
-            />
-            <label htmlFor={"vm_check_" + index}></label>
+    if (items) {
+      let output = items.map((item) => {
+        return (
+          <div className={s.row + " " + (item.selected ? "selected" : "")} key={"row_" + item.id}>
+            <div className={"checkbox-container " + (this.props.actions == null ? "hidden" : "")}>
+              <input
+                id={"vm_check_" + item.id}
+                type="checkbox"
+                checked={item.selected}
+                onChange={(e) => this.checkItem(e, item)}
+                className="checkbox-normal"
+              />
+              <label htmlFor={"vm_check_" + item.id}></label>
+            </div>
+            {this.props.renderItem(item)}
           </div>
-          <span className="cell cell-icon" onClick={(e) => this.connectionDetail(e, item)}>
-              <span className="details">
-                {item.name}
-                <span className={s.description}>{item.description == "" ? "N/A" : item.description}</span>
-              </span>
-            </span>
-          <span className="cell">
-              <div className={s.cloudImage + " icon small-cloud " + item.type}></div>
-            </span>
-          <span className={"cell " + s.composite}>
-              <span className={s.label}>Created</span>
-              <span className={s.value}>
-                <Moment fromNow ago date={item.created_at}/> ago
-              </span>
-            </span>
-          <span className={"cell " + s.composite}>
-              <span className={s.label}>URL</span>
-              <span className={s.value}>
-                <TextTruncate line={1} truncateText="..." text={item.connection_info.secret_ref}/>
-              </span>
-            </span>
-        </div>
-      ), this)
+        )
+      })
+      return output
+    } else {
+      return (<div className="no-results">Your search returned no results</div>)
     }
-    return output
   }
 
-  onProjectChange(project) {
-    this.setState({ currentProject: project.value })
+  onActionChange(option) {
+    let items = this.state.items.filter(item => item.selected)
+    if (this.props.actions[option.value].confirm) {
+      this.setState({
+        confirmationDialog: {
+          visible: true,
+          onConfirm: () => {
+            this.setState({ confirmationDialog: { visible: false }})
+            if (this.props.actions[option.value].action) {
+              items.forEach((item) => {
+                this.props.actions[option.value].action(item)
+              })
+            }
+          },
+          onCancel: () => {
+            this.setState({ confirmationDialog: { visible: false }})
+          }
+        }
+      })
+    } else {
+      if (this.props.actions[option.value].action) {
+        items.forEach((item) => {
+          this.props.actions[option.value].action(item)
+        })
+      }
+    }
   }
 
-  showNewConnectionModal() {
-    this.setState({ showModal: true })
+  refreshList() {
+    this.props.refresh()
   }
 
   render() {
-    let itemStates = connectionTypes.map((state, index) => (
+    let tableFilters = this.props.filters.map(filter => {
+      let filterTemplate = filter.options.map((option) => (
         <a
-          className={this.state.filterType == state.type || (this.state.filterType == null && state.type == "all") ?
-            "selected" : ""}
-          onClick={(e) => this.filterType(e, state.type)} key={"status_" + index}
-        >{state.label}</a>
+          className={this.state.filters[filter.field] == option.value ? "selected" : ""}
+          onClick={() => this.filter(filter, option)} key={filter.field + "_" + option.value}
+        >{option.label}</a>
       ), this)
-
-    let modalStyle = {
-      content: {
-        padding: "0px",
-        borderRadius: "4px",
-        bottom: "auto",
-        width: "576px",
-        height: "auto",
-        left: "50%",
-        top: "70px",
-        marginLeft: "-288px"
-      }
-    }
+      return <div className="category-filter" key={"filter_" + filter.field}>{filterTemplate}</div>
+    }, this)
 
     return (
       <div className={s.root}>
         <div className={s.container}>
-          <div className={s.pageHeader}>
-            <div className={s.top}>
-              <h1>{title}</h1>
-              <div className={s.topActions}>
-                <button onClick={(e) => this.showNewConnectionModal(e)}>New Connection</button>
-                <UserIcon />
-              </div>
-            </div>
+          <div className={s.listHeader}>
             <div className="filters">
-              <div className="category-filter">
-                {itemStates}
+              <div className={"checkbox-container " + (this.props.actions == null ? "hidden" : "")}>
+                <input
+                  id={"vm_check_all"}
+                  type="checkbox"
+                  checked={this.state.selectedAll[this.state.filterType]}
+                  onChange={(e) => this.checkAll()}
+                  className="checkbox-normal"
+                />
+                <label htmlFor={"vm_check_all"}></label>
               </div>
+              {tableFilters}
+              {this.props.refresh && (
+                <div className={s.refreshBtn}>
+                  <div className="icon refresh" onClick={(e) => this.refreshList(e)}></div>
+                </div>
+              )}
               <div className="name-filter">
                 <SearchBox
                   placeholder="Search"
@@ -223,41 +265,37 @@ class MainList extends Reflux.Component {
                   className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
                 />
               </div>
-              <div className={s.bulkActions}>
-                <div className={s.connectionsCount}>
-                  {this.connectionsSelected()}
+              <div className={s.bulkActions + (this.selectedCount() === 0 ? " invisible " : " ") +
+                (this.props.actions == null ? "hidden" : "")}
+              >
+                <div className={s.itemsCount}>
+                  {this.itemsSelected()}
                 </div>
                 <Dropdown
-                  options={Config.connectionsActions}
-                  placeholder="Select"
-                  onChange={(e) => this.bulkActions(e)}
+                  options={this.listActions}
+                  onChange={(e) => this.onActionChange(e)}
+                  placeholder="More Actions"
                 />
               </div>
             </div>
           </div>
-          <div className={s.pageContent}>
+          <div className={s.listContent}>
             <FilteredTable
-              items={this.state.connections}
+              items={this.state.items}
               filterFn={this.filterFn}
+              filters={this.state.filters}
               queryText={this.state.queryText}
-              filterType={this.state.filterType}
               renderSearch={(e) => this.renderSearch(e)}
+              customClassName={s.mainTable}
             ></FilteredTable>
           </div>
-          <div className={s.pageFooter}>
-
-          </div>
         </div>
-        <Modal
-          isOpen={this.state.showModal}
-          contentLabel="Add new cloud connection"
-          style={modalStyle}
-        >
-          <AddCloudConnection
-            closeHandle={(e) => this.closeModal(e)}
-            addHandle={(e) => this.closeModal(e)}
-          />
-        </Modal>
+        <ConfirmationDialog
+          visible={this.state.confirmationDialog.visible}
+          message={this.state.confirmationDialog.message}
+          onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
+          onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
+        />
       </div>
     );
   }

+ 36 - 36
src/components/MainList/MainList.scss

@@ -20,36 +20,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 .root {
   height: 100%;
 }
-.pageHeader {
-  padding: 44px 64px 0;
+.listHeader {
+  padding: 0px 64px 0;
   flex: 1;
 
   h1 {
     float: left;
     margin: 0;
   }
-  .top {
-    &:after {
-      content: " ";
-      clear: both;
-      display: block;
-      height: 0;
-    }
-    margin-bottom: 48px;
-  }
-  .topActions {
-    float: right;
-    margin-top: 3px;
-    :global(.Dropdown-root) {
-      margin-right: 16px;
-      float: left;
-      width: 160px;
-      :global(.Dropdown-control) {
-        text-align: left;
-      }
-    }
-
-  }
   :global(.filters) {
     :global(.category-filter) {
       float: left;
@@ -59,7 +37,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     }
     :global(.name-filter) {
       float: left;
-      padding-left: 24px;
+      padding-left: 8px;
       margin-top: 2px;
     }
     &:after {
@@ -70,10 +48,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     }
   }
 }
-.pageContent {
+.listContent {
   flex: 10;
   overflow-y: auto;
   padding: 0 64px;
+
+}
+.mainTable {
+  border-top: 1px solid $gray-light;
 }
 .pageFooter {
   flex: 1;
@@ -95,7 +77,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     width: 160px;
     float: left;
   }
-  .connectionsCount {
+  .itemsCount {
     float: left;
     line-height: 32px;
     margin-right: 16px;
@@ -109,25 +91,36 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   display: flex;
   flex-direction: column;
   :global(.items-list) {
+    .row {
+      position: relative;
+      :global(.checkbox-container) {
+        position: absolute;
+        left: -32px;
+        top: 21px;
+        opacity: 0;
+        transition: opacity $animation-swift-out;
+      }
+      &:hover, &:global(.selected) {
+        :global(.checkbox-container) {
+          opacity: 1;
+        }
+      }
+    }
     :global(.item) {
       position: relative;
-      span:global(.cell):nth-child(2) {
+      span:global(.cell):nth-child(1) {
         flex: 3;
       }
-      span:global(.cell):nth-child(3) {
+      span:global(.cell):nth-child(2) {
         flex: 2;
       }
-      span:global(.cell):nth-child(4) {
+      span:global(.cell):nth-child(3) {
         flex: 2;
       }
-      span:global(.cell):nth-child(5) {
+      span:global(.cell):nth-child(4) {
         flex: 3;
       }
-      :global(.checkbox-container) {
-        position: absolute;
-        left: -32px;
-        top: 21px;
-      }
+
       .description {
         display: block;
         color: $gray-dark;
@@ -174,3 +167,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   }
 
 }
+.refreshBtn {
+  float: left;
+  padding: 5px 5px;
+  margin-top: 6px;
+  margin-left: 11px;
+  cursor: pointer;
+}

+ 1 - 1
src/components/MigrationDetail/MigrationDetail.js

@@ -132,7 +132,7 @@ class MigrationDetail extends Component {
                   Created
                 </div>
                 <div className={s.value}>
-                  <Moment format="MM/DD/YYYY HH:MM" date={item.created} />
+                  <Moment format="MM/DD/YYYY HH:mm" date={item.created_at} />
                 </div>
               </div>
               <div className={s.formGroup}>

+ 91 - 353
src/components/MigrationList/MigrationList.js

@@ -19,343 +19,142 @@ import React, { PropTypes } from 'react';
 import Reflux from 'reflux';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import Location from '../../core/Location';
-import Dropdown from '../NewDropdown';
 import UserIcon from '../UserIcon';
 import NotificationIcon from '../NotificationIcon';
-import SearchBox from '../SearchBox';
 import Moment from 'react-moment';
 import s from './MigrationList.scss';
 import MigrationStore from '../../stores/MigrationStore';
-import ProjectStore from '../../stores/MigrationStore';
 import MigrationActions from '../../actions/MigrationActions';
-import FilteredTable from '../FilteredTable';
 import TextTruncate from 'react-text-truncate';
-import LoadingIcon from "../LoadingIcon/LoadingIcon";
-import ConfirmationDialog from '../ConfirmationDialog'
 import ProjectsDropdown from '../ProjectsDropdown';
-
-const title = 'Migrations';
-const migrationTypes = [
-  { label: "Replicas", type: "replica" },
-  { label: "Migrations", type: "migration" },
-  { label: "All", type: "all" }
-]
-const statusTypes = [
-  { label: "Running", type: "RUNNING" },
-  { label: "Error", type: "ERROR" },
-  { label: "Completed", type: "COMPLETED" },
-  { label: "All", type: "all" }
-]
-const migrationActions = [
-  { label: "Execute", value: "execute" },
-  { label: "Cancel", value: "cancel" },
-  { label: "Delete", value: "delete" }
+import MainList from '../MainList';
+import Helper from '../Helper';
+
+const title = 'Coriolis Migrations';
+
+const filters = [
+  {
+    field: "status",
+    options: [
+      { value: null, label: "All" },
+      { value: "RUNNING", label: "Running" },
+      { value: "ERROR", label: "Error" },
+      { value: "COMPLETED", label: "Completed" }
+    ]
+  }
 ]
 
+const migrationActions = {
+  delete_action: {
+    label: "Delete",
+    action: (item) => {
+      MigrationActions.deleteMigration(item)
+    },
+    confirm: true
+  },
+  cancel_action: {
+    label: "Cancel",
+    action: (item) => {
+      MigrationActions.cancelMigration(item)
+    },
+    confirm: true
+  }
+}
+
 class MigrationList extends Reflux.Component {
+
   constructor(props) {
     super(props)
     this.store = MigrationStore;
 
     this.state = {
-      title: props.type == "migrations" ? "Migrations" : "Replicas",
-      queryText: '',
-      filterType: props.type == "migrations" ? "migration" : "replica",
-      filterStatus: "all",
-      currentProject: "My Project",
-      searchMin: true,
-      filteredData: [],
-      selectedAll: {
-        migration: false,
-        replica: false
-      },
-      confirmationDialog: {
-        visible: false,
-        message: "Are you sure?",
-        onConfirm: null,
-        onCancel: null
-      }
+      migrations: null,
     }
+
+    this.renderItem = this.renderItem.bind(this)
   }
 
   static contextTypes = {
     onSetTitle: PropTypes.func.isRequired
   };
 
-  componentWillReceiveProps(newProps, oldProps) {
-    this.setState({
-      title: newProps.type == "migrations" ? "Migrations" : "Replicas",
-      filterType: newProps.type == "migrations" ? "migration" : "replica"
-    })
-  }
-
   componentWillMount() {
     super.componentWillMount.call(this)
 
-    this.context.onSetTitle(this.state.title);
+    this.context.onSetTitle(title);
 
     MigrationActions.loadMigrations()
-
-    this.projects = [
-      { label: "My Project", value: "Project1" },
-      { label: "Project 2", value: "Project2" }
-    ]
-  }
-
-  componentDidMount() {
-    this.setState({ filteredData: this.state.migrations }) // eslint-disable-line react/no-did-mount-set-state
   }
 
   newMigration() {
     Location.push('/migrations/new')
   }
 
-  migrationsSelected() {
-    let count = 0
-    let total = 0
-    if (this.state.migrations) {
-      count = this.migrationsSelectedCount()
-      if (this.state.filterType == "all") {
-        total = this.state.migrations.length
-      } else {
-        this.state.migrations.forEach(item => {
-          if (item.type == this.state.filterType) {
-            total++
-          }
-        })
-      }
-    }
-    let term = "migration"
-    if (this.state.filterType == "replica") term = "replica"
-
-    return `${count} of ${total} ${term}(s) selected`;
+  migrationDetail(e, item) {
+    Location.push('/migration/' + item.id + "/")
   }
 
-  migrationsSelectedCount() {
+  renderItem(item) {
     let count = 0
-    if (this.state.migrations) {
-      this.state.migrations.forEach((item) => {
-        if (item.selected) {
-          if (this.state.filterType == "all") {
-            count++
-          } else {
-            if (item.type == this.state.filterType && item.selected) {
-              count++
-            }
-          }
-        }
-      })
-    }
-    return count
-  }
 
-  migrationDetail(e, item) {
-    if (item.type == "migration") {
-      Location.push('/migration/' + item.id + "/")
-    } else {
-      Location.push('/replica/' + item.id + "/")
+    if (!item.tasks) {
+      item.tasks = []
     }
-
-  }
-
-  checkItem(e, itemRef) {
-    let items = this.state.migrations
-
-    items.forEach((item) => {
-      if (item == itemRef) {
-        item.selected = !item.selected
-      }
+    item.tasks.forEach((task) => {
+      if (task.status != "COMPLETED") count++
     })
-    let selectedAll = this.state.selectedAll
-    selectedAll[this.state.filterType] = false
-    this.setState({ migrations: items, selectedAll: selectedAll })
-  }
-
-  checkAll(e) {
-    let items = this.state.migrations
-    let selectedAll = this.state.selectedAll
 
-    items.forEach((item) => {
-      if (item.type == this.state.filterType) {
-        item.selected = !selectedAll[this.state.filterType]
-      }
-    })
-    selectedAll[this.state.filterType] = !selectedAll[this.state.filterType]
-    this.setState({ migrations: items, selectedAll: selectedAll })
-  }
+    let tasksRemaining = count + " out of " + item.tasks.length
 
-  searchItem(queryText) {
-    if (queryText.target) {
-      this.setState({queryText: queryText.target.value })
-    } else {
-      this.setState({queryText: queryText })
+    if (count == 0) {
+      tasksRemaining = "-"
     }
-  }
 
-  filterType(e, type) {
-    this.setState({ filterType: type }, () => {
-      this.searchItem({ target: { value: this.state.queryText } })
-    })
-  }
-
-  filterStatus(e, status) {
-    this.setState({ filterStatus: status }, () => {
-      this.searchItem({ target: { value: this.state.queryText } })
-    })
-  }
+    let createdAt = Helper.getTimeObject(item.created_at)
 
-  filterFn(item, queryText, filterType, filterStatus) {
     return (
-      item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
-      (filterType == "all" || filterType == item.type) &&
-      (filterStatus == "all" || filterStatus == item.status)
+      <div className={"item " + (item.selected ? "selected" : "")} key={"migration_" + item.id}>
+        <span className="cell cell-icon" onClick={(e) => this.migrationDetail(e, item)}>
+          <div className={"icon " + item.type}></div>
+          <span className="details">
+            <TextTruncate line={1} truncateText="..." text={item.name} />
+            <span className={s.migrationStatus + " status-pill " + item.status}>{item.status}</span>
+          </span>
+        </span>
+        <span className="cell" onClick={(e) => this.migrationDetail(e, item)}>
+          <div className={s.cloudImage + " icon small-cloud " + item.origin_endpoint_type}></div>
+          <span className={s.chevronRight}></span>
+          <div className={s.cloudImage + " icon small-cloud " + item.destination_endpoint_type}></div>
+        </span>
+        <span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
+          <span className={s.label}>Created</span>
+          <span className={s.value}>
+            <Moment format="MMM Do YYYY HH:mm" date={createdAt} />
+          </span>
+        </span>
+        {/*<span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
+         <span className={s.label}>Notes</span>
+         <TextTruncate line={2} truncateText="..." text={item.notes} />
+         </span>*/}
+        <span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
+          <span className={s.label}>Tasks remaining</span>
+          <span className={s.value}>{tasksRemaining}</span>
+        </span>
+        {/*<span className={"cell " + s.composite}>
+         <span className={s.label}>Current instance</span>
+         <span className={s.value}>{this.currentInstance(item)}</span>
+         </span>*/}
+      </div>
     )
   }
 
-  renderSearch(items) {
-    if (items) {
-      let output = items.map((item, index) => {
-        let count = 0
-
-        if (item.type == 'replica' && item.executions.length) {
-          item.tasks = item.executions[item.executions.length - 1].tasks
-        }
-        if (!item.tasks) {
-          item.tasks = []
-        }
-        item.tasks.forEach((task) => {
-          if (task.status != "COMPLETED") count++
-        })
-
-
-        let tasksRemaining = count + " out of " + item.tasks.length
-
-        return (
-          <div className={"item " + (item.selected ? "selected" : "")} key={"vm_" + index}>
-            <div className="checkbox-container">
-              <input
-                id={"vm_check_" + index}
-                type="checkbox"
-                checked={item.selected}
-                onChange={(e) => this.checkItem(e, item)}
-                className="checkbox-normal"
-              />
-              <label htmlFor={"vm_check_" + index}></label>
-            </div>
-            <span className="cell cell-icon" onClick={(e) => this.migrationDetail(e, item)}>
-              <div className={"icon " + item.type}></div>
-              <span className="details">
-                {/*{item.name ? item.name : "N/A"}*/}
-                <TextTruncate line={1} truncateText="..." text={item.name} />
-                <span className={s.migrationStatus + " status-pill " + item.status}>{item.status}</span>
-              </span>
-            </span>
-            <span className="cell" onClick={(e) => this.migrationDetail(e, item)}>
-              <div className={s.cloudImage + " icon small-cloud " + item.origin_endpoint_type}></div>
-              <span className={s.chevronRight}></span>
-              <div className={s.cloudImage + " icon small-cloud " + item.destination_endpoint_type}></div>
-            </span>
-            <span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
-              <span className={s.label}>Created</span>
-              <span className={s.value}>
-                <Moment format="MMM Do YYYY HH:ss" date={item.created_at} />
-              </span>
-            </span>
-            {/*<span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
-              <span className={s.label}>Notes</span>
-              <TextTruncate line={2} truncateText="..." text={item.notes} />
-            </span>*/}
-            <span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
-              <span className={s.label}>Tasks remaining</span>
-              <span className={s.value}>{tasksRemaining}</span>
-            </span>
-            {/*<span className={"cell " + s.composite}>
-              <span className={s.label}>Current instance</span>
-              <span className={s.value}>{this.currentInstance(item)}</span>
-            </span>*/}
-          </div>
-        )
-      })
-      return output
-    } else {
-      return (<div className="no-results">Your search returned no results</div>)
-    }
-  }
-
-  onProjectChange(project) {
-    // TODO: Move setstate from here
-    //this.setState({ currentProject: project.value })
-  }
-
-  onMigrationActionChange(option) {
-    switch (option.value) {
-      case "delete":
-        let deletedItems = [] // we put here the items for deletion
-        this.state.migrations.forEach((item) => {
-          if (item.selected) {
-            if (this.state.filterType == "all") {
-              deletedItems.push(item)
-            } else {
-              if (item.type == this.state.filterType) {
-                deletedItems.push(item)
-              }
-            }
-          }
-        })
-        this.setState({
-          confirmationDialog: {
-            visible: true,
-            onConfirm: () => {
-              this.setState({ confirmationDialog: { visible: false }})
-              deletedItems.forEach(item => {
-                MigrationActions.deleteMigration(item)
-              })
-            },
-            onCancel: () => {
-              this.setState({ confirmationDialog: { visible: false }})
-            }
-          }
-        })
-        break
-      case "execute":
-        this.state.migrations.forEach((item) => {
-          if (item.selected) {
-            if (this.state.filterType == "all") {
-              MigrationActions.executeReplica(item)
-            } else {
-              if (item.type == this.state.filterType) {
-                MigrationActions.executeReplica(item)
-              }
-            }
-
-          }
-        })
-        break
-      case "cancel":
-        this.state.migrations.forEach((item) => {
-          if (item.selected) {
-            if (this.state.filterType == "all") {
-              MigrationActions.cancelMigration(item)
-            } else {
-              if (item.type == this.state.filterType) {
-                MigrationActions.cancelMigration(item)
-              }
-            }
-          }
-        })
-        break
-      default:
-        break
-    }
-  }
-
   currentInstance(migration) {
     let instance = "N/A"
-    /*migration.vms.forEach((item) => {
+    /* migration.vms.forEach((item) => {
       if (item.selected) {
         instance = item.name
       }
-    })*/
+    }) */
     return instance
   }
 
@@ -364,90 +163,29 @@ class MigrationList extends Reflux.Component {
   }
 
   render() {
-    let _this = this
-    let itemStates = statusTypes.map((state, index) => (
-        <a
-          className={_this.state.filterStatus == state.type || (_this.state.filterStatus == null && state.type == "all") ?
-            "selected" : ""}
-          onClick={(e) => _this.filterStatus(e, state.type)} key={"status_" + index}
-        >{state.label}</a>
-      )
-    )
     return (
       <div className={s.root}>
         <div className={s.container}>
           <div className={s.pageHeader}>
             <div className={s.top}>
-              <h1>Coriolis {this.state.title}</h1>
+              <h1>{title}</h1>
               <div className={s.topActions}>
-                {/*<Dropdown
-                  options={this.projects}
-                  onChange={(e) => this.onProjectChange(e)}
-                  placeholder="Select"
-                  value={this.state.currentProject}
-                />*/}
                 <ProjectsDropdown />
                 <button onClick={this.newMigration}>New</button>
                 <UserIcon />
                 <NotificationIcon />
               </div>
             </div>
-            <div className="filters">
-              <div className="checkbox-container">
-                <input
-                  id={"vm_check_all"}
-                  type="checkbox"
-                  checked={this.state.selectedAll[this.state.filterType]}
-                  onChange={(e) => this.checkAll()}
-                  className="checkbox-normal"
-                />
-                <label htmlFor={"vm_check_all"}></label>
-              </div>
-              <div className="category-filter">
-                {itemStates}
-              </div>
-              <div className={s.refreshBtn}>
-                <div className="icon refresh" onClick={this.refreshList}></div>
-              </div>
-              <div className="name-filter">
-                <SearchBox
-                  placeholder="Search"
-                  value={this.state.queryText}
-                  onChange={(e) => this.searchItem(e)}
-                  minimize={true} // eslint-disable-line react/jsx-boolean-value
-                  onClick={(e) => this.toggleSearch(e)}
-                  className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
-                />
-              </div>
-              <div className={s.bulkActions + (this.migrationsSelectedCount() === 0 ? " invisible": "")}>
-                <div className={s.migrationsCount}>
-                  {this.migrationsSelected()}
-                </div>
-                <Dropdown
-                  options={migrationActions}
-                  onChange={(e) => this.onMigrationActionChange(e)}
-                  placeholder="More Actions"
-                />
-              </div>
-            </div>
-          </div>
-          <div className={s.pageContent}>
-            <FilteredTable
-              items={this.state.migrations}
-              filterFn={this.filterFn}
-              queryText={this.state.queryText}
-              filterType={this.state.filterType}
-              filterStatus={this.state.filterStatus}
-              renderSearch={(e) => this.renderSearch(e)}
-            ></FilteredTable>
           </div>
+          <MainList
+            items={this.state.migrations}
+            actions={migrationActions}
+            itemName="migration"
+            renderItem={this.renderItem}
+            filters={filters}
+            refresh={this.refreshList}
+          />
         </div>
-        <ConfirmationDialog
-          visible={this.state.confirmationDialog.visible}
-          message={this.state.confirmationDialog.message}
-          onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
-          onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
-        />
       </div>
     );
   }

+ 5 - 5
src/components/MigrationList/MigrationList.scss

@@ -108,24 +108,24 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     :global(.item) {
       position: relative;
       cursor: pointer;
-      span:global(.cell):nth-child(2) {
+      span:global(.cell):nth-child(1) {
         flex: 3;
       }
-      span:global(.cell):nth-child(3) {
+      span:global(.cell):nth-child(2) {
         flex: 1;
         padding-left: 2%;
         padding-right: 2%;
       }
-      span:global(.cell):nth-child(4) {
+      span:global(.cell):nth-child(3) {
         flex: 1;
         padding-right: 8px;
         min-width: 156px;
       }
-      span:global(.cell):nth-child(5) {
+      span:global(.cell):nth-child(4) {
         flex: 1;
         min-width: 126px;
       }
-      span:global(.cell):nth-child(6) {
+      span:global(.cell):nth-child(5) {
 
       }
       :global(.checkbox-container) {

+ 0 - 6
src/components/MigrationSchedule/package.json

@@ -1,6 +0,0 @@
-{
-  "name": "MigrationSchedule",
-  "version": "0.0.0",
-  "private": true,
-  "main": "./MigrationSchedule.js"
-}

+ 23 - 64
src/components/MigrationView/MigrationView.js

@@ -29,15 +29,7 @@ import TextTruncate from 'react-text-truncate';
 import Location from '../../core/Location';
 import ConfirmationDialog from '../ConfirmationDialog'
 
-const migrationActions = [
-  { label: "Cancel", value: "cancel" },
-  { label: "Delete", value: "delete" }
-]
-
-const replicaActions = [
-  { label: "Execute", value: "execute" },
-  { label: "Delete", value: "delete" }
-]
+const title = "Coriolis: View Migration"
 
 // TODO: Create ReplicaView
 class MigrationView extends Reflux.Component {
@@ -56,7 +48,6 @@ class MigrationView extends Reflux.Component {
 
     this.state = {
       migration: null,
-      title: 'Coriolis: View Migration',
       confirmationDialog: {
         visible: false,
         message: "Are you sure?",
@@ -72,45 +63,33 @@ class MigrationView extends Reflux.Component {
   }
 
   componentDidMount() {
-    this.context.onSetTitle(this.state.title);
-  }
-
-  cancelMigration() {
-    let item = this.state.migrations.filter(migration => migration.id == this.props.migrationId)[0]
-    MigrationActions.cancelMigration(item)
+    this.context.onSetTitle(title);
   }
 
-  executeReplica() {
-    let item = this.state.migrations.filter(migration => migration.id == this.props.migrationId)[0]
-    MigrationActions.executeReplica(item)
+  goBack() {
+    Location.push('/migrations')
   }
 
-  goBack() {
+  deleteMigration() {
     let item = this.state.migrations.filter(migration => migration.id == this.props.migrationId)[0]
-    if (item.type == "migration") {
-      Location.push('/migrations')
-    } else {
-      Location.push('/replicas')
-    }
-
+    this.setState({
+      confirmationDialog: {
+        visible: true,
+        onConfirm: () => {
+          this.setState({ confirmationDialog: { visible: false }})
+          MigrationActions.deleteMigration(item)
+          Location.push('/migrations')
+        },
+        onCancel: () => {
+          this.setState({ confirmationDialog: { visible: false }})
+        }
+      }
+    })
   }
 
-  onMigrationActionsChange(option) {
+  cancelMigration() {
     let item = this.state.migrations.filter(migration => migration.id == this.props.migrationId)[0]
-    switch (option.value) {
-      case "delete":
-        MigrationActions.deleteMigration(item)
-        Location.push('/cloud-endpoints')
-        break
-      case "start":
-        MigrationActions.executeReplica(item)
-        break
-      case "cancel":
-        MigrationActions.cancelMigration(item)
-        break 
-      default:
-        break
-    }
+    MigrationActions.cancelMigration(item)
   }
 
   currentMigration(migrationId) {
@@ -123,33 +102,13 @@ class MigrationView extends Reflux.Component {
 
   render() {
     let item = this.currentMigration(this.props.migrationId)
-    let title = "Edit"
     let buttons = null
 
     if (item) {
-      title = "Edit Migration"
-      if (item.type == 'replica') {
-        title = "Edit Replica"
-
-        let disabled = item.executions.length && item.executions[item.executions.length - 1].status != "COMPLETED"
-        if (item.executions.length == 0) {
-          disabled = true
-        }
-        buttons = (
-          <div>
-            <button
-              className="gray"
-              disabled={item.status === "RUNNING"}
-              onClick={(e) => this.executeReplica(e)}>
-              Execute Now
-            </button>
-          </div>)
+      if (item.status == "RUNNING") {
+        buttons = <button className="gray" onClick={(e) => (this.cancelMigration(e))}>Cancel</button>
       } else {
-        if (item.status == "RUNNING") {
-          buttons = <button className="gray" onClick={(e) => (this.cancelMigration(e))}>Cancel</button>
-        } else {
-          buttons = <button className="gray" onClick={(e) => this.deleteMigration(e)}>Delete</button>
-        }
+        buttons = <button className="gray" onClick={(e) => this.deleteMigration(e)}>Delete</button>
       }
 
       let itemStatus = item.status

+ 19 - 1
src/components/MigrationWizard/MigrationWizard.js

@@ -41,6 +41,14 @@ import WizardActions from '../../actions/WizardActions';
 const title = 'New Migration';
 class MigrationWizard extends Reflux.Component {
 
+  static propTypes = {
+    wizard_type: PropTypes.string
+  }
+
+  static defaultProps = {
+    wizard_type: "migration"
+  }
+
   static contextTypes = {
     onSetTitle: PropTypes.func.isRequired
   }
@@ -49,11 +57,17 @@ class MigrationWizard extends Reflux.Component {
     super(props)
     this.stores = [MigrationStore, ConnectionStore, WizardStore]
     WizardActions.newState()
+
   }
 
   componentWillMount() {
     super.componentWillMount.call(this)
     this.context.onSetTitle(title);
+    if (this.props.wizard_type == "replica") {
+      WizardActions.updateWizardState({
+        migrationType: "replica"
+      })
+    }
   }
 
 
@@ -64,7 +78,11 @@ class MigrationWizard extends Reflux.Component {
       if (this.state.currentStep != "WizardMigrationType") {
         this.initBackStep()
       } else {
-        Location.push("/migrations")
+        if (this.props.wizard_type == "replica") {
+          Location.push("/replicas")
+        } else {
+          Location.push("/migrations")
+        }
       }
     }
   }

+ 2 - 2
src/components/MigrationWizard/MigrationWizard.scss

@@ -59,7 +59,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   .breadcrumbs {
     span {
       display: inline-block;
-      margin-right: 7px;
+      margin-right: 4px;
       color: $gray;
       &:after {
         content: " ";
@@ -67,7 +67,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
         width: 7px;
         height: 13px;
         display: inline-block;
-        margin-left: 5px;
+        margin-left: 3px;
         margin-top: 3px;
         float:right;
       }

+ 71 - 255
src/components/ProjectList/ProjectList.js

@@ -19,50 +19,60 @@ import React, { PropTypes } from 'react';
 import Reflux from 'reflux';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import Location from '../../core/Location';
-import Dropdown from '../NewDropdown';
-import SearchBox from '../SearchBox';
 import s from './ProjectList.scss';
-import AddCloudConnection from '../AddCloudConnection';
-import Modal from 'react-modal';
 import UserStore from '../../stores/UserStore';
 import UserActions from '../../actions/UserActions';
 import TextTruncate from 'react-text-truncate';
 import UserIcon from '../UserIcon';
-import FilteredTable from '../FilteredTable';
 import NotificationIcon from '../NotificationIcon';
-import ConfirmationDialog from '../ConfirmationDialog'
 import ProjectsDropdown from '../ProjectsDropdown';
-
+import MainList from '../MainList';
 
 const title = 'Projects';
-const connectionTypes = [
-  { label: "All", type: "all" }
-]
-
-const connectionActions = [
-  { label: "Delete", value: "delete" }
-]
 
+const projectActions = null
+/* {
+  delete_action: {
+    label: "Delete",
+    action: (item) => {
+      console.log("Delete project action needed here")
+    },
+    confirm: true
+  }
+}
+*/
 class ProjectList extends Reflux.Component {
+  filters = [
+    {
+      field: "enabled",
+      options: [
+        { value: null, label: "All"},
+        { value: true, label: "Enabled"},
+        { value: false, label: "Disabled"}
+      ]
+    },
+    {
+      field: "is_domain",
+      options: [
+        { value: null, label: "All"},
+        { value: true, label: "Is Domain"},
+        { value: false, label: "Is Not Domain"}
+      ]
+    }
+  ]
+
   constructor(props) {
     super(props)
 
     this.store = UserStore
 
     this.state = {
-      showModal: false,
-      queryText: '',
-      filterType: 'all',
-      selectedAll: false,
-      searchMin: true,
-      projects: null,
-      confirmationDialog: {
-        visible: false,
-        message: "Are you sure?",
-        onConfirm: null,
-        onCancel: null
+      currentUser: {
+        projects: null
       }
     }
+
+    this.renderItem = this.renderItem.bind(this)
   }
 
   static contextTypes = {
@@ -76,187 +86,49 @@ class ProjectList extends Reflux.Component {
     UserActions.getScopedProjects()
   }
 
-  projectsSelected() {
-    let count = this.projectsSelectedCount(),
-        total = 0
-    if (this.state.currentUser.projects) {
-      total = this.state.currentUser.projects.length
-    }
-
-    return `${count} of ${total} project(s) selected`;
-  }
-
-  projectsSelectedCount() {
-    let count = 0
-    if (this.state.currentUser.projects) {
-      this.state.currentUser.projects.forEach((item) => {
-        if (item.selected) count++
-      })
-    }
-    return count
-  }
-
-  projectDetail(e, item) {
+  projectDetail(item) {
     Location.push('/project/details/' + item.id + "/")
   }
 
-  checkItem(e, itemRef) {
-    let items = this.state.currentUser.projects
-    items.forEach((item) => {
-      if (item == itemRef) {
-        item.selected = !item.selected
-      }
-    })
-    this.setState({ projects: items, selectedAll: false })
-  }
-
-  checkAll() {
-    let items = this.state.currentUser.projects
-    let selectedAll = this.state.selectedAll
-
-    items.forEach((item) => {
-      item.selected = !selectedAll
-    })
-
-    this.setState({ projects: items, selectedAll: !selectedAll })
-  }
-
-  filterFn(item, queryText, filterType) {
-    return (
-      item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
-      (filterType == "all" || filterType == item.type)
-    )
-  }
-
-  searchItem(queryText) {
-    this.setState({ queryText: queryText })
-  }
-
-  filterType(e, type) {
-    this.setState({ filterType: type })
-  }
-
-  closeModal() {
-    this.setState({ showModal: false })
-  }
-
   switchProject(project) {
-    console.log(project)
     UserActions.switchProject(project.id)
   }
 
-  bulkActions(action) {
-    switch (action.value) {
-      case "delete":
-        this.setState({
-          confirmationDialog: {
-            visible: true,
-            onConfirm: () => {
-              this.setState({ confirmationDialog: { visible: false }})
-              let selectedProjects = this.state.currentUser.projects.filter((connection) => connection.selected)
-              selectedProjects.forEach(project => {
-                // TODO: Delete project action here
-              })
-            },
-            onCancel: () => {
-              this.setState({ confirmationDialog: { visible: false }})
-            }
-          }
-        })
-
-        break;
-    }
-  }
-
-  renderSearch(items) {
-    let output = null
+  renderItem(item) {
     let projectId = Reflux.GlobalState.userStore.currentUser.project.id
-
-    if (items && items.length) {
-      output = items.map((item, index) => (
-        <div className={"item " + (item.selected ? " selected" : "")} key={"vm_" + index}>
-          <div className="checkbox-container">
-            <input
-              id={"vm_check_" + index}
-              type="checkbox"
-              checked={item.selected}
-              onChange={(e) => this.checkItem(e, item)}
-              className="checkbox-normal"
-            />
-            <label htmlFor={"vm_check_" + index}></label>
-          </div>
-          <span className="cell cell-icon" onClick={(e) => this.projectDetail(e, item)}>
-            <div className={"icon project"}></div>
-            <span className="details">
-              {/*{item.name ? item.name : "N/A"}*/}
-              <TextTruncate line={1} truncateText="..." text={item.name} />
-              <span className={s.description}>{item.description == "" ? "N/A" : item.description}</span>
-            </span>
-          </span>
-          <span className="cell">
-            <div className={s.cloudImage + " icon small-cloud " + item.type}></div>
-          </span>
-          <span className={"cell " + s.composite}>
-            <span className={s.label}>Is Domain</span>
-            <span className={s.value}>
-              {item.is_domain ? "Yes" : "No"}
-            </span>
+    return (
+      <div className={"item"} key={"project_" + item.id}>
+        <span className="cell cell-icon" onClick={() => this.projectDetail(item)}>
+          <div className={"icon project"}></div>
+          <span className="details">
+            <TextTruncate line={1} truncateText="..." text={item.name} />
+            <span className={s.description}>{item.description == "" ? "N/A" : item.description}</span>
           </span>
-          <span className={"cell " + s.composite}>
-            <span className={s.label}>Enabled</span>
-            <span className={s.value}>
-              {item.enabled ? "Yes" : "No"}
-            </span>
+        </span>
+        <span className={"cell " + s.composite}>
+          <span className={s.label}>Is Domain</span>
+          <span className={s.value}>
+            {item.is_domain ? "Yes" : "No"}
           </span>
-          <span className={"cell "}>
-            <button
-              className="wire gray"
-              disabled={item.id == projectId}
-              onClick={(e) => this.switchProject(item)}
-            >{item.id == projectId ? "Current" : "Switch"}</button>
+        </span>
+        <span className={"cell " + s.composite}>
+          <span className={s.label}>Enabled</span>
+          <span className={s.value}>
+            {item.enabled ? "Yes" : "No"}
           </span>
-        </div>
-      ), this)
-    }
-    return output
-  }
-  
-  currentInstance(migration) {
-    let instance = "N/A"
-    migration.vms.forEach((item) => {
-      if (item.selected) {
-        instance = item.name
-      }
-    })
-    return instance
-  }
-
-  showNewConnectionModal() {
-    this.setState({ showModal: true })
+        </span>
+        <span className={"cell "}>
+          <button
+            className="wire gray"
+            disabled={item.id == projectId}
+            onClick={(e) => this.switchProject(item)}
+          >{item.id == projectId ? "Current" : "Switch"}</button>
+        </span>
+      </div>
+    )
   }
 
   render() {
-    let itemStates = connectionTypes.map((state, index) => (
-        <a
-          className={this.state.filterType == state.type || (this.state.filterType == null && state.type == "all") ?
-            "selected" : ""}
-          onClick={(e) => this.filterType(e, state.type)} key={"status_" + index}
-        >{state.label}</a>
-      ), this)
-
-    let modalStyle = {
-      content: {
-        padding: "0px",
-        borderRadius: "4px",
-        bottom: "auto",
-        width: "576px",
-        height: "auto",
-        left: "50%",
-        top: "70px",
-        marginLeft: "-288px"
-      }
-    }
-
     return (
       <div className={s.root}>
         <div className={s.container}>
@@ -270,71 +142,15 @@ class ProjectList extends Reflux.Component {
                 <NotificationIcon />
               </div>
             </div>
-            <div className="filters">
-              <div className="checkbox-container">
-                <input
-                  id={"vm_check_all"}
-                  type="checkbox"
-                  checked={this.state.selectedAll}
-                  onChange={(e) => this.checkAll()}
-                  className="checkbox-normal"
-                />
-                <label htmlFor={"vm_check_all"}></label>
-              </div>
-              <div className="category-filter">
-                {itemStates}
-              </div>
-              <div className="name-filter">
-                <SearchBox
-                  placeholder="Search"
-                  value={this.state.queryText}
-                  onChange={(e) => this.searchItem(e)}
-                  minimize={true} // eslint-disable-line react/jsx-boolean-value
-                  onClick={(e) => this.toggleSearch(e)}
-                  className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
-                />
-              </div>
-              <div className={s.bulkActions + (this.projectsSelectedCount() === 0 ? " invisible": "")}>
-                <div className={s.projectsCount}>
-                  {this.projectsSelected()}
-                </div>
-                <Dropdown
-                  options={connectionActions}
-                  placeholder="More Actions"
-                  onChange={(e) => this.bulkActions(e)}
-                />
-              </div>
-            </div>
           </div>
-          <div className={s.pageContent}>
-            <FilteredTable
-              items={this.state.currentUser.projects}
-              filterFn={this.filterFn}
-              queryText={this.state.queryText}
-              filterType={this.state.filterType}
-              renderSearch={(e) => this.renderSearch(e)}
-            ></FilteredTable>
-          </div>
-          <div className={s.pageFooter}>
-
-          </div>
-        </div>
-        <Modal
-          isOpen={this.state.showModal}
-          contentLabel="Add new cloud connection"
-          style={modalStyle}
-        >
-          <AddCloudConnection
-            closeHandle={(e) => this.closeModal(e)}
-            addHandle={(e) => this.closeModal(e)}
+          <MainList
+            items={this.state.currentUser.projects}
+            actions={projectActions}
+            itemName="project"
+            renderItem={this.renderItem}
+            filters={this.filters}
           />
-        </Modal>
-        <ConfirmationDialog
-          visible={this.state.confirmationDialog.visible}
-          message={this.state.confirmationDialog.message}
-          onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
-          onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
-        />
+        </div>
       </div>
     );
   }

+ 1 - 21
src/components/ProjectList/ProjectList.scss

@@ -106,13 +106,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 .container {
   margin: 0 auto;
   padding: 0;
-  max-width: $max-content-width;
+  max-width: $max-content-widt9h;
   height: 100%;
   display: flex;
   flex-direction: column;
   :global(.items-list) {
     :global(.item) {
-      position: relative;
       padding-right: 16px;
       span:global(.cell):nth-child(2) {
         flex: 3;
@@ -127,18 +126,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
       span:global(.cell):nth-child(5) {
         flex: 3;
       }
-      :global(.checkbox-container) {
-        position: absolute;
-        left: -32px;
-        top: 21px;
-        opacity: 0;
-        transition: opacity $animation-swift-out;
-      }
-      &:hover, &:global(.selected) {
-        :global(.checkbox-container) {
-          opacity: 1;
-        }
-      }
       .description {
         display: block;
         color: $gray-dark;
@@ -164,12 +151,5 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
       }
     }
   }
-  .chevronRight {
-    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOHB4IiBoZWlnaHQ9IjEzcHgiIHZpZXdCb3g9IjQgMiA4IDEzIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwb2x5Z29uIGlkPSJDb21iaW5lZC1TaGFwZSIgc3Ryb2tlPSJub25lIiBmaWxsPSIjNjE2NzcwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDguMDAwMDAwLCA4LjM2Mzk2MSkgcm90YXRlKC05MC4wMDAwMDApIHRyYW5zbGF0ZSgtOC4wMDAwMDAsIC04LjM2Mzk2MSkgIiBwb2ludHM9IjggMTAuMzgyMzM3NiAyLjUwMjYwNzAyIDUgMS44MTU0MzI4OSA1LjY3Mjc5MjIxIDggMTEuNzI3OTIyMSAxNC4xODQ1NjcxIDUuNjcyNzkyMjEgMTMuNDk3MzkzIDUgOCAxMC4zODIzMzc2Ij48L3BvbHlnb24+PC9zdmc+');
-    width: 8px;
-    height: 13px;
-    display: inline-block;
-    margin: 0 16px;
-  }
 
 }

+ 11 - 9
src/components/ProjectsDropdown/ProjectsDropdown.js

@@ -40,18 +40,20 @@ class ProjectsDropdown extends Reflux.Component {
   }
 
   render() {
-    let projects = this.state.currentUser.projects.map(project => {
-      return { label: project.name, value: project.id }
-    })
-
+    let projects = []
     let currentProject = null
-    if (Reflux.GlobalState.userStore.currentUser.project) {
-      currentProject = {
-        label: Reflux.GlobalState.userStore.currentUser.project.name,
-        value: Reflux.GlobalState.userStore.currentUser.project.id
+    if (this.state.currentUser.projects) {
+      projects = this.state.currentUser.projects.map(project => {
+        return {label: project.name, value: project.id}
+      })
+
+      if (Reflux.GlobalState.userStore.currentUser.project) {
+        currentProject = {
+          label: Reflux.GlobalState.userStore.currentUser.project.name,
+          value: Reflux.GlobalState.userStore.currentUser.project.id
+        }
       }
     }
-
     return <div className={s.root}>
       <Dropdown
         options={projects}

+ 189 - 0
src/components/ReplicaDetail/ReplicaDetail.js

@@ -0,0 +1,189 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { Component, PropTypes } from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './ReplicaDetail.scss';
+import Moment from 'react-moment';
+import Helper from "../Helper";
+import EndpointLink from '../EndpointLink';
+import ConfirmationDialog from '../ConfirmationDialog'
+import MigrationActions from '../../actions/MigrationActions';
+import MigrationNetworks from '../MigrationNetworks';
+
+const title = 'Migration details';
+
+class MigrationDetail extends Component {
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired,
+  };
+
+  static propTypes = {
+    replica: PropTypes.object
+  }
+
+  constructor(props) {
+    super(props)
+    this.state = {
+      confirmationDialog: {
+        visible: false,
+        message: "Are you sure?",
+        onConfirm: null,
+        onCancel: null
+      }
+    }
+  }
+
+  componentWillMount() {
+    this.context.onSetTitle(title);
+  }
+
+  createMigrationFromReplica(e, replica) {
+    MigrationActions.createMigrationFromReplica(replica)
+  }
+
+  deleteMigration() {
+    this.setState({
+      confirmationDialog: {
+        visible: true,
+        onConfirm: () => {
+          this.setState({ confirmationDialog: { visible: false }})
+          let item = this.state.migrations.filter(migration => migration.id == this.props.replicaId)[0]
+          MigrationActions.deleteMigration(item)
+          Location.push('/cloud-endpoints')
+        },
+        onCancel: () => {
+          this.setState({ confirmationDialog: { visible: false }})
+        }
+      }
+    })
+  }
+
+  render() {
+    let item = this.props.replica
+    let output = null
+    if (item) {
+      let disabled = false
+      if (item.type == "replica") {
+        disabled = item.executions && item.executions.length &&
+          item.executions[item.executions.length - 1].status != "COMPLETED"
+        if (item.executions.length == 0) {
+          disabled = true
+        }
+      }
+
+      let createdAt = Helper.getTimeObject(item.created_at)
+
+      output = (
+        <div className={s.root}>
+          <div className={s.container}>
+            <div className={s.columnLeft}>
+              <div className={s.formGroup}>
+                <div className={s.title}>
+                  Source
+                </div>
+                <div className={s.value}>
+                  <EndpointLink connectionId={item.origin_endpoint_id} />
+                </div>
+                <div className={s.cloudImg + " icon large-cloud " + item.origin_endpoint_type + " dim"}></div>
+                <div className="arrow large"></div>
+              </div>
+              <div className={s.formGroup}>
+                <div className={s.title}>
+                  Type
+                </div>
+                <div className={s.value}>
+                  {item.migrationType == "replica" ? "Coriolis Replica" : "Coriolis Migration"}
+                </div>
+              </div>
+              <div className={s.formGroup}>
+                <div className={s.title}>
+                  Notes
+                </div>
+                <div className={s.value}>
+                  {item.notes}
+                </div>
+              </div>
+            </div>
+            <div className={s.columnRight}>
+              <div className={s.formGroup}>
+                <div className={s.title}>
+                  Target
+                </div>
+                <div className={s.value}>
+                  <EndpointLink connectionId={item.destination_endpoint_id} />
+                </div>
+                <div className={s.cloudImg + " icon large-cloud " + item.destination_endpoint_type + " dim"}></div>
+              </div>
+              <div className={s.formGroup}>
+                <div className={s.title}>
+                  Created
+                </div>
+                <div className={s.value}>
+                  <Moment format="MM/DD/YYYY HH:mm" date={createdAt} />
+                </div>
+              </div>
+              <div className={s.formGroup}>
+                <div className={s.titleIp}>
+                  Id
+                </div>
+                <div className={s.value}>
+                  <a>{item.id}</a>
+                </div>
+              </div>
+              {/*<div className={s.formGroup}>
+               <div className={s.title}>
+               Flavours
+               </div>
+               <div className={s.value}>
+               {item.autoFlavors ? "Automatic flavour selection" : "Manual flavour selection"}
+               </div>
+               </div>*/}
+              {/*<div className={s.formGroup}>
+               <div className={s.title}>
+               Disk Format
+               </div>
+               <div className={s.value}>
+               {item.diskFormat}
+               </div>
+               </div>*/}
+            </div>
+          </div>
+          <MigrationNetworks migration={item} />
+          <div className={s.container + " " + s.buttons}>
+            { item.type == "replica" && <button
+              onClick={(e) => this.createMigrationFromReplica(e, item)}
+              disabled={disabled} className={disabled ? "disabled": ""} >
+              Migrate Replica
+            </button>}
+            <button className="wire" onClick={(e) => this.deleteMigration(e)}>Delete</button>
+          </div>
+          <ConfirmationDialog
+            visible={this.state.confirmationDialog.visible}
+            message={this.state.confirmationDialog.message}
+            onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
+            onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
+          />
+        </div>
+      )
+    }
+    return output
+  }
+
+}
+
+export default withStyles(MigrationDetail, s);

+ 78 - 0
src/components/ReplicaDetail/ReplicaDetail.scss

@@ -0,0 +1,78 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  :global(.arrow) {
+    width: 64px;
+    height: 32px;
+    margin-top: 60px;
+    margin-left: 70px;
+    position: absolute;
+  }
+}
+
+.container {
+  margin: 0 auto;
+  &:after {
+    clear: both;
+    height: 0;
+    display: block;
+    content: ' ';
+  }
+  &.buttons {
+    margin-top: 16px;
+    button:first-child {
+      float: left;
+    }
+    button:nth-child(2) {
+      float: right;
+    }
+  }
+}
+.columnLeft, .columnRight {
+  width: 50%;
+  float: left;
+}
+.formGroup {
+  margin-bottom: 32px;
+  .title {
+    font-weight: $weight-semibold;
+    font-size: 10px;
+    color: $gray-dark;
+    text-transform: uppercase;
+    margin-bottom: 8px;
+  }
+  .titleIp {
+    font-weight: $weight-semibold;
+    font-size: 14px;
+    color: $gray;
+  }
+  .value {
+    font-weight: $weight-regular;
+    font-size: 14px;
+    color: $black;
+    a {
+      color: $blue;
+    }
+  }
+}
+.cloudImg {
+  width: 96px;
+  margin-top: 16px;
+}

+ 6 - 0
src/components/ReplicaDetail/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ReplicaDetail",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ReplicaDetail.js"
+}

+ 91 - 52
src/components/ReplicaExecutions/ReplicaExecutions.js

@@ -18,7 +18,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React, { Component, PropTypes } from 'react';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import s from './ReplicaExecutions.scss';
-import Dropdown from '../NewDropdown';
+import Helper from '../Helper';
 import LoadingIcon from '../LoadingIcon';
 import moment from 'moment';
 import MigrationActions from '../../actions/MigrationActions';
@@ -36,7 +36,7 @@ class ReplicaExecutions extends Component {
   };
 
   static propTypes = {
-    migration: PropTypes.object
+    replica: PropTypes.object
   }
 
   constructor(props) {
@@ -70,24 +70,29 @@ class ReplicaExecutions extends Component {
     clearInterval(this.timeout)
   }
 
-  componentWillReceiveProps(newProps, oldProps) {
-    if (newProps.migration && newProps.migration.executions.length) {
-      let execution = newProps.migration.executions[newProps.migration.executions.length - 1]
+  componentWillReceiveProps(newProps) {
+    if (newProps.replica && newProps.replica.executions.length) {
+      let execution = newProps.replica.executions[newProps.replica.executions.length - 1]
       this.setState({
         executionRef: execution,
         tasks: execution.tasks
       })
+    } else if (newProps.replica.executions.length == 0) {
+      this.setState({
+        executionRef: null,
+        tasks: null
+      })
     }
   }
 
   executeNow() {
-    MigrationActions.executeReplica(this.props.migration)
+    MigrationActions.executeReplica(this.props.replica)
     clearInterval(this.timeout)
     this.timeout = setInterval((e) => this.pollTasks(e), tasksPollTimeout)
   }
 
   cancelExecution() {
-    MigrationActions.cancelMigration(this.props.migration, (replica, response) => {
+    MigrationActions.cancelMigration(this.props.replica, (replica, response) => {
       this.refreshExecution()
     })
   }
@@ -98,7 +103,17 @@ class ReplicaExecutions extends Component {
         visible: true,
         onConfirm: () => {
           this.setState({ confirmationDialog: { visible: false }})
-          MigrationActions.deleteReplicaExecution(this.props.migration, this.state.executionRef.id)
+          let index = this.props.replica.executions.indexOf(this.state.executionRef)
+
+          MigrationActions.deleteReplicaExecution(this.props.replica, this.state.executionRef.id, () => {
+            if (this.props.replica.executions[index - 1]) {
+              this.changeExecution(this.props.replica.executions[index - 1])
+            } else if (this.props.replica.executions[index + 1]) {
+              this.changeExecution(this.props.replica.executions[index + 1])
+            } else {
+              this.changeExecution(null)
+            }
+          })
         },
         onCancel: () => {
           this.setState({ confirmationDialog: { visible: false }})
@@ -108,7 +123,7 @@ class ReplicaExecutions extends Component {
   }
 
   refreshExecution() {
-    MigrationActions.getReplicaExecutionDetail(this.props.migration, this.state.executionRef.id,
+    MigrationActions.getReplicaExecutionDetail(this.props.replica, this.state.executionRef.id,
       (replica, executionId, response) => {
         let props = this.props
         props.migration.tasks = response.data.execution.tasks
@@ -119,9 +134,9 @@ class ReplicaExecutions extends Component {
   }
 
   pollTasks() {
-    if (this.props && this.props.migration) {
-      if (this.props.migration.executions[this.props.migration.executions.length - 1].status == "RUNNING") {
-        MigrationActions.getReplicaExecutionDetail(this.props.migration, this.state.executionRef.id,
+    if (this.props && this.props.replica && this.props.replica.executions.length) {
+      if (this.props.replica.executions[this.props.replica.executions.length - 1].status == "RUNNING") {
+        MigrationActions.getReplicaExecutionDetail(this.props.replica, this.state.executionRef.id,
           (replica, executionId, response) => {
             this.setState({
               tasks: response.data.execution.tasks
@@ -132,58 +147,82 @@ class ReplicaExecutions extends Component {
   }
 
   changeExecution(execution) {
-    this.setState({
-      executionRef: execution,
-      tasks: execution.tasks
-    })
+    if (execution == null) {
+      this.setState({
+        executionRef: null,
+        tasks: null
+      })
+    } else {
+      this.setState({
+        executionRef: execution,
+        tasks: execution.tasks
+      })
+    }
   }
 
   render() {
-    if (this.props.migration) {
-      let executionBtn = <button className="wire" onClick={(e) => this.deleteExecution(e)}>Delete</button>
-      if (this.props.migration.executions &&
-        this.props.migration.executions[this.props.migration.executions.length - 1].status == "RUNNING") {
-        executionBtn = <button className="gray wire" onClick={(e) => this.cancelExecution(e)}>Cancel execution</button>
-      }
-
-      let executionsSorted = this.props.migration.executions
-      executionsSorted.sort((a, b) => a.number - b.number)
+    if (this.props.replica) {
+      if (this.props.replica.executions.length && this.state.executionRef) {
+        let executionBtn = <button className="wire" onClick={(e) => this.deleteExecution(e)}>Delete</button>
+        if (this.props.replica.executions && this.props.replica.executions[this.props.replica.executions.length - 1] &&
+          this.props.replica.executions[this.props.replica.executions.length - 1].status == "RUNNING") {
+          executionBtn =
+            <button className="gray wire" onClick={(e) => this.cancelExecution(e)}>Cancel execution</button>
+        }
 
-      return (
-        <div className={s.root}>
-          <div className={s.container}>
-            <ExecutionsTimeline
-              executions={this.props.migration.executions}
-              currentExecution={this.state.executionRef}
-              handleChangeExecution={this.changeExecution}
-            />
-            <div className={s.executionsWrapper}>
-              <div className={s.leftSide}>
-                <h4>Execution #{this.state.executionRef && this.state.executionRef.number}</h4>
-                <span className={s.date}>
-                  {this.state.executionRef && moment(this.state.executionRef.created_at).format("MMM Do YYYY HH:mm")}
-                </span>
-                <span className={"status-pill " + this.state.executionRef.status}>{this.state.executionRef.status}</span>
+        let executionsSorted = this.props.replica.executions
+
+        executionsSorted.sort((a, b) => a.number - b.number)
+        let executionTime = Helper.getTimeObject(this.state.executionRef.created_at)
+
+        return (
+          <div className={s.root}>
+            <div className={s.container}>
+              <ExecutionsTimeline
+                executions={this.props.replica.executions}
+                currentExecution={this.state.executionRef}
+                handleChangeExecution={this.changeExecution}
+              />
+              <div className={s.executionsWrapper}>
+                <div className={s.leftSide}>
+                  <h4>Execution #{this.state.executionRef && this.state.executionRef.number}</h4>
+                  <span className={s.date}>
+                    {this.state.executionRef && moment(executionTime).format("MMM Do YYYY HH:mm")}
+                  </span>
+                  <span
+                    className={"status-pill " + this.state.executionRef.status}
+                  >{this.state.executionRef.status}</span>
+                </div>
+                <div className={s.rightSide}>
+                  {executionBtn}
+                </div>
               </div>
-              <div className={s.rightSide}>
-                {executionBtn}
+              <Tasks tasks={this.state.tasks}/>
+            </div>
+            <ConfirmationDialog
+              visible={this.state.confirmationDialog.visible}
+              message={this.state.confirmationDialog.message}
+              onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
+              onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
+            />
+          </div>
+        );
+      } else {
+        return (
+          <div className={s.root}>
+            <div className={s.container}>
+              <div className="no-results">No executions for this replica <br /> <br />
+                <button onClick={(e) => this.executeNow(e)}>Execute Now</button>
               </div>
             </div>
-            <Tasks tasks={this.state.tasks}/>
           </div>
-          <ConfirmationDialog
-            visible={this.state.confirmationDialog.visible}
-            message={this.state.confirmationDialog.message}
-            onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
-            onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
-          />
-        </div>
-      );
+        )
+      }
     } else {
       return (
         <div className={s.root}>
           <div className={s.container}>
-            <LoadingIcon/>
+            <LoadingIcon />
           </div>
         </div>
       )

+ 196 - 0
src/components/ReplicaList/ReplicaList.js

@@ -0,0 +1,196 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { PropTypes } from 'react';
+import Reflux from 'reflux';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import Location from '../../core/Location';
+import UserIcon from '../UserIcon';
+import NotificationIcon from '../NotificationIcon';
+import Moment from 'react-moment';
+import s from './ReplicaList.scss';
+import MigrationStore from '../../stores/MigrationStore';
+import MigrationActions from '../../actions/MigrationActions';
+import TextTruncate from 'react-text-truncate';
+import ProjectsDropdown from '../ProjectsDropdown';
+import MainList from '../MainList';
+import Helper from '../Helper';
+
+const title = 'Coriolis Replicas';
+
+const filters = [
+  {
+    field: "status",
+    options: [
+      { value: null, label: "All" },
+      { value: "RUNNING", label: "Running" },
+      { value: "ERROR", label: "Error" },
+      { value: "COMPLETED", label: "Completed" }
+    ]
+  }
+]
+
+const replicaActions = {
+  execute_action: {
+    label: "Execute",
+    action: (item) => {
+      MigrationActions.executeReplica(item)
+    },
+    confirm: false
+  },
+  delete_action: {
+    label: "Delete",
+    action: (item) => {
+      MigrationActions.deleteReplica(item)
+    },
+    confirm: true
+  }
+}
+
+class ReplicaList extends Reflux.Component {
+  constructor(props) {
+    super(props)
+    this.store = MigrationStore;
+
+    this.state = {
+      replicas: null
+    }
+
+    this.renderItem = this.renderItem.bind(this)
+  }
+
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired
+  };
+
+  componentWillMount() {
+    super.componentWillMount.call(this)
+
+    this.context.onSetTitle(title);
+
+    MigrationActions.loadReplicas()
+  }
+
+  newMigration() {
+    Location.push('/replicas/new')
+  }
+
+  replicaDetail(e, item) {
+    Location.push('/replica/' + item.id + "/")
+  }
+
+  renderItem(item) {
+    let count = 0
+
+    if (item.executions.length) {
+      item.tasks = item.executions[item.executions.length - 1].tasks
+    }
+    if (!item.tasks) {
+      item.tasks = []
+    }
+    item.tasks.forEach((task) => {
+      if (task.status != "COMPLETED") count++
+    })
+
+    let tasksRemaining = count + " out of " + item.tasks.length
+
+    if (count == 0) {
+      tasksRemaining = "-"
+    }
+
+    let lastExecution = null
+    if (item.executions.length) {
+      lastExecution = Helper.getTimeObject(item.executions[item.executions.length - 1].created_at)
+    }
+
+    return (
+      <div className={"item " + (item.selected ? "selected" : "")} key={"replica_" + item.id}>
+        <span className="cell cell-icon" onClick={(e) => this.replicaDetail(e, item)}>
+          <div className={"icon " + item.type}></div>
+          <span className="details">
+            <TextTruncate line={1} truncateText="..." text={item.name} />
+            <span className={s.migrationStatus + " status-pill " + item.status}>{item.status}</span>
+          </span>
+        </span>
+        <span className="cell" onClick={(e) => this.replicaDetail(e, item)}>
+          <div className={s.cloudImage + " icon small-cloud " + item.origin_endpoint_type}></div>
+          <span className={s.chevronRight}></span>
+          <div className={s.cloudImage + " icon small-cloud " + item.destination_endpoint_type}></div>
+        </span>
+        <span className={"cell " + s.composite} onClick={(e) => this.replicaDetail(e, item)}>
+          <span className={s.label}>Last execution</span>
+          <span className={s.value}>
+            {lastExecution ? <Moment format="MMM Do YYYY HH:mm" date={lastExecution} /> : "-"}
+          </span>
+        </span>
+        <span className={"cell " + s.composite} onClick={(e) => this.replicaDetail(e, item)}>
+          <span className={s.label}>Tasks remaining</span>
+          <span className={s.value}>{tasksRemaining}</span>
+        </span>
+        {/*<span className={"cell " + s.composite}>
+         <span className={s.label}>Current instance</span>
+         <span className={s.value}>{this.currentInstance(item)}</span>
+         </span>*/}
+      </div>
+    )
+  }
+
+  currentInstance(migration) {
+    let instance = "N/A"
+    /*migration.vms.forEach((item) => {
+      if (item.selected) {
+        instance = item.name
+      }
+    })*/
+    return instance
+  }
+
+  refreshList() {
+    MigrationActions.loadReplicas()
+  }
+
+  render() {
+    return (
+      <div className={s.root}>
+        <div className={s.container}>
+          <div className={s.pageHeader}>
+            <div className={s.top}>
+              <h1>{title}</h1>
+              <div className={s.topActions}>
+                <ProjectsDropdown />
+                <button onClick={this.newMigration}>New</button>
+                <UserIcon />
+                <NotificationIcon />
+              </div>
+            </div>
+          </div>
+          <MainList
+            items={this.state.replicas}
+            actions={replicaActions}
+            itemName="replica"
+            renderItem={this.renderItem}
+            filters={filters}
+            refresh={this.refreshList}
+          />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default withStyles(ReplicaList, s);

+ 177 - 0
src/components/ReplicaList/ReplicaList.scss

@@ -0,0 +1,177 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  height: 100%;
+}
+.pageHeader {
+  padding: 44px 64px 0;
+  flex: 1;
+
+  h1 {
+    float: left;
+    margin: 0;
+  }
+  .top {
+    &:after {
+      content: " ";
+      clear: both;
+      display: block;
+      height: 0;
+    }
+    margin-bottom: 48px;
+  }
+  .topActions {
+    float: right;
+    margin-top: 3px;
+    :global(.Dropdown-root) {
+      margin-right: 16px;
+      float: left;
+      width: 160px;
+      :global(.Dropdown-control) {
+        text-align: left;
+      }
+    }
+
+  }
+  :global(.filters) {
+    :global(.category-filter) {
+      float: left;
+      padding: 2px 8px 2px 0;
+      margin-top: 7px;
+      border-right: 1px solid $gray-dark;
+
+    }
+    :global(.name-filter) {
+      float: left;
+      padding-left: 0px;
+      margin-top: 2px;
+    }
+    &:after {
+      clear: both;
+      content: ' ';
+      display: block;
+      height: 0;
+    }
+  }
+}
+.pageContent {
+  flex: 10;
+  overflow-y: auto;
+  padding: 0 64px;
+}
+.bulkActions {
+  float: right;
+  padding-top: 2px;
+  transition: opacity $animation-swift-out;
+  :global(.Dropdown-root) {
+    width: 160px;
+    float: left;
+  }
+  .migrationsCount {
+    float: left;
+    line-height: 32px;
+    margin-right: 16px;
+  }
+}
+.refreshBtn {
+  float: left;
+  padding: 5px 5px;
+  margin-top: 6px;
+  margin-left: 11px;
+  cursor: pointer;
+}
+.container {
+  margin: 0 auto;
+  padding: 0;
+  max-width: $max-content-width;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  :global(.items-list) {
+    :global(.item) {
+      position: relative;
+      cursor: pointer;
+      span:global(.cell):nth-child(1) {
+        flex: 3;
+      }
+      span:global(.cell):nth-child(2) {
+        flex: 1;
+        padding-left: 2%;
+        padding-right: 2%;
+      }
+      span:global(.cell):nth-child(3) {
+        flex: 1;
+        padding-right: 8px;
+        min-width: 156px;
+      }
+      span:global(.cell):nth-child(4) {
+        flex: 1;
+        min-width: 126px;
+      }
+      span:global(.cell):nth-child(5) {
+
+      }
+      :global(.checkbox-container) {
+        position: absolute;
+        left: -32px;
+        top: 21px;
+        opacity: 0;
+        transition: opacity $animation-swift-out;
+      }
+      &:hover, &:global(.selected) {
+        :global(.checkbox-container) {
+          opacity: 1;
+        }
+      }
+    }
+    .composite {
+      flex-direction: column;
+      align-items: flex-start;
+      .label {
+        display: block;
+        color: $gray-dark;
+        margin-top: 4px;
+      }
+      .value {
+        color: $blue;
+      }
+    }
+  }
+  .migrationStatus {
+    width: 96px;
+    border-radius: 4px;
+    color: #FFF;
+    line-height: 16px;
+    display: block;
+    font-size: 10px;
+    text-align: center;
+    &:global(.PAUSED) {
+      background-color: $gray;
+    }
+  }
+  .chevronRight {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOHB4IiBoZWlnaHQ9IjEzcHgiIHZpZXdCb3g9IjQgMiA4IDEzIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwb2x5Z29uIGlkPSJDb21iaW5lZC1TaGFwZSIgc3Ryb2tlPSJub25lIiBmaWxsPSIjNjE2NzcwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDguMDAwMDAwLCA4LjM2Mzk2MSkgcm90YXRlKC05MC4wMDAwMDApIHRyYW5zbGF0ZSgtOC4wMDAwMDAsIC04LjM2Mzk2MSkgIiBwb2ludHM9IjggMTAuMzgyMzM3NiAyLjUwMjYwNzAyIDUgMS44MTU0MzI4OSA1LjY3Mjc5MjIxIDggMTEuNzI3OTIyMSAxNC4xODQ1NjcxIDUuNjcyNzkyMjEgMTMuNDk3MzkzIDUgOCAxMC4zODIzMzc2Ij48L3BvbHlnb24+PC9zdmc+');
+    width: 8px;
+    height: 13px;
+    display: inline-block;
+    margin: 0 16px;
+  }
+
+}

+ 6 - 0
src/components/ReplicaList/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ReplicaList",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ReplicaList.js"
+}

+ 7 - 7
src/components/MigrationSchedule/MigrationSchedule.js → src/components/ReplicaSchedule/ReplicaSchedule.js

@@ -20,18 +20,18 @@ import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import MigrationStore from '../../stores/MigrationStore';
 import MigrationActions from '../../actions/MigrationActions';
 import ScheduleItem from '../ScheduleItem';
-import s from './MigrationSchedule.scss';
+import s from './ReplicaSchedule.scss';
 
 const title = 'Migration Schedule';
 
-class MigrationSchedule extends Component {
+class ReplicaSchedule extends Component {
 
   static contextTypes = {
     onSetTitle: PropTypes.func.isRequired,
   };
 
   static propTypes = {
-    migration: PropTypes.object,
+    replica: PropTypes.object,
     schedules: PropTypes.array
   }
 
@@ -48,7 +48,7 @@ class MigrationSchedule extends Component {
     }
 
     this.state = {
-      schedules: this.props.migration.schedules
+      schedules: this.props.replica.schedules
     }
   }
 
@@ -79,7 +79,7 @@ class MigrationSchedule extends Component {
 
   updateSchedule(schedules) {
     this.setState({ schedules: schedules })
-    MigrationActions.setMigrationProperty(this.props.migration.id, 'schedules', schedules)
+    MigrationActions.setMigrationProperty(this.props.replica.id, 'schedules', schedules)
   }
 
   render() {
@@ -102,7 +102,7 @@ class MigrationSchedule extends Component {
     }
 
     let newScheduleBtn = null
-    if (this.props.migration.migrationType == 'replica') {
+    if (this.props.replica.migrationType == 'replica') {
       newScheduleBtn = <button className={s.addScheduleBtn} onClick={(e) => this.addSchedule(e)}>Add schedule</button>
     }
 
@@ -121,4 +121,4 @@ class MigrationSchedule extends Component {
 
 }
 
-export default withStyles(MigrationSchedule, s);
+export default withStyles(ReplicaSchedule, s);

+ 0 - 0
src/components/MigrationSchedule/MigrationSchedule.scss → src/components/ReplicaSchedule/ReplicaSchedule.scss


+ 6 - 0
src/components/ReplicaSchedule/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ReplicaSchedule",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ReplicaSchedule.js"
+}

+ 184 - 0
src/components/ReplicaView/ReplicaView.js

@@ -0,0 +1,184 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { PropTypes } from 'react';
+import Reflux from 'reflux';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './ReplicaView.scss';
+import Header from '../Header';
+import Link from '../Link';
+import MigrationStore from '../../stores/MigrationStore';
+import MigrationActions from '../../actions/MigrationActions';
+import LoadingIcon from '../LoadingIcon';
+import TextTruncate from 'react-text-truncate';
+import Location from '../../core/Location';
+import ConfirmationDialog from '../ConfirmationDialog'
+
+class ReplicaView extends Reflux.Component {
+
+  static propTypes = {
+    type: PropTypes.string
+  }
+
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired,
+  };
+
+  constructor(props) {
+    super(props)
+    this.store = MigrationStore
+
+    this.state = {
+      title: 'Coriolis: View Replica',
+      confirmationDialog: {
+        visible: false,
+        message: "Are you sure?",
+        onConfirm: null,
+        onCancel: null
+      }
+    }
+  }
+
+  componentWillMount() {
+    super.componentWillMount.call(this)
+    MigrationActions.setReplica(this.props.replicaId)
+  }
+
+  componentDidMount() {
+    this.context.onSetTitle(this.state.title);
+  }
+
+  executeReplica() {
+    let item = this.state.replicas.filter(replica => replica.id == this.props.replicaId)[0]
+    MigrationActions.executeReplica(item)
+  }
+
+  goBack() {
+    Location.push('/replicas')
+  }
+
+  onMigrationActionsChange(option) {
+    let item = this.state.replicas.filter(replica => replica.id == this.props.replicaId)[0]
+    switch (option.value) {
+      case "delete":
+        MigrationActions.deleteReplica(item)
+        Location.push('/cloud-endpoints')
+        break
+      case "start":
+        MigrationActions.executeReplica(item)
+        break
+      default:
+        break
+    }
+  }
+
+  currentReplica(replicaId) {
+    if (this.state.replicas) {
+      return this.state.replicas.filter(replica => replica.id == replicaId)[0]
+    } else {
+      return null
+    }
+  }
+
+  render() {
+    let item = this.currentReplica(this.props.replicaId)
+    let title = "Edit"
+
+    if (item) {
+      title = "Edit Replica"
+
+      let disabled = item.executions.length && item.executions[item.executions.length - 1].status != "COMPLETED"
+      if (item.executions.length == 0) {
+        disabled = true
+      }
+
+      let itemStatus = item.status
+      if (item.executions.length) {
+        itemStatus = item.executions[item.executions.length - 1].status
+      }
+
+      return (
+        <div className={s.root}>
+          <Header title={title} linkUrl="/replicas" />
+          <div className={s.migrationHead}>
+            <div className={s.container}>
+              <div className="backBtn" onClick={(e) => this.goBack(e)}></div>
+              <div className={s.migrationTypeImg + ' icon ' + item.type + "-large"}></div>
+              <div className={s.migrationInfo}>
+                <h2>
+                  <TextTruncate line={1} truncateText="..." text={item.name} />
+                </h2>
+                <div className={s.migrationStats}>
+                  <span className={s.migrationType + " " + item.type}>{item.type}</span>
+                  <span className={s.migrationStatus + " " + itemStatus + " status-pill"}>{itemStatus}</span>
+                </div>
+              </div>
+              <div className={s.migrationActions}>
+                <div>
+                  <button
+                    className="gray"
+                    disabled={item.status === "RUNNING"}
+                    onClick={(e) => this.executeReplica(e)}>
+                    Execute Now
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div className={s.container}>
+            {item ? (
+              <div className={s.sidebar}>
+                <Link
+                  to={"/replica/" + item.id + "/"}
+                  className={this.props.type == 'detail' ? "active" : ""}
+                >Replica</Link>
+                <Link
+                  to={"/replica/executions/" + item.id + "/"}
+                  className={this.props.type == 'tasks' ? "active" : ""}
+                >Executions</Link>
+                <Link
+                  to={"/replica/schedule/" + item.id + "/"}
+                  className={this.props.type == 'schedule' ? "active" : ""}
+                >Schedule</Link>
+              </div>
+            ) : ""}
+
+            <div className={s.content}>
+              {React.cloneElement(this.props.children, { replica: item })}
+            </div>
+          </div>
+          <ConfirmationDialog
+            visible={this.state.confirmationDialog.visible}
+            message={this.state.confirmationDialog.message}
+            onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
+            onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
+          />
+        </div>
+      )
+    } else {
+      return (<div className={s.root}>
+        <div className={s.container}>
+          <LoadingIcon />
+        </div>
+      </div>)
+    }
+
+  }
+
+}
+
+export default withStyles(ReplicaView, s);

+ 112 - 0
src/components/ReplicaView/ReplicaView.scss

@@ -0,0 +1,112 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import '../variables.scss';
+
+$migrationHeaderBg: #D9DCE3;
+
+.root {
+  .loadingWrapper {
+    padding-top: 10%;
+  }
+}
+.migrationHead {
+  background-color: $migrationHeaderBg;
+  margin-top: 64px;
+  padding: 16px;
+  .migrationTypeImg {
+    margin-right: 64px;
+    float: left;
+  }
+  .migrationInfo {
+    float: left;
+    width: 425px;
+    .migrationStatus {
+      width: 80px;
+      border-radius: 4px;
+      color: #FFF;
+      line-height: 16px;
+      display: inline-block;
+      font-size: 10px;
+      text-align: center;
+      &:global(.PAUSED) {
+        background-color: $gray;
+      }
+    }
+    .migrationType {
+      width: 80px;
+      background-color: #FFF;
+      border-radius: 4px;
+      line-height: 14px;
+      font-size: 10px;
+      text-align: center;
+      display: inline-block;
+      text-transform: uppercase;
+      border: 1px solid $blue;
+      color: $blue;
+      margin-right: 16px;
+      &:global(.replica) {
+        border: 1px solid $color-replica;
+        color: $color-replica;
+      }
+    }
+  }
+  .migrationActions {
+    float: right;
+    margin-top: 16px;
+    :global(.Dropdown-root) {
+      width: 160px;
+      float: left;
+    }
+    button {
+      margin-left: 16px;
+    }
+  }
+  &:after {
+    clear: both;
+    content: " ";
+    display: block;
+    height: 0;
+  }
+}
+.container {
+  margin: 0 auto;
+  padding: 0;
+  max-width: $narrow-content-width;
+}
+.sidebar {
+  padding-top: 32px;
+  width: 128px;
+  float: left;
+  a {
+    display: block;
+    /* Tasks: */
+    font-weight: $weight-regular;
+    font-size: 16px;
+    color: $gray;
+    text-decoration: none;
+    margin-bottom: 16px;
+    &:global(.active) {
+      color: $blue;
+    }
+  }
+}
+.content {
+  padding-top: 32px;
+  float: left;
+  width: 800px;
+}

+ 6 - 0
src/components/ReplicaView/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ReplicaView",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ReplicaView.js"
+}

+ 3 - 2
src/components/SearchBox/SearchBox.js

@@ -25,7 +25,8 @@ class SearchBox extends Component {
     maxLines: PropTypes.number,
     minimize: PropTypes.bool,
     placeholder: PropTypes.string,
-    onChange: PropTypes.func
+    onChange: PropTypes.func,
+    className: PropTypes.string
   };
 
   static defaultProps = {
@@ -71,7 +72,7 @@ class SearchBox extends Component {
           onChange={(e) => this.onChange(e)}
           onClick={(e) => this.toggleSearch(e)}
           onBlur={(e) => this.onBlurAction(e)}
-          className={s.searchBox + " " + (this.state.isMin ? s.minimize : "") + " searchBox"}
+          className={s.searchBox + " " + (this.state.isMin ? s.minimize : "") + " searchBox " + this.props.className}
         />
       </div>
     );

+ 9 - 6
src/components/Tasks/Tasks.js

@@ -23,6 +23,7 @@ import s from './Tasks.scss';
 import TextTruncate from 'react-text-truncate';
 import LoadingIcon from "../LoadingIcon/LoadingIcon";
 import ProgressBar from '../ProgressBar';
+import Helper from '../Helper';
 
 function hasProgress(msg) {
   if (msg.indexOf('progress:') > -1) {
@@ -80,7 +81,8 @@ class Tasks extends Component {
           for (let i = item.progress_updates.length - 1; i >= 0; i--) {
             let date = "-"
             if (item.progress_updates[i]) {
-              date = moment(item.progress_updates[i].created_at).format("YYYY-MM-DD HH:mm:ss")
+              let createdAt = Helper.getTimeObject(item.progress_updates[i].created_at)
+              date = moment(createdAt).format("YYYY-MM-DD HH:mm:ss")
 
               progressUpdates.push(
                 <div key={"progress_" + i} className={first ? " first" : ""}>
@@ -134,11 +136,12 @@ class Tasks extends Component {
         let newItem = {
           task_type: (<span>
             <span className={"taskIcon " + item.status}/>
-            <TextTruncate line={1} truncateText="..." text={taskType}/>
+            <TextTruncate line={1} truncateText="..." text={taskType} />
           </span>),
-          instance: <TextTruncate line={1} text={item.instance} truncateText="..."/>,
-          latest_message: <TextTruncate line={1} truncateText="..." text={latestMessage}/>,
-          timestamp: item.updated_at ? moment(item.updated_at).format("YYYY-MM-DD HH:mm:ss") : "-",
+          instance: <TextTruncate line={1} text={item.instance} truncateText="..." />,
+          latest_message: <TextTruncate line={1} truncateText="..." text={latestMessage} />,
+          timestamp: item.updated_at ? Helper.getTimeObject(moment(item.updated_at)).format("YYYY-MM-DD HH:mm:ss") :
+            "-",
           detailView: taskDetails,
           openState: item.status === 'RUNNING'
         }
@@ -160,7 +163,7 @@ class Tasks extends Component {
               customClassName={s.table}
               show={this.state !== null}
             />
-          </div>) : <LoadingIcon/>
+          </div>) : <LoadingIcon />
         }
       </div>
     );

+ 32 - 38
src/components/WizardVms/WizardVms.js

@@ -19,11 +19,11 @@ import React, { Component, PropTypes } from 'react';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import SearchBox from '../SearchBox';
 import s from './WizardVms.scss';
-import {itemsPerPage} from '../../config';
+import { itemsPerPage } from '../../config';
 import ConnectionsActions from '../../actions/ConnectionsActions';
 import LoadingIcon from '../LoadingIcon';
 
-const title = 'Select VMs to migrate';
+const title = 'Select instances to migrate';
 const vmStatesConst = ["All", "RUNNING", "PAUSED", "STOPPED"]
 const searchTimeout = 1000;
 
@@ -41,7 +41,6 @@ class WizardVms extends Component {
   constructor(props) {
     super(props)
     let valid = false
-    let timeout = null
 
     if (this.props.data.instances) {
       this.props.data.instances.forEach((vm) => {
@@ -60,13 +59,6 @@ class WizardVms extends Component {
     }
   }
 
-  processProps(props) {
-    if (props.data.instances) {
-      this.setState({filteredData: props.data.instances.slice(
-        this.state.page * itemsPerPage, this.state.page * itemsPerPage + itemsPerPage)})
-    }
-  }
-
   componentWillMount() {
     this.context.onSetTitle(title);
     this.props.setWizardState(this.state)
@@ -75,13 +67,20 @@ class WizardVms extends Component {
   componentWillReceiveProps(newProps) {
     this.processProps(newProps)
     if (newProps.data.selectedInstances) {
-      let valid =  newProps.data.selectedInstances.length > 0
+      let valid = newProps.data.selectedInstances.length > 0
       if (this.props.data.valid != valid) {
-        this.props.setWizardState({valid: valid})
+        this.props.setWizardState({ valid: valid })
       }
     }
   }
 
+  processProps(props) {
+    if (props.data.instances) {
+      this.setState({ filteredData: props.data.instances.slice(
+        this.state.page * itemsPerPage, this.state.page * itemsPerPage + itemsPerPage) })
+    }
+  }
+
   checkVm(e, item) {
     let instances = this.props.data.instances
     instances.forEach((vm) => {
@@ -101,7 +100,7 @@ class WizardVms extends Component {
         } else {
           selectedInstances.splice(index, 1)
         }
-        this.props.setWizardState({selectedInstances: selectedInstances})
+        this.props.setWizardState({ selectedInstances: selectedInstances })
       }
     })
 
@@ -114,8 +113,7 @@ class WizardVms extends Component {
     if (this.props.data.instances) {
       this.props.data.instances.forEach((vm) => {
         if (
-          //vm.name.toLowerCase().indexOf(queryText.target.value.toLowerCase()) != -1 &&
-          (this.state.filterStatus == "All" || this.state.filterStatus == vm.status)
+          (this.state.filterStatus === "All" || this.state.filterStatus === vm.status)
         ) {
           queryResult.push(vm)
         }
@@ -127,10 +125,10 @@ class WizardVms extends Component {
         clearTimeout(this.timeout)
       }
       this.timeout = setTimeout(() => {
-        this.setState({page: 0, filteredData: null, queryText: queryText}, () => {
-          this.props.setWizardState({instances: null})
+        this.setState({ page: 0, filteredData: null, queryText: queryText }, () => {
+          this.props.setWizardState({ instances: null })
           ConnectionsActions.loadInstances(
-            {id: this.props.data.sourceCloud.credential.id},
+            { id: this.props.data.sourceCloud.credential.id },
             this.state.page,
             queryText,
             false
@@ -142,7 +140,6 @@ class WizardVms extends Component {
         filteredData: queryResult
       })
     }
-
   }
 
 
@@ -177,26 +174,26 @@ class WizardVms extends Component {
 
   nextPage() {
     if (this.state.filteredData && this.state.filteredData.length == itemsPerPage) {
-      this.setState({page: this.state.page + 1}, () => {
+      this.setState({ page: this.state.page + 1 }, () => {
         ConnectionsActions.loadInstances(
-          {id: this.props.data.sourceCloud.credential.id},
+          { id: this.props.data.sourceCloud.credential.id },
           this.state.page,
           this.state.queryText
         )
-        this.processProps({data: {instances: this.props.data.instances}})
+        this.processProps({ data: { instances: this.props.data.instances } })
       })
     }
   }
 
   previousPage() {
     if (this.state.page > 0) {
-      this.setState({page: this.state.page + -1}, () => {
+      this.setState({ page: this.state.page + -1 }, () => {
         ConnectionsActions.loadInstances(
-          {id: this.props.data.sourceCloud.credential.id},
+          { id: this.props.data.sourceCloud.credential.id },
           this.state.page,
           this.state.queryText
         )
-        this.processProps({data: {instances: this.props.data.instances}})
+        this.processProps({ data: { instances: this.props.data.instances } })
       })
     }
   }
@@ -222,9 +219,6 @@ class WizardVms extends Component {
               {item.instance_name}
             </span>
           </span>
-          {/*<span className="cell">
-            <span className={ item.status + " status-pill" }>{ item.status }</span>
-          </span>*/}
           <span className="cell">{item.num_cpu} vCPU | {item.memory_mb} MB RAM | {item.flavor_name}</span>
         </div>
        )
@@ -232,7 +226,7 @@ class WizardVms extends Component {
     } else if (this.props.data.instances && this.props.data.instances.length == 0) {
       return <div className="no-results">Your search returned no results</div>
     } else {
-      return <LoadingIcon/>
+      return <LoadingIcon padding={64} text="Loading instances.." />
     }
   }
 
@@ -253,25 +247,26 @@ class WizardVms extends Component {
             placeholder="Search VMs"
             value={this.state.queryText}
             onChange={(e) => this.searchVm(e)}
-            className="searchBox"
+            className={"searchBox" + (this.state.filteredData.length == 0 ? " hidden" : " ")}
           />
-          <div className="category-filter">
+          <div className={"category-filter" + (this.state.filteredData.length == 0 ? " hidden" : " ")}>
             {vmStates}
           </div>
-          <div className="items-list">
+          <div className="items-list instances">
             {this.renderSearch()}
           </div>
-          <div className={s.selectionCount}>
-            {this.instancesSelected()} VMs selected
+          <div className={s.selectionCount + (this.state.filteredData.length == 0 ? " hidden" : " ")}>
+            {this.instancesSelected()} instances selected
           </div>
-          <div className={s.pagination}>
+          <div className={s.pagination + (this.state.filteredData.length == 0 ? " hidden" : " ")}>
             <span
-              className={(this.state.page == 0 ? "disabled " : "")+ s.prev}
+              className={(this.state.page == 0 ? "disabled " : "") + s.prev}
               onClick={(e) => this.previousPage(e)}
             ></span>
             <span className={s.currentPage}>{this.state.page + 1}</span>
             <span
-              className={(this.state.filteredData && this.state.filteredData.length == itemsPerPage ? " " : "disabled ") + s.next}
+              className={(this.state.filteredData && this.state.filteredData.length == itemsPerPage ?
+                " " : "disabled ") + s.next}
               onClick={(e) => this.nextPage(e)}
             ></span>
           </div>
@@ -279,7 +274,6 @@ class WizardVms extends Component {
       </div>
     );
   }
-
 }
 
 export default withStyles(WizardVms, s);

+ 2 - 0
src/components/WizardVms/WizardVms.scss

@@ -30,10 +30,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   padding: 10px 0 40px;
   max-width: $max-content-width;
   :global(.items-list) {
+    margin-top: 16px;
     :global(.item) {
       position: relative;
       cursor: pointer;
       padding-left: 0;
+      width: 100%;
       span:nth-child(2) {
         flex: 2;
       }

+ 34 - 33
src/config.sample.js

@@ -20,7 +20,7 @@
 
 export const port = process.env.PORT || 3000;
 export const host = process.env.WEBSITE_HOSTNAME || `localhost:${port}`;
-export const coriolisUrl = "/"
+export const coriolisUrl = CORIOLIS_URL || "/"
 export const defaultDomain = "default";
 
 // Number of instances in wizard Migrate VMs step
@@ -45,18 +45,19 @@ export const servicesUrl = {
   endpoints: coriolisUrl + "coriolis/endpoints",
   coriolis: coriolisUrl + "coriolis",
   migrations: coriolisUrl + "coriolis/migrations",
-  barbican: coriolisUrl + "barbican"
+  barbican: coriolisUrl + "barbican",
+  openId: coriolisUrl + "identity/OS-FEDERATION/identity_providers/google/protocols/openid/auth"
 }
 
 export const providerType = {
-  "import_migration": 1,
-  "export_migration": 2,
-  "import_replica": 4,
-  "export_replica": 8,
-  "endpoint": 16
+  import_migration: 1,
+  export_migration: 2,
+  import_replica: 4,
+  export_replica: 8,
+  endpoint: 16
 }
 
-export const useSecret = false; // flag to use secret_ref for endpoints
+export const useSecret = true; // flag to use secret_ref for endpoints
 
 export const tasksPollTimeout = 5000 // milliseconds
 
@@ -77,8 +78,8 @@ export const migrationSteps = [
     component: "WizardTarget"
   },
   {
-    name: "Migrate VMs",
-    title: "Select VMs",
+    name: "Migrate instances",
+    title: "Select instances",
     component: "WizardVms"
   },
   {
@@ -127,27 +128,27 @@ export const auth = {
 };
 
 export const defaultLabels = {
-  "username": "Username",
-  "password": "Password",
-  "host": "Host",
-  "port": "Port",
-  "allow_untrusted": "Allow untrusted",
-  "identity_api_version": "Identity Version",
-  "auth_url": "Auth URL",
-  "user_domain_name": "User Domain Name",
-  "project_name": "Project Name",
-  "project_domain_name": "Project Domain Name",
-  "flavor_name": "Flavor Name",
-  "hypervisor_type": "Hypervisor Type",
-  "container_format": "Container Format",
-  "disk_format": "Disk Format",
-  "glance_upload": "Glance Upload",
-  "keypair_name": "Keypair name",
-  "fip_pool_name": "Floating IP Pool",
-  "migr_fip_pool_name": "Migration Floating IP Pool",
-  "migr_flavor_name": "Migration Flavor Name",
-  "migr_image_name": "Migration Image Name",
-  "migr_image_name_map": "Migration Image Name Map",
-  "delete_disks_on_vm_termination": "Delete disks on VM termination",
-  "set_dhcp": "Set DHCP",
+  username: "Username",
+  password: "Password",
+  host: "Host",
+  port: "Port",
+  allow_untrusted: "Allow untrusted",
+  identity_api_version: "Identity Version",
+  auth_url: "Auth URL",
+  user_domain_name: "User Domain Name",
+  project_name: "Project Name",
+  project_domain_name: "Project Domain Name",
+  flavor_name: "Flavor Name",
+  hypervisor_type: "Hypervisor Type",
+  container_format: "Container Format",
+  disk_format: "Disk Format",
+  glance_upload: "Glance Upload",
+  keypair_name: "Keypair name",
+  fip_pool_name: "Floating IP Pool",
+  migr_fip_pool_name: "Migration Floating IP Pool",
+  migr_flavor_name: "Migration Flavor Name",
+  migr_image_name: "Migration Image Name",
+  migr_image_name_map: "Migration Image Name Map",
+  delete_disks_on_vm_termination: "Delete disks on VM termination",
+  set_dhcp: "Set DHCP",
 }

+ 16 - 14
src/routes.js

@@ -18,20 +18,22 @@
 
 import React from 'react';
 import Router from 'react-routing/src/Router';
-import fetch from './core/fetch';
 import App from './components/App';
 import MigrationWizard from './components/MigrationWizard';
 import WithSidebar from './components/WithSidebar';
 import MigrationList from './components/MigrationList';
 import MigrationView from './components/MigrationView';
+import ReplicaList from './components/ReplicaList';
+import ReplicaView from './components/ReplicaView';
+import ReplicaDetail from './components/ReplicaDetail';
 import MigrationDetail from './components/MigrationDetail';
 import MigrationTasks from './components/MigrationTasks';
-import MigrationSchedule from './components/MigrationSchedule';
+import ReplicaSchedule from './components/ReplicaSchedule';
 import CloudConnection from './components/CloudConnection';
 import CloudConnectionsView from './components/CloudConnectionsView';
 import CloudConnectionDetail from './components/CloudConnectionDetail';
 import CloudConnectionAuth from './components/CloudConnectionAuth';
-import ConnectionsList from './components/ConnectionsList';
+import ConnectionsList from './components/EndpointList';
 import Project from './components/Project';
 import ProjectDetail from './components/ProjectDetail';
 import ProjectList from './components/ProjectList';
@@ -57,9 +59,9 @@ const router = new Router(on => {
 
   on('/federate/:token', async (params) => <Federate token={params.params.token} />)
 
-  on('/migrations', async () => <WithSidebar route="/migrations"><MigrationList type="migrations"/></WithSidebar>)
+  on('/migrations', async () => <WithSidebar route="/migrations"><MigrationList /></WithSidebar>)
 
-  on('/migrations/new', async () => <MigrationWizard />)
+  on('/migrations/new', async () => <MigrationWizard wizard_type="migration" />)
 
   on('/migration/:migrationId/', async (params) =>
     <MigrationView migrationId={params.params.migrationId} type="detail"><MigrationDetail /></MigrationView>
@@ -70,23 +72,23 @@ const router = new Router(on => {
   )
 
   on('/migration/schedule/:migrationId/', async (params) =>
-    <MigrationView migrationId={params.params.migrationId} type="schedule"><MigrationSchedule /></MigrationView>
+    <MigrationView migrationId={params.params.migrationId} type="schedule"><ReplicaSchedule /></MigrationView>
   )
   // TODO: IMPORTANT Separate views migration/replica
-  on('/replicas', async () => <WithSidebar route="/replicas"><MigrationList type="replicas"/></WithSidebar>)
+  on('/replicas', async () => <WithSidebar route="/replicas"><ReplicaList /></WithSidebar>)
 
-  on('/replicas/new', async () => <MigrationWizard />)
+  on('/replicas/new', async () => <MigrationWizard wizard_type="replica" />)
 
-  on('/replica/:migrationId/', async (params) =>
-    <MigrationView migrationId={params.params.migrationId} type="detail"><MigrationDetail /></MigrationView>
+  on('/replica/:replicaId/', async (params) =>
+    <ReplicaView replicaId={params.params.replicaId} type="detail"><ReplicaDetail /></ReplicaView>
   )
 
-  on('/replica/executions/:migrationId/', async (params) =>
-    <MigrationView migrationId={params.params.migrationId} type="tasks"><ReplicaExecutions /></MigrationView>
+  on('/replica/executions/:replicaId/', async (params) =>
+    <ReplicaView replicaId={params.params.replicaId} type="tasks"><ReplicaExecutions /></ReplicaView>
   )
 
-  on('/replica/schedule/:migrationId/', async (params) =>
-    <MigrationView migrationId={params.params.migrationId} type="schedule"><MigrationSchedule /></MigrationView>
+  on('/replica/schedule/:replicaId/', async (params) =>
+    <ReplicaView replicaId={params.params.replicaId} type="schedule"><ReplicaSchedule /></ReplicaView>
   )
 
   on('/cloud-endpoints', async () =>

+ 1 - 0
src/stores/ConnectionsStore/ConnectionsStore.js

@@ -210,6 +210,7 @@ class ConnectionsStore extends Reflux.Store
     this.setState({connections: connections})
     ConnectionsActions.assignConnectionProvider()
     MigrationActions.loadMigrations()
+    MigrationActions.loadReplicas()
   }
 
   onLoadConnectionDetail(connectionId) {

+ 112 - 60
src/stores/MigrationStore/MigrationStore.js

@@ -32,62 +32,75 @@ class MigrationStore extends Reflux.Store
 
     this.state = {
       migrations: null,
+      replicas: null,
       migration: null,
-      migrationsLoaded: false,
-      replicasLoaded: false,
-      queryInProgress: false
+      replica: null
     }
   }
 
   onLoadMigrations() {
-    this.setState({ migrationsLoaded: false, replicasLoaded: false, queryInProgress: true })
+    //this.setState({ migrations: null })
   }
 
   onLoadMigrationsCompleted(response) {
     let connections = Reflux.GlobalState.connectionStore.connections
-    // if none are loaded yet, clear the collection
-    if (!this.state.replicasLoaded && !this.state.migrationsLoaded) {
-      this.setState({ migrations: [] })
-    }
+    this.setState({ migrations: [] })
 
-    if (typeof response.data.replicas !== "undefined") {
-      this.setState({ replicasLoaded: response.data.replicas})
-    }
-    if (typeof response.data.migrations !== "undefined") {
-      this.setState({ migrationsLoaded: response.data.migrations})
-    }
-    if (this.state.migrationsLoaded && this.state.replicasLoaded) {
-      let allMigrations = this.state.migrationsLoaded.concat(this.state.replicasLoaded)
-      allMigrations.forEach(migration => {
-        let connection = connections.filter(connection => connection.id == migration.destination_endpoint_id)[0]
-        if (connection) {
-          migration.destination_endpoint_type = connection.type
-        }
-        connection = connections.filter(connection => connection.id == migration.origin_endpoint_id)[0]
-        if (connection) {
-          migration.origin_endpoint_type = connection.type
-        }
+    let migrations = response.data.migrations
+    migrations.forEach(migration => {
+      let connection = connections.filter(connection => connection.id == migration.destination_endpoint_id)[0]
+      if (connection) {
+        migration.destination_endpoint_type = connection.type
+      }
+      connection = connections.filter(connection => connection.id == migration.origin_endpoint_id)[0]
+      if (connection) {
+        migration.origin_endpoint_type = connection.type
+      }
 
-        migration.name = migration.instances.join(", ");
+      migration.name = migration.instances.join(", ");
+    })
 
-        if (migration.type == 'replica') {
-          if (migration.executions.length) {
-            MigrationActions.getReplicaExecutions(migration)
-          }
-        }
-      })
+    migrations.sort((a, b) => {
+      return moment(b.created_at).isAfter(moment(a.created_at))
+    })
 
-      allMigrations.sort((a, b) => {
-        return moment(b.created_at).isAfter(moment(a.created_at))
-      })
+    this.setState({
+      migrations: migrations
+    })
+  }
 
-      this.setState({
-        migrations: allMigrations,
-        migrationsLoaded: false,
-        replicasLoaded: false,
-        queryInProgress: false
-      })
-    }
+  onLoadReplicas() {
+    //this.setState({ replicas: null })
+  }
+
+  onLoadReplicasCompleted(response) {
+    let connections = Reflux.GlobalState.connectionStore.connections
+
+    let replicas = response.data.replicas
+    replicas.forEach(replica => {
+      let connection = connections.filter(connection => connection.id == replica.destination_endpoint_id)[0]
+      if (connection) {
+        replica.destination_endpoint_type = connection.type
+      }
+      connection = connections.filter(connection => connection.id == replica.origin_endpoint_id)[0]
+      if (connection) {
+        replica.origin_endpoint_type = connection.type
+      }
+
+      replica.name = replica.instances.join(", ");
+
+      if (replica.executions.length) {
+        MigrationActions.getReplicaExecutions(replica)
+      }
+    })
+
+    replicas.sort((a, b) => {
+      return moment(b.created_at).isAfter(moment(a.created_at))
+    })
+
+    this.setState({
+      replicas: replicas,
+    })
   }
 
   onLoadMigrationCompleted(response) {
@@ -114,33 +127,58 @@ class MigrationStore extends Reflux.Store
   }
 
   onGetReplicaExecutionsCompleted(replica, response) {
-    let migrations = this.state.migrations
-    migrations.forEach((migration, index) => {
-      if (migration.id == replica.id) {
-        migration.executions = response.data.executions.sort((a, b) => a.number - b.number)
-        migration.tasks = migration.executions[migration.executions.length - 1].tasks
-        migration.status = migration.executions[migration.executions.length - 1].status
-        migration.executions.forEach(execution => {
+    let replicas = this.state.replicas
+    replicas.forEach((item) => {
+      if (item.id == replica.id) {
+        item.executions = response.data.executions.sort((a, b) => a.number - b.number)
+        item.tasks = item.executions[item.executions.length - 1].tasks
+        item.status = item.executions[item.executions.length - 1].status
+        item.executions.forEach(execution => {
           //MigrationActions.getReplicaExecutionDetail(replica, execution.id)
         })
       }
     })
-    this.setState({ migrations: migrations })
+    this.setState({ replicas: replicas })
   }
 
   onGetReplicaExecutionDetailCompleted(replica, executionId, response) {
-    let replicas = this.state.migrations
+    let replicas = this.state.replicas
     let index = replicas.indexOf(replica)
     replicas[index].executions.forEach((execution, execIndex) => {
       if (execution.id == executionId) {
         replicas[index].executions[execIndex] = response.data.execution
       }
       replicas[index].tasks = replica.executions[replica.executions.length - 1].tasks
+      replicas[index].status = replica.executions[replica.executions.length - 1].status
     })
-    this.setState({ migrations: replicas })
+    this.setState({ replicas: replicas })
 
   }
 
+  onDeleteReplicaExecutionCompleted(replica, executionId) {
+    let replicas = this.state.replicas
+    let index = replicas.indexOf(replica)
+    let execIndex = -1
+    replicas[index].executions.forEach((execution, i) => {
+      if (execution.id == executionId) {
+        execIndex = i
+      }
+    })
+    console.log(replicas[index].executions)
+    replicas[index].executions.splice(execIndex, 1)
+    console.log(replicas[index].executions)
+    if (replicas[index].executions[replicas[index].executions]) {
+      replicas[index].tasks = replicas[index].executions[replicas[index].executions.length - 1].tasks
+      replicas[index].status = replicas[index].executions[replicas[index].executions.length - 1].status
+    } else {
+      replicas[index].tasks = []
+      replicas[index].status = null
+    }
+    console.log(replicas[index])
+
+    this.setState({ replicas: replicas })
+  }
+
   onGetReplicaExecutionDetailFailed(response) {
   }
 
@@ -184,16 +222,16 @@ class MigrationStore extends Reflux.Store
   }
 
   onExecuteReplicaCompleted(replica, response) {
-    let migrations = this.state.migrations
-    migrations.forEach((migration, index) => {
-      if (migration.id == replica.id) {
-        migrations[index].executions.push(response.data.execution)
-        migrations[index].tasks = migration.executions[migration.executions.length - 1].tasks
-        migrations[index].status = migration.executions[migration.executions.length - 1].status
+    let replicas = this.state.replicas
+    replicas.forEach((item, index) => {
+      if (item.id == replica.id) {
+        replicas[index].executions.push(response.data.execution)
+        replicas[index].tasks = item.executions[item.executions.length - 1].tasks
+        replicas[index].status = item.executions[item.executions.length - 1].status
       }
     })
 
-    this.setState({ migrations: migrations })
+    this.setState({ migrations: replicas })
 
   }
 
@@ -211,6 +249,20 @@ class MigrationStore extends Reflux.Store
     }
   }
 
+  onSetReplica(replicaId) {
+    let replica = null
+    if (this.state.replicas) {
+      this.state.replicas.forEach(function(item) {
+        if (item.id == replicaId) {
+          replica = item
+        }
+      })
+    }
+    if (replica) {
+      this.setState({replica: replica})
+    }
+  }
+
   onGetMigration(migrationId)
   {
     let migration = null
@@ -224,7 +276,7 @@ class MigrationStore extends Reflux.Store
     return migration
   }
 
-  onSetMigrationProperty(migrationId, property, value) {migara
+  onSetMigrationProperty(migrationId, property, value) {
     this.state.migrations.forEach(function(item) {
       if (item.id == migrationId) {
         item[property] = value

+ 44 - 25
src/stores/NotificationsStore/NotificationsStore.js

@@ -41,7 +41,7 @@ class NotificationsStore extends Reflux.Store
       message: "Signed in",
       type: 'success'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onTokenLoginFailed(response) {
@@ -51,7 +51,7 @@ class NotificationsStore extends Reflux.Store
         type: 'error'
       }
     ]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onLogout(response) {
@@ -61,7 +61,7 @@ class NotificationsStore extends Reflux.Store
         type: 'info'
       }
     ]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onSaveEndpoint(response) {
@@ -71,7 +71,7 @@ class NotificationsStore extends Reflux.Store
       type: 'success',
       keep: true
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onAddMigrationCompleted(response) {
@@ -105,7 +105,7 @@ class NotificationsStore extends Reflux.Store
       }]
     }
 
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onDeleteConnection() {
@@ -113,44 +113,55 @@ class NotificationsStore extends Reflux.Store
       message: "Connection deleted successfully",
       type: 'success'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onDeleteMigrationCompleted() {
     let notifications = [{
-      message: "Item deleted successfully",
+      message: "Migration deleted successfully",
       type: 'success'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
-  onLoginFailed(response) {
+  onDeleteMigrationFailed() {
     let notifications = [{
-      message: "Login failed",
+      message: "Could not delete migration",
       type: 'error'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
-  /*onLoadMigrationCompleted(response) {
+  onDeleteReplicaCompleted() {
     let notifications = [{
-      message: "sadaf",
-      type: 'success',
-      hideDelay: 10000,
-      callback: () => {
-        console.log(response.data.migration.id)
-      }
+      message: "Replica deleted successfully",
+      type: 'success'
     }]
-    this.setState({notifications: notifications})
-  }*/
+    this.setState({ notifications: notifications })
+  }
 
+  onDeleteReplicaFailed() {
+    let notifications = [{
+      message: "Could not delete replica",
+      type: 'error'
+    }]
+    this.setState({ notifications: notifications })
+  }
+
+  onLoginFailed(response) {
+    let notifications = [{
+      message: "Login failed",
+      type: 'error'
+    }]
+    this.setState({ notifications: notifications })
+  }
 
   onExecuteReplicaCompleted() {
     let notifications = [{
       message: "Executing replica",
       type: 'info'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onDeleteReplicaExecutionCompleted() {
@@ -158,7 +169,7 @@ class NotificationsStore extends Reflux.Store
       message: "Execution deleted",
       type: 'info'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onCancelMigrationCompleted(migration) {
@@ -170,7 +181,7 @@ class NotificationsStore extends Reflux.Store
       message: message,
       type: 'success'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onCreateMigrationFromReplicaCompleted(response) {
@@ -187,7 +198,15 @@ class NotificationsStore extends Reflux.Store
         }
       }
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
+  }
+
+  onLoadInstancesFailed() {
+    let notifications = [{
+      message: "Could not load instances.",
+      type: 'error'
+    }]
+    this.setState({ notifications: notifications })
   }
 
   onNotify(message, type = "info", title = null, callback = null) {
@@ -223,7 +242,7 @@ class NotificationsStore extends Reflux.Store
       message: "Switching project",
       type: 'info'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
 }

+ 6 - 5
src/stores/UserStore/UserStore.js

@@ -22,6 +22,7 @@ import ConnectionsActions from '../../actions/ConnectionsActions'
 import Location from '../../core/Location';
 import Api from '../../components/ApiCaller';
 import cookie from 'react-cookie';
+import moment from 'moment';
 import { servicesUrl } from '../../config'
 
 class UserStore extends Reflux.Store
@@ -33,7 +34,7 @@ class UserStore extends Reflux.Store
     created: new Date(),
     project: {},
     roles: [],
-    projects: [],
+    projects: null,
     token: null,
     settings: {
       notifications: true
@@ -65,7 +66,7 @@ class UserStore extends Reflux.Store
   onLoginSuccess(response) {
     let token = response.headers['X-Subject-Token']
     Api.setDefaultHeader('X-Auth-Token', token)
-    cookie.save('unscopedToken', token, { path: "/" })
+    cookie.save('unscopedToken', token, { path: "/", expires: moment().add(1, 'hour').toDate() })
 
     UserActions.getScopedProjects(response => {
       if (response.data.projects) {
@@ -89,8 +90,8 @@ class UserStore extends Reflux.Store
     currentUser.token = response.headers['X-Subject-Token']
     currentUser.project = response.data.token.project
 
-    cookie.save('token', currentUser.token, { path: "/" })
-    cookie.save('projectId', currentUser.project.id, { path: "/" })
+    cookie.save('token', currentUser.token, { path: "/", expires: moment().add(1, 'hour').toDate() })
+    cookie.save('projectId', currentUser.project.id, { path: "/", expires: moment().add(1, 'months').toDate() })
     Api.setDefaultHeader('X-Auth-Token', currentUser.token)
 
     this.setState({currentUser: currentUser})
@@ -172,7 +173,7 @@ class UserStore extends Reflux.Store
 
   onFederateToken(token) {
     Api.setDefaultHeader('X-Auth-Token', token)
-    cookie.save('unscopedToken', token, { path: "/" })
+    cookie.save('unscopedToken', token, { path: "/", expires: moment().add(1, 'hour').toDate() })
     UserActions.getScopedProjects(response => {
       if (response.data.projects) {
         UserActions.loginScope(token, response.data.projects[0].id)

+ 4 - 0
src/stores/WizardStore/WizardStore.js

@@ -104,6 +104,10 @@ class UsersStore extends Reflux.Store
     this.setState({ instances: instances })
   }
 
+  onLoadInstancesFailed() {
+    this.setState({ instances: [] })
+  }
+
   onNewState() {
     this.setState(this.blankState)
   }

+ 7 - 2
tools/webpack.config.js

@@ -125,7 +125,11 @@ const clientConfig = extend(true, {}, config, {
   devtool: DEBUG ? 'cheap-module-eval-source-map' : false,
   plugins: [
     ...config.plugins,
-    new webpack.DefinePlugin({ ...GLOBALS, 'process.env.BROWSER': true }),
+    new webpack.DefinePlugin({
+      ...GLOBALS,
+      'process.env.BROWSER': true,
+      CORIOLIS_URL: JSON.stringify(process.env.CORIOLIS_URL)
+    }),
     new AssetsPlugin({
       path: path.join(__dirname, '../build'),
       filename: 'assets.js',
@@ -180,7 +184,8 @@ const serverConfig = extend(true, {}, config, {
   devtool: 'source-map',
   plugins: [
     ...config.plugins,
-    new webpack.DefinePlugin({ ...GLOBALS, 'process.env.BROWSER': false }),
+    new webpack.DefinePlugin({ ...GLOBALS, 'process.env.BROWSER': false,
+      CORIOLIS_URL: JSON.stringify(process.env.CORIOLIS_URL) }),
     new webpack.BannerPlugin('require("source-map-support").install();',
       { raw: true, entryOnly: false }),
   ],