Przeglądaj źródła

- 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

George Vrancianu 8 lat temu
rodzic
commit
14b9f2d1e7
29 zmienionych plików z 1416 dodań i 632 usunięć
  1. 1 0
      package.json
  2. 62 41
      src/actions/MigrationActions/MigrationActions.js
  3. 1 1
      src/components/LoginPage/LoginPage.scss
  4. 1 1
      src/components/MainList/MainList.scss
  5. 89 354
      src/components/MigrationList/MigrationList.js
  6. 5 5
      src/components/MigrationList/MigrationList.scss
  7. 0 6
      src/components/MigrationSchedule/package.json
  8. 23 64
      src/components/MigrationView/MigrationView.js
  9. 19 1
      src/components/MigrationWizard/MigrationWizard.js
  10. 187 0
      src/components/ReplicaDetail/ReplicaDetail.js
  11. 78 0
      src/components/ReplicaDetail/ReplicaDetail.scss
  12. 6 0
      src/components/ReplicaDetail/package.json
  13. 87 51
      src/components/ReplicaExecutions/ReplicaExecutions.js
  14. 190 0
      src/components/ReplicaList/ReplicaList.js
  15. 177 0
      src/components/ReplicaList/ReplicaList.scss
  16. 6 0
      src/components/ReplicaList/package.json
  17. 7 7
      src/components/ReplicaSchedule/ReplicaSchedule.js
  18. 0 0
      src/components/ReplicaSchedule/ReplicaSchedule.scss
  19. 6 0
      src/components/ReplicaSchedule/package.json
  20. 184 0
      src/components/ReplicaView/ReplicaView.js
  21. 112 0
      src/components/ReplicaView/ReplicaView.scss
  22. 6 0
      src/components/ReplicaView/package.json
  23. 1 1
      src/components/WizardVms/WizardVms.js
  24. 2 0
      src/components/WizardVms/WizardVms.scss
  25. 16 14
      src/routes.js
  26. 1 0
      src/stores/ConnectionsStore/ConnectionsStore.js
  27. 112 60
      src/stores/MigrationStore/MigrationStore.js
  28. 36 25
      src/stores/NotificationsStore/NotificationsStore.js
  29. 1 1
      src/stores/UserStore/UserStore.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'

+ 1 - 1
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;
       }

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

@@ -37,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 {

+ 89 - 354
src/components/MigrationList/MigrationList.js

@@ -19,343 +19,139 @@ 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';
+
+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
+    let tasksRemaining = count + " out of " + item.tasks.length
 
-    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 })
-  }
-
-  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 } })
-    })
-  }
 
-  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: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>
     )
   }
 
-  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 +160,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")
+        }
       }
     }
   }

+ 187 - 0
src/components/ReplicaDetail/ReplicaDetail.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, { Component, PropTypes } from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './ReplicaDetail.scss';
+import Moment from 'react-moment';
+import LoadingIcon from "../LoadingIcon";
+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
+        }
+      }
+
+      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={item.created} />
+                </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"
+}

+ 87 - 51
src/components/ReplicaExecutions/ReplicaExecutions.js

@@ -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,79 @@ 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)
+
+        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(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>
+                </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>
       )

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

@@ -0,0 +1,190 @@
+/*
+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';
+
+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 = "-"
+    }
+
+    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}>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.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"
+}

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

@@ -258,7 +258,7 @@ class WizardVms extends Component {
           <div className="category-filter">
             {vmStates}
           </div>
-          <div className="items-list">
+          <div className="items-list instances">
             {this.renderSearch()}
           </div>
           <div className={s.selectionCount}>

+ 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;
       }

+ 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

+ 36 - 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 })
+  }
+
+  onDeleteReplicaFailed() {
+    let notifications = [{
+      message: "Could not delete replica",
+      type: 'error'
     }]
-    this.setState({notifications: notifications})
-  }*/
+    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,7 @@ class NotificationsStore extends Reflux.Store
         }
       }
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
   onNotify(message, type = "info", title = null, callback = null) {
@@ -223,7 +234,7 @@ class NotificationsStore extends Reflux.Store
       message: "Switching project",
       type: 'info'
     }]
-    this.setState({notifications: notifications})
+    this.setState({ notifications: notifications })
   }
 
 }

+ 1 - 1
src/stores/UserStore/UserStore.js

@@ -33,7 +33,7 @@ class UserStore extends Reflux.Store
     created: new Date(),
     project: {},
     roles: [],
-    projects: [],
+    projects: null,
     token: null,
     settings: {
       notifications: true