Jelajahi Sumber

Replace alt.js with MobX for state management

MobX makes state management much more easier, going a step further than
alt.js (which is based on Flux), using observable patterns with ES7
decorators.
While alt.js got rid of mananging reducers and action constants, MobX
uses ES7 decorators to declare observable variables and actions in
stores.
Actions and state can be easily kept and maintained in the same file.
React components can listen to store's observable variables by using a
simple ES7 decorator.

As a result, the codebase is now smaller and faster.

More info on MobX can be found here:
https://mobx.js.org/getting-started.html
Sergiu Miclea 8 tahun lalu
induk
melakukan
7c2332e6d3
73 mengubah file dengan 1342 tambahan dan 2666 penghapusan
  1. 2 1
      .babelrc
  2. 1 0
      .eslintrc
  3. 1 0
      .flowconfig
  4. 4 0
      flow-typed/module_vx.x.x.js
  5. 3 2
      package.json
  6. 0 133
      src/actions/EndpointActions.js
  7. 0 143
      src/actions/InstanceActions.js
  8. 0 113
      src/actions/MigrationActions.js
  9. 0 47
      src/actions/NetworkActions.js
  10. 0 76
      src/actions/NotificationActions.js
  11. 0 76
      src/actions/ProviderActions.js
  12. 0 200
      src/actions/ReplicaActions.js
  13. 0 113
      src/actions/ScheduleActions.js
  14. 0 135
      src/actions/UserActions.js
  15. 0 107
      src/actions/WizardActions.js
  16. 2 2
      src/components/App.jsx
  17. 3 3
      src/components/atoms/CopyValue/index.jsx
  18. 2 0
      src/components/molecules/EndpointListItem/index.jsx
  19. 2 0
      src/components/molecules/MainListItem/index.jsx
  20. 1 1
      src/components/molecules/NotificationDropdown/index.jsx
  21. 2 2
      src/components/molecules/ScheduleItem/index.jsx
  22. 3 3
      src/components/molecules/TaskItem/index.jsx
  23. 3 2
      src/components/molecules/Timeline/index.jsx
  24. 1 1
      src/components/molecules/UserDropdown/index.jsx
  25. 2 1
      src/components/organisms/ChooseProvider/index.jsx
  26. 8 18
      src/components/organisms/DetailsPageHeader/index.jsx
  27. 80 67
      src/components/organisms/Endpoint/index.jsx
  28. 1 1
      src/components/organisms/EndpointDetailsContent/index.jsx
  29. 5 4
      src/components/organisms/EndpointValidation/index.jsx
  30. 10 10
      src/components/organisms/Executions/index.jsx
  31. 2 2
      src/components/organisms/LoginForm/index.jsx
  32. 18 14
      src/components/organisms/MainDetails/index.jsx
  33. 3 3
      src/components/organisms/MigrationDetailsContent/index.jsx
  34. 10 13
      src/components/organisms/Notifications/index.jsx
  35. 19 37
      src/components/organisms/PageHeader/index.jsx
  36. 13 10
      src/components/organisms/ReplicaDetailsContent/index.jsx
  37. 16 6
      src/components/organisms/Schedule/index.jsx
  38. 2 2
      src/components/organisms/WizardEndpointList/index.jsx
  39. 1 1
      src/components/organisms/WizardInstances/index.jsx
  40. 2 2
      src/components/organisms/WizardNetworks/index.jsx
  41. 3 3
      src/components/organisms/WizardOptions/index.jsx
  42. 29 14
      src/components/organisms/WizardPageContent/index.jsx
  43. 12 8
      src/components/organisms/WizardSummary/index.jsx
  44. 32 43
      src/components/pages/EndpointDetailsPage/index.jsx
  45. 50 66
      src/components/pages/EndpointsPage/index.jsx
  46. 12 40
      src/components/pages/LoginPage/index.jsx
  47. 25 37
      src/components/pages/MigrationDetailsPage/index.jsx
  48. 37 55
      src/components/pages/MigrationsPage/index.jsx
  49. 58 65
      src/components/pages/ReplicaDetailsPage/index.jsx
  50. 38 56
      src/components/pages/ReplicasPage/index.jsx
  51. 108 116
      src/components/pages/WizardPage/index.jsx
  52. 2 2
      src/sources/EndpointSource.js
  53. 2 2
      src/sources/ReplicaSource.js
  54. 2 2
      src/sources/WizardSource.js
  55. 62 83
      src/stores/EndpointStore.js
  56. 124 137
      src/stores/InstanceStore.js
  57. 54 71
      src/stores/MigrationStore.js
  58. 20 26
      src/stores/NetworkStore.js
  59. 24 25
      src/stores/NotificationStore.js
  60. 16 26
      src/stores/ProjectStore.js
  61. 36 47
      src/stores/ProviderStore.js
  62. 78 105
      src/stores/ReplicaStore.js
  63. 54 65
      src/stores/ScheduleStore.js
  64. 76 41
      src/stores/UserStore.js
  65. 80 86
      src/stores/WizardStore.js
  66. 5 0
      src/types/Endpoint.js
  67. 8 6
      src/types/Network.js
  68. 11 4
      src/types/NotificationItem.js
  69. 6 2
      src/types/Providers.js
  70. 10 21
      src/types/User.js
  71. 14 7
      src/types/WizardData.js
  72. 3 3
      src/utils/ApiCaller.js
  73. 29 51
      yarn.lock

+ 2 - 1
.babelrc

@@ -11,7 +11,8 @@
     "stage-1"
     "stage-1"
   ],
   ],
   "plugins": [
   "plugins": [
-    "react-hot-loader/babel"
+    "react-hot-loader/babel",
+    "transform-decorators-legacy"
   ],
   ],
   "env": {
   "env": {
     "development": {
     "development": {

+ 1 - 0
.eslintrc

@@ -51,6 +51,7 @@
     "global-require": 0,
     "global-require": 0,
     "no-unused-expressions": 0,
     "no-unused-expressions": 0,
     "no-confusing-arrow": 0,
     "no-confusing-arrow": 0,
+    "no-console": "off",
     "no-nested-ternary": 0,
     "no-nested-ternary": 0,
     "import/no-dynamic-require": 0,
     "import/no-dynamic-require": 0,
     "import/no-unresolved": 0,
     "import/no-unresolved": 0,

+ 1 - 0
.flowconfig

@@ -8,6 +8,7 @@
 esproposal.class_static_fields=enable
 esproposal.class_static_fields=enable
 esproposal.class_instance_fields=enable
 esproposal.class_instance_fields=enable
 esproposal.export_star_as=enable
 esproposal.export_star_as=enable
+esproposal.decorators=ignore
 module.name_mapper.extension='css' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
 module.name_mapper.extension='css' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
 module.name_mapper.extension='styl' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
 module.name_mapper.extension='styl' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
 module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'
 module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/internals/flow/CSSModule.js.flow'

+ 4 - 0
flow-typed/module_vx.x.x.js

@@ -5,3 +5,7 @@ declare module 'module' {
 declare module 'moment/locale/en-gb' {
 declare module 'moment/locale/en-gb' {
   declare module.exports: any;
   declare module.exports: any;
 }
 }
+
+declare module 'mobx' {
+  declare module.exports: any;
+}

+ 3 - 2
package.json

@@ -54,11 +54,10 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@webpack-blocks/webpack2": "^0.4.0",
     "@webpack-blocks/webpack2": "^0.4.0",
-    "alt": "^0.18.6",
-    "alt-utils": "^2.0.0",
     "babel-core": "^6.26.0",
     "babel-core": "^6.26.0",
     "babel-loader": "^7.1.2",
     "babel-loader": "^7.1.2",
     "babel-plugin-styled-components": "^1.2.1",
     "babel-plugin-styled-components": "^1.2.1",
+    "babel-plugin-transform-decorators-legacy": "^1.3.4",
     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
     "babel-plugin-transform-react-remove-prop-types": "^0.4.9",
     "babel-plugin-transform-react-remove-prop-types": "^0.4.9",
     "babel-preset-env": "^1.6.0",
     "babel-preset-env": "^1.6.0",
@@ -75,6 +74,8 @@
     "html-webpack-plugin": "^2.30.1",
     "html-webpack-plugin": "^2.30.1",
     "js-cookie": "^2.1.4",
     "js-cookie": "^2.1.4",
     "lodash": "^4.17.4",
     "lodash": "^4.17.4",
+    "mobx": "^3.6.1",
+    "mobx-react": "^4.4.2",
     "moment": "^2.18.1",
     "moment": "^2.18.1",
     "path": "^0.12.7",
     "path": "^0.12.7",
     "raw-loader": "^0.5.1",
     "raw-loader": "^0.5.1",

+ 0 - 133
src/actions/EndpointActions.js

@@ -1,133 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import EndpointSource from '../sources/EndpointSource'
-
-class EndpointActions {
-  getEndpoints(options) {
-    return {
-      ...options,
-      promise: EndpointSource.getEndpoints().then(
-        endpoints => { this.getEndpointsCompleted(endpoints) },
-        response => { this.getEndpointsFailed(response) }
-      ),
-    }
-  }
-
-  getEndpointsCompleted(endpoints) {
-    return endpoints
-  }
-
-  getEndpointsFailed(response) {
-    return response || true
-  }
-
-  delete(endpoint) {
-    EndpointSource.delete(endpoint).then(
-      () => { this.deleteSuccess(endpoint.id) },
-      response => { this.deleteFailed(response) },
-    )
-    return endpoint
-  }
-
-  deleteSuccess(endpointId) {
-    return endpointId
-  }
-
-  deleteFailed(response) {
-    return response || true
-  }
-
-  getConnectionInfo(endpoint) {
-    EndpointSource.getConnectionInfo(endpoint).then(
-      connectionInfo => { this.getConnectionInfoSuccess(connectionInfo) },
-      response => { this.getConnectionInfoFailed(response) },
-    )
-    return endpoint || true
-  }
-
-  getConnectionInfoSuccess(connectionInfo) {
-    return connectionInfo
-  }
-
-  getConnectionInfoFailed(response) {
-    return response || true
-  }
-
-  validate(endpoint) {
-    EndpointSource.validate(endpoint).then(
-      validation => { this.validateSuccess(validation) },
-      response => { this.validateFailed(response) },
-    )
-    return endpoint
-  }
-
-  validateSuccess(validation) {
-    return validation
-  }
-
-  validateFailed(response) {
-    return response || true
-  }
-
-  clearValidation() {
-    return true
-  }
-
-  update(endpoint) {
-    return {
-      endpoint,
-      promise: EndpointSource.update(endpoint).then(
-        endpointResponse => { this.updateSuccess(endpointResponse) },
-        response => { this.updateFailed(response) },
-      ),
-    }
-  }
-
-  updateSuccess(endpoint) {
-    return endpoint
-  }
-
-  updateFailed(response) {
-    return response || true
-  }
-
-  clearConnectionInfo() {
-    return true
-  }
-
-  add(endpoint) {
-    return {
-      endpoint,
-      promise: EndpointSource.add(endpoint).then(
-        endpointResponse => { this.addSuccess(endpointResponse) },
-        response => { this.addFailed(response) },
-      ),
-    }
-  }
-
-  addSuccess(endpoint) {
-    return endpoint
-  }
-
-  addFailed(response) {
-    throw response
-  }
-}
-
-export default alt.createActions(EndpointActions)

+ 0 - 143
src/actions/InstanceActions.js

@@ -1,143 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import InstanceSource from '../sources/InstanceSource'
-import InstanceStore from '../stores/InstanceStore'
-import { wizardConfig } from '../config'
-
-class InstanceActions {
-  loadInstances(endpointId) {
-    InstanceSource.loadInstances(endpointId).then(
-      instances => { this.loadInstancesSuccess(endpointId, instances) },
-      response => { this.loadInstancesFailed(endpointId, response) },
-    )
-    return endpointId
-  }
-
-  loadInstancesSuccess(endpointId, instances) {
-    return { endpointId, instances }
-  }
-
-  loadInstancesFailed(endpointId, response) {
-    return { endpointId, response: response || true }
-  }
-
-  searchInstances(endpointId, searchText) {
-    InstanceSource.loadInstances(endpointId, searchText).then(
-      instances => { this.searchInstancesSuccess(instances, searchText) },
-      response => { this.searchInstancesFailed(response) },
-    )
-    return true
-  }
-
-  searchInstancesSuccess(instances, searchText) {
-    return { instances, searchText }
-  }
-
-  searchInstancesFailed(response) {
-    return response || true
-  }
-
-  loadNextPage(endpointId, searchText) {
-    let instanceStore = InstanceStore.getState()
-
-    if (instanceStore.cachedInstances.length > wizardConfig.instancesItemsPerPage * instanceStore.currentPage) {
-      return { fromCache: true }
-    }
-
-    InstanceSource.loadInstances(
-      endpointId,
-      searchText,
-      instanceStore.instances[instanceStore.instances.length - 1].id
-    ).then(
-      instances => { this.loadNextPageSuccess(instances) },
-      response => { this.loadNextPageFailed(response) },
-    )
-    return { fromCache: false }
-  }
-
-  loadNextPageFromCache() {
-    return true
-  }
-
-  loadNextPageSuccess(instances) {
-    return instances
-  }
-
-  loadNextPageFailed(response) {
-    return response || true
-  }
-
-  loadPreviousPage() {
-    return true
-  }
-
-  reloadInstances(endpointId, searchText) {
-    InstanceSource.loadInstances(endpointId, searchText).then(
-      instances => { this.reloadInstancesSuccess(instances, searchText) },
-      response => { this.reloadInstancesFailed(response) },
-    )
-
-    return true
-  }
-
-  reloadInstancesSuccess(instances, searchText) {
-    return { instances, searchText }
-  }
-
-  reloadInstancesFailed(response) {
-    return response || true
-  }
-
-  loadInstancesDetails(endpointId, instances) {
-    let store = InstanceStore.getState()
-    instances.sort((a, b) => a.instance_name.localeCompare(b.instance_name))
-    let hash = i => `${i.instance_name}-${i.id}`
-    if (store.instancesDetails.map(hash).join('_') === instances.map(hash).join('_')) {
-      return { fromCache: true }
-    }
-
-    instances.forEach(instance => {
-      InstanceSource.loadInstanceDetails(endpointId, instance.instance_name).then(
-        instance => { this.loadInstanceDetailsSuccess(instance) },
-        response => { this.loadInstanceDetailsFailed(response) },
-      )
-    })
-
-    return { count: instances.length }
-  }
-
-  loadInstanceDetails(endpointId, instanceName) {
-    InstanceSource.loadInstanceDetails(endpointId, instanceName).then(
-      instance => { this.loadInstanceDetailsSuccess(instance) },
-      response => { this.loadInstanceDetailsFailed(response) },
-    )
-
-    return true
-  }
-
-  loadInstanceDetailsSuccess(instance) {
-    return instance
-  }
-
-  loadInstanceDetailsFailed(response) {
-    return response || true
-  }
-}
-
-export default alt.createActions(InstanceActions)

+ 0 - 113
src/actions/MigrationActions.js

@@ -1,113 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import MigrationSource from '../sources/MigrationSource'
-
-class MigrationActions {
-  getMigrations(options) {
-    return {
-      ...options,
-      promise: MigrationSource.getMigrations().then(
-        response => { this.getMigrationsSuccess(response) },
-        response => { this.getMigrationsFailed(response) },
-      ),
-    }
-  }
-
-  getMigrationsSuccess(migrations) {
-    return migrations || true
-  }
-
-  getMigrationsFailed(response) {
-    return response || true
-  }
-
-  getMigration(migrationId, showLoading) {
-    MigrationSource.getMigration(migrationId).then(
-      migration => { this.getMigrationSuccess(migration) },
-      response => { this.getMigrationFailed(response) },
-    )
-
-    return { migrationId, showLoading }
-  }
-
-  getMigrationSuccess(migration) {
-    return migration
-  }
-
-  getMigrationFailed(response) {
-    return response || true
-  }
-
-  cancel(migrationId) {
-    return {
-      migrationId,
-      promise: MigrationSource.cancel(migrationId).then(
-        () => { this.cancelSuccess(migrationId) },
-        response => { this.cancelFailed(response) },
-      ),
-    }
-  }
-
-  cancelSuccess(migrationId) {
-    return { migrationId }
-  }
-
-  cancelFailed(response) {
-    return response || true
-  }
-
-  delete(migrationId) {
-    MigrationSource.delete(migrationId).then(
-      () => { this.deleteSuccess(migrationId) },
-      response => { this.deleteFailed(response) },
-    )
-    return migrationId
-  }
-
-  deleteSuccess(migrationId) {
-    return migrationId
-  }
-
-  deleteFailed(response) {
-    return response || true
-  }
-
-  migrateReplica(replicaId, options) {
-    MigrationSource.migrateReplica(replicaId, options).then(
-      migration => { this.migrateReplicaSuccess(migration) },
-      response => { this.migrateReplicaFailed(response) },
-    )
-
-    return { replicaId, options }
-  }
-
-  migrateReplicaSuccess(migration) {
-    return migration
-  }
-
-  migrateReplicaFailed(response) {
-    return response || true
-  }
-
-  clearDetails() {
-    return true
-  }
-}
-
-export default alt.createActions(MigrationActions)

+ 0 - 47
src/actions/NetworkActions.js

@@ -1,47 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import NetworkSource from '../sources/NetworkSource'
-import NetworkStore from '../stores/NetworkStore'
-
-class NetworkActions {
-  loadNetworks(endpointId, environment) {
-    let storedCacheId = NetworkStore.getState().cacheId
-    let cacheId = `${endpointId}-${btoa(JSON.stringify(environment))}`
-    if (cacheId === storedCacheId) {
-      return { fromCache: true }
-    }
-
-    NetworkSource.loadNetworks(endpointId, environment).then(
-      networks => { this.loadNetworksSuccess(networks, cacheId) },
-      response => { this.loadNetworksFailed(response) }
-    )
-
-    return { fromCache: false }
-  }
-
-  loadNetworksSuccess(networks, cacheId) {
-    return { networks, cacheId }
-  }
-
-  loadNetworksFailed(response) {
-    return response || true
-  }
-}
-
-export default alt.createActions(NetworkActions)

+ 0 - 76
src/actions/NotificationActions.js

@@ -1,76 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import NotificationSource from '../sources/NotificationSource'
-
-class NotificationActions {
-  notify(message, level, options) {
-    if (options && options.persist) {
-      NotificationSource.notify(message, level, options).then(
-        notification => { this.notifySuccess(notification) },
-        response => { this.notifyFailed(response) }
-      )
-    }
-
-    return { message, level, ...options }
-  }
-
-  notifySuccess(notification) {
-    return notification
-  }
-
-  notifyFailed(response) {
-    return response || true
-  }
-
-  loadNotifications() {
-    NotificationSource.loadNotifications().then(
-      notifications => { this.loadNotificationsSuccess(notifications) },
-      response => { this.loadNotificationsFailed(response) }
-    )
-
-    return true
-  }
-
-  loadNotificationsSuccess(notifications) {
-    return notifications
-  }
-
-  loadNotificationsFailed(response) {
-    return response || true
-  }
-
-  clearNotifications() {
-    NotificationSource.clearNotifications().then(
-      () => { this.clearNotificationsSuccess() },
-      response => { this.clearNotificationsFailed(response) }
-    )
-
-    return true
-  }
-
-  clearNotificationsSuccess() {
-    return true
-  }
-
-  clearNotificationsFailed(response) {
-    return response || true
-  }
-}
-
-export default alt.createActions(NotificationActions)

+ 0 - 76
src/actions/ProviderActions.js

@@ -1,76 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import ProviderSource from '../sources/ProviderSource'
-
-class ProviderActions {
-  getConnectionInfoSchema(providerName) {
-    ProviderSource.getConnectionInfoSchema(providerName).then(
-      schema => { this.getConnectionInfoSchemaSuccess(schema) },
-      response => { this.getConnectionInfoSchemaFailed(response) },
-    )
-    return true
-  }
-
-  getConnectionInfoSchemaSuccess(schema) {
-    return schema
-  }
-
-  getConnectionInfoSchemaFailed(response) {
-    return response || true
-  }
-
-  clearConnectionInfoSchema() {
-    return true
-  }
-
-  loadProviders() {
-    ProviderSource.loadProviders().then(
-      providers => { this.loadProvidersSuccess(providers) },
-      response => { this.loadProvidersFailed(response) },
-    )
-
-    return true
-  }
-
-  loadProvidersSuccess(providers) {
-    return providers
-  }
-
-  loadProvidersFailed(response) {
-    return response || true
-  }
-
-  loadOptionsSchema(providerName, schemaType) {
-    ProviderSource.loadOptionsSchema(providerName, schemaType).then(
-      schema => { this.loadOptionsSchemaSuccess(schema) },
-      response => { this.loadOptionsSchemaFailed(response) },
-    )
-    return true
-  }
-
-  loadOptionsSchemaSuccess(schema) {
-    return schema
-  }
-
-  loadOptionsSchemaFailed(response) {
-    return response || true
-  }
-}
-
-export default alt.createActions(ProviderActions)

+ 0 - 200
src/actions/ReplicaActions.js

@@ -1,200 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import ReplicaSource from '../sources/ReplicaSource'
-
-class ReplicaActions {
-  getReplicas(options) {
-    return {
-      ...options,
-      promise: ReplicaSource.getReplicas().then(
-        response => { this.getReplicasSuccess(response) },
-        response => { this.getReplicasFailed(response) },
-      ),
-    }
-  }
-
-  getReplicasSuccess(replicas) {
-    return replicas || true
-  }
-
-  getReplicasFailed(response) {
-    return response || true
-  }
-
-  getReplicasExecutions(replicas) {
-    let count = 0
-    let replicasExecutions = []
-    replicas.forEach(replica => {
-      ReplicaSource.getReplicaExecutions(replica.id).then(
-        response => {
-          count += 1
-          replicasExecutions.push(response)
-
-          if (count === replicas.length) {
-            this.getReplicasExecutionsSuccess(replicasExecutions)
-          }
-        },
-        response => {
-          count += 1
-          if (count === replicas.length) {
-            if (replicasExecutions.length > 0) {
-              this.getReplicasExecutionsSuccess(replicasExecutions)
-            } else {
-              this.getReplicasExecutionsFailed(response)
-            }
-          }
-        },
-      )
-    })
-
-    return replicas
-  }
-
-  getReplicasExecutionsSuccess(replicasExecutions) {
-    return replicasExecutions
-  }
-
-  getReplicasExecutionsFailed(response) {
-    return response || true
-  }
-
-  getReplicaExecutions(replicaId) {
-    return {
-      replicaId,
-      promise: ReplicaSource.getReplicaExecutions(replicaId).then(
-        response => { this.getReplicaExecutionsSuccess(response) },
-        response => { this.getReplicaExecutionsFailed(response) },
-      ),
-    }
-  }
-
-  getReplicaExecutionsSuccess({ replicaId, executions }) {
-    return { replicaId, executions }
-  }
-
-  getReplicaExecutionsFailed(response) {
-    return response || true
-  }
-
-  getReplica(replicaId) {
-    ReplicaSource.getReplica(replicaId).then(
-      replica => { this.getReplicaSuccess(replica) },
-      response => { this.getReplicaFailed(response) },
-    )
-
-    return replicaId
-  }
-
-  getReplicaSuccess(replica) {
-    return replica
-  }
-
-  getReplicaFailed(response) {
-    return response || true
-  }
-
-  execute(replicaId, fields) {
-    ReplicaSource.execute(replicaId, fields).then(
-      executions => { this.executeSuccess(executions) },
-      response => { this.executeFailed(response) },
-    )
-
-    return replicaId
-  }
-
-  executeSuccess({ replicaId, execution }) {
-    return { replicaId, execution }
-  }
-
-  executeFailed(response) {
-    return response || true
-  }
-
-  cancelExecution(replicaId, executionId) {
-    ReplicaSource.cancelExecution(replicaId, executionId).then(
-      () => { this.cancelExecutionSuccess(replicaId, executionId) },
-      response => { this.cancelExecutionFailed(response) },
-    )
-
-    return { replicaId, executionId }
-  }
-
-  cancelExecutionSuccess(replicaId, executionId) {
-    return { replicaId, executionId }
-  }
-
-  cancelExecutionFailed(response) {
-    return response || true
-  }
-
-  deleteExecution(replicaId, executionId) {
-    ReplicaSource.deleteExecution(replicaId, executionId).then(
-      () => { this.deleteExecutionSuccess(replicaId, executionId) },
-      response => { this.deleteExecutionFailed(response) },
-    )
-
-    return { replicaId, executionId }
-  }
-
-  deleteExecutionSuccess(replicaId, executionId) {
-    return { replicaId, executionId }
-  }
-
-  deleteExecutionFailed(response) {
-    return response || true
-  }
-
-  delete(replicaId) {
-    ReplicaSource.delete(replicaId).then(
-      () => { this.deleteSuccess(replicaId) },
-      response => { this.deleteFailed(response) },
-    )
-    return replicaId
-  }
-
-  deleteSuccess(replicaId) {
-    return replicaId
-  }
-
-  deleteFailed(response) {
-    return response || true
-  }
-
-  clearDetails() {
-    return true
-  }
-
-  deleteDisks(replicaId) {
-    ReplicaSource.deleteDisks(replicaId).then(
-      execution => { this.deleteDisksSuccess(replicaId, execution) },
-      response => { this.deleteDisksFailed(response) },
-    )
-    return replicaId
-  }
-
-  deleteDisksSuccess(replicaId, execution) {
-    return { replicaId, execution }
-  }
-
-  deleteDisksFailed(execution) {
-    return execution || true
-  }
-}
-
-export default alt.createActions(ReplicaActions)

+ 0 - 113
src/actions/ScheduleActions.js

@@ -1,113 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import ScheduleSource from '../sources/ScheduleSource'
-
-class ScheduleActions {
-  scheduleMultiple(replicaId, schedules) {
-    ScheduleSource.scheduleMultiple(replicaId, schedules).then(
-      s => { this.scheduleMultipleSuccess(s) },
-      response => { this.scheduleMultipleFailed(response) },
-    )
-    return { replicaId, schedules }
-  }
-
-  scheduleMultipleSuccess(schedules) {
-    return schedules
-  }
-
-  scheduleMultipleFailed(response) {
-    return response || true
-  }
-
-  getSchedules(replicaId) {
-    ScheduleSource.getSchedules(replicaId).then(
-      schedules => { this.getSchedulesSuccess(schedules) },
-      response => { this.getSchedulesFailed(response) },
-    )
-
-    return replicaId
-  }
-
-  getSchedulesSuccess(schedules) {
-    return schedules
-  }
-
-  getSchedulesFailed(response) {
-    return response || true
-  }
-
-  addSchedule(replicaId, schedule) {
-    ScheduleSource.addSchedule(replicaId, schedule).then(
-      schedule => { this.addScheduleSuccess(schedule) },
-      response => { this.addScheduleFailed(response) },
-    )
-
-    return replicaId
-  }
-
-  addScheduleSuccess(schedule) {
-    return schedule
-  }
-
-  addScheduleFailed(response) {
-    return response || true
-  }
-
-  removeSchedule(replicaId, scheduleId) {
-    ScheduleSource.removeSchedule(replicaId, scheduleId).then(
-      () => { this.removeScheduleSuccess() },
-      response => { this.removeScheduleFailed(response) },
-    )
-
-    return { replicaId, scheduleId }
-  }
-
-  removeScheduleSuccess() {
-    return true
-  }
-
-  removeScheduleFailed(response) {
-    return response || true
-  }
-
-  updateSchedule(replicaId, scheduleId, data, oldData, unsavedData, forceSave) {
-    if (forceSave) {
-      ScheduleSource.updateSchedule(replicaId, scheduleId, data, oldData, unsavedData).then(
-        schedule => { this.updateScheduleSuccess(schedule) },
-        response => { this.updateScheduleFailed(response) },
-      )
-    }
-
-    return { replicaId, scheduleId, data, forceSave }
-  }
-
-  updateScheduleSuccess(schedule) {
-    return schedule
-  }
-
-  updateScheduleFailed(response) {
-    return response || null
-  }
-
-  clearUnsavedSchedules() {
-    return true
-  }
-}
-
-export default alt.createActions(ScheduleActions)

+ 0 - 135
src/actions/UserActions.js

@@ -1,135 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import UserSource from '../sources/UserSource'
-import ProjectActions from './ProjectActions'
-import ProjectStore from '../stores/ProjectStore'
-import NotificationActions from './NotificationActions'
-
-/**
- * This is the authentication / authorization flow:
- * 1. Post username and password unscoped login. Set unscoped token in cookies.
- * 2. Post unscoped token with project id. Set scoped token and project id in cookies.
- * 3. Get token login on subsequent app reloads to retrieve the user info.
- * 
- * After token expiration, the app is redirected to login page.
- */
-class UserActions {
-  login(data) {
-    UserSource.login(data).then(this.loginSuccess, this.loginFailed)
-    return data
-  }
-
-  loginSuccess() {
-    this.loginScoped()
-    return true
-  }
-
-  loginFailed(response) {
-    return response || true
-  }
-
-  loginScoped(projectId) {
-    let projectStore = ProjectStore.getState()
-    if (projectStore.projects && projectStore.projects.length) {
-      UserSource.loginScoped(projectId || projectStore.projects[0].id)
-        .then(this.loginScopedSuccess, this.loginScopedFailed)
-    } else {
-      ProjectActions.getProjects().promise.then(() => {
-        UserSource.loginScoped(projectId || ProjectStore.getState().projects[0].id)
-          .then(this.loginScopedSuccess, this.loginScopedFailed)
-      })
-    }
-    return projectId || true
-  }
-
-  loginScopedSuccess(response) {
-    this.getUserInfo(response)
-    NotificationActions.notify('Signed in', 'success')
-    return response || true
-  }
-
-  loginScopedFailed(response) {
-    return response || true
-  }
-
-  tokenLogin() {
-    UserSource.tokenLogin().then(this.tokenLoginSuccess, this.tokenLoginFailed)
-    return true
-  }
-
-  tokenLoginSuccess(response) {
-    NotificationActions.notify('Signed in', 'success')
-    this.getUserInfo(response)
-    return response || true
-  }
-
-  tokenLoginFailed(response) {
-    return response || true
-  }
-
-  switchProject(projectId) {
-    NotificationActions.notify('Switching projects')
-    UserSource.switchProject().then(
-      () => { this.switchProjectSuccess(projectId) },
-      response => { this.switchProjectFailed(response) }
-    )
-    return projectId || true
-  }
-
-  switchProjectSuccess(projectId) {
-    this.loginScoped(projectId)
-    return projectId || true
-  }
-
-  switchProjectFailed(response) {
-    this.logout()
-    return response || true
-  }
-
-  logout() {
-    UserSource.logout().then(() => { this.logoutSuccess() }, () => { this.logoutFailed() })
-    return true
-  }
-
-  logoutSuccess() {
-    return true
-  }
-
-  logoutFailed() {
-    return true
-  }
-
-  getUserInfo(user) {
-    UserSource.getUserInfo(user).then(
-      response => { this.getUserInfoSuccess(response) },
-      response => { this.getUserInfoFailed(response) }
-    )
-    return user || true
-  }
-
-  getUserInfoSuccess(response) {
-    return response || true
-  }
-
-  getUserInfoFailed(response) {
-    return response || true
-  }
-}
-
-export default alt.createActions(UserActions)

+ 0 - 107
src/actions/WizardActions.js

@@ -1,107 +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/>.
-*/
-
-// @flow
-
-import alt from '../alt'
-
-import WizardSource from '../sources/WizardSource'
-
-class WizardActions {
-  updateData(data) {
-    return data
-  }
-
-  toggleInstanceSelection(instance) {
-    return instance
-  }
-
-  clearData() {
-    return true
-  }
-
-  setCurrentPage(page) {
-    return page
-  }
-
-  updateOptions({ field, value }) {
-    return { field, value }
-  }
-
-  updateNetworks({ sourceNic, targetNetwork }) {
-    return { sourceNic, targetNetwork }
-  }
-
-  addSchedule(schedule) {
-    return schedule || true
-  }
-
-  updateSchedule(scheduleId, data) {
-    return { scheduleId, data }
-  }
-
-  removeSchedule(scheduleId) {
-    return scheduleId
-  }
-
-  create(type, data) {
-    return {
-      type,
-      data,
-      promise: WizardSource.create(type, data).then(
-        item => { this.createSuccess(item) },
-        response => { this.createFailed(response) }
-      ),
-    }
-  }
-
-  createSuccess(item) {
-    return item
-  }
-
-  createFailed(reponse) {
-    return reponse || true
-  }
-
-  createMultiple(type, data) {
-    return {
-      type,
-      data,
-      promise: WizardSource.createMultiple(type, data).then(
-        items => { this.createMultipleSuccess(items) },
-        response => { this.createMultipleFailed(response) }
-      ),
-    }
-  }
-
-  createMultipleSuccess(items) {
-    return items
-  }
-
-  createMultipleFailed(response) {
-    return response || true
-  }
-
-  setPermalink(data) {
-    WizardSource.setPermalink(data)
-    return data || true
-  }
-
-  getDataFromPermalink() {
-    let data = WizardSource.getDataFromPermalink()
-    return data || true
-  }
-}
-
-export default alt.createActions(WizardActions)

+ 2 - 2
src/components/App.jsx

@@ -29,10 +29,10 @@ import MigrationDetailsPage from './pages/MigrationDetailsPage'
 import EndpointsPage from './pages/EndpointsPage'
 import EndpointsPage from './pages/EndpointsPage'
 import EndpointDetailsPage from './pages/EndpointDetailsPage'
 import EndpointDetailsPage from './pages/EndpointDetailsPage'
 import WizardPage from './pages/WizardPage'
 import WizardPage from './pages/WizardPage'
+import UserStore from '../stores/UserStore'
 
 
 import Palette from './styleUtils/Palette'
 import Palette from './styleUtils/Palette'
 import StyleProps from './styleUtils/StyleProps'
 import StyleProps from './styleUtils/StyleProps'
-import UserActions from '../actions/UserActions'
 
 
 injectGlobal`
 injectGlobal`
   ${Fonts}
   ${Fonts}
@@ -50,7 +50,7 @@ const Wrapper = styled.div``
 
 
 class App extends React.Component<{}> {
 class App extends React.Component<{}> {
   componentWillMount() {
   componentWillMount() {
-    UserActions.tokenLogin()
+    UserStore.tokenLogin()
   }
   }
 
 
   render() {
   render() {

+ 3 - 3
src/components/atoms/CopyValue/index.jsx

@@ -18,8 +18,8 @@ import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
 
 
 import CopyButton from '../CopyButton'
 import CopyButton from '../CopyButton'
-import NotificationActions from '../../../actions/NotificationActions'
 import DomUtils from '../../../utils/DomUtils'
 import DomUtils from '../../../utils/DomUtils'
+import NotificationStore from '../../../stores/NotificationStore'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   cursor: pointer;
   cursor: pointer;
@@ -50,9 +50,9 @@ class CopyValue extends React.Component<Props> {
     let succesful = DomUtils.copyTextToClipboard(this.props.value)
     let succesful = DomUtils.copyTextToClipboard(this.props.value)
 
 
     if (succesful) {
     if (succesful) {
-      NotificationActions.notify('The value has been copied to clipboard.')
+      NotificationStore.notify('The value has been copied to clipboard.')
     } else {
     } else {
-      NotificationActions.notify('The value couldn\'t be copied', 'error')
+      NotificationStore.notify('The value couldn\'t be copied', 'error')
     }
     }
   }
   }
 
 

+ 2 - 0
src/components/molecules/EndpointListItem/index.jsx

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Endpoint } from '../../../types/Endpoint'
 import Checkbox from '../../atoms/Checkbox'
 import Checkbox from '../../atoms/Checkbox'
@@ -99,6 +100,7 @@ type Props = {
   onSelectedChange: (value: boolean) => void,
   onSelectedChange: (value: boolean) => void,
   getUsage: (item: Endpoint) => { replicasCount: number, migrationsCount: number },
   getUsage: (item: Endpoint) => { replicasCount: number, migrationsCount: number },
 }
 }
+@observer
 class EndpointListItem extends React.Component<Props> {
 class EndpointListItem extends React.Component<Props> {
   render() {
   render() {
     return (
     return (

+ 2 - 0
src/components/molecules/MainListItem/index.jsx

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
 import Checkbox from '../../atoms/Checkbox'
 import Checkbox from '../../atoms/Checkbox'
 import StatusPill from '../../atoms/StatusPill'
 import StatusPill from '../../atoms/StatusPill'
@@ -113,6 +114,7 @@ type Props = {
   endpointType: (endpointId: string) => string,
   endpointType: (endpointId: string) => string,
   onSelectedChange: (value: boolean) => void,
   onSelectedChange: (value: boolean) => void,
 }
 }
+@observer
 class MainListItem extends React.Component<Props> {
 class MainListItem extends React.Component<Props> {
   getLastExecution(): ?Execution | ?MainItem {
   getLastExecution(): ?Execution | ?MainItem {
     if (this.props.item.executions && this.props.item.executions.length) {
     if (this.props.item.executions && this.props.item.executions.length) {

+ 1 - 1
src/components/molecules/NotificationDropdown/index.jsx

@@ -213,7 +213,7 @@ class NotificationDropdown extends React.Component<Props, State> {
     let list = (
     let list = (
       <List>
       <List>
         {this.props.items.map(item => {
         {this.props.items.map(item => {
-          let title = (item.options.persistInfo && item.options.persistInfo.title) || item.message
+          let title = (item.options && item.options.persistInfo && item.options.persistInfo.title) || item.message
           let message = title === item.message ? '' : item.message
           let message = title === item.message ? '' : item.message
 
 
           return (
           return (

+ 2 - 2
src/components/molecules/ScheduleItem/index.jsx

@@ -28,7 +28,7 @@ import { executionOptions } from '../../../config'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 import DateUtils from '../../../utils/DateUtils'
 import DateUtils from '../../../utils/DateUtils'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import deleteImage from './images/delete.svg'
 import deleteImage from './images/delete.svg'
 import deleteHoverImage from './images/delete-hover.svg'
 import deleteHoverImage from './images/delete-hover.svg'
 import saveImage from './images/save.svg'
 import saveImage from './images/save.svg'
@@ -145,7 +145,7 @@ class ScheduleItem extends React.Component<Props> {
   handleExpirationDateChange(date: Date) {
   handleExpirationDateChange(date: Date) {
     let newDate = moment(date)
     let newDate = moment(date)
     if (newDate.diff(new Date(), 'minutes') < 60) {
     if (newDate.diff(new Date(), 'minutes') < 60) {
-      NotificationActions.notify('Please select a further expiration date.', 'error')
+      NotificationStore.notify('Please select a further expiration date.', 'error')
       return
       return
     }
     }
 
 

+ 3 - 3
src/components/molecules/TaskItem/index.jsx

@@ -25,7 +25,7 @@ import StatusPill from '../../atoms/StatusPill'
 import CopyValue from '../../atoms/CopyValue'
 import CopyValue from '../../atoms/CopyValue'
 import ProgressBar from '../../atoms/ProgressBar'
 import ProgressBar from '../../atoms/ProgressBar'
 import CopyButton from '../../atoms/CopyButton'
 import CopyButton from '../../atoms/CopyButton'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import DomUtils from '../../../utils/DomUtils'
 import DomUtils from '../../../utils/DomUtils'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -159,7 +159,7 @@ class TaskItem extends React.Component<Props> {
     let succesful = DomUtils.copyTextToClipboard(exceptionText)
     let succesful = DomUtils.copyTextToClipboard(exceptionText)
 
 
     if (succesful) {
     if (succesful) {
-      NotificationActions.notify('The message has been copied to clipboard.')
+      NotificationStore.notify('The message has been copied to clipboard.')
     }
     }
   }
   }
 
 
@@ -189,7 +189,7 @@ class TaskItem extends React.Component<Props> {
   }
   }
 
 
   renderDependsOnValue() {
   renderDependsOnValue() {
-    if (this.props.item.depends_on && this.props.item.depends_on[0]) {
+    if (this.props.item.depends_on && this.props.item.depends_on.length > 0 && this.props.item.depends_on[0]) {
       return (
       return (
         <Value
         <Value
           width="calc(100% - 16px)"
           width="calc(100% - 16px)"

+ 3 - 2
src/components/molecules/Timeline/index.jsx

@@ -82,7 +82,7 @@ const ItemLabel = styled.div`
 `
 `
 
 
 type Props = {
 type Props = {
-  items: Execution[],
+  items: ?Execution[],
   selectedItem: ?Execution,
   selectedItem: ?Execution,
   onPreviousClick: () => void,
   onPreviousClick: () => void,
   onNextClick: () => void,
   onNextClick: () => void,
@@ -113,7 +113,7 @@ class Timeline extends React.Component<Props> {
   }
   }
 
 
   moveToSelectedItem() {
   moveToSelectedItem() {
-    if (!this.progressLineRef || !this.endLineRef) {
+    if (!this.progressLineRef || !this.endLineRef || !this.props.items) {
       return
       return
     }
     }
 
 
@@ -132,6 +132,7 @@ class Timeline extends React.Component<Props> {
 
 
     this.itemsRef.style.marginLeft = `${offset}px`
     this.itemsRef.style.marginLeft = `${offset}px`
 
 
+    // $FlowIssue
     let lastItemPos = (itemGap * (this.props.items.length - 1)) + offset + itemHalfWidth
     let lastItemPos = (itemGap * (this.props.items.length - 1)) + offset + itemHalfWidth
     this.progressLineRef.style.width = `${lastItemPos}px`
     this.progressLineRef.style.width = `${lastItemPos}px`
     this.endLineRef.style.width = `${Math.max(this.wrapperRef.offsetWidth - lastItemPos, 0)}px`
     this.endLineRef.style.width = `${Math.max(this.wrapperRef.offsetWidth - lastItemPos, 0)}px`

+ 1 - 1
src/components/molecules/UserDropdown/index.jsx

@@ -100,7 +100,7 @@ type User = { name: string, email: string }
 type DictItem = { label: string, value: string }
 type DictItem = { label: string, value: string }
 type Props = {
 type Props = {
   onItemClick: (item: DictItem) => void,
   onItemClick: (item: DictItem) => void,
-  user: User,
+  user: ?User,
   white?: boolean,
   white?: boolean,
 }
 }
 type State = {
 type State = {

+ 2 - 1
src/components/organisms/ChooseProvider/index.jsx

@@ -20,6 +20,7 @@ import styled from 'styled-components'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import Button from '../../atoms/Button'
 import Button from '../../atoms/Button'
 import StatusImage from '../../atoms/StatusImage'
 import StatusImage from '../../atoms/StatusImage'
+import type { Providers as ProvidersType } from '../../../types/Providers'
 
 
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 
 
@@ -53,7 +54,7 @@ const LoadingText = styled.div`
 `
 `
 
 
 type Props = {
 type Props = {
-  providers: { [string]: any },
+  providers: ?ProvidersType,
   onCancelClick: () => void,
   onCancelClick: () => void,
   onProviderClick: (provider: string) => void,
   onProviderClick: (provider: string) => void,
   loading: boolean,
   loading: boolean,

+ 8 - 18
src/components/organisms/DetailsPageHeader/index.jsx

@@ -16,13 +16,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import SideMenu from '../../molecules/SideMenu'
 import SideMenu from '../../molecules/SideMenu'
 import NotificationDropdown from '../../molecules/NotificationDropdown'
 import NotificationDropdown from '../../molecules/NotificationDropdown'
 import UserDropdown from '../../molecules/UserDropdown'
 import UserDropdown from '../../molecules/UserDropdown'
+import type { User as UserType } from '../../../types/User'
 
 
-import NotificationActions from '../../../actions/NotificationActions'
 import NotificationStore from '../../../stores/NotificationStore'
 import NotificationStore from '../../../stores/NotificationStore'
 
 
 import backgroundImage from './images/star-bg.jpg'
 import backgroundImage from './images/star-bg.jpg'
@@ -55,27 +55,17 @@ const User = styled.div`
 `
 `
 
 
 type Props = {
 type Props = {
-  user: { username: string, email: string },
+  user?: ?UserType,
   onUserItemClick: (userItem: { label: string, value: string }) => void,
   onUserItemClick: (userItem: { label: string, value: string }) => void,
-  notificationStore?: any,
 }
 }
+@observer
 export class DetailsPageHeader extends React.Component<Props> {
 export class DetailsPageHeader extends React.Component<Props> {
-  static getStores() {
-    return [NotificationStore]
-  }
-
-  static getPropsFromStores(): $Shape<Props> {
-    return {
-      notificationStore: NotificationStore.getState(),
-    }
-  }
-
   componentDidMount() {
   componentDidMount() {
-    NotificationActions.loadNotifications()
+    NotificationStore.loadNotifications()
   }
   }
 
 
   handleNotificationsClose() {
   handleNotificationsClose() {
-    NotificationActions.clearNotifications()
+    NotificationStore.clearNotifications()
   }
   }
 
 
   render() {
   render() {
@@ -86,7 +76,7 @@ export class DetailsPageHeader extends React.Component<Props> {
           <Logo href="/#/replicas" />
           <Logo href="/#/replicas" />
         </Menu>
         </Menu>
         <User>
         <User>
-          <NotificationDropdown white items={this.props.notificationStore ? this.props.notificationStore.persistedNotifications : []} onClose={() => this.handleNotificationsClose()} />
+          <NotificationDropdown white items={NotificationStore.persistedNotifications} onClose={() => this.handleNotificationsClose()} />
           <UserDropdownStyled
           <UserDropdownStyled
             white
             white
             user={this.props.user}
             user={this.props.user}
@@ -98,4 +88,4 @@ export class DetailsPageHeader extends React.Component<Props> {
   }
   }
 }
 }
 
 
-export default connectToStores(DetailsPageHeader)
+export default DetailsPageHeader

+ 80 - 67
src/components/organisms/Endpoint/index.jsx

@@ -16,7 +16,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
+import { observe } from 'mobx'
 
 
 import EndpointLogos from '../../atoms/EndpointLogos'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import StatusIcon from '../../atoms/StatusIcon'
 import StatusIcon from '../../atoms/StatusIcon'
@@ -27,11 +28,10 @@ import Button from '../../atoms/Button'
 import LoadingButton from '../../molecules/LoadingButton'
 import LoadingButton from '../../molecules/LoadingButton'
 
 
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
-import NotificationActions from '../../../actions/NotificationActions'
+import type { Field } from '../../../types/Field'
+import NotificationStore from '../../../stores/NotificationStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
-import EndpointActions from '../../../actions/EndpointActions'
 import ProviderStore from '../../../stores/ProviderStore'
 import ProviderStore from '../../../stores/ProviderStore'
-import ProviderActions from '../../../actions/ProviderActions'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import DomUtils from '../../../utils/DomUtils'
 import DomUtils from '../../../utils/DomUtils'
@@ -105,43 +105,32 @@ const Buttons = styled.div`
 `
 `
 
 
 type Props = {
 type Props = {
-  type: string,
+  type: ?string,
   cancelButtonText: string,
   cancelButtonText: string,
   deleteOnCancel: boolean,
   deleteOnCancel: boolean,
-  endpoint: EndpointType,
-  connectionInfo: { [string]: mixed },
+  endpoint: ?EndpointType,
   onCancelClick: (opts?: { autoClose?: boolean }) => void,
   onCancelClick: (opts?: { autoClose?: boolean }) => void,
   onResizeUpdate: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
   onResizeUpdate: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
-  endpointStore: any,
-  providerStore: any,
 }
 }
 type State = {
 type State = {
   invalidFields: any[],
   invalidFields: any[],
   validating: boolean,
   validating: boolean,
   showErrorMessage: boolean,
   showErrorMessage: boolean,
-  endpoint: EndpointType | {},
+  endpoint: ?EndpointType,
   isNew: ?boolean,
   isNew: ?boolean,
 }
 }
+@observer
 class Endpoint extends React.Component<Props, State> {
 class Endpoint extends React.Component<Props, State> {
   static defaultProps: $Shape<Props> = {
   static defaultProps: $Shape<Props> = {
     cancelButtonText: 'Cancel',
     cancelButtonText: 'Cancel',
   }
   }
 
 
-  static getStores() {
-    return [EndpointStore, ProviderStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      endpointStore: EndpointStore.getState(),
-      providerStore: ProviderStore.getState(),
-    }
-  }
-
   scrollableRef: HTMLElement
   scrollableRef: HTMLElement
   closeTimeout: TimeoutID
   closeTimeout: TimeoutID
   contentPluginRef: DefaultContentPlugin
   contentPluginRef: DefaultContentPlugin
   isValidateButtonEnabled: boolean
   isValidateButtonEnabled: boolean
+  providerStoreObserver: any
+  endpointStoreObserver: any
 
 
   constructor() {
   constructor() {
     super()
     super()
@@ -150,28 +139,38 @@ class Endpoint extends React.Component<Props, State> {
       invalidFields: [],
       invalidFields: [],
       validating: false,
       validating: false,
       showErrorMessage: false,
       showErrorMessage: false,
-      endpoint: {},
+      endpoint: null,
       isNew: null,
       isNew: null,
     }
     }
   }
   }
 
 
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props)
+    this.providerStoreObserver = observe(ProviderStore, 'connectionInfoSchema', () => {
+      this.props.onResizeUpdate(this.scrollableRef)
+    })
+    this.endpointStoreObserver = observe(EndpointStore, 'validation', () => {
+      this.componentWillReceiveProps(this.props)
+    })
+  }
+
   componentDidMount() {
   componentDidMount() {
-    ProviderActions.getConnectionInfoSchema(this.getEndpointType())
+    ProviderStore.getConnectionInfoSchema(this.getEndpointType())
     KeyboardManager.onEnter('endpoint', () => { if (this.isValidateButtonEnabled) this.handleValidateClick() }, 2)
     KeyboardManager.onEnter('endpoint', () => { if (this.isValidateButtonEnabled) this.handleValidateClick() }, 2)
   }
   }
 
 
-  componentWillReceiveProps(props) {
+  componentWillReceiveProps(props: Props) {
     if (this.state.validating) {
     if (this.state.validating) {
-      if (props.endpointStore.validation && !props.endpointStore.validation.valid) {
+      if (EndpointStore.validation && !EndpointStore.validation.valid) {
         this.setState({ validating: false })
         this.setState({ validating: false })
       }
       }
     }
     }
 
 
-    if (props.endpoint && props.endpointStore.connectionInfo) {
+    if (props.endpoint && EndpointStore.connectionInfo) {
       this.setState({
       this.setState({
         endpoint: {
         endpoint: {
-          ...ObjectUtils.flatten(props.endpoint),
-          ...ObjectUtils.flatten(props.endpointStore.connectionInfo),
+          ...ObjectUtils.flatten(props.endpoint || {}),
+          ...ObjectUtils.flatten(EndpointStore.connectionInfo || {}),
         },
         },
       })
       })
     } else {
     } else {
@@ -179,19 +178,21 @@ class Endpoint extends React.Component<Props, State> {
         isNew: this.state.isNew === null || this.state.isNew,
         isNew: this.state.isNew === null || this.state.isNew,
         endpoint: {
         endpoint: {
           type: props.type,
           type: props.type,
-          ...ObjectUtils.flatten(this.state.endpoint),
+          ...ObjectUtils.flatten(this.state.endpoint || {}),
         },
         },
       })
       })
     }
     }
 
 
-    this.props.onResizeUpdate(this.scrollableRef)
+    props.onResizeUpdate(this.scrollableRef)
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    EndpointActions.clearValidation()
-    ProviderActions.clearConnectionInfoSchema()
+    EndpointStore.clearValidation()
+    ProviderStore.clearConnectionInfoSchema()
     clearTimeout(this.closeTimeout)
     clearTimeout(this.closeTimeout)
     KeyboardManager.removeKeyDown('endpoint')
     KeyboardManager.removeKeyDown('endpoint')
+    this.providerStoreObserver()
+    this.endpointStoreObserver()
   }
   }
 
 
   getEndpointType() {
   getEndpointType() {
@@ -199,11 +200,11 @@ class Endpoint extends React.Component<Props, State> {
       return this.props.endpoint.type
       return this.props.endpoint.type
     }
     }
 
 
-    return this.props.type
+    return this.props.type || ''
   }
   }
 
 
-  getFieldValue(field) {
-    if (!field) {
+  getFieldValue(field: ?Field) {
+    if (!field || !this.state.endpoint) {
       return ''
       return ''
     }
     }
     if (this.state.endpoint[field.name]) {
     if (this.state.endpoint[field.name]) {
@@ -217,11 +218,7 @@ class Endpoint extends React.Component<Props, State> {
     return ''
     return ''
   }
   }
 
 
-  isValidating() {
-    return this.state.validating
-  }
-
-  handleFieldsChange(items) {
+  handleFieldsChange(items: { field: Field, value: any }[]) {
     let endpoint: EndpointType = { ...this.state.endpoint }
     let endpoint: EndpointType = { ...this.state.endpoint }
 
 
     items.forEach(item => {
     items.forEach(item => {
@@ -235,8 +232,8 @@ class Endpoint extends React.Component<Props, State> {
     if (!this.highlightRequired()) {
     if (!this.highlightRequired()) {
       this.setState({ validating: true })
       this.setState({ validating: true })
 
 
-      NotificationActions.notify('Saving endpoint ...')
-      EndpointActions.clearValidation()
+      NotificationStore.notify('Saving endpoint ...')
+      EndpointStore.clearValidation()
 
 
       if (this.state.isNew) {
       if (this.state.isNew) {
         this.add()
         this.add()
@@ -244,25 +241,31 @@ class Endpoint extends React.Component<Props, State> {
         this.update()
         this.update()
       }
       }
     } else {
     } else {
-      NotificationActions.notify('Please fill all the required fields', 'error')
+      NotificationStore.notify('Please fill all the required fields', 'error')
     }
     }
   }
   }
 
 
   handleShowErrorMessageClick() {
   handleShowErrorMessageClick() {
-    this.setState({ showErrorMessage: !this.state.showErrorMessage })
+    this.setState({ showErrorMessage: !this.state.showErrorMessage }, () => {
+      this.props.onResizeUpdate(this.scrollableRef)
+    })
   }
   }
 
 
   handleCopyErrorMessageClick() {
   handleCopyErrorMessageClick() {
-    let succesful = DomUtils.copyTextToClipboard(this.props.endpointStore.validation.message)
+    if (!EndpointStore.validation) {
+      return
+    }
+    // $FlowIssue
+    let succesful = DomUtils.copyTextToClipboard(EndpointStore.validation.message)
 
 
     if (succesful) {
     if (succesful) {
-      NotificationActions.notify('The message has been copied to clipboard.')
+      NotificationStore.notify('The message has been copied to clipboard.')
     }
     }
   }
   }
 
 
   handleCancelClick() {
   handleCancelClick() {
     if (this.props.deleteOnCancel && this.state.isNew === false) {
     if (this.props.deleteOnCancel && this.state.isNew === false) {
-      EndpointActions.delete(EndpointStore.getState().endpoints[0])
+      EndpointStore.delete(EndpointStore.endpoints[0])
     }
     }
     this.props.onCancelClick()
     this.props.onCancelClick()
   }
   }
@@ -274,24 +277,33 @@ class Endpoint extends React.Component<Props, State> {
   }
   }
 
 
   update() {
   update() {
-    EndpointActions.update(this.state.endpoint).promise.then(() => {
-      NotificationActions.notify('Validating endpoint ...')
-      EndpointActions.validate(this.state.endpoint)
+    if (!this.state.endpoint) {
+      return
+    }
+
+    EndpointStore.update(this.state.endpoint).then(() => {
+      NotificationStore.notify('Validating endpoint ...')
+      // $FlowIssue
+      EndpointStore.validate(this.state.endpoint)
     })
     })
   }
   }
 
 
   add() {
   add() {
-    EndpointActions.add(this.state.endpoint).promise.then(() => {
-      let endpoint = EndpointStore.getState().endpoints[0]
+    if (!this.state.endpoint) {
+      return
+    }
+
+    EndpointStore.add(this.state.endpoint).then(() => {
+      let endpoint = EndpointStore.endpoints[0]
       this.setState({ isNew: false, endpoint })
       this.setState({ isNew: false, endpoint })
-      NotificationActions.notify('Validating endpoint ...')
-      EndpointActions.validate(endpoint)
+      NotificationStore.notify('Validating endpoint ...')
+      EndpointStore.validate(endpoint)
     })
     })
   }
   }
 
 
   renderEndpointStatus() {
   renderEndpointStatus() {
-    let validation = this.props.endpointStore.validation
-    if (!this.isValidating() && !validation) {
+    let validation = EndpointStore.validation
+    if (!this.state.validating && !validation) {
       return null
       return null
     }
     }
 
 
@@ -334,8 +346,8 @@ class Endpoint extends React.Component<Props, State> {
     let actionButton = <Button large onClick={() => this.handleValidateClick()}>Validate and save</Button>
     let actionButton = <Button large onClick={() => this.handleValidateClick()}>Validate and save</Button>
 
 
     let message = 'Validating Endpoint ...'
     let message = 'Validating Endpoint ...'
-    if (this.state.validating || (this.props.endpointStore.validation && this.props.endpointStore.validation.valid)) {
-      if (this.props.endpointStore.validation && this.props.endpointStore.validation.valid) {
+    if (this.state.validating || (EndpointStore.validation && EndpointStore.validation.valid)) {
+      if (EndpointStore.validation && EndpointStore.validation.valid) {
         message = 'Saving ...'
         message = 'Saving ...'
       }
       }
 
 
@@ -352,19 +364,20 @@ class Endpoint extends React.Component<Props, State> {
   }
   }
 
 
   renderContent() {
   renderContent() {
-    if (this.props.providerStore.connectionSchemaLoading) {
+    const endpointType = this.getEndpointType()
+    if (ProviderStore.connectionSchemaLoading || !endpointType) {
       return null
       return null
     }
     }
-
     return (
     return (
       <Content>
       <Content>
         {this.renderEndpointStatus()}
         {this.renderEndpointStatus()}
-        {React.createElement(ContentPlugin[this.getEndpointType()] || ContentPlugin.default, {
-          connectionInfoSchema: this.props.providerStore.connectionInfoSchema,
-          validation: this.props.endpointStore.validation,
+        {React.createElement(ContentPlugin[endpointType] || ContentPlugin.default, {
+          connectionInfoSchema: ProviderStore.connectionInfoSchema,
+          // $FlowIgnore
+          validation: EndpointStore.validation,
           invalidFields: this.state.invalidFields,
           invalidFields: this.state.invalidFields,
           validating: this.state.validating,
           validating: this.state.validating,
-          disabled: this.isValidating() || (this.props.endpointStore.validation && this.props.endpointStore.validation.valid),
+          disabled: this.state.validating,
           cancelButtonText: this.props.cancelButtonText,
           cancelButtonText: this.props.cancelButtonText,
           getFieldValue: field => this.getFieldValue(field),
           getFieldValue: field => this.getFieldValue(field),
           highlightRequired: () => this.highlightRequired(),
           highlightRequired: () => this.highlightRequired(),
@@ -384,7 +397,7 @@ class Endpoint extends React.Component<Props, State> {
   }
   }
 
 
   renderLoading() {
   renderLoading() {
-    if (!this.props.providerStore.connectionSchemaLoading) {
+    if (!ProviderStore.connectionSchemaLoading) {
       return null
       return null
     }
     }
 
 
@@ -397,7 +410,7 @@ class Endpoint extends React.Component<Props, State> {
   }
   }
 
 
   render() {
   render() {
-    if (this.props.endpointStore.validation && this.props.endpointStore.validation.valid
+    if (EndpointStore.validation && EndpointStore.validation.valid
       && !this.closeTimeout) {
       && !this.closeTimeout) {
       this.closeTimeout = setTimeout(() => {
       this.closeTimeout = setTimeout(() => {
         this.props.onCancelClick({ autoClose: true })
         this.props.onCancelClick({ autoClose: true })
@@ -414,4 +427,4 @@ class Endpoint extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(Endpoint)
+export default Endpoint

+ 1 - 1
src/components/organisms/EndpointDetailsContent/index.jsx

@@ -73,7 +73,7 @@ const LoadingWrapper = styled.div`
 
 
 type Props = {
 type Props = {
   item: ?Endpoint,
   item: ?Endpoint,
-  connectionInfo: { [string]: mixed },
+  connectionInfo: ?$PropertyType<Endpoint, 'connection_info'>,
   loading: boolean,
   loading: boolean,
   onDeleteClick: () => void,
   onDeleteClick: () => void,
   onValidateClick: () => void,
   onValidateClick: () => void,

+ 5 - 4
src/components/organisms/EndpointValidation/index.jsx

@@ -22,8 +22,9 @@ import CopyButton from '../../atoms/CopyButton'
 import StatusImage from '../../atoms/StatusImage'
 import StatusImage from '../../atoms/StatusImage'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
+import type { Validation as ValidationType } from '../../../types/Endpoint'
 
 
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import DomUtils from '../../../utils/DomUtils'
 import DomUtils from '../../../utils/DomUtils'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
@@ -71,7 +72,7 @@ const Error = styled.div`
 
 
 type Props = {
 type Props = {
   loading: boolean,
   loading: boolean,
-  validation?: { valid: boolean, message: string },
+  validation?: ?ValidationType,
   onCancelClick: () => void,
   onCancelClick: () => void,
   onRetryClick: () => void,
   onRetryClick: () => void,
 }
 }
@@ -80,9 +81,9 @@ class EndpointValidation extends React.Component<Props> {
     let succesful = DomUtils.copyTextToClipboard(message)
     let succesful = DomUtils.copyTextToClipboard(message)
 
 
     if (succesful) {
     if (succesful) {
-      NotificationActions.notify('The value has been copied to clipboard.')
+      NotificationStore.notify('The value has been copied to clipboard.')
     } else {
     } else {
-      NotificationActions.notify('The value couldn\'t be copied', 'error')
+      NotificationStore.notify('The value couldn\'t be copied', 'error')
     }
     }
   }
   }
 
 

+ 10 - 10
src/components/organisms/Executions/index.jsx

@@ -76,7 +76,7 @@ const NoExecutionText = styled.div`
 `
 `
 
 
 type Props = {
 type Props = {
-  item: MainItem,
+  item: ?MainItem,
   onCancelExecutionClick: (execution: ?Execution) => void,
   onCancelExecutionClick: (execution: ?Execution) => void,
   onDeleteExecutionClick: (execution: ?Execution) => void,
   onDeleteExecutionClick: (execution: ?Execution) => void,
   onExecuteClick: () => void,
   onExecuteClick: () => void,
@@ -105,9 +105,9 @@ class Executions extends React.Component<Props, State> {
     let lastExecution = this.getLastExecution(props)
     let lastExecution = this.getLastExecution(props)
     let selectExecution = null
     let selectExecution = null
 
 
-    if (props.item.executions && this.props.item.executions) {
+    if (props.item && props.item.executions && this.props.item && this.props.item.executions) {
       if (this.props.item.executions.length !== props.item.executions.length
       if (this.props.item.executions.length !== props.item.executions.length
-        && lastExecution.status === 'RUNNING') {
+        && lastExecution && lastExecution.status === 'RUNNING') {
         selectExecution = lastExecution
         selectExecution = lastExecution
       }
       }
 
 
@@ -117,9 +117,9 @@ class Executions extends React.Component<Props, State> {
         if (!isSelectedAvailable) {
         if (!isSelectedAvailable) {
           // $FlowIssue
           // $FlowIssue
           let lastIndex = this.props.item.executions.findIndex(e => e.id === this.state.selectedExecution.id)
           let lastIndex = this.props.item.executions.findIndex(e => e.id === this.state.selectedExecution.id)
-
+          // $FlowIssue
           if (props.item.executions.length) {
           if (props.item.executions.length) {
-            if (props.item.executions[lastIndex]) {
+            if (props.item.executions.length - 1 >= lastIndex) {
               selectExecution = props.item.executions[lastIndex]
               selectExecution = props.item.executions[lastIndex]
             } else {
             } else {
               selectExecution = props.item.executions[lastIndex - 1]
               selectExecution = props.item.executions[lastIndex - 1]
@@ -149,11 +149,11 @@ class Executions extends React.Component<Props, State> {
   }
   }
 
 
   getLastExecution(props: Props) {
   getLastExecution(props: Props) {
-    return this.hasExecutions(props) && props.item.executions[props.item.executions.length - 1]
+    return this.hasExecutions(props) && props.item && props.item.executions[props.item.executions.length - 1]
   }
   }
 
 
   hasExecutions(props: Props) {
   hasExecutions(props: Props) {
-    return props.item.executions && props.item.executions.length
+    return props.item && props.item.executions && props.item.executions.length
   }
   }
 
 
   handlePreviousExecutionClick() {
   handlePreviousExecutionClick() {
@@ -164,14 +164,14 @@ class Executions extends React.Component<Props, State> {
       return
       return
     }
     }
 
 
-    this.setState({ selectedExecution: this.props.item.executions[selectedIndex - 1] })
+    this.setState({ selectedExecution: this.props.item ? this.props.item.executions[selectedIndex - 1] : null })
   }
   }
 
 
   handleNextExecutionClick() {
   handleNextExecutionClick() {
     // $FlowIssue
     // $FlowIssue
     let selectedIndex = this.props.item.executions.findIndex(e => e.id === this.state.selectedExecution.id)
     let selectedIndex = this.props.item.executions.findIndex(e => e.id === this.state.selectedExecution.id)
 
 
-    if (selectedIndex >= this.props.item.executions.length - 1) {
+    if (!this.props.item || selectedIndex >= this.props.item.executions.length - 1) {
       return
       return
     }
     }
 
 
@@ -189,7 +189,7 @@ class Executions extends React.Component<Props, State> {
   renderTimeline() {
   renderTimeline() {
     return (
     return (
       <Timeline
       <Timeline
-        items={this.props.item.executions || null}
+        items={this.props.item ? this.props.item.executions : null}
         selectedItem={this.state.selectedExecution}
         selectedItem={this.state.selectedExecution}
         onPreviousClick={() => { this.handlePreviousExecutionClick() }}
         onPreviousClick={() => { this.handlePreviousExecutionClick() }}
         onNextClick={() => { this.handleNextExecutionClick() }}
         onNextClick={() => { this.handleNextExecutionClick() }}

+ 2 - 2
src/components/organisms/LoginForm/index.jsx

@@ -27,7 +27,7 @@ import StyleProps from '../../styleUtils/StyleProps'
 import errorIcon from './images/error.svg'
 import errorIcon from './images/error.svg'
 
 
 import { loginButtons } from '../../../config'
 import { loginButtons } from '../../../config'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 
 
 const Form = styled.form`
 const Form = styled.form`
   background: rgba(221, 224, 229, 0.5);
   background: rgba(221, 224, 229, 0.5);
@@ -118,7 +118,7 @@ class LoginForm extends React.Component<Props, State> {
     e.preventDefault()
     e.preventDefault()
 
 
     if (this.state.username.length === 0 || this.state.password.length === 0) {
     if (this.state.username.length === 0 || this.state.password.length === 0) {
-      NotificationActions.notify('Please fill in all fields')
+      NotificationStore.notify('Please fill in all fields')
     } else {
     } else {
       this.props.onFormSubmit({ username: this.state.username, password: this.state.password })
       this.props.onFormSubmit({ username: this.state.username, password: this.state.password })
     }
     }

+ 18 - 14
src/components/organisms/MainDetails/index.jsx

@@ -106,24 +106,24 @@ const PropertyValue = styled.div`
 `
 `
 
 
 type Props = {
 type Props = {
-  item: MainItem,
+  item: ?MainItem,
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   bottomControls: React.Node,
   bottomControls: React.Node,
   loading: boolean,
   loading: boolean,
 }
 }
 class MainDetails extends React.Component<Props> {
 class MainDetails extends React.Component<Props> {
   getSourceEndpoint(): ?Endpoint {
   getSourceEndpoint(): ?Endpoint {
-    let endpoint = this.props.endpoints.find(e => e.id === this.props.item.origin_endpoint_id)
+    let endpoint = this.props.endpoints.find(e => this.props.item && e.id === this.props.item.origin_endpoint_id)
     return endpoint
     return endpoint
   }
   }
 
 
   getDestinationEndpoint(): ?Endpoint {
   getDestinationEndpoint(): ?Endpoint {
-    let endpoint = this.props.endpoints.find(e => e.id === this.props.item.destination_endpoint_id)
+    let endpoint = this.props.endpoints.find(e => this.props.item && e.id === this.props.item.destination_endpoint_id)
     return endpoint
     return endpoint
   }
   }
 
 
   getLastExecution() {
   getLastExecution() {
-    if (this.props.item.executions && this.props.item.executions.length) {
+    if (this.props.item && this.props.item.executions && this.props.item.executions.length) {
       return this.props.item.executions[this.props.item.executions.length - 1]
       return this.props.item.executions[this.props.item.executions.length - 1]
     }
     }
 
 
@@ -132,7 +132,11 @@ class MainDetails extends React.Component<Props> {
 
 
   getConnectedVms(networkId: string) {
   getConnectedVms(networkId: string) {
     let vms = []
     let vms = []
+    if (!this.props.item) {
+      return '-'
+    }
     Object.keys(this.props.item.info).forEach(key => {
     Object.keys(this.props.item.info).forEach(key => {
+      // $FlowIssue
       let instance = this.props.item.info[key]
       let instance = this.props.item.info[key]
       if (instance.export_info && instance.export_info.devices.nics.length) {
       if (instance.export_info && instance.export_info.devices.nics.length) {
         instance.export_info.devices.nics.forEach(nic => {
         instance.export_info.devices.nics.forEach(nic => {
@@ -153,7 +157,7 @@ class MainDetails extends React.Component<Props> {
     let networks = []
     let networks = []
     Object.keys(this.props.item.destination_environment.network_map).forEach(key => {
     Object.keys(this.props.item.destination_environment.network_map).forEach(key => {
       let newItem
       let newItem
-      if (typeof this.props.item.destination_environment.network_map[key] === 'object') {
+      if (this.props.item && typeof this.props.item.destination_environment.network_map[key] === 'object') {
         newItem = [
         newItem = [
           this.props.item.destination_environment.network_map[key].source_network,
           this.props.item.destination_environment.network_map[key].source_network,
           this.getConnectedVms(key),
           this.getConnectedVms(key),
@@ -165,7 +169,7 @@ class MainDetails extends React.Component<Props> {
         newItem = [
         newItem = [
           key,
           key,
           this.getConnectedVms(key),
           this.getConnectedVms(key),
-          this.props.item.destination_environment.network_map[key],
+          this.props.item ? this.props.item.destination_environment.network_map[key] : '-',
           'Existing network',
           'Existing network',
         ]
         ]
       }
       }
@@ -237,7 +241,7 @@ class MainDetails extends React.Component<Props> {
 
 
     return (
     return (
       <PropertiesTable>
       <PropertiesTable>
-        {Object.keys(this.props.item.destination_environment).map(propName => {
+        {this.props.item ? Object.keys(this.props.item.destination_environment).map(propName => {
           let skipProps = ['description', 'network_map']
           let skipProps = ['description', 'network_map']
           if (skipProps.find(p => p === propName)) {
           if (skipProps.find(p => p === propName)) {
             return null
             return null
@@ -245,10 +249,10 @@ class MainDetails extends React.Component<Props> {
           return (
           return (
             <PropertyRow key={propName}>
             <PropertyRow key={propName}>
               <PropertyName>{LabelDictionary.get(propName)}</PropertyName>
               <PropertyName>{LabelDictionary.get(propName)}</PropertyName>
-              <PropertyValue>{renderValue(this.props.item.destination_environment[propName])}</PropertyValue>
+              <PropertyValue>{renderValue(this.props.item ? this.props.item.destination_environment[propName] : '')}</PropertyValue>
             </PropertyRow>
             </PropertyRow>
           )
           )
-        })}
+        }) : null}
       </PropertiesTable>
       </PropertiesTable>
     )
     )
   }
   }
@@ -275,25 +279,25 @@ class MainDetails extends React.Component<Props> {
           <Row>
           <Row>
             <Field>
             <Field>
               <Label>Id</Label>
               <Label>Id</Label>
-              <CopyValue value={this.props.item.id} width="192px" />
+              <CopyValue value={this.props.item ? this.props.item.id : '-'} width="192px" />
             </Field>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <Field>
               <Label>Created</Label>
               <Label>Created</Label>
-              {this.props.item.created_at ? this.renderValue(DateUtils.getLocalTime(this.props.item.created_at).format('YYYY-MM-DD HH:mm:ss')) : <Value>-</Value>}
+              {this.props.item && this.props.item.created_at ? this.renderValue(DateUtils.getLocalTime(this.props.item.created_at).format('YYYY-MM-DD HH:mm:ss')) : <Value>-</Value>}
             </Field>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <Field>
               <Label>Description</Label>
               <Label>Description</Label>
-              {this.props.item.destination_environment && this.props.item.destination_environment.description ? this.renderValue(this.props.item.destination_environment.description) : <Value>-</Value>}
+              {this.props.item && this.props.item.destination_environment && this.props.item.destination_environment.description ? this.renderValue(this.props.item.destination_environment.description) : <Value>-</Value>}
             </Field>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <Field>
               <Label>Type</Label>
               <Label>Type</Label>
-              <Value capitalize>Coriolis {this.props.item.type}</Value>
+              <Value capitalize>Coriolis {this.props.item && this.props.item.type}</Value>
             </Field>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
@@ -325,7 +329,7 @@ class MainDetails extends React.Component<Props> {
           <Row>
           <Row>
             <Field>
             <Field>
               <Label>Instances</Label>
               <Label>Instances</Label>
-              <Value>{this.props.item.instances.join(', ')}</Value>
+              <Value>{this.props.item && this.props.item.instances.join(', ')}</Value>
             </Field>
             </Field>
           </Row>
           </Row>
         </Column>
         </Column>

+ 3 - 3
src/components/organisms/MigrationDetailsContent/index.jsx

@@ -51,7 +51,7 @@ const NavigationItems = [
 ]
 ]
 
 
 type Props = {
 type Props = {
-  item: MainItem,
+  item: ?MainItem,
   detailsLoading: boolean,
   detailsLoading: boolean,
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   page: string,
   page: string,
@@ -86,7 +86,7 @@ class MigrationDetailsContent extends React.Component<Props> {
   }
   }
 
 
   renderTasks() {
   renderTasks() {
-    if (this.props.page !== 'tasks' || !this.props.item.tasks) {
+    if (this.props.page !== 'tasks' || !this.props.item || !this.props.item.tasks) {
       return null
       return null
     }
     }
 
 
@@ -103,7 +103,7 @@ class MigrationDetailsContent extends React.Component<Props> {
         <DetailsNavigation
         <DetailsNavigation
           items={NavigationItems}
           items={NavigationItems}
           selectedValue={this.props.page}
           selectedValue={this.props.page}
-          itemId={this.props.item.id}
+          itemId={this.props.item ? this.props.item.id : ''}
           itemType="migration"
           itemType="migration"
         />
         />
         <DetailsBody>
         <DetailsBody>

+ 10 - 13
src/components/organisms/Notifications/index.jsx

@@ -17,8 +17,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React from 'react'
 import React from 'react'
 import styled, { injectGlobal } from 'styled-components'
 import styled, { injectGlobal } from 'styled-components'
 import NotificationSystem from 'react-notification-system'
 import NotificationSystem from 'react-notification-system'
+import { observe } from 'mobx'
 
 
 import NotificationStore from '../../../stores/NotificationStore'
 import NotificationStore from '../../../stores/NotificationStore'
+import type { NotificationItem } from '../../../types/NotificationItem'
 
 
 import NotificationsStyle from './style.js'
 import NotificationsStyle from './style.js'
 
 
@@ -28,9 +30,6 @@ injectGlobal`
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
 
 
-type StoreState = {
-  notifications: { message: string, level: string, action: string }[],
-}
 class Notifications extends React.Component<{}> {
 class Notifications extends React.Component<{}> {
   notificationsCount: number
   notificationsCount: number
   notificationSystem: NotificationSystem
   notificationSystem: NotificationSystem
@@ -41,29 +40,27 @@ class Notifications extends React.Component<{}> {
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
-    NotificationStore.listen((state) => { this.onStoreChange(state) })
-  }
-
-  componentWillUnmount() {
-    NotificationStore.unlisten(this.onStoreChange.bind(this))
+    observe(NotificationStore.notifications, change => {
+      this.handleStoreChange(change.object)
+    })
   }
   }
 
 
-  onStoreChange(state: StoreState) {
-    if (!state.notifications.length || state.notifications.length <= this.notificationsCount) {
+  handleStoreChange(notifications: NotificationItem[]) {
+    if (!notifications.length || notifications.length <= this.notificationsCount) {
       return
       return
     }
     }
 
 
-    let lastNotification = state.notifications[state.notifications.length - 1]
+    let lastNotification = notifications[notifications.length - 1]
     this.notificationSystem.addNotification({
     this.notificationSystem.addNotification({
       title: lastNotification.title || lastNotification.message,
       title: lastNotification.title || lastNotification.message,
       message: lastNotification.title ? lastNotification.message : null,
       message: lastNotification.title ? lastNotification.message : null,
       level: lastNotification.level || 'info',
       level: lastNotification.level || 'info',
       position: 'br',
       position: 'br',
       autoDismiss: 10,
       autoDismiss: 10,
-      action: lastNotification.action,
+      action: lastNotification.options ? lastNotification.options.action : null,
     })
     })
 
 
-    this.notificationsCount = state.notifications.length
+    this.notificationsCount = notifications.length
   }
   }
 
 
   render() {
   render() {

+ 19 - 37
src/components/organisms/PageHeader/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import Dropdown from '../../molecules/Dropdown'
 import Dropdown from '../../molecules/Dropdown'
 import NewItemDropdown from '../../molecules/NewItemDropdown'
 import NewItemDropdown from '../../molecules/NewItemDropdown'
@@ -29,10 +29,7 @@ import Endpoint from '../../organisms/Endpoint'
 
 
 import ProjectStore from '../../../stores/ProjectStore'
 import ProjectStore from '../../../stores/ProjectStore'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import NotificationActions from '../../../actions/NotificationActions'
 import NotificationStore from '../../../stores/NotificationStore'
 import NotificationStore from '../../../stores/NotificationStore'
-import ProviderActions from '../../../actions/ProviderActions'
 import ProviderStore from '../../../stores/ProviderStore'
 import ProviderStore from '../../../stores/ProviderStore'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -64,32 +61,16 @@ const Controls = styled.div`
 type Props = {
 type Props = {
   title: string,
   title: string,
   onProjectChange: (project: Project) => void,
   onProjectChange: (project: Project) => void,
-  onModalOpen: () => void,
-  onModalClose: () => void,
-  projectStore: any,
-  userStore: any,
-  providerStore: any,
-  notificationStore: any,
+  onModalOpen?: () => void,
+  onModalClose?: () => void,
 }
 }
 type State = {
 type State = {
   showChooseProviderModal: boolean,
   showChooseProviderModal: boolean,
   showEndpointModal: boolean,
   showEndpointModal: boolean,
   providerType?: string,
   providerType?: string,
 }
 }
+@observer
 class PageHeader extends React.Component<Props, State> {
 class PageHeader extends React.Component<Props, State> {
-  static getStores() {
-    return [UserStore, ProjectStore, ProviderStore, NotificationStore]
-  }
-
-  static getPropsFromStores(): $Shape<Props> {
-    return {
-      userStore: UserStore.getState(),
-      projectStore: ProjectStore.getState(),
-      providerStore: ProviderStore.getState(),
-      notificationStore: NotificationStore.getState(),
-    }
-  }
-
   constructor() {
   constructor() {
     super()
     super()
 
 
@@ -100,21 +81,22 @@ class PageHeader extends React.Component<Props, State> {
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
-    NotificationActions.loadNotifications()
+    NotificationStore.loadNotifications()
   }
   }
 
 
   getCurrentProject() {
   getCurrentProject() {
-    if (this.props.userStore.user && this.props.userStore.user.project) {
-      return this.props.projectStore.projects.find(p => p.id === this.props.userStore.user.project.id)
+    if (UserStore.user && UserStore.user.project) {
+      // $FlowIssue
+      return ProjectStore.projects.find(p => p.id === UserStore.user.project.id)
     }
     }
 
 
     return null
     return null
   }
   }
 
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
     switch (item.value) {
       case 'signout':
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
         return
       case 'profile':
       case 'profile':
         window.location.href = '/#/profile'
         window.location.href = '/#/profile'
@@ -126,7 +108,7 @@ class PageHeader extends React.Component<Props, State> {
   handleNewItem(item: ItemType) {
   handleNewItem(item: ItemType) {
     switch (item.value) {
     switch (item.value) {
       case 'endpoint':
       case 'endpoint':
-        ProviderActions.loadProviders()
+        ProviderStore.loadProviders()
         if (this.props.onModalOpen) {
         if (this.props.onModalOpen) {
           this.props.onModalOpen()
           this.props.onModalOpen()
         }
         }
@@ -137,7 +119,7 @@ class PageHeader extends React.Component<Props, State> {
   }
   }
 
 
   handleNotificationsClose() {
   handleNotificationsClose() {
-    NotificationActions.clearNotifications()
+    NotificationStore.clearNotifications()
   }
   }
 
 
   handleCloseChooseProviderModal() {
   handleCloseChooseProviderModal() {
@@ -162,7 +144,7 @@ class PageHeader extends React.Component<Props, State> {
     this.setState({ showEndpointModal: false })
     this.setState({ showEndpointModal: false })
   }
   }
 
 
-  handleBackEndpointModal(options) {
+  handleBackEndpointModal(options?: { autoClose?: boolean }) {
     this.setState({ showChooseProviderModal: !options || !options.autoClose, showEndpointModal: false })
     this.setState({ showChooseProviderModal: !options || !options.autoClose, showEndpointModal: false })
   }
   }
 
 
@@ -173,14 +155,14 @@ class PageHeader extends React.Component<Props, State> {
         <Controls>
         <Controls>
           <Dropdown
           <Dropdown
             selectedItem={this.getCurrentProject()}
             selectedItem={this.getCurrentProject()}
-            items={this.props.projectStore.projects}
+            items={ProjectStore.projects}
             onChange={this.props.onProjectChange}
             onChange={this.props.onProjectChange}
             noItemsMessage="Loading..."
             noItemsMessage="Loading..."
             labelField="name"
             labelField="name"
           />
           />
           <NewItemDropdown onChange={item => { this.handleNewItem(item) }} />
           <NewItemDropdown onChange={item => { this.handleNewItem(item) }} />
-          <NotificationDropdown items={this.props.notificationStore.persistedNotifications} onClose={() => this.handleNotificationsClose()} />
-          <UserDropdown user={this.props.userStore.user} onItemClick={item => { this.handleUserItemClick(item) }} />
+          <NotificationDropdown items={NotificationStore.persistedNotifications} onClose={() => this.handleNotificationsClose()} />
+          <UserDropdown user={UserStore.user} onItemClick={item => { this.handleUserItemClick(item) }} />
         </Controls>
         </Controls>
         <Modal
         <Modal
           isOpen={this.state.showChooseProviderModal}
           isOpen={this.state.showChooseProviderModal}
@@ -189,8 +171,8 @@ class PageHeader extends React.Component<Props, State> {
         >
         >
           <ChooseProvider
           <ChooseProvider
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
-            providers={this.props.providerStore.providers}
-            loading={this.props.providerStore.providersLoading}
+            providers={ProviderStore.providers}
+            loading={ProviderStore.providersLoading}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
           />
           />
         </Modal>
         </Modal>
@@ -211,4 +193,4 @@ class PageHeader extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(PageHeader)
+export default PageHeader

+ 13 - 10
src/components/organisms/ReplicaDetailsContent/index.jsx

@@ -16,7 +16,9 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
+import ScheduleStore from '../../../stores/ScheduleStore'
 import Button from '../../atoms/Button'
 import Button from '../../atoms/Button'
 import DetailsNavigation from '../../molecules/DetailsNavigation'
 import DetailsNavigation from '../../molecules/DetailsNavigation'
 import MainDetails from '../../organisms/MainDetails'
 import MainDetails from '../../organisms/MainDetails'
@@ -67,9 +69,9 @@ const NavigationItems = [
 
 
 type TimezoneValue = 'utc' | 'local'
 type TimezoneValue = 'utc' | 'local'
 type Props = {
 type Props = {
-  item: MainItem,
+  item: ?MainItem,
   endpoints: Endpoint[],
   endpoints: Endpoint[],
-  scheduleStore: any,
+  scheduleStore: typeof ScheduleStore,
   page: string,
   page: string,
   detailsLoading: boolean,
   detailsLoading: boolean,
   onCancelExecutionClick: (execution: ?Execution) => void,
   onCancelExecutionClick: (execution: ?Execution) => void,
@@ -78,14 +80,15 @@ type Props = {
   onCreateMigrationClick: () => void,
   onCreateMigrationClick: () => void,
   onDeleteReplicaClick: () => void,
   onDeleteReplicaClick: () => void,
   onDeleteReplicaDisksClick: () => void,
   onDeleteReplicaDisksClick: () => void,
-  onAddScheduleClick: () => void,
-  onScheduleChange: () => void,
-  onScheduleRemove: () => void,
+  onAddScheduleClick: (schedule: ScheduleType) => void,
+  onScheduleChange: (scheduleId: ?string, data: ScheduleType, forceSave?: boolean) => void,
+  onScheduleRemove: (scheduleId: ?string) => void,
   onScheduleSave: (schedule: ScheduleType) => void,
   onScheduleSave: (schedule: ScheduleType) => void,
 }
 }
 type State = {
 type State = {
   timezone: TimezoneValue,
   timezone: TimezoneValue,
 }
 }
+@observer
 class ReplicaDetailsContent extends React.Component<Props, State> {
 class ReplicaDetailsContent extends React.Component<Props, State> {
   constructor() {
   constructor() {
     super()
     super()
@@ -96,7 +99,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
   }
   }
 
 
   getLastExecution() {
   getLastExecution() {
-    return this.props.item.executions && this.props.item.executions.length
+    return this.props.item && this.props.item.executions && this.props.item.executions.length
       && this.props.item.executions[this.props.item.executions.length - 1]
       && this.props.item.executions[this.props.item.executions.length - 1]
   }
   }
 
 
@@ -106,8 +109,8 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
   }
   }
 
 
   isEndpointMissing() {
   isEndpointMissing() {
-    let originEndpoint = this.props.endpoints.find(e => e.id === this.props.item.origin_endpoint_id)
-    let targetEndpoint = this.props.endpoints.find(e => e.id === this.props.item.destination_endpoint_id)
+    let originEndpoint = this.props.endpoints.find(e => this.props.item && e.id === this.props.item.origin_endpoint_id)
+    let targetEndpoint = this.props.endpoints.find(e => this.props.item && e.id === this.props.item.destination_endpoint_id)
 
 
     return Boolean(!originEndpoint || !targetEndpoint)
     return Boolean(!originEndpoint || !targetEndpoint)
   }
   }
@@ -132,7 +135,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
             hollow
             hollow
             secondary
             secondary
             onClick={this.props.onDeleteReplicaDisksClick}
             onClick={this.props.onDeleteReplicaDisksClick}
-            disabled={!this.props.item.executions || this.props.item.executions.length === 0}
+            disabled={!this.props.item || !this.props.item.executions || this.props.item.executions.length === 0}
           >Delete Replica Disks</Button>
           >Delete Replica Disks</Button>
           <Button
           <Button
             alert
             alert
@@ -201,7 +204,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
         <DetailsNavigation
         <DetailsNavigation
           items={NavigationItems}
           items={NavigationItems}
           selectedValue={this.props.page}
           selectedValue={this.props.page}
-          itemId={this.props.item.id}
+          itemId={this.props.item ? this.props.item.id : ''}
           itemType="replica"
           itemType="replica"
         />
         />
         <DetailsBody>
         <DetailsBody>

+ 16 - 6
src/components/organisms/Schedule/index.jsx

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
 import Button from '../../atoms/Button'
 import Button from '../../atoms/Button'
 import StatusImage from '../../atoms/StatusImage'
 import StatusImage from '../../atoms/StatusImage'
@@ -106,13 +107,13 @@ const Buttons = styled.div`
 
 
 type TimeZoneValue = 'local' | 'utc'
 type TimeZoneValue = 'local' | 'utc'
 type Props = {
 type Props = {
-  schedules: ScheduleType[],
+  schedules: ?ScheduleType[],
   unsavedSchedules: ScheduleType[],
   unsavedSchedules: ScheduleType[],
   timezone: TimeZoneValue,
   timezone: TimeZoneValue,
   onTimezoneChange: (timezone: TimeZoneValue) => void,
   onTimezoneChange: (timezone: TimeZoneValue) => void,
   onAddScheduleClick: (schedule: ScheduleType) => void,
   onAddScheduleClick: (schedule: ScheduleType) => void,
-  onChange: (scheduleId: ?string, schedule: ScheduleType, forceSave?: boolean) => void,
-  onRemove: (scheduleId: ?string) => void,
+  onChange: (scheduleId: string, schedule: ScheduleType, forceSave?: boolean) => void,
+  onRemove: (scheduleId: string) => void,
   onSaveSchedule?: (schedule: ScheduleType) => void,
   onSaveSchedule?: (schedule: ScheduleType) => void,
   adding?: boolean,
   adding?: boolean,
   loading?: boolean,
   loading?: boolean,
@@ -126,6 +127,7 @@ type State = {
 }
 }
 
 
 const colWidths = ['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']
 const colWidths = ['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']
+@observer
 class Schedule extends React.Component<Props, State> {
 class Schedule extends React.Component<Props, State> {
   static defaultProps: $Shape<Props> = {
   static defaultProps: $Shape<Props> = {
     unsavedSchedules: [],
     unsavedSchedules: [],
@@ -156,7 +158,9 @@ class Schedule extends React.Component<Props, State> {
 
 
   handleDeleteConfirmation() {
   handleDeleteConfirmation() {
     this.setState({ showDeleteConfirmation: false })
     this.setState({ showDeleteConfirmation: false })
-    this.props.onRemove(this.state.selectedSchedule ? this.state.selectedSchedule.id : null)
+    if (this.state.selectedSchedule && this.state.selectedSchedule.id) {
+      this.props.onRemove(this.state.selectedSchedule.id)
+    }
   }
   }
 
 
   handleShowOptions(selectedSchedule: $Subtype<ScheduleType>) {
   handleShowOptions(selectedSchedule: $Subtype<ScheduleType>) {
@@ -174,7 +178,9 @@ class Schedule extends React.Component<Props, State> {
       options[f.name] = f.value || false
       options[f.name] = f.value || false
     })
     })
 
 
-    this.props.onChange(this.state.selectedSchedule ? this.state.selectedSchedule.id : null, options, true)
+    if (this.state.selectedSchedule && this.state.selectedSchedule.id) {
+      this.props.onChange(this.state.selectedSchedule.id, options, true)
+    }
   }
   }
 
 
   handleExecutionOptionsChange(fieldName: string, value: string) {
   handleExecutionOptionsChange(fieldName: string, value: string) {
@@ -256,6 +262,10 @@ class Schedule extends React.Component<Props, State> {
   }
   }
 
 
   renderBody() {
   renderBody() {
+    if (!this.props.schedules) {
+      return null
+    }
+
     return (
     return (
       <Body>
       <Body>
         {this.props.schedules.map(schedule => (
         {this.props.schedules.map(schedule => (
@@ -265,7 +275,7 @@ class Schedule extends React.Component<Props, State> {
             item={schedule}
             item={schedule}
             unsavedSchedules={this.props.unsavedSchedules}
             unsavedSchedules={this.props.unsavedSchedules}
             timezone={this.props.timezone}
             timezone={this.props.timezone}
-            onChange={(data, forceSave) => { this.props.onChange(schedule.id, data, forceSave) }}
+            onChange={(data, forceSave) => { if (schedule.id) this.props.onChange(schedule.id, data, forceSave) }}
             onSaveSchedule={() => { if (this.props.onSaveSchedule) this.props.onSaveSchedule(schedule) }}
             onSaveSchedule={() => { if (this.props.onSaveSchedule) this.props.onSaveSchedule(schedule) }}
             onShowOptionsClick={() => { this.handleShowOptions(schedule) }}
             onShowOptionsClick={() => { this.handleShowOptions(schedule) }}
             onDeleteClick={() => { this.handleDeleteClick(schedule) }}
             onDeleteClick={() => { this.handleDeleteClick(schedule) }}

+ 2 - 2
src/components/organisms/WizardEndpointList/index.jsx

@@ -65,8 +65,8 @@ type Props = {
   providers: string[],
   providers: string[],
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   loading: boolean,
   loading: boolean,
-  selectedEndpoint: Endpoint,
-  otherEndpoint: Endpoint,
+  selectedEndpoint: ?Endpoint,
+  otherEndpoint: ?Endpoint,
   onChange: (endpoint: Endpoint) => void,
   onChange: (endpoint: Endpoint) => void,
   onAddEndpoint: (provider: string) => void,
   onAddEndpoint: (provider: string) => void,
 }
 }

+ 1 - 1
src/components/organisms/WizardInstances/index.jsx

@@ -180,7 +180,7 @@ const BigInstanceImage = styled.div`
 
 
 type Props = {
 type Props = {
   instances: InstanceType[],
   instances: InstanceType[],
-  selectedInstances: InstanceType[],
+  selectedInstances: ?InstanceType[],
   currentPage: number,
   currentPage: number,
   loading: boolean,
   loading: boolean,
   searching: boolean,
   searching: boolean,

+ 2 - 2
src/components/organisms/WizardNetworks/index.jsx

@@ -23,7 +23,7 @@ import Dropdown from '../../molecules/Dropdown'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 import type { Instance, Nic as NicType } from '../../../types/Instance'
 import type { Instance, Nic as NicType } from '../../../types/Instance'
-import type { Network } from '../../../types/Network'
+import type { Network, NetworkMap } from '../../../types/Network'
 
 
 import networkImage from './images/network.svg'
 import networkImage from './images/network.svg'
 import bigNetworkImage from './images/network-big.svg'
 import bigNetworkImage from './images/network-big.svg'
@@ -109,7 +109,7 @@ type Props = {
   loadingInstancesDetails: boolean,
   loadingInstancesDetails: boolean,
   networks: Network[],
   networks: Network[],
   instancesDetails: Instance[],
   instancesDetails: Instance[],
-  selectedNetworks: Network[],
+  selectedNetworks: ?NetworkMap[],
   onChange: (nic: NicType, network: Network) => void,
   onChange: (nic: NicType, network: Network) => void,
 }
 }
 class WizardNetworks extends React.Component<Props> {
 class WizardNetworks extends React.Component<Props> {

+ 3 - 3
src/components/organisms/WizardOptions/index.jsx

@@ -42,8 +42,8 @@ const WizardOptionsFieldStyled = styled(WizardOptionsField) `
 
 
 type Props = {
 type Props = {
   fields: Field[],
   fields: Field[],
-  selectedInstances: Instance[],
-  data: { [string]: mixed },
+  selectedInstances: ?Instance[],
+  data: ?{ [string]: mixed },
   onChange: (field: Field, value: any) => void,
   onChange: (field: Field, value: any) => void,
   useAdvancedOptions: boolean,
   useAdvancedOptions: boolean,
   onAdvancedOptionsToggle: (showAdvanced: boolean) => void,
   onAdvancedOptionsToggle: (showAdvanced: boolean) => void,
@@ -67,7 +67,7 @@ class WizardOptions extends React.Component<Props> {
       fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'strict-boolean', default: false })
       fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'strict-boolean', default: false })
     }
     }
 
 
-    if (this.props.selectedInstances.length > 1) {
+    if (this.props.selectedInstances && this.props.selectedInstances.length > 1) {
       fieldsSchema.unshift({ name: 'separate_vm', type: 'strict-boolean', default: true })
       fieldsSchema.unshift({ name: 'separate_vm', type: 'strict-boolean', default: true })
     }
     }
 
 

+ 29 - 14
src/components/organisms/WizardPageContent/index.jsx

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
 import EndpointLogos from '../../atoms/EndpointLogos'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import WizardType from '../../molecules/WizardType'
 import WizardType from '../../molecules/WizardType'
@@ -33,8 +34,15 @@ import Palette from '../../styleUtils/Palette'
 import { providerTypes, wizardConfig } from '../../../config'
 import { providerTypes, wizardConfig } from '../../../config'
 import type { WizardData } from '../../../types/WizardData'
 import type { WizardData } from '../../../types/WizardData'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Endpoint } from '../../../types/Endpoint'
+import type { Instance, Nic } from '../../../types/Instance'
+import type { Field } from '../../../types/Field'
+import type { Network } from '../../../types/Network'
+import type { Schedule as ScheduleType } from '../../../types/Schedule'
+import InstanceStore from '../../../stores/InstanceStore'
+import ProviderStore from '../../../stores/ProviderStore'
 
 
 import migrationArrowImage from './images/migration.js'
 import migrationArrowImage from './images/migration.js'
+import NetworkStore from '../../../stores/NetworkStore'
 
 
 const bodyWidth = 800
 const bodyWidth = 800
 const Wrapper = styled.div`
 const Wrapper = styled.div`
@@ -86,9 +94,9 @@ type Props = {
   page: { id: string, title: string },
   page: { id: string, title: string },
   type: 'replica' | 'migration',
   type: 'replica' | 'migration',
   nextButtonDisabled: boolean,
   nextButtonDisabled: boolean,
-  providerStore: any,
-  instanceStore: any,
-  networkStore: any,
+  providerStore: typeof ProviderStore,
+  instanceStore: typeof InstanceStore,
+  networkStore: typeof NetworkStore,
   wizardData: WizardData,
   wizardData: WizardData,
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   onTypeChange: (isReplicaChecked: ?boolean) => void,
   onTypeChange: (isReplicaChecked: ?boolean) => void,
@@ -97,16 +105,16 @@ type Props = {
   onSourceEndpointChange: (endpoint: Endpoint) => void,
   onSourceEndpointChange: (endpoint: Endpoint) => void,
   onTargetEndpointChange: (endpoint: Endpoint) => void,
   onTargetEndpointChange: (endpoint: Endpoint) => void,
   onAddEndpoint: (provider: string, fromSource: boolean) => void,
   onAddEndpoint: (provider: string, fromSource: boolean) => void,
-  onInstancesSearchInputChange: () => void,
-  onInstancesNextPageClick: () => void,
+  onInstancesSearchInputChange: (searchText: string) => void,
+  onInstancesNextPageClick: (searchText: string) => void,
   onInstancesPreviousPageClick: () => void,
   onInstancesPreviousPageClick: () => void,
-  onInstancesReloadClick: () => void,
-  onInstanceClick: () => void,
-  onOptionsChange: () => void,
-  onNetworkChange: () => void,
-  onAddScheduleClick: () => void,
-  onScheduleChange: () => void,
-  onScheduleRemove: () => void,
+  onInstancesReloadClick: (searchText: string) => void,
+  onInstanceClick: (instance: Instance) => void,
+  onOptionsChange: (field: Field, value: any) => void,
+  onNetworkChange: (nic: Nic, network: Network) => void,
+  onAddScheduleClick: (schedule: ScheduleType) => void,
+  onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
+  onScheduleRemove: (scheudleId: string) => void,
   onContentRef: (ref: any) => void,
   onContentRef: (ref: any) => void,
 }
 }
 type TimezoneValue = 'local' | 'utc'
 type TimezoneValue = 'local' | 'utc'
@@ -114,6 +122,8 @@ type State = {
   useAdvancedOptions: boolean,
   useAdvancedOptions: boolean,
   timezone: TimezoneValue,
   timezone: TimezoneValue,
 }
 }
+
+@observer
 class WizardPageContent extends React.Component<Props, State> {
 class WizardPageContent extends React.Component<Props, State> {
   constructor() {
   constructor() {
     super()
     super()
@@ -149,9 +159,14 @@ class WizardPageContent extends React.Component<Props, State> {
   getProviders(type: string) {
   getProviders(type: string) {
     let providers = []
     let providers = []
     let providerType = this.getProvidersType(type)
     let providerType = this.getProvidersType(type)
+    let providersObject = this.props.providerStore.providers
+
+    if (!providersObject) {
+      return []
+    }
 
 
-    Object.keys(this.props.providerStore.providers || {}).forEach(provider => {
-      if (this.props.providerStore.providers[provider].types.findIndex(t => t === providerType) > -1) {
+    Object.keys(providersObject).forEach(provider => {
+      if (providersObject[provider].types.findIndex(t => t === providerType) > -1) {
         providers.push(provider)
         providers.push(provider)
       }
       }
     })
     })

+ 12 - 8
src/components/organisms/WizardSummary/index.jsx

@@ -236,9 +236,10 @@ class WizardSummary extends React.Component<Props> {
         <SectionTitle>{type} Options</SectionTitle>
         <SectionTitle>{type} Options</SectionTitle>
         <OptionsList>
         <OptionsList>
           {this.props.wizardType === 'replica' ? executeNowOption : null}
           {this.props.wizardType === 'replica' ? executeNowOption : null}
-          {this.props.data.selectedInstances.length > 1 ? separateVmOption : null}
+          {this.props.data.selectedInstances && this.props.data.selectedInstances.length > 1 ? separateVmOption : null}
           {data.options ? Object.keys(data.options).map(optionName => {
           {data.options ? Object.keys(data.options).map(optionName => {
             if (optionName === 'execute_now' || optionName === 'separate_vm'
             if (optionName === 'execute_now' || optionName === 'separate_vm'
+              // $FlowIssue  
               || data.options[optionName] === null || data.options[optionName] === undefined) {
               || data.options[optionName] === null || data.options[optionName] === undefined) {
               return null
               return null
             }
             }
@@ -246,7 +247,10 @@ class WizardSummary extends React.Component<Props> {
             return (
             return (
               <Option key={optionName}>
               <Option key={optionName}>
                 <OptionLabel>{LabelDictionary.get(optionName)}</OptionLabel>
                 <OptionLabel>{LabelDictionary.get(optionName)}</OptionLabel>
-                <OptionValue>{this.renderOptionValue(data.options[optionName])}</OptionValue>
+                <OptionValue>{
+                  // $FlowIssue
+                  this.renderOptionValue(data.options[optionName])
+                }</OptionValue>
               </Option>
               </Option>
             )
             )
           }) : null}
           }) : null}
@@ -287,7 +291,7 @@ class WizardSummary extends React.Component<Props> {
       <Section>
       <Section>
         <SectionTitle>Instances</SectionTitle>
         <SectionTitle>Instances</SectionTitle>
         <Table>
         <Table>
-          {data.selectedInstances.map(instance => {
+          {data.selectedInstances ? data.selectedInstances.map(instance => {
             let flavorName = instance.flavor_name ? `/${instance.flavor_name}` : ''
             let flavorName = instance.flavor_name ? `/${instance.flavor_name}` : ''
             return (
             return (
               <Row key={instance.id}>
               <Row key={instance.id}>
@@ -295,7 +299,7 @@ class WizardSummary extends React.Component<Props> {
                 <InstanceRowSubtitle>{`${instance.num_cpu}vCPU/${instance.memory_mb}MB${flavorName}`}</InstanceRowSubtitle>
                 <InstanceRowSubtitle>{`${instance.num_cpu}vCPU/${instance.memory_mb}MB${flavorName}`}</InstanceRowSubtitle>
               </Row>
               </Row>
             )
             )
-          })}
+          }) : null}
         </Table>
         </Table>
       </Section>
       </Section>
     )
     )
@@ -311,15 +315,15 @@ class WizardSummary extends React.Component<Props> {
           <OverviewRow>
           <OverviewRow>
             <OverviewLabel>Source</OverviewLabel>
             <OverviewLabel>Source</OverviewLabel>
             <OverviewRowData>
             <OverviewRowData>
-              <StatusPill secondary small label={LabelDictionary.get(data.source.type).toUpperCase()} />
-              <OverviewRowLabel>{data.source.name}</OverviewRowLabel>
+              <StatusPill secondary small label={LabelDictionary.get(data.source && data.source.type).toUpperCase()} />
+              <OverviewRowLabel>{data.source ? data.source.name : ''}</OverviewRowLabel>
             </OverviewRowData>
             </OverviewRowData>
           </OverviewRow>
           </OverviewRow>
           <OverviewRow>
           <OverviewRow>
             <OverviewLabel>Target</OverviewLabel>
             <OverviewLabel>Target</OverviewLabel>
             <OverviewRowData>
             <OverviewRowData>
-              <StatusPill secondary small label={LabelDictionary.get(data.target.type).toUpperCase()} />
-              <OverviewRowLabel>{data.target.name}</OverviewRowLabel>
+              <StatusPill secondary small label={LabelDictionary.get(data.target && data.target.type).toUpperCase()} />
+              <OverviewRowLabel>{data.target && data.target.name}</OverviewRowLabel>
             </OverviewRowData>
             </OverviewRowData>
           </OverviewRow>
           </OverviewRow>
           <OverviewRow>
           <OverviewRow>

+ 32 - 43
src/components/pages/EndpointDetailsPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -28,13 +28,9 @@ import EndpointValidation from '../../organisms/EndpointValidation'
 import Endpoint from '../../organisms/Endpoint'
 import Endpoint from '../../organisms/Endpoint'
 
 
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
-import EndpointActions from '../../../actions/EndpointActions'
 import MigrationStore from '../../../stores/MigrationStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
-import MigrationActions from '../../../actions/MigrationActions'
-import ReplicaActions from '../../../actions/ReplicaActions'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 
 
 import endpointImage from './images/endpoint.svg'
 import endpointImage from './images/endpoint.svg'
@@ -43,10 +39,6 @@ const Wrapper = styled.div``
 
 
 type Props = {
 type Props = {
   match: any,
   match: any,
-  endpointStore: any,
-  userStore: any,
-  migrationStore: any,
-  replicaStore: any,
 }
 }
 type State = {
 type State = {
   showDeleteEndpointConfirmation: boolean,
   showDeleteEndpointConfirmation: boolean,
@@ -55,20 +47,8 @@ type State = {
   showEndpointInUseModal: boolean,
   showEndpointInUseModal: boolean,
   showEndpointInUseLoadingModal: boolean,
   showEndpointInUseLoadingModal: boolean,
 }
 }
+@observer
 class EndpointDetailsPage extends React.Component<Props, State> {
 class EndpointDetailsPage extends React.Component<Props, State> {
-  static getStores() {
-    return [EndpointStore, UserStore, MigrationStore, ReplicaStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      endpointStore: EndpointStore.getState(),
-      userStore: UserStore.getState(),
-      migrationStore: MigrationStore.getState(),
-      replicaStore: ReplicaStore.getState(),
-    }
-  }
-
   constructor() {
   constructor() {
     super()
     super()
 
 
@@ -88,18 +68,18 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    EndpointActions.clearConnectionInfo()
+    EndpointStore.clearConnectionInfo()
   }
   }
 
 
   getEndpoint(): ?EndpointType {
   getEndpoint(): ?EndpointType {
-    return this.props.endpointStore.endpoints.find(e => e.id === this.props.match.params.id) || null
+    return EndpointStore.endpoints.find(e => e.id === this.props.match.params.id) || null
   }
   }
 
 
   getEndpointUsage() {
   getEndpointUsage() {
     let endpointId = this.props.match.params.id
     let endpointId = this.props.match.params.id
-    let replicasCount = this.props.replicaStore.replicas.filter(
+    let replicasCount = ReplicaStore.replicas.filter(
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId).length
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId).length
-    let migrationsCount = this.props.migrationStore.migrations.filter(
+    let migrationsCount = MigrationStore.migrations.filter(
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId).length
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId).length
 
 
     return { migrationsCount, replicasCount }
     return { migrationsCount, replicasCount }
@@ -108,7 +88,7 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   handleUserItemClick(item: { value: string }) {
   handleUserItemClick(item: { value: string }) {
     switch (item.value) {
     switch (item.value) {
       case 'signout':
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
         return
       case 'profile':
       case 'profile':
         window.location.href = '/#/profile'
         window.location.href = '/#/profile'
@@ -124,7 +104,7 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   handleDeleteEndpointClick() {
   handleDeleteEndpointClick() {
     this.setState({ showEndpointInUseLoadingModal: true })
     this.setState({ showEndpointInUseLoadingModal: true })
 
 
-    Promise.all([ReplicaActions.getReplicas().promise, MigrationActions.getMigrations().promise]).then(() => {
+    Promise.all([ReplicaStore.getReplicas(), MigrationStore.getMigrations()]).then(() => {
       let endpointUsage = this.getEndpointUsage()
       let endpointUsage = this.getEndpointUsage()
 
 
       if (endpointUsage.migrationsCount === 0 && endpointUsage.replicasCount === 0) {
       if (endpointUsage.migrationsCount === 0 && endpointUsage.replicasCount === 0) {
@@ -138,7 +118,10 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   handleDeleteEndpointConfirmation() {
   handleDeleteEndpointConfirmation() {
     this.setState({ showDeleteEndpointConfirmation: false })
     this.setState({ showDeleteEndpointConfirmation: false })
     window.location.href = '/#/endpoints'
     window.location.href = '/#/endpoints'
-    EndpointActions.delete(this.getEndpoint())
+    let endpoint = this.getEndpoint()
+    if (endpoint) {
+      EndpointStore.delete(endpoint)
+    }
   }
   }
 
 
   handleCloseDeleteEndpointConfirmation() {
   handleCloseDeleteEndpointConfirmation() {
@@ -146,16 +129,22 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleValidateClick() {
   handleValidateClick() {
-    EndpointActions.validate(this.getEndpoint())
+    let endpoint = this.getEndpoint()
+    if (endpoint) {
+      EndpointStore.validate(endpoint)
+    }
     this.setState({ showValidationModal: true })
     this.setState({ showValidationModal: true })
   }
   }
 
 
   handleRetryValidation() {
   handleRetryValidation() {
-    EndpointActions.validate(this.getEndpoint())
+    let endpoint = this.getEndpoint()
+    if (endpoint) {
+      EndpointStore.validate(endpoint)
+    }
   }
   }
 
 
   handleCloseValidationModal() {
   handleCloseValidationModal() {
-    EndpointActions.clearValidation()
+    EndpointStore.clearValidation()
     this.setState({ showValidationModal: false })
     this.setState({ showValidationModal: false })
   }
   }
 
 
@@ -163,8 +152,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
     this.setState({ showEndpointModal: true })
     this.setState({ showEndpointModal: true })
   }
   }
 
 
-  handleEditValidateClick(endpoint) {
-    EndpointActions.validate(endpoint)
+  handleEditValidateClick(endpoint: EndpointType) {
+    EndpointStore.validate(endpoint)
   }
   }
 
 
   handleCloseEndpointModal() {
   handleCloseEndpointModal() {
@@ -176,13 +165,13 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   loadData() {
   loadData() {
-    EndpointActions.getEndpoints().promise.then(() => {
+    EndpointStore.getEndpoints().then(() => {
       let endpoint = this.getEndpoint()
       let endpoint = this.getEndpoint()
 
 
       if (endpoint && endpoint.connection_info && endpoint.connection_info.secret_ref) {
       if (endpoint && endpoint.connection_info && endpoint.connection_info.secret_ref) {
-        EndpointActions.getConnectionInfo(endpoint)
+        EndpointStore.getConnectionInfo(endpoint)
       } else if (endpoint && endpoint.connection_info) {
       } else if (endpoint && endpoint.connection_info) {
-        EndpointActions.getConnectionInfoSuccess(endpoint.connection_info)
+        EndpointStore.setConnectionInfo(endpoint.connection_info)
       }
       }
     })
     })
   }
   }
@@ -193,7 +182,7 @@ class EndpointDetailsPage extends React.Component<Props, State> {
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
           pageHeaderComponent={<DetailsPageHeader
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           />}
           contentHeaderComponent={<DetailsContentHeader
           contentHeaderComponent={<DetailsContentHeader
@@ -206,8 +195,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
           />}
           />}
           contentComponent={<EndpointDetailsContent
           contentComponent={<EndpointDetailsContent
             item={endpoint}
             item={endpoint}
-            loading={this.props.endpointStore.connectionInfoLoading || this.props.endpointStore.loading}
-            connectionInfo={this.props.endpointStore.connectionInfo}
+            loading={EndpointStore.connectionInfoLoading || EndpointStore.loading}
+            connectionInfo={EndpointStore.connectionInfo}
             onDeleteClick={() => { this.handleDeleteEndpointClick() }}
             onDeleteClick={() => { this.handleDeleteEndpointClick() }}
             onValidateClick={() => { this.handleValidateClick() }}
             onValidateClick={() => { this.handleValidateClick() }}
             onEditClick={() => { this.handleEditClick() }}
             onEditClick={() => { this.handleEditClick() }}
@@ -240,8 +229,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
           onRequestClose={() => { this.handleCloseValidationModal() }}
           onRequestClose={() => { this.handleCloseValidationModal() }}
         >
         >
           <EndpointValidation
           <EndpointValidation
-            validation={this.props.endpointStore.validation}
-            loading={this.props.endpointStore.validating}
+            validation={EndpointStore.validation}
+            loading={EndpointStore.validating}
             onCancelClick={() => { this.handleCloseValidationModal() }}
             onCancelClick={() => { this.handleCloseValidationModal() }}
             onRetryClick={() => { this.handleRetryValidation() }}
             onRetryClick={() => { this.handleRetryValidation() }}
           />
           />
@@ -262,4 +251,4 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(EndpointDetailsPage)
+export default EndpointDetailsPage

+ 50 - 66
src/components/pages/EndpointsPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import MainTemplate from '../../templates/MainTemplate'
 import MainTemplate from '../../templates/MainTemplate'
 import Navigation from '../../organisms/Navigation'
 import Navigation from '../../organisms/Navigation'
@@ -28,6 +28,7 @@ import Modal from '../../molecules/Modal'
 import ChooseProvider from '../../organisms/ChooseProvider'
 import ChooseProvider from '../../organisms/ChooseProvider'
 import Endpoint from '../../organisms/Endpoint'
 import Endpoint from '../../organisms/Endpoint'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
+import type { Project } from '../../../types/Project'
 
 
 import endpointImage from './images/endpoint-large.svg'
 import endpointImage from './images/endpoint-large.svg'
 
 
@@ -36,13 +37,7 @@ import UserStore from '../../../stores/UserStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
-import ProjectActions from '../../../actions/ProjectActions'
 import ProviderStore from '../../../stores/ProviderStore'
 import ProviderStore from '../../../stores/ProviderStore'
-import ProviderActions from '../../../actions/ProviderActions'
-import EndpointActions from '../../../actions/EndpointActions'
-import MigrationActions from '../../../actions/MigrationActions'
-import ReplicaActions from '../../../actions/ReplicaActions'
-import UserActions from '../../../actions/UserActions'
 import Wait from '../../../utils/Wait'
 import Wait from '../../../utils/Wait'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import LabelDictionary from '../../../utils/LabelDictionary'
 
 
@@ -52,14 +47,6 @@ const BulkActions = [
   { label: 'Delete', value: 'delete' },
   { label: 'Delete', value: 'delete' },
 ]
 ]
 
 
-type Props = {
-  projectStore: any,
-  userStore: any,
-  endpointStore: any,
-  migrationStore: any,
-  replicaStore: any,
-  providerStore: any,
-}
 type State = {
 type State = {
   showDeleteEndpointsConfirmation: boolean,
   showDeleteEndpointsConfirmation: boolean,
   confirmationItems: ?EndpointType[],
   confirmationItems: ?EndpointType[],
@@ -67,22 +54,8 @@ type State = {
   showEndpointModal: boolean,
   showEndpointModal: boolean,
   providerType: ?string,
   providerType: ?string,
 }
 }
-class EndpointsPage extends React.Component<Props, State> {
-  static getStores() {
-    return [UserStore, ProjectStore, EndpointStore, MigrationStore, ReplicaStore, ProviderStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      userStore: UserStore.getState(),
-      projectStore: ProjectStore.getState(),
-      endpointStore: EndpointStore.getState(),
-      migrationStore: MigrationStore.getState(),
-      replicaStore: ReplicaStore.getState(),
-      providerStore: ProviderStore.getState(),
-    }
-  }
-
+@observer
+class EndpointsPage extends React.Component<{}, State> {
   pollInterval: IntervalID
   pollInterval: IntervalID
 
 
   constructor() {
   constructor() {
@@ -100,10 +73,10 @@ class EndpointsPage extends React.Component<Props, State> {
   componentDidMount() {
   componentDidMount() {
     document.title = 'Coriolis Endpoints'
     document.title = 'Coriolis Endpoints'
 
 
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints()
-    MigrationActions.getMigrations()
-    ReplicaActions.getReplicas()
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints()
+    MigrationStore.getMigrations()
+    ReplicaStore.getReplicas()
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
@@ -112,7 +85,7 @@ class EndpointsPage extends React.Component<Props, State> {
 
 
   getFilterItems() {
   getFilterItems() {
     let types = [{ label: 'All', value: 'all' }]
     let types = [{ label: 'All', value: 'all' }]
-    this.props.endpointStore.endpoints.forEach(endpoint => {
+    EndpointStore.endpoints.forEach(endpoint => {
       if (!types.find(t => t.value === endpoint.type)) {
       if (!types.find(t => t.value === endpoint.type)) {
         types.push({ label: LabelDictionary.get(endpoint.type), value: endpoint.type })
         types.push({ label: LabelDictionary.get(endpoint.type), value: endpoint.type })
       }
       }
@@ -121,38 +94,39 @@ class EndpointsPage extends React.Component<Props, State> {
     return types
     return types
   }
   }
 
 
-  getEndpointUsage(endpoint: Endpoint) {
-    let replicasCount = this.props.replicaStore.replicas.filter(
+  getEndpointUsage(endpoint: EndpointType) {
+    let replicasCount = ReplicaStore.replicas.filter(
       r => r.origin_endpoint_id === endpoint.id || r.destination_endpoint_id === endpoint.id).length
       r => r.origin_endpoint_id === endpoint.id || r.destination_endpoint_id === endpoint.id).length
-    let migrationsCount = this.props.migrationStore.migrations.filter(
+    let migrationsCount = MigrationStore.migrations.filter(
       r => r.origin_endpoint_id === endpoint.id || r.destination_endpoint_id === endpoint.id).length
       r => r.origin_endpoint_id === endpoint.id || r.destination_endpoint_id === endpoint.id).length
 
 
     return { migrationsCount, replicasCount }
     return { migrationsCount, replicasCount }
   }
   }
 
 
-  handleProjectChange(project) {
-    Wait.for(() => this.props.userStore.user.project.id === project.id, () => {
-      ProjectActions.getProjects()
-      EndpointActions.getEndpoints({ showLoading: true })
-      MigrationActions.getMigrations()
-      ReplicaActions.getReplicas()
+  handleProjectChange(project: Project) {
+    // $FlowIssue
+    Wait.for(() => UserStore.user.project.id === project.id, () => {
+      ProjectStore.getProjects()
+      EndpointStore.getEndpoints({ showLoading: true })
+      MigrationStore.getMigrations()
+      ReplicaStore.getReplicas()
     })
     })
 
 
-    UserActions.switchProject(project.id)
+    UserStore.switchProject(project.id)
   }
   }
 
 
   handleReloadButtonClick() {
   handleReloadButtonClick() {
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints({ showLoading: true })
-    MigrationActions.getMigrations()
-    ReplicaActions.getReplicas()
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints({ showLoading: true })
+    MigrationStore.getMigrations()
+    ReplicaStore.getReplicas()
   }
   }
 
 
-  handleItemClick(item) {
+  handleItemClick(item: EndpointType) {
     window.location.href = `/#/endpoint/${item.id}`
     window.location.href = `/#/endpoint/${item.id}`
   }
   }
 
 
-  handleActionChange(items, action) {
+  handleActionChange(items: EndpointType[], action: string) {
     if (action === 'delete') {
     if (action === 'delete') {
       this.setState({
       this.setState({
         showDeleteEndpointsConfirmation: true,
         showDeleteEndpointsConfirmation: true,
@@ -172,14 +146,14 @@ class EndpointsPage extends React.Component<Props, State> {
   handleDeleteEndpointsConfirmation() {
   handleDeleteEndpointsConfirmation() {
     if (this.state.confirmationItems) {
     if (this.state.confirmationItems) {
       this.state.confirmationItems.forEach(endpoint => {
       this.state.confirmationItems.forEach(endpoint => {
-        EndpointActions.delete(endpoint)
+        EndpointStore.delete(endpoint)
       })
       })
     }
     }
     this.handleCloseDeleteEndpointsConfirmation()
     this.handleCloseDeleteEndpointsConfirmation()
   }
   }
 
 
   handleEmptyListButtonClick() {
   handleEmptyListButtonClick() {
-    ProviderActions.loadProviders()
+    ProviderStore.loadProviders()
     this.setState({ showChooseProviderModal: true })
     this.setState({ showChooseProviderModal: true })
   }
   }
 
 
@@ -187,7 +161,7 @@ class EndpointsPage extends React.Component<Props, State> {
     this.setState({ showChooseProviderModal: false })
     this.setState({ showChooseProviderModal: false })
   }
   }
 
 
-  handleProviderClick(providerType) {
+  handleProviderClick(providerType: string) {
     this.setState({
     this.setState({
       showChooseProviderModal: false,
       showChooseProviderModal: false,
       showEndpointModal: true,
       showEndpointModal: true,
@@ -199,11 +173,12 @@ class EndpointsPage extends React.Component<Props, State> {
     this.setState({ showEndpointModal: false })
     this.setState({ showEndpointModal: false })
   }
   }
 
 
-  itemFilterFunction(item, filterItem, filterText) {
-    if ((filterItem !== 'all' && (item.type !== filterItem)) ||
-      (item.name.toLowerCase().indexOf(filterText || '') === -1 &&
+  itemFilterFunction(item: any, filterItem?: ?string, filterText?: string) {
+    let endpoint: EndpointType = item
+    if ((filterItem !== 'all' && (endpoint.type !== filterItem)) ||
+      (endpoint.name.toLowerCase().indexOf(filterText || '') === -1 &&
       // $FlowIssue
       // $FlowIssue
-      item.description.toLowerCase().indexOf(filterText) === -1)
+      endpoint.description.toLowerCase().indexOf(filterText) === -1)
     ) {
     ) {
       return false
       return false
     }
     }
@@ -212,6 +187,7 @@ class EndpointsPage extends React.Component<Props, State> {
   }
   }
 
 
   render() {
   render() {
+    let items: any = EndpointStore.endpoints
     return (
     return (
       <Wrapper>
       <Wrapper>
         <MainTemplate
         <MainTemplate
@@ -220,12 +196,20 @@ class EndpointsPage extends React.Component<Props, State> {
             <FilterList
             <FilterList
               filterItems={this.getFilterItems()}
               filterItems={this.getFilterItems()}
               selectionLabel="endpoint"
               selectionLabel="endpoint"
-              loading={this.props.endpointStore.loading}
-              items={this.props.endpointStore.endpoints}
-              onItemClick={item => { this.handleItemClick(item) }}
+              loading={EndpointStore.loading}
+              items={items}
+              onItemClick={item => {
+                let anyItem: any = item
+                let endpoint: EndpointType = anyItem
+                this.handleItemClick(endpoint)
+              }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               actions={BulkActions}
               actions={BulkActions}
-              onActionChange={(items, action) => { this.handleActionChange(items, action) }}
+              onActionChange={(items, action) => {
+                let anyItems: any = items
+                let endpoints: EndpointType[] = anyItems
+                this.handleActionChange(endpoints, action)
+              }}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               renderItemComponent={options =>
               renderItemComponent={options =>
                 // $FlowIssue
                 // $FlowIssue
@@ -263,8 +247,8 @@ class EndpointsPage extends React.Component<Props, State> {
         >
         >
           <ChooseProvider
           <ChooseProvider
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
-            providers={this.props.providerStore.providers}
-            loading={this.props.providerStore.providersLoading}
+            providers={ProviderStore.providers}
+            loading={ProviderStore.providersLoading}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
           />
           />
         </Modal>
         </Modal>
@@ -284,4 +268,4 @@ class EndpointsPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(EndpointsPage)
+export default EndpointsPage

+ 12 - 40
src/components/pages/LoginPage/index.jsx

@@ -16,14 +16,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import EmptyTemplate from '../../templates/EmptyTemplate'
 import EmptyTemplate from '../../templates/EmptyTemplate'
 import Logo from '../../atoms/Logo'
 import Logo from '../../atoms/Logo'
 import LoginForm from '../../organisms/LoginForm'
 import LoginForm from '../../organisms/LoginForm'
 
 
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
-import UserActions from '../../../actions/UserActions'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
 
 
 import backgroundImage from './images/star-bg.jpg'
 import backgroundImage from './images/star-bg.jpg'
@@ -76,51 +75,24 @@ const CbsLogo = styled.a`
   cursor: pointer;
   cursor: pointer;
 `
 `
 
 
-type Props = {
-  loading: boolean,
-  loginFailedResponse: { status: string },
-  user: { username: string, email: string },
-}
-type State = {
-  loading: boolean,
-}
-class LoginPage extends React.Component<Props, State> {
-  static getStores() {
-    return [UserStore]
-  }
-
-  static getPropsFromStores() {
-    return UserStore.getState()
-  }
-
-  constructor() {
-    super()
-
-    this.state = {
-      loading: false,
-    }
-  }
-
+@observer
+class LoginPage extends React.Component<{}> {
   componentDidMount() {
   componentDidMount() {
     document.title = 'Log In'
     document.title = 'Log In'
   }
   }
 
 
-  componentWillReceiveProps(props) {
-    if (props.user) {
-      window.location.href = '/#/replicas'
-    }
-  }
-
-  handleFormSubmit(data) {
-    this.setState({ loading: true })
-
-    UserActions.login({
+  handleFormSubmit(data: { username: string, password: string }) {
+    UserStore.login({
       name: data.username,
       name: data.username,
       password: data.password,
       password: data.password,
     })
     })
   }
   }
 
 
   render() {
   render() {
+    if (UserStore.user) {
+      window.location.href = '/#/replicas'
+    }
+
     return (
     return (
       <EmptyTemplate>
       <EmptyTemplate>
         <Wrapper>
         <Wrapper>
@@ -128,8 +100,8 @@ class LoginPage extends React.Component<Props, State> {
             <Logo />
             <Logo />
             <StyledLoginForm
             <StyledLoginForm
               onFormSubmit={data => this.handleFormSubmit(data)}
               onFormSubmit={data => this.handleFormSubmit(data)}
-              loading={this.props.loading}
-              loginFailedResponse={this.props.loginFailedResponse}
+              loading={UserStore.loading}
+              loginFailedResponse={UserStore.loginFailedResponse}
             />
             />
             <Footer>
             <Footer>
               <FooterText>Coriolis® is a service offered by</FooterText>
               <FooterText>Coriolis® is a service offered by</FooterText>
@@ -142,4 +114,4 @@ class LoginPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(LoginPage)
+export default LoginPage

+ 25 - 37
src/components/pages/MigrationDetailsPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -26,11 +26,8 @@ import AlertModal from '../../organisms/AlertModal'
 
 
 import MigrationStore from '../../../stores/MigrationStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import MigrationActions from '../../../actions/MigrationActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
-import EndpointActions from '../../../actions/EndpointActions'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import { requestPollTimeout } from '../../../config'
 import { requestPollTimeout } from '../../../config'
 
 
 import migrationImage from './images/migration.svg'
 import migrationImage from './images/migration.svg'
@@ -39,27 +36,13 @@ const Wrapper = styled.div``
 
 
 type Props = {
 type Props = {
   match: any,
   match: any,
-  migrationStore: any,
-  endpointStore: any,
-  userStore: any,
 }
 }
 type State = {
 type State = {
   showDeleteMigrationConfirmation: boolean,
   showDeleteMigrationConfirmation: boolean,
   showCancelConfirmation: boolean,
   showCancelConfirmation: boolean,
 }
 }
+@observer
 class MigrationDetailsPage extends React.Component<Props, State> {
 class MigrationDetailsPage extends React.Component<Props, State> {
-  static getStores() {
-    return [MigrationStore, EndpointStore, UserStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      migrationStore: MigrationStore.getState(),
-      endpointStore: EndpointStore.getState(),
-      userStore: UserStore.getState(),
-    }
-  }
-
   pollInterval: IntervalID
   pollInterval: IntervalID
 
 
   constructor() {
   constructor() {
@@ -74,20 +57,20 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   componentDidMount() {
   componentDidMount() {
     document.title = 'Migration Details'
     document.title = 'Migration Details'
 
 
-    EndpointActions.getEndpoints()
+    EndpointStore.getEndpoints()
     this.pollData(true)
     this.pollData(true)
     this.pollInterval = setInterval(() => { this.pollData() }, requestPollTimeout)
     this.pollInterval = setInterval(() => { this.pollData() }, requestPollTimeout)
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    MigrationActions.clearDetails()
+    MigrationStore.clearDetails()
     clearInterval(this.pollInterval)
     clearInterval(this.pollInterval)
   }
   }
 
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
     switch (item.value) {
       case 'signout':
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
         return
       case 'profile':
       case 'profile':
         window.location.href = '/#/profile'
         window.location.href = '/#/profile'
@@ -107,7 +90,9 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   handleDeleteMigrationConfirmation() {
   handleDeleteMigrationConfirmation() {
     this.setState({ showDeleteMigrationConfirmation: false })
     this.setState({ showDeleteMigrationConfirmation: false })
     window.location.href = '/#/migrations'
     window.location.href = '/#/migrations'
-    MigrationActions.delete(this.props.migrationStore.migrationDetails.id)
+    if (MigrationStore.migrationDetails) {
+      MigrationStore.delete(MigrationStore.migrationDetails.id)
+    }
   }
   }
 
 
   handleCloseDeleteMigrationConfirmation() {
   handleCloseDeleteMigrationConfirmation() {
@@ -124,17 +109,20 @@ class MigrationDetailsPage extends React.Component<Props, State> {
 
 
   handleCancelConfirmation() {
   handleCancelConfirmation() {
     this.setState({ showCancelConfirmation: false })
     this.setState({ showCancelConfirmation: false })
-    MigrationActions.cancel(this.props.migrationStore.migrationDetails.id).promise.then(() => {
-      if (MigrationStore.getState().canceling === false) {
-        NotificationActions.notify('Canceled', 'success')
+    if (!MigrationStore.migrationDetails) {
+      return
+    }
+    MigrationStore.cancel(MigrationStore.migrationDetails.id).then(() => {
+      if (MigrationStore.canceling === false) {
+        NotificationStore.notify('Canceled', 'success')
       } else {
       } else {
-        NotificationActions.notify('The migration couldn\'t be canceled', 'error')
+        NotificationStore.notify('The migration couldn\'t be canceled', 'error')
       }
       }
     })
     })
   }
   }
 
 
-  pollData(showLoading) {
-    MigrationActions.getMigration(this.props.match.params.id, showLoading)
+  pollData(showLoading?: boolean) {
+    MigrationStore.getMigration(this.props.match.params.id, showLoading || false)
   }
   }
 
 
   render() {
   render() {
@@ -142,21 +130,21 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
           pageHeaderComponent={<DetailsPageHeader
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           />}
           contentHeaderComponent={<DetailsContentHeader
           contentHeaderComponent={<DetailsContentHeader
-            item={this.props.migrationStore.migrationDetails}
+            item={MigrationStore.migrationDetails}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             typeImage={migrationImage}
             typeImage={migrationImage}
             primaryInfoPill
             primaryInfoPill
             onCancelClick={() => { this.handleCancelMigrationClick() }}
             onCancelClick={() => { this.handleCancelMigrationClick() }}
           />}
           />}
           contentComponent={<MigrationDetailsContent
           contentComponent={<MigrationDetailsContent
-            item={this.props.migrationStore.migrationDetails}
-            endpoints={this.props.endpointStore.endpoints}
+            item={MigrationStore.migrationDetails}
+            endpoints={EndpointStore.endpoints}
             page={this.props.match.params.page || ''}
             page={this.props.match.params.page || ''}
-            detailsLoading={this.props.endpointStore.loading || this.props.migrationStore.detailsLoading}
+            detailsLoading={EndpointStore.loading || MigrationStore.detailsLoading}
             onDeleteMigrationClick={() => { this.handleDeleteMigrationClick() }}
             onDeleteMigrationClick={() => { this.handleDeleteMigrationClick() }}
           />}
           />}
         />
         />
@@ -182,4 +170,4 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(MigrationDetailsPage)
+export default MigrationDetailsPage

+ 37 - 55
src/components/pages/MigrationsPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import MainTemplate from '../../templates/MainTemplate'
 import MainTemplate from '../../templates/MainTemplate'
 import Navigation from '../../organisms/Navigation'
 import Navigation from '../../organisms/Navigation'
@@ -25,6 +25,7 @@ import PageHeader from '../../organisms/PageHeader'
 import AlertModal from '../../organisms/AlertModal'
 import AlertModal from '../../organisms/AlertModal'
 import MainListItem from '../../molecules/MainListItem'
 import MainListItem from '../../molecules/MainListItem'
 import type { MainItem } from '../../../types/MainItem'
 import type { MainItem } from '../../../types/MainItem'
+import type { Project } from '../../../types/Project'
 
 
 import migrationItemImage from './images/migration.svg'
 import migrationItemImage from './images/migration.svg'
 import migrationLargeImage from './images/migration-large.svg'
 import migrationLargeImage from './images/migration-large.svg'
@@ -33,12 +34,8 @@ import ProjectStore from '../../../stores/ProjectStore'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
-import ProjectActions from '../../../actions/ProjectActions'
-import MigrationActions from '../../../actions/MigrationActions'
-import EndpointActions from '../../../actions/EndpointActions'
-import UserActions from '../../../actions/UserActions'
 import Wait from '../../../utils/Wait'
 import Wait from '../../../utils/Wait'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
 
 
@@ -47,31 +44,13 @@ const BulkActions = [
   { label: 'Delete', value: 'delete' },
   { label: 'Delete', value: 'delete' },
 ]
 ]
 
 
-type Props = {
-  projectStore: any,
-  migrationStore: any,
-  userStore: any,
-  endpointStore: any,
-}
 type State = {
 type State = {
   showDeleteMigrationConfirmation: boolean,
   showDeleteMigrationConfirmation: boolean,
   showCancelMigrationConfirmation: boolean,
   showCancelMigrationConfirmation: boolean,
   confirmationItems: ?MainItem[],
   confirmationItems: ?MainItem[],
 }
 }
-class MigrationsPage extends React.Component<Props, State> {
-  static getStores() {
-    return [UserStore, ProjectStore, MigrationStore, EndpointStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      userStore: UserStore.getState(),
-      projectStore: ProjectStore.getState(),
-      migrationStore: MigrationStore.getState(),
-      endpointStore: EndpointStore.getState(),
-    }
-  }
-
+@observer
+class MigrationsPage extends React.Component<{}, State> {
   constructor() {
   constructor() {
     super()
     super()
 
 
@@ -85,17 +64,13 @@ class MigrationsPage extends React.Component<Props, State> {
   componentDidMount() {
   componentDidMount() {
     document.title = 'Coriolis Migrations'
     document.title = 'Coriolis Migrations'
 
 
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints()
-    MigrationActions.getMigrations()
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints()
+    MigrationStore.getMigrations()
   }
   }
 
 
-  getEndpoint(endpointId) {
-    if (!this.props.endpointStore.endpoints || this.props.endpointStore.endpoints === 0) {
-      return {}
-    }
-
-    return this.props.endpointStore.endpoints.find(endpoint => endpoint.id === endpointId) || {}
+  getEndpoint(endpointId: string) {
+    return EndpointStore.endpoints.find(endpoint => endpoint.id === endpointId)
   }
   }
 
 
   getFilterItems() {
   getFilterItems() {
@@ -107,23 +82,24 @@ class MigrationsPage extends React.Component<Props, State> {
     ]
     ]
   }
   }
 
 
-  handleProjectChange(project) {
-    Wait.for(() => this.props.userStore.user.project.id === project.id, () => {
-      ProjectActions.getProjects()
-      EndpointActions.getEndpoints()
-      MigrationActions.getMigrations({ showLoading: true })
+  handleProjectChange(project: Project) {
+    // $FlowIssue
+    Wait.for(() => UserStore.user.project.id === project.id, () => {
+      ProjectStore.getProjects()
+      EndpointStore.getEndpoints()
+      MigrationStore.getMigrations({ showLoading: true })
     })
     })
 
 
-    UserActions.switchProject(project.id)
+    UserStore.switchProject(project.id)
   }
   }
 
 
   handleReloadButtonClick() {
   handleReloadButtonClick() {
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints()
-    MigrationActions.getMigrations({ showLoading: true })
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints()
+    MigrationStore.getMigrations({ showLoading: true })
   }
   }
 
 
-  handleItemClick(item) {
+  handleItemClick(item: MainItem) {
     if (item.status === 'RUNNING') {
     if (item.status === 'RUNNING') {
       window.location.href = `/#/migration/tasks/${item.id}`
       window.location.href = `/#/migration/tasks/${item.id}`
     } else {
     } else {
@@ -131,7 +107,7 @@ class MigrationsPage extends React.Component<Props, State> {
     }
     }
   }
   }
 
 
-  handleActionChange(confirmationItems, action) {
+  handleActionChange(confirmationItems: MainItem[], action: string) {
     if (action === 'cancel') {
     if (action === 'cancel') {
       this.setState({
       this.setState({
         showCancelMigrationConfirmation: true,
         showCancelMigrationConfirmation: true,
@@ -150,9 +126,9 @@ class MigrationsPage extends React.Component<Props, State> {
       return
       return
     }
     }
     this.state.confirmationItems.forEach(migration => {
     this.state.confirmationItems.forEach(migration => {
-      MigrationActions.cancel(migration.id)
+      MigrationStore.cancel(migration.id)
     })
     })
-    NotificationActions.notify('Canceling migrations')
+    NotificationStore.notify('Canceling migrations')
     this.handleCloseCancelMigration()
     this.handleCloseCancelMigration()
   }
   }
 
 
@@ -175,7 +151,7 @@ class MigrationsPage extends React.Component<Props, State> {
       return
       return
     }
     }
     this.state.confirmationItems.forEach(migration => {
     this.state.confirmationItems.forEach(migration => {
-      MigrationActions.delete(migration.id)
+      MigrationStore.delete(migration.id)
     })
     })
     this.handleCloseDeleteMigrationConfirmation()
     this.handleCloseDeleteMigrationConfirmation()
   }
   }
@@ -184,7 +160,7 @@ class MigrationsPage extends React.Component<Props, State> {
     window.location.href = '/#/wizard/migration'
     window.location.href = '/#/wizard/migration'
   }
   }
 
 
-  searchText(item, text) {
+  searchText(item: MainItem, text?: string) {
     let result = false
     let result = false
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
       return true
       return true
@@ -201,7 +177,7 @@ class MigrationsPage extends React.Component<Props, State> {
     return result
     return result
   }
   }
 
 
-  itemFilterFunction(item, filterStatus, filterText) {
+  itemFilterFunction(item: MainItem, filterStatus?: ?string, filterText?: string) {
     if ((filterStatus !== 'all' && (item.status !== filterStatus)) ||
     if ((filterStatus !== 'all' && (item.status !== filterStatus)) ||
       !this.searchText(item, filterText)
       !this.searchText(item, filterText)
     ) {
     ) {
@@ -234,8 +210,8 @@ class MigrationsPage extends React.Component<Props, State> {
             <FilterList
             <FilterList
               filterItems={this.getFilterItems()}
               filterItems={this.getFilterItems()}
               selectionLabel="migration"
               selectionLabel="migration"
-              loading={this.props.migrationStore.loading}
-              items={this.props.migrationStore.migrations}
+              loading={MigrationStore.loading}
+              items={MigrationStore.migrations}
               onItemClick={item => { this.handleItemClick(item) }}
               onItemClick={item => { this.handleItemClick(item) }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               actions={BulkActions}
               actions={BulkActions}
@@ -245,7 +221,13 @@ class MigrationsPage extends React.Component<Props, State> {
                 (<MainListItem
                 (<MainListItem
                   {...options}
                   {...options}
                   image={migrationItemImage}
                   image={migrationItemImage}
-                  endpointType={id => this.getEndpoint(id).type}
+                  endpointType={id => {
+                    let endpoint = this.getEndpoint(id)
+                    if (endpoint) {
+                      return endpoint.type
+                    }
+                    return ''
+                  }}
                   useTasksRemaining
                   useTasksRemaining
                 />)
                 />)
               }
               }
@@ -269,4 +251,4 @@ class MigrationsPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(MigrationsPage)
+export default MigrationsPage

+ 58 - 65
src/components/pages/ReplicaDetailsPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -28,15 +28,13 @@ import AlertModal from '../../organisms/AlertModal'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import type { MainItem } from '../../../types/MainItem'
 import type { MainItem } from '../../../types/MainItem'
 import type { Execution } from '../../../types/Execution'
 import type { Execution } from '../../../types/Execution'
+import type { Schedule } from '../../../types/Schedule'
+import type { Field } from '../../../types/Field'
 
 
 import ReplicaStore from '../../../stores/ReplicaStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
+import MigrationStore from '../../../stores/MigrationStore'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import ReplicaActions from '../../../actions/ReplicaActions'
-import MigrationActions from '../../../actions/MigrationActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
-import EndpointActions from '../../../actions/EndpointActions'
-import ScheduleActions from '../../../actions/ScheduleActions'
 import ScheduleStore from '../../../stores/ScheduleStore'
 import ScheduleStore from '../../../stores/ScheduleStore'
 import { requestPollTimeout } from '../../../config'
 import { requestPollTimeout } from '../../../config'
 
 
@@ -46,10 +44,6 @@ const Wrapper = styled.div``
 
 
 type Props = {
 type Props = {
   match: any,
   match: any,
-  replicaStore: any,
-  endpointStore: any,
-  userStore: any,
-  scheduleStore: any,
 }
 }
 type State = {
 type State = {
   showOptionsModal: boolean,
   showOptionsModal: boolean,
@@ -60,20 +54,8 @@ type State = {
   confirmationItem: ?MainItem | ?Execution,
   confirmationItem: ?MainItem | ?Execution,
   showCancelConfirmation: boolean,
   showCancelConfirmation: boolean,
 }
 }
+@observer
 class ReplicaDetailsPage extends React.Component<Props, State> {
 class ReplicaDetailsPage extends React.Component<Props, State> {
-  static getStores() {
-    return [ReplicaStore, EndpointStore, UserStore, ScheduleStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      replicaStore: ReplicaStore.getState(),
-      endpointStore: EndpointStore.getState(),
-      userStore: UserStore.getState(),
-      scheduleStore: ScheduleStore.getState(),
-    }
-  }
-
   pollTimeout: TimeoutID
   pollTimeout: TimeoutID
 
 
   constructor() {
   constructor() {
@@ -93,32 +75,32 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   componentDidMount() {
   componentDidMount() {
     document.title = 'Replica Details'
     document.title = 'Replica Details'
 
 
-    ReplicaActions.getReplica(this.props.match.params.id)
-    EndpointActions.getEndpoints()
-    ScheduleActions.getSchedules(this.props.match.params.id)
+    ReplicaStore.getReplica(this.props.match.params.id)
+    EndpointStore.getEndpoints()
+    ScheduleStore.getSchedules(this.props.match.params.id)
     this.pollData()
     this.pollData()
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    ReplicaActions.clearDetails()
-    ScheduleActions.clearUnsavedSchedules()
+    ReplicaStore.clearDetails()
+    ScheduleStore.clearUnsavedSchedules()
     clearTimeout(this.pollTimeout)
     clearTimeout(this.pollTimeout)
   }
   }
 
 
   isActionButtonDisabled() {
   isActionButtonDisabled() {
-    let originEndpoint = this.props.endpointStore.endpoints.find(e => e.id === this.props.replicaStore.replicaDetails.origin_endpoint_id)
-    let targetEndpoint = this.props.endpointStore.endpoints.find(e => e.id === this.props.replicaStore.replicaDetails.destination_endpoint_id)
-    let lastExecution = this.props.replicaStore.replicaDetails.executions && this.props.replicaStore.replicaDetails.executions.length
-      && this.props.replicaStore.replicaDetails.executions[this.props.replicaStore.replicaDetails.executions.length - 1]
+    let originEndpoint = EndpointStore.endpoints.find(e => ReplicaStore.replicaDetails && e.id === ReplicaStore.replicaDetails.origin_endpoint_id)
+    let targetEndpoint = EndpointStore.endpoints.find(e => ReplicaStore.replicaDetails && e.id === ReplicaStore.replicaDetails.destination_endpoint_id)
+    let lastExecution = ReplicaStore.replicaDetails && ReplicaStore.replicaDetails.executions && ReplicaStore.replicaDetails.executions.length
+      && ReplicaStore.replicaDetails.executions[ReplicaStore.replicaDetails.executions.length - 1]
     let status = lastExecution && lastExecution.status
     let status = lastExecution && lastExecution.status
 
 
     return Boolean(!originEndpoint || !targetEndpoint || status === 'RUNNING')
     return Boolean(!originEndpoint || !targetEndpoint || status === 'RUNNING')
   }
   }
 
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
     switch (item.value) {
       case 'signout':
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
         return
       case 'profile':
       case 'profile':
         window.location.href = '/#/profile'
         window.location.href = '/#/profile'
@@ -143,11 +125,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     if (!this.state.confirmationItem) {
     if (!this.state.confirmationItem) {
       return
       return
     }
     }
-    ReplicaActions.deleteExecution(this.props.replicaStore.replicaDetails.id, this.state.confirmationItem.id)
+    ReplicaStore.deleteExecution(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '', this.state.confirmationItem.id)
     this.handleCloseExecutionConfirmation()
     this.handleCloseExecutionConfirmation()
   }
   }
 
 
-  handleDeleteExecutionClick(execution) {
+  handleDeleteExecutionClick(execution: ?Execution) {
     this.setState({
     this.setState({
       showDeleteExecutionConfirmation: true,
       showDeleteExecutionConfirmation: true,
       confirmationItem: execution,
       confirmationItem: execution,
@@ -172,7 +154,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   handleDeleteReplicaConfirmation() {
   handleDeleteReplicaConfirmation() {
     this.setState({ showDeleteReplicaConfirmation: false })
     this.setState({ showDeleteReplicaConfirmation: false })
     window.location.href = '/#/replicas'
     window.location.href = '/#/replicas'
-    ReplicaActions.delete(this.props.replicaStore.replicaDetails.id)
+    ReplicaStore.delete(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '')
   }
   }
 
 
   handleCloseDeleteReplicaConfirmation() {
   handleCloseDeleteReplicaConfirmation() {
@@ -181,8 +163,8 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
   handleDeleteReplicaDisksConfirmation() {
   handleDeleteReplicaDisksConfirmation() {
     this.setState({ showDeleteReplicaDisksConfirmation: false })
     this.setState({ showDeleteReplicaDisksConfirmation: false })
-    ReplicaActions.deleteDisks(this.props.replicaStore.replicaDetails.id)
-    window.location.href = `/#/replica/executions/${this.props.replicaStore.replicaDetails.id}`
+    ReplicaStore.deleteDisks(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '')
+    window.location.href = `/#/replica/executions/${ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : ''}`
   }
   }
 
 
   handleCloseDeleteReplicaDisksConfirmation() {
   handleCloseDeleteReplicaDisksConfirmation() {
@@ -197,25 +179,32 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     this.setState({ showMigrationModal: true })
     this.setState({ showMigrationModal: true })
   }
   }
 
 
-  handleAddScheduleClick(schedule) {
-    ScheduleActions.addSchedule(this.props.match.params.id, schedule)
+  handleAddScheduleClick(schedule: Schedule) {
+    ScheduleStore.addSchedule(this.props.match.params.id, schedule)
   }
   }
 
 
-  handleScheduleChange(scheduleId, data, forceSave) {
-    let oldData = this.props.scheduleStore.schedules.find(s => s.id === scheduleId)
-    let unsavedData = this.props.scheduleStore.unsavedSchedules.find(s => s.id === scheduleId)
-    ScheduleActions.updateSchedule(this.props.match.params.id, scheduleId, data, oldData, unsavedData, forceSave)
+  handleScheduleChange(scheduleId: ?string, data: Schedule, forceSave?: boolean) {
+    let oldData = ScheduleStore.schedules.find(s => s.id === scheduleId)
+    let unsavedData = ScheduleStore.unsavedSchedules.find(s => s.id === scheduleId)
+
+    if (scheduleId) {
+      ScheduleStore.updateSchedule(this.props.match.params.id, scheduleId, data, oldData, unsavedData, forceSave)
+    }
   }
   }
 
 
-  handleScheduleSave(schedule) {
-    ScheduleActions.updateSchedule(this.props.match.params.id, schedule.id, schedule, schedule, schedule, true)
+  handleScheduleSave(schedule: Schedule) {
+    if (schedule.id) {
+      ScheduleStore.updateSchedule(this.props.match.params.id, schedule.id, schedule, schedule, schedule, true)
+    }
   }
   }
 
 
-  handleScheduleRemove(scheduleId) {
-    ScheduleActions.removeSchedule(this.props.match.params.id, scheduleId)
+  handleScheduleRemove(scheduleId: ?string) {
+    if (scheduleId) {
+      ScheduleStore.removeSchedule(this.props.match.params.id, scheduleId)
+    }
   }
   }
 
 
-  handleCancelExecutionClick(confirmationItem) {
+  handleCancelExecutionClick(confirmationItem: ?Execution) {
     this.setState({ confirmationItem, showCancelConfirmation: true })
     this.setState({ confirmationItem, showCancelConfirmation: true })
   }
   }
 
 
@@ -227,23 +216,23 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     if (!this.state.confirmationItem) {
     if (!this.state.confirmationItem) {
       return
       return
     }
     }
-    ReplicaActions.cancelExecution(this.props.replicaStore.replicaDetails.id, this.state.confirmationItem.id)
+    ReplicaStore.cancelExecution(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '', this.state.confirmationItem.id)
     this.setState({ showCancelConfirmation: false })
     this.setState({ showCancelConfirmation: false })
   }
   }
 
 
-  migrateReplica(options) {
-    MigrationActions.migrateReplica(this.props.replicaStore.replicaDetails.id, options)
+  migrateReplica(options: Field[]) {
+    MigrationStore.migrateReplica(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '', options)
     this.handleCloseMigrationModal()
     this.handleCloseMigrationModal()
   }
   }
 
 
-  executeReplica(fields) {
-    ReplicaActions.execute(this.props.replicaStore.replicaDetails.id, fields)
+  executeReplica(fields: Field[]) {
+    ReplicaStore.execute(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '', fields)
     this.handleCloseOptionsModal()
     this.handleCloseOptionsModal()
-    window.location.href = `/#/replica/executions/${this.props.replicaStore.replicaDetails.id}`
+    window.location.href = `/#/replica/executions/${ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : ''}`
   }
   }
 
 
   pollData() {
   pollData() {
-    ReplicaActions.getReplicaExecutions(this.props.match.params.id).promise.then(() => {
+    ReplicaStore.getReplicaExecutions(this.props.match.params.id).then(() => {
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
     })
     })
   }
   }
@@ -253,24 +242,28 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
           pageHeaderComponent={<DetailsPageHeader
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           />}
           contentHeaderComponent={<DetailsContentHeader
           contentHeaderComponent={<DetailsContentHeader
-            item={this.props.replicaStore.replicaDetails}
+            item={ReplicaStore.replicaDetails}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onActionButtonClick={() => { this.handleActionButtonClick() }}
             onActionButtonClick={() => { this.handleActionButtonClick() }}
-            onCancelClick={execution => { this.handleCancelExecutionClick(execution) }}
+            onCancelClick={item => {
+              let any: any = item
+              let execution: Execution = any
+              this.handleCancelExecutionClick(execution)
+            }}
             actionButtonDisabled={this.isActionButtonDisabled()}
             actionButtonDisabled={this.isActionButtonDisabled()}
             typeImage={replicaImage}
             typeImage={replicaImage}
             alertInfoPill
             alertInfoPill
             buttonLabel="Execute Now"
             buttonLabel="Execute Now"
           />}
           />}
           contentComponent={<ReplicaDetailsContent
           contentComponent={<ReplicaDetailsContent
-            item={this.props.replicaStore.replicaDetails}
-            endpoints={this.props.endpointStore.endpoints}
-            scheduleStore={this.props.scheduleStore}
-            detailsLoading={this.props.replicaStore.detailsLoading || this.props.endpointStore.loading}
+            item={ReplicaStore.replicaDetails}
+            endpoints={EndpointStore.endpoints}
+            scheduleStore={ScheduleStore}
+            detailsLoading={ReplicaStore.detailsLoading || EndpointStore.loading}
             page={this.props.match.params.page || ''}
             page={this.props.match.params.page || ''}
             onCancelExecutionClick={execution => { this.handleCancelExecutionClick(execution) }}
             onCancelExecutionClick={execution => { this.handleCancelExecutionClick(execution) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
@@ -341,4 +334,4 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(ReplicaDetailsPage)
+export default ReplicaDetailsPage

+ 38 - 56
src/components/pages/ReplicasPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import MainTemplate from '../../templates/MainTemplate'
 import MainTemplate from '../../templates/MainTemplate'
 import Navigation from '../../organisms/Navigation'
 import Navigation from '../../organisms/Navigation'
@@ -25,6 +25,7 @@ import PageHeader from '../../organisms/PageHeader'
 import AlertModal from '../../organisms/AlertModal'
 import AlertModal from '../../organisms/AlertModal'
 import MainListItem from '../../molecules/MainListItem'
 import MainListItem from '../../molecules/MainListItem'
 import type { MainItem } from '../../../types/MainItem'
 import type { MainItem } from '../../../types/MainItem'
+import type { Project } from '../../../types/Project'
 
 
 import replicaItemImage from './images/replica.svg'
 import replicaItemImage from './images/replica.svg'
 import replicaLargeImage from './images/replica-large.svg'
 import replicaLargeImage from './images/replica-large.svg'
@@ -33,12 +34,8 @@ import ProjectStore from '../../../stores/ProjectStore'
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
-import ProjectActions from '../../../actions/ProjectActions'
-import ReplicaActions from '../../../actions/ReplicaActions'
-import EndpointActions from '../../../actions/EndpointActions'
-import UserActions from '../../../actions/UserActions'
 import Wait from '../../../utils/Wait'
 import Wait from '../../../utils/Wait'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import { requestPollTimeout } from '../../../config'
 import { requestPollTimeout } from '../../../config'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
@@ -48,31 +45,13 @@ const BulkActions = [
   { label: 'Delete', value: 'delete' },
   { label: 'Delete', value: 'delete' },
 ]
 ]
 
 
-type Props = {
-  projectStore: any,
-  replicaStore: any,
-  userStore: any,
-  endpointStore: any,
-}
 type State = {
 type State = {
   showDeleteReplicaConfirmation: boolean,
   showDeleteReplicaConfirmation: boolean,
   confirmationItems: ?MainItem[],
   confirmationItems: ?MainItem[],
   modalIsOpen: boolean,
   modalIsOpen: boolean,
 }
 }
-class ReplicasPage extends React.Component<Props, State> {
-  static getStores() {
-    return [UserStore, ProjectStore, ReplicaStore, EndpointStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      userStore: UserStore.getState(),
-      projectStore: ProjectStore.getState(),
-      replicaStore: ReplicaStore.getState(),
-      endpointStore: EndpointStore.getState(),
-    }
-  }
-
+@observer
+class ReplicasPage extends React.Component<{}, State> {
   pollTimeout: TimeoutID
   pollTimeout: TimeoutID
 
 
   constructor() {
   constructor() {
@@ -88,8 +67,8 @@ class ReplicasPage extends React.Component<Props, State> {
   componentDidMount() {
   componentDidMount() {
     document.title = 'Coriolis Replicas'
     document.title = 'Coriolis Replicas'
 
 
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints()
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints()
 
 
     this.pollData()
     this.pollData()
   }
   }
@@ -98,12 +77,8 @@ class ReplicasPage extends React.Component<Props, State> {
     clearTimeout(this.pollTimeout)
     clearTimeout(this.pollTimeout)
   }
   }
 
 
-  getEndpoint(endpointId) {
-    if (!this.props.endpointStore.endpoints || this.props.endpointStore.endpoints === 0) {
-      return {}
-    }
-
-    return this.props.endpointStore.endpoints.find(endpoint => endpoint.id === endpointId) || {}
+  getEndpoint(endpointId: string) {
+    return EndpointStore.endpoints.find(endpoint => endpoint.id === endpointId)
   }
   }
 
 
   getFilterItems() {
   getFilterItems() {
@@ -115,30 +90,31 @@ class ReplicasPage extends React.Component<Props, State> {
     ]
     ]
   }
   }
 
 
-  getLastExecution(item) {
+  getLastExecution(item: MainItem) {
     let lastExecution = item.executions && item.executions.length ?
     let lastExecution = item.executions && item.executions.length ?
       item.executions[item.executions.length - 1] : null
       item.executions[item.executions.length - 1] : null
 
 
     return lastExecution
     return lastExecution
   }
   }
 
 
-  handleProjectChange(project) {
-    Wait.for(() => this.props.userStore.user.project.id === project.id, () => {
-      ProjectActions.getProjects()
-      ReplicaActions.getReplicas()
-      EndpointActions.getEndpoints()
+  handleProjectChange(project: Project) {
+    // $FlowIssue
+    Wait.for(() => UserStore.user.project.id === project.id, () => {
+      ProjectStore.getProjects()
+      ReplicaStore.getReplicas()
+      EndpointStore.getEndpoints()
     })
     })
 
 
-    UserActions.switchProject(project.id)
+    UserStore.switchProject(project.id)
   }
   }
 
 
   handleReloadButtonClick() {
   handleReloadButtonClick() {
-    ProjectActions.getProjects()
-    ReplicaActions.getReplicas({ showLoading: true })
-    EndpointActions.getEndpoints()
+    ProjectStore.getProjects()
+    ReplicaStore.getReplicas({ showLoading: true })
+    EndpointStore.getEndpoints()
   }
   }
 
 
-  handleItemClick(item) {
+  handleItemClick(item: MainItem) {
     let lastExecution = this.getLastExecution(item)
     let lastExecution = this.getLastExecution(item)
     if (lastExecution && lastExecution.status === 'RUNNING') {
     if (lastExecution && lastExecution.status === 'RUNNING') {
       window.location.href = `/#/replica/executions/${item.id}`
       window.location.href = `/#/replica/executions/${item.id}`
@@ -147,12 +123,12 @@ class ReplicasPage extends React.Component<Props, State> {
     }
     }
   }
   }
 
 
-  handleActionChange(items, action) {
+  handleActionChange(items: MainItem[], action: string) {
     if (action === 'execute') {
     if (action === 'execute') {
       items.forEach(replica => {
       items.forEach(replica => {
-        ReplicaActions.execute(replica.id)
+        ReplicaStore.execute(replica.id)
       })
       })
-      NotificationActions.notify('Executing replicas')
+      NotificationStore.notify('Executing replicas')
     } else if (action === 'delete') {
     } else if (action === 'delete') {
       this.setState({
       this.setState({
         showDeleteReplicaConfirmation: true,
         showDeleteReplicaConfirmation: true,
@@ -173,7 +149,7 @@ class ReplicasPage extends React.Component<Props, State> {
       return
       return
     }
     }
     this.state.confirmationItems.forEach(replica => {
     this.state.confirmationItems.forEach(replica => {
-      ReplicaActions.delete(replica.id)
+      ReplicaStore.delete(replica.id)
     })
     })
     this.handleCloseDeleteReplicaConfirmation()
     this.handleCloseDeleteReplicaConfirmation()
   }
   }
@@ -196,12 +172,12 @@ class ReplicasPage extends React.Component<Props, State> {
     if (this.state.modalIsOpen) {
     if (this.state.modalIsOpen) {
       return
       return
     }
     }
-    ReplicaActions.getReplicas().promise.then(() => {
+    ReplicaStore.getReplicas().then(() => {
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
     })
     })
   }
   }
 
 
-  searchText(item, text) {
+  searchText(item: MainItem, text: ?string) {
     let result = false
     let result = false
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
       return true
       return true
@@ -218,7 +194,7 @@ class ReplicasPage extends React.Component<Props, State> {
     return result
     return result
   }
   }
 
 
-  itemFilterFunction(item, filterStatus, filterText) {
+  itemFilterFunction(item: MainItem, filterStatus?: ?string, filterText?: string) {
     let lastExecution = this.getLastExecution(item)
     let lastExecution = this.getLastExecution(item)
     if ((filterStatus !== 'all' && (!lastExecution || lastExecution.status !== filterStatus)) ||
     if ((filterStatus !== 'all' && (!lastExecution || lastExecution.status !== filterStatus)) ||
       !this.searchText(item, filterText)
       !this.searchText(item, filterText)
@@ -238,8 +214,8 @@ class ReplicasPage extends React.Component<Props, State> {
             <FilterList
             <FilterList
               filterItems={this.getFilterItems()}
               filterItems={this.getFilterItems()}
               selectionLabel="replica"
               selectionLabel="replica"
-              loading={this.props.replicaStore.loading}
-              items={this.props.replicaStore.replicas}
+              loading={ReplicaStore.loading}
+              items={ReplicaStore.replicas}
               onItemClick={item => { this.handleItemClick(item) }}
               onItemClick={item => { this.handleItemClick(item) }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               actions={BulkActions}
               actions={BulkActions}
@@ -249,7 +225,13 @@ class ReplicasPage extends React.Component<Props, State> {
                 (<MainListItem
                 (<MainListItem
                   {...options}
                   {...options}
                   image={replicaItemImage}
                   image={replicaItemImage}
-                  endpointType={id => this.getEndpoint(id).type}
+                  endpointType={id => {
+                    let endpoint = this.getEndpoint(id)
+                    if (endpoint) {
+                      return endpoint.type
+                    }
+                    return ''
+                  }}
                 />)
                 />)
               }
               }
               emptyListImage={replicaLargeImage}
               emptyListImage={replicaLargeImage}
@@ -281,4 +263,4 @@ class ReplicasPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(ReplicasPage)
+export default ReplicasPage

+ 108 - 116
src/components/pages/WizardPage/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 
 import WizardTemplate from '../../templates/WizardTemplate'
 import WizardTemplate from '../../templates/WizardTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -25,34 +25,28 @@ import Modal from '../../molecules/Modal'
 import Endpoint from '../../organisms/Endpoint'
 import Endpoint from '../../organisms/Endpoint'
 
 
 import UserStore from '../../../stores/UserStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import ProviderActions from '../../../actions/ProviderActions'
 import ProviderStore from '../../../stores/ProviderStore'
 import ProviderStore from '../../../stores/ProviderStore'
-import EndpointActions from '../../../actions/EndpointActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import WizardStore from '../../../stores/WizardStore'
 import WizardStore from '../../../stores/WizardStore'
-import WizardActions from '../../../actions/WizardActions'
 import InstanceStore from '../../../stores/InstanceStore'
 import InstanceStore from '../../../stores/InstanceStore'
-import InstanceActions from '../../../actions/InstanceActions'
 import NetworkStore from '../../../stores/NetworkStore'
 import NetworkStore from '../../../stores/NetworkStore'
-import NetworkActions from '../../../actions/NetworkActions'
-import NotificationActions from '../../../actions/NotificationActions'
-import ReplicaActions from '../../../actions/ReplicaActions'
-import ScheduleActions from '../../../actions/ScheduleActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import ScheduleStore from '../../../stores/ScheduleStore'
 import ScheduleStore from '../../../stores/ScheduleStore'
+import ReplicaStore from '../../../stores/ReplicaStore'
 import Wait from '../../../utils/Wait'
 import Wait from '../../../utils/Wait'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import { wizardConfig, executionOptions } from '../../../config'
 import { wizardConfig, executionOptions } from '../../../config'
+import type { MainItem } from '../../../types/MainItem'
+import type { Endpoint as EndpointType } from '../../../types/Endpoint'
+import type { Instance, Nic } from '../../../types/Instance'
+import type { Field } from '../../../types/Field'
+import type { Network } from '../../../types/Network'
+import type { Schedule } from '../../../types/Schedule'
+import type { WizardPage as WizardPageType } from '../../../types/WizardData'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
 
 
 type Props = {
 type Props = {
-  userStore: any,
-  wizardStore: any,
-  providerStore: any,
-  endpointStore: any,
-  instanceStore: any,
-  networkStore: any,
   match: any,
   match: any,
 }
 }
 type WizardType = 'migration' | 'replica'
 type WizardType = 'migration' | 'replica'
@@ -63,22 +57,8 @@ type State = {
   newEndpointType?: string,
   newEndpointType?: string,
   newEndpointFromSource?: boolean,
   newEndpointFromSource?: boolean,
 }
 }
+@observer
 class WizardPage extends React.Component<Props, State> {
 class WizardPage extends React.Component<Props, State> {
-  static getStores() {
-    return [UserStore, WizardStore, ProviderStore, EndpointStore, InstanceStore, NetworkStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      userStore: UserStore.getState(),
-      wizardStore: WizardStore.getState(),
-      providerStore: ProviderStore.getState(),
-      endpointStore: EndpointStore.getState(),
-      instanceStore: InstanceStore.getState(),
-      networkStore: NetworkStore.getState(),
-    }
-  }
-
   contentRef: WizardPageContent
   contentRef: WizardPageContent
 
 
   constructor() {
   constructor() {
@@ -92,7 +72,7 @@ class WizardPage extends React.Component<Props, State> {
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {
-    WizardActions.getDataFromPermalink()
+    WizardStore.getDataFromPermalink()
     let type = this.props.match && this.props.match.params.type
     let type = this.props.match && this.props.match.params.type
     if (type === 'migration' || type === 'replica') {
     if (type === 'migration' || type === 'replica') {
       this.setState({ type })
       this.setState({ type })
@@ -106,7 +86,7 @@ class WizardPage extends React.Component<Props, State> {
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    WizardActions.clearData()
+    WizardStore.clearData()
     KeyboardManager.removeKeyDown('wizard')
     KeyboardManager.removeKeyDown('wizard')
   }
   }
 
 
@@ -120,9 +100,9 @@ class WizardPage extends React.Component<Props, State> {
     this.handleBackClick()
     this.handleBackClick()
   }
   }
 
 
-  handleCreationSuccess(items) {
+  handleCreationSuccess(items: MainItem[]) {
     let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
     let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
-    NotificationActions.notify(`${typeLabel} was succesfully created`, 'success', { persist: true, persistInfo: { title: `${typeLabel} created` } })
+    NotificationStore.notify(`${typeLabel} was succesfully created`, 'success', { persist: true, persistInfo: { title: `${typeLabel} created` } })
 
 
     if (this.state.type === 'replica') {
     if (this.state.type === 'replica') {
       items.forEach(replica => {
       items.forEach(replica => {
@@ -139,7 +119,7 @@ class WizardPage extends React.Component<Props, State> {
         location += 'tasks/'
         location += 'tasks/'
       }
       }
 
 
-      Wait.for(() => !ScheduleStore.getState().scheduling, () => {
+      Wait.for(() => !ScheduleStore.scheduling, () => {
         window.location.href = location + items[0].id
         window.location.href = location + items[0].id
       })
       })
     } else {
     } else {
@@ -147,10 +127,10 @@ class WizardPage extends React.Component<Props, State> {
     }
     }
   }
   }
 
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
     switch (item.value) {
       case 'signout':
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
         return
       case 'profile':
       case 'profile':
         window.location.href = '/#/profile'
         window.location.href = '/#/profile'
@@ -159,13 +139,13 @@ class WizardPage extends React.Component<Props, State> {
     }
     }
   }
   }
 
 
-  handleTypeChange(isReplica) {
+  handleTypeChange(isReplica: ?boolean) {
     this.setState({ type: isReplica ? 'replica' : 'migration' })
     this.setState({ type: isReplica ? 'replica' : 'migration' })
   }
   }
 
 
   handleBackClick() {
   handleBackClick() {
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
-    let currentPageIndex = pages.findIndex(p => p.id === this.props.wizardStore.currentPage.id)
+    let currentPageIndex = pages.findIndex(p => p.id === WizardStore.currentPage.id)
 
 
     if (currentPageIndex === 0) {
     if (currentPageIndex === 0) {
       window.history.back()
       window.history.back()
@@ -174,12 +154,12 @@ class WizardPage extends React.Component<Props, State> {
 
 
     let page = pages[currentPageIndex - 1]
     let page = pages[currentPageIndex - 1]
     this.loadDataForPage(page)
     this.loadDataForPage(page)
-    WizardActions.setCurrentPage(page)
+    WizardStore.setCurrentPage(page)
   }
   }
 
 
   handleNextClick() {
   handleNextClick() {
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
-    let currentPageIndex = pages.findIndex(p => p.id === this.props.wizardStore.currentPage.id)
+    let currentPageIndex = pages.findIndex(p => p.id === WizardStore.currentPage.id)
 
 
     if (currentPageIndex === pages.length - 1) {
     if (currentPageIndex === pages.length - 1) {
       this.create()
       this.create()
@@ -188,22 +168,22 @@ class WizardPage extends React.Component<Props, State> {
 
 
     let page = pages[currentPageIndex + 1]
     let page = pages[currentPageIndex + 1]
     this.loadDataForPage(page)
     this.loadDataForPage(page)
-    WizardActions.setCurrentPage(page)
+    WizardStore.setCurrentPage(page)
   }
   }
 
 
-  handleSourceEndpointChange(source) {
-    WizardActions.updateData({ source, selectedInstances: null, networks: null })
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleSourceEndpointChange(source: EndpointType) {
+    WizardStore.updateData({ source, selectedInstances: null, networks: null })
+    WizardStore.setPermalink(WizardStore.data)
     // Preload instances for 'vms' page
     // Preload instances for 'vms' page
-    InstanceActions.loadInstances(source.id)
+    InstanceStore.loadInstances(source.id)
   }
   }
 
 
-  handleTargetEndpointChange(target) {
-    WizardActions.updateData({ target, networks: null, options: null })
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleTargetEndpointChange(target: EndpointType) {
+    WizardStore.updateData({ target, networks: null, options: null })
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  handleAddEndpoint(newEndpointType, newEndpointFromSource) {
+  handleAddEndpoint(newEndpointType: string, newEndpointFromSource: boolean) {
     this.setState({
     this.setState({
       showNewEndpointModal: true,
       showNewEndpointModal: true,
       newEndpointType,
       newEndpointType,
@@ -211,84 +191,96 @@ class WizardPage extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
-  handleCloseNewEndpointModal(autoClose) {
-    if (autoClose) {
+  handleCloseNewEndpointModal(options?: { autoClose?: boolean }) {
+    if (options) {
       if (this.state.newEndpointFromSource) {
       if (this.state.newEndpointFromSource) {
-        WizardActions.updateData({ source: this.props.endpointStore.endpoints[0] })
+        WizardStore.updateData({ source: EndpointStore.endpoints[0] })
       } else {
       } else {
-        WizardActions.updateData({ target: this.props.endpointStore.endpoints[0] })
+        WizardStore.updateData({ target: EndpointStore.endpoints[0] })
       }
       }
     }
     }
-    WizardActions.setPermalink(WizardStore.getState().data)
+    WizardStore.setPermalink(WizardStore.data)
     this.setState({ showNewEndpointModal: false })
     this.setState({ showNewEndpointModal: false })
   }
   }
 
 
-  handleInstancesSearchInputChange(searchText) {
-    InstanceActions.searchInstances(this.props.wizardStore.data.source.id, searchText)
+  handleInstancesSearchInputChange(searchText: string) {
+    if (WizardStore.data.source) {
+      InstanceStore.searchInstances(WizardStore.data.source.id, searchText)
+    }
   }
   }
 
 
-  handleInstancesNextPageClick(searchText) {
-    InstanceActions.loadNextPage(this.props.wizardStore.data.source.id, searchText)
+  handleInstancesNextPageClick(searchText: string) {
+    if (WizardStore.data.source) {
+      InstanceStore.loadNextPage(WizardStore.data.source.id, searchText)
+    }
   }
   }
 
 
   handleInstancesPreviousPageClick() {
   handleInstancesPreviousPageClick() {
-    InstanceActions.loadPreviousPage()
+    InstanceStore.loadPreviousPage()
   }
   }
 
 
-  handleInstancesReloadClick(searchText) {
-    InstanceActions.reloadInstances(this.props.wizardStore.data.source.id, searchText)
+  handleInstancesReloadClick(searchText: string) {
+    if (WizardStore.data.source) {
+      InstanceStore.reloadInstances(WizardStore.data.source.id, searchText)
+    }
   }
   }
 
 
-  handleInstanceClick(instance) {
-    WizardActions.updateData({ networks: null })
-    WizardActions.toggleInstanceSelection(instance)
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleInstanceClick(instance: Instance) {
+    WizardStore.updateData({ networks: null })
+    WizardStore.toggleInstanceSelection(instance)
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  handleOptionsChange(field, value) {
-    WizardActions.updateData({ networks: null })
-    WizardActions.updateOptions({ field, value })
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleOptionsChange(field: Field, value: any) {
+    WizardStore.updateData({ networks: null })
+    WizardStore.updateOptions({ field, value })
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  handleNetworkChange(sourceNic, targetNetwork) {
-    WizardActions.updateNetworks({ sourceNic, targetNetwork })
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleNetworkChange(sourceNic: Nic, targetNetwork: Network) {
+    WizardStore.updateNetworks({ sourceNic, targetNetwork })
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  handleAddScheduleClick(schedule) {
-    WizardActions.addSchedule(schedule)
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleAddScheduleClick(schedule: Schedule) {
+    WizardStore.addSchedule(schedule)
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  handleScheduleChange(scheduleId, data) {
-    WizardActions.updateSchedule(scheduleId, data)
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleScheduleChange(scheduleId: string, data: Schedule) {
+    WizardStore.updateSchedule(scheduleId, data)
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  handleScheduleRemove(scheduleId) {
-    WizardActions.removeSchedule(scheduleId)
-    WizardActions.setPermalink(WizardStore.getState().data)
+  handleScheduleRemove(scheduleId: string) {
+    WizardStore.removeSchedule(scheduleId)
+    WizardStore.setPermalink(WizardStore.data)
   }
   }
 
 
-  loadDataForPage(page) {
+  loadDataForPage(page: WizardPageType) {
     switch (page.id) {
     switch (page.id) {
       case 'source': {
       case 'source': {
-        ProviderActions.loadProviders()
-        EndpointActions.getEndpoints()
+        ProviderStore.loadProviders()
+        EndpointStore.getEndpoints()
         // Preload instances if data is set from 'Permalink'
         // Preload instances if data is set from 'Permalink'
-        let source = WizardStore.getState().data.source
-        if (InstanceStore.getState().instances.length === 0 && source) {
-          InstanceActions.loadInstances(source.id)
+        let source = WizardStore.data.source
+        if (InstanceStore.instances.length === 0 && source) {
+          InstanceStore.loadInstances(source.id)
         }
         }
         break
         break
       }
       }
       case 'options':
       case 'options':
-        ProviderActions.loadOptionsSchema(this.props.wizardStore.data.target.type, this.state.type)
+        if (WizardStore.data.target) {
+          ProviderStore.loadOptionsSchema(WizardStore.data.target.type, this.state.type)
+        }
         break
         break
       case 'networks':
       case 'networks':
-        InstanceActions.loadInstancesDetails(this.props.wizardStore.data.source.id, this.props.wizardStore.data.selectedInstances)
-        NetworkActions.loadNetworks(this.props.wizardStore.data.target.id, this.props.wizardStore.data.options)
+        if (WizardStore.data.source && WizardStore.data.selectedInstances) {
+          InstanceStore.loadInstancesDetails(WizardStore.data.source.id, WizardStore.data.selectedInstances)
+        }
+        if (WizardStore.data.target) {
+          NetworkStore.loadNetworks(WizardStore.data.target.id, WizardStore.data.options)
+        }
         break
         break
       default:
       default:
     }
     }
@@ -296,11 +288,11 @@ class WizardPage extends React.Component<Props, State> {
 
 
   createMultiple() {
   createMultiple() {
     let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
     let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
-    NotificationActions.notify(`Creating ${typeLabel}s ...`)
-    WizardActions.createMultiple(this.state.type, this.props.wizardStore.data).promise.then(() => {
-      let items = WizardStore.getState().createdItems
+    NotificationStore.notify(`Creating ${typeLabel}s ...`)
+    WizardStore.createMultiple(this.state.type, WizardStore.data).then(() => {
+      let items = WizardStore.createdItems
       if (!items) {
       if (!items) {
-        NotificationActions.notify(`${typeLabel}s couldn't be created`, 'error')
+        NotificationStore.notify(`${typeLabel}s couldn't be created`, 'error')
         this.setState({ nextButtonDisabled: false })
         this.setState({ nextButtonDisabled: false })
         return
         return
       }
       }
@@ -310,11 +302,11 @@ class WizardPage extends React.Component<Props, State> {
 
 
   createSingle() {
   createSingle() {
     let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
     let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
-    NotificationActions.notify(`Creating ${typeLabel} ...`)
-    WizardActions.create(this.state.type, this.props.wizardStore.data).promise.then(() => {
-      let item = WizardStore.getState().createdItem
+    NotificationStore.notify(`Creating ${typeLabel} ...`)
+    WizardStore.create(this.state.type, WizardStore.data).then(() => {
+      let item = WizardStore.createdItem
       if (!item) {
       if (!item) {
-        NotificationActions.notify(`${typeLabel} couldn't be created`, 'error')
+        NotificationStore.notify(`${typeLabel} couldn't be created`, 'error')
         this.setState({ nextButtonDisabled: false })
         this.setState({ nextButtonDisabled: false })
         return
         return
       }
       }
@@ -323,14 +315,14 @@ class WizardPage extends React.Component<Props, State> {
   }
   }
 
 
   separateVms() {
   separateVms() {
-    let data = WizardStore.getState().data
+    let data = WizardStore.data
     let separateVms = true
     let separateVms = true
 
 
     if (data.options && data.options.separate_vm !== null && data.options.separate_vm !== undefined) {
     if (data.options && data.options.separate_vm !== null && data.options.separate_vm !== undefined) {
       separateVms = data.options.separate_vm
       separateVms = data.options.separate_vm
     }
     }
 
 
-    if (data.selectedInstances.length === 1) {
+    if (data.selectedInstances && data.selectedInstances.length === 1) {
       separateVms = false
       separateVms = false
     }
     }
 
 
@@ -346,18 +338,18 @@ class WizardPage extends React.Component<Props, State> {
     this.separateVms()
     this.separateVms()
   }
   }
 
 
-  scheduleReplica(replica) {
-    let data = WizardStore.getState().data
+  scheduleReplica(replica: MainItem) {
+    let data = WizardStore.data
 
 
     if (!data.schedules || data.schedules.length === 0) {
     if (!data.schedules || data.schedules.length === 0) {
       return
       return
     }
     }
 
 
-    ScheduleActions.scheduleMultiple(replica.id, data.schedules)
+    ScheduleStore.scheduleMultiple(replica.id, data.schedules)
   }
   }
 
 
-  executeCreatedReplica(replica) {
-    let options = WizardStore.getState().data.options
+  executeCreatedReplica(replica: MainItem) {
+    let options = WizardStore.data.options
     let executeNow = true
     let executeNow = true
     if (options && options.execute_now !== null && options.execute_now !== undefined) {
     if (options && options.execute_now !== null && options.execute_now !== undefined) {
       executeNow = options.execute_now
       executeNow = options.execute_now
@@ -373,7 +365,7 @@ class WizardPage extends React.Component<Props, State> {
       return field
       return field
     })
     })
 
 
-    ReplicaActions.execute(replica.id, executeNowOptions)
+    ReplicaStore.execute(replica.id, executeNowOptions)
   }
   }
 
 
   render() {
   render() {
@@ -381,16 +373,16 @@ class WizardPage extends React.Component<Props, State> {
       <Wrapper>
       <Wrapper>
         <WizardTemplate
         <WizardTemplate
           pageHeaderComponent={<DetailsPageHeader
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           />}
           pageContentComponent={<WizardPageContent
           pageContentComponent={<WizardPageContent
-            page={this.props.wizardStore.currentPage}
-            providerStore={this.props.providerStore}
-            instanceStore={this.props.instanceStore}
-            networkStore={this.props.networkStore}
-            endpoints={this.props.endpointStore.endpoints}
-            wizardData={this.props.wizardStore.data}
+            page={WizardStore.currentPage}
+            providerStore={ProviderStore}
+            instanceStore={InstanceStore}
+            networkStore={NetworkStore}
+            endpoints={EndpointStore.endpoints}
+            wizardData={WizardStore.data}
             nextButtonDisabled={this.state.nextButtonDisabled}
             nextButtonDisabled={this.state.nextButtonDisabled}
             type={this.state.type}
             type={this.state.type}
             onTypeChange={isReplica => { this.handleTypeChange(isReplica) }}
             onTypeChange={isReplica => { this.handleTypeChange(isReplica) }}
@@ -428,4 +420,4 @@ class WizardPage extends React.Component<Props, State> {
   }
   }
 }
 }
 
 
-export default connectToStores(WizardPage)
+export default WizardPage

+ 2 - 2
src/sources/EndpointSource.js

@@ -20,7 +20,7 @@ import moment from 'moment'
 import Api from '../utils/ApiCaller'
 import Api from '../utils/ApiCaller'
 import { SchemaParser } from './Schemas'
 import { SchemaParser } from './Schemas'
 import ObjectUtils from '../utils/ObjectUtils'
 import ObjectUtils from '../utils/ObjectUtils'
-import type { Endpoint } from '../types/Endpoint'
+import type { Endpoint, Validation } from '../types/Endpoint'
 
 
 import { servicesUrl, useSecret } from '../config'
 import { servicesUrl, useSecret } from '../config'
 
 
@@ -101,7 +101,7 @@ class EdnpointSource {
     })
     })
   }
   }
 
 
-  static validate(endpoint: Endpoint): Promise<{ valid: boolean, validation: { message: string } }> {
+  static validate(endpoint: Endpoint): Promise<Validation> {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       let projectId = cookie.get('projectId')
       let projectId = cookie.get('projectId')
       Api.sendAjaxRequest({
       Api.sendAjaxRequest({

+ 2 - 2
src/sources/ReplicaSource.js

@@ -104,7 +104,7 @@ class ReplicaSource {
         let executions = response.data.executions
         let executions = response.data.executions
         ReplicaSourceUtils.sortExecutionsAndTaskUpdates(executions)
         ReplicaSourceUtils.sortExecutionsAndTaskUpdates(executions)
 
 
-        resolve({ replicaId, executions })
+        resolve(executions)
       }, reject).catch(reject)
       }, reject).catch(reject)
     })
     })
   }
   }
@@ -142,7 +142,7 @@ class ReplicaSource {
       }).then((response) => {
       }).then((response) => {
         let execution = response.data.execution
         let execution = response.data.execution
         ReplicaSourceUtils.sortTaskUpdates(execution)
         ReplicaSourceUtils.sortTaskUpdates(execution)
-        resolve({ replicaId, execution })
+        resolve(execution)
       }, reject).catch(reject)
       }, reject).catch(reject)
     })
     })
   }
   }

+ 2 - 2
src/sources/WizardSource.js

@@ -15,7 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import cookie from 'js-cookie'
 import cookie from 'js-cookie'
 
 
 import Api from '../utils/ApiCaller'
 import Api from '../utils/ApiCaller'
-import NotificationActions from '../actions/NotificationActions'
+import NotificationStore from '../stores/NotificationStore'
 import { servicesUrl, executionOptions } from '../config'
 import { servicesUrl, executionOptions } from '../config'
 
 
 class WizardSourceUtils {
 class WizardSourceUtils {
@@ -93,7 +93,7 @@ class WizardSource {
           }
           }
         }, () => {
         }, () => {
           count += 1
           count += 1
-          NotificationActions.notify(`Error while creating ${type} for instance ${instance.name}`, 'error', {
+          NotificationStore.notify(`Error while creating ${type} for instance ${instance.name}`, 'error', {
             persist: true,
             persist: true,
             persistInfo: { title: `${type} creation error` },
             persistInfo: { title: `${type} creation error` },
           })
           })

+ 62 - 83
src/stores/EndpointStore.js

@@ -12,8 +12,10 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import EndpointActions from '../actions/EndpointActions'
+// @flow
+import { observable, action } from 'mobx'
+import type { Endpoint, Validation } from '../types/Endpoint'
+import EndpointSource from '../sources/EndpointSource'
 
 
 const updateEndpoint = (endpoint, endpoints) => endpoints.map(e => {
 const updateEndpoint = (endpoint, endpoints) => endpoints.map(e => {
   if (e.id === endpoint.id) {
   if (e.id === endpoint.id) {
@@ -23,121 +25,98 @@ const updateEndpoint = (endpoint, endpoints) => endpoints.map(e => {
 })
 })
 
 
 class EndpointStore {
 class EndpointStore {
-  constructor() {
-    this.endpoints = []
-    this.loading = false
-    this.connectionInfo = null
-    this.validation = null
-    this.validating = false
-    this.updating = false
-    this.adding = false
-    this.connectionInfoLoading = false
-
-    this.bindListeners({
-      handleGetEndpoints: EndpointActions.GET_ENDPOINTS,
-      handleGetEndpointsCompleted: EndpointActions.GET_ENDPOINTS_COMPLETED,
-      handleGetEndpointsFailed: EndpointActions.GET_ENDPOINTS_FAILED,
-      handleDeleteSuccess: EndpointActions.DELETE_SUCCESS,
-      handleGetConnectionInfo: EndpointActions.GET_CONNECTION_INFO,
-      handleGetConnectionInfoSuccess: EndpointActions.GET_CONNECTION_INFO_SUCCESS,
-      handleGetConnectionInfoFailed: EndpointActions.GET_CONNECTION_INFO_FAILED,
-      handleValidate: EndpointActions.VALIDATE,
-      handleValidateSuccess: EndpointActions.VALIDATE_SUCCESS,
-      handleValidateFailed: EndpointActions.VALIDATE_FAILED,
-      handleClearValidation: EndpointActions.CLEAR_VALIDATION,
-      handleUpdateSuccess: EndpointActions.UPDATE_SUCCESS,
-      handleUpdate: EndpointActions.UPDATE,
-      handleClearConnectionInfo: EndpointActions.CLEAR_CONNECTION_INFO,
-      handleAdd: EndpointActions.ADD,
-      handleAddSuccess: EndpointActions.ADD_SUCCESS,
-      handleAddFailed: EndpointActions.ADD_FAILED,
-    })
-  }
-
-  handleGetEndpoints({ showLoading }) {
-    if (showLoading || this.endpoints.length === 0) {
+  @observable endpoints: Endpoint[] = []
+  @observable loading = false
+  @observable loading = false
+  @observable connectionInfo: ?$PropertyType<Endpoint, 'connection_info'> = null
+  @observable validation: ?Validation = null
+  @observable validating = false
+  @observable updating = false
+  @observable adding = false
+  @observable connectionInfoLoading = false
+
+  @action getEndpoints(options?: { showLoading: boolean }) {
+    if ((options && options.showLoading) || this.endpoints.length === 0) {
       this.loading = true
       this.loading = true
     }
     }
-  }
-
-  handleGetEndpointsCompleted(endpoints) {
-    this.endpoints = endpoints
-    this.loading = false
-  }
 
 
-  handleGetEndpointsFailed() {
-    this.loading = false
+    return EndpointSource.getEndpoints().then(endpoints => {
+      this.endpoints = endpoints
+      this.loading = false
+    }).catch(() => {
+      this.loading = false
+    })
   }
   }
 
 
-  handleDeleteSuccess(endpointId) {
-    this.endpoints = this.endpoints.filter(e => e.id !== endpointId)
+  @action delete(endpoint: Endpoint) {
+    return EndpointSource.delete(endpoint).then(() => {
+      this.endpoints = this.endpoints.filter(e => e.id !== endpoint.id)
+    })
   }
   }
 
 
-  handleGetConnectionInfo() {
+  @action getConnectionInfo(endpoint: Endpoint) {
     this.connectionInfoLoading = true
     this.connectionInfoLoading = true
-  }
 
 
-  handleGetConnectionInfoSuccess(connectionInfo) {
-    this.connectionInfo = connectionInfo
-    this.connectionInfoLoading = false
+    return EndpointSource.getConnectionInfo(endpoint).then(connectionInfo => {
+      this.setConnectionInfo(connectionInfo)
+    }).catch(() => {
+      this.connectionInfoLoading = false
+    })
   }
   }
 
 
-  handleGetConnectionInfoFailed() {
+  @action setConnectionInfo(connectionInfo: $PropertyType<Endpoint, 'connection_info'>) {
+    this.connectionInfo = connectionInfo
     this.connectionInfoLoading = false
     this.connectionInfoLoading = false
   }
   }
 
 
-  handleValidate() {
+  @action validate(endpoint: Endpoint) {
     this.validating = true
     this.validating = true
-  }
-
-  handleValidateSuccess(validation) {
-    this.validation = validation
-    this.validating = false
-  }
 
 
-  handleValidateFailed() {
-    this.validating = false
-    this.validation = { valid: false }
+    return EndpointSource.validate(endpoint).then(validation => {
+      this.validation = validation
+      this.validating = false
+    }).catch(() => {
+      this.validating = false
+      this.validation = { valid: false, message: '' }
+    })
   }
   }
 
 
-  handleClearValidation() {
+  @action clearValidation() {
     this.validating = false
     this.validating = false
     this.validation = null
     this.validation = null
   }
   }
 
 
-  handleUpdate({ endpoint }) {
+  @action update(endpoint: Endpoint) {
     this.endpoints = updateEndpoint(endpoint, this.endpoints)
     this.endpoints = updateEndpoint(endpoint, this.endpoints)
     this.connectionInfo = { ...endpoint.connection_info }
     this.connectionInfo = { ...endpoint.connection_info }
     this.updating = true
     this.updating = true
-  }
 
 
-  handleUpdateSuccess(endpoint) {
-    this.endpoints = updateEndpoint(endpoint, this.endpoints)
-    this.connectionInfo = { ...endpoint.connection_info }
-    this.updating = false
+    return EndpointSource.update(endpoint).then(updatedEndpoint => {
+      this.endpoints = updateEndpoint(updatedEndpoint, this.endpoints)
+      this.connectionInfo = { ...updatedEndpoint.connection_info }
+      this.updating = false
+    })
   }
   }
 
 
-  handleClearConnectionInfo() {
+  @action clearConnectionInfo() {
     this.connectionInfo = null
     this.connectionInfo = null
   }
   }
 
 
-  handleAdd() {
+  @action add(endpoint: Endpoint) {
     this.adding = true
     this.adding = true
-  }
-
-  handleAddSuccess(endpoint) {
-    this.endpoints = [
-      endpoint,
-      ...this.endpoints,
-    ]
 
 
-    this.connectionInfo = endpoint.connection_info
-    this.adding = false
-  }
+    return EndpointSource.add(endpoint).then(addedEndpoint => {
+      this.endpoints = [
+        addedEndpoint,
+        ...this.endpoints,
+      ]
 
 
-  handleAddFailed() {
-    this.adding = false
+      this.connectionInfo = addedEndpoint.connection_info
+      this.adding = false
+    }).catch(() => {
+      this.adding = false
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(EndpointStore)
+export default new EndpointStore()

+ 124 - 137
src/stores/InstanceStore.js

@@ -12,10 +12,13 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import InstanceActions from '../actions/InstanceActions'
+// @flow
+
+import { observable, action } from 'mobx'
 
 
 import { wizardConfig } from '../config'
 import { wizardConfig } from '../config'
+import type { Instance } from '../types/Instance'
+import InstanceSource from '../sources/InstanceSource'
 
 
 class InstanceStoreUtils {
 class InstanceStoreUtils {
   static hasNextPage(instances) {
   static hasNextPage(instances) {
@@ -42,167 +45,151 @@ class InstanceStoreUtils {
 }
 }
 
 
 class InstanceStore {
 class InstanceStore {
-  constructor() {
-    this.instances = []
-    this.instancesLoading = false
-    this.searching = false
-    this.searchNotFound = false
-    this.loadingPage = false
-    this.currentPage = 1
-    this.hasNextPage = false
-    this.cachedHasNextPage = false
-    this.cachedInstances = []
-    this.reloading = false
-    this.instancesDetails = []
-    this.loadingInstancesDetails = true
-
-    this.bindListeners({
-      handleLoadInstances: InstanceActions.LOAD_INSTANCES,
-      handleLoadInstancesSuccess: InstanceActions.LOAD_INSTANCES_SUCCESS,
-      handleLoadInstancesFailed: InstanceActions.LOAD_INSTANCES_FAILED,
-      handleSearchInstances: InstanceActions.SEARCH_INSTANCES,
-      handleSearchInstancesSuccess: InstanceActions.SEARCH_INSTANCES_SUCCESS,
-      handleSearchInstancesFailed: InstanceActions.SEARCH_INSTANCES_FAILED,
-      handleLoadNextPage: InstanceActions.LOAD_NEXT_PAGE,
-      handleLoadNextPageSuccess: InstanceActions.LOAD_NEXT_PAGE_SUCCESS,
-      handleLoadNextPageFailed: InstanceActions.LOAD_NEXT_PAGE_FAILED,
-      handleLoadPreviousPage: InstanceActions.LOAD_PREVIOUS_PAGE,
-      handleReloadInstances: InstanceActions.RELOAD_INSTANCES,
-      handleReloadInstancesSuccess: InstanceActions.RELOAD_INSTANCES_SUCCESS,
-      handleReloadInstancesFailed: InstanceActions.RELOAD_INSTANCES_FAILED,
-      handleLoadInstancesDetails: InstanceActions.LOAD_INSTANCES_DETAILS,
-      handleLoadInstanceDetailsSuccess: InstanceActions.LOAD_INSTANCE_DETAILS_SUCCESS,
-      handleLoadInstanceDetailsFailed: InstanceActions.LOAD_INSTANCE_DETAILS_FAILED,
-    })
-  }
-
-  handleLoadInstances(endpointId) {
-    this.endpointId = endpointId
+  @observable instances: Instance[] = []
+  @observable instancesLoading = false
+  @observable searching = false
+  @observable searchNotFound: boolean = false
+  @observable loadingPage = false
+  @observable currentPage = 1
+  @observable hasNextPage = false
+  @observable cachedHasNextPage = false
+  @observable cachedInstances: Instance[] = []
+  @observable reloading = false
+  @observable instancesDetails: Instance[] = []
+  @observable loadingInstancesDetails = true
+
+  lastEndpointId: string
+
+  @action loadInstances(endpointId: string) {
     this.instancesLoading = true
     this.instancesLoading = true
     this.searchNotFound = false
     this.searchNotFound = false
-  }
-
-  handleLoadInstancesSuccess({ endpointId, instances }) {
-    if (endpointId !== this.endpointId) {
-      return
-    }
+    this.lastEndpointId = endpointId
 
 
-    this.currentPage = 1
-    this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
-    this.instances = instances
-    this.cachedInstances = instances
-    this.instancesLoading = false
-  }
-
-  handleLoadInstancesFailed({ endpointId }) {
-    if (endpointId !== this.endpointId) {
-      return
-    }
-
-    this.instancesLoading = false
+    return InstanceSource.loadInstances(endpointId).then(instances => {
+      if (endpointId !== this.lastEndpointId) {
+        return
+      }
+      this.currentPage = 1
+      this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
+      this.instances = instances
+      this.cachedInstances = instances
+      this.instancesLoading = false
+    }).catch(() => {
+      if (endpointId !== this.lastEndpointId) {
+        return
+      }
+      this.instancesLoading = false
+    })
   }
   }
 
 
-  handleSearchInstances() {
+  @action searchInstances(endpointId: string, searchText: string) {
     this.searching = true
     this.searching = true
+    return InstanceSource.loadInstances(endpointId, searchText).then(instances => {
+      this.currentPage = 1
+      this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
+      this.instances = instances
+      this.cachedInstances = instances
+      this.searching = false
+      this.searchNotFound = Boolean(instances.length === 0 && searchText)
+    }).catch(() => {
+      this.searching = false
+      this.searchNotFound = true
+    })
   }
   }
 
 
-  handleSearchInstancesSuccess({ instances, searchText }) {
-    this.currentPage = 1
-    this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
-    this.instances = instances
-    this.cachedInstances = instances
-    this.searching = false
-    this.searchNotFound = instances.length === 0 && searchText
-  }
-
-  handleSearchInstancesFailed() {
-    this.searching = false
-    this.searchNotFound = true
-  }
-
-  handleLoadNextPage({ fromCache }) {
-    if (!fromCache) {
-      this.loadingPage = true
-      return
-    }
-    this.currentPage = this.currentPage + 1
-    let numCachedPages = Math.ceil(this.cachedInstances.length / wizardConfig.instancesItemsPerPage)
-    if (this.currentPage === numCachedPages) {
-      this.hasNextPage = this.cachedHasNextPage
-    } else {
-      this.hasNextPage = true
+  @action loadNextPage(endpointId: string, searchText: string): Promise<void> {
+    if (this.cachedInstances.length > wizardConfig.instancesItemsPerPage * this.currentPage) {
+      this.currentPage = this.currentPage + 1
+      let numCachedPages = Math.ceil(this.cachedInstances.length / wizardConfig.instancesItemsPerPage)
+      if (this.currentPage === numCachedPages) {
+        this.hasNextPage = this.cachedHasNextPage
+      } else {
+        this.hasNextPage = true
+      }
+      this.instances = InstanceStoreUtils.loadFromCache(this.cachedInstances, this.currentPage)
+      return Promise.resolve()
     }
     }
-    this.instances = InstanceStoreUtils.loadFromCache(this.cachedInstances, this.currentPage)
-  }
 
 
-  handleLoadNextPageSuccess(instances) {
-    this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
-    this.cachedHasNextPage = this.hasNextPage
-    this.cachedInstances = [...this.cachedInstances, ...instances]
-    this.instances = instances
-    this.loadingPage = false
-    this.currentPage = this.currentPage + 1
-  }
-
-  handleLoadNextPageFailed() {
-    this.loadingPage = false
+    this.loadingPage = true
+    return InstanceSource.loadInstances(
+      endpointId,
+      searchText,
+      this.instances[this.instances.length - 1].id
+    ).then(instances => {
+      this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
+      this.cachedHasNextPage = this.hasNextPage
+      this.cachedInstances = [...this.cachedInstances, ...instances]
+      this.instances = instances
+      this.loadingPage = false
+      this.currentPage = this.currentPage + 1
+    }).catch(() => {
+      this.loadingPage = false
+    })
   }
   }
 
 
-  handleLoadPreviousPage() {
+  @action loadPreviousPage() {
     this.hasNextPage = true
     this.hasNextPage = true
     this.currentPage = this.currentPage - 1
     this.currentPage = this.currentPage - 1
     this.instances = InstanceStoreUtils.loadFromCache(this.cachedInstances, this.currentPage)
     this.instances = InstanceStoreUtils.loadFromCache(this.cachedInstances, this.currentPage)
   }
   }
 
 
-  handleReloadInstances() {
+  @action reloadInstances(endpointId: string, searchText: string) {
     this.reloading = true
     this.reloading = true
     this.searchNotFound = false
     this.searchNotFound = false
-  }
 
 
-  handleReloadInstancesSuccess({ instances, searchText }) {
-    this.reloading = false
-    this.currentPage = 1
-    this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
-    this.instances = instances
-    this.cachedInstances = instances
-    this.searching = false
-    this.searchNotFound = instances.length === 0 && searchText
-  }
-
-  handleReloadInstancesFailed() {
-    this.reloading = false
-    this.searchNotFound = true
+    InstanceSource.loadInstances(endpointId, searchText).then(instances => {
+      this.reloading = false
+      this.currentPage = 1
+      this.hasNextPage = InstanceStoreUtils.hasNextPage(instances)
+      this.instances = instances
+      this.cachedInstances = instances
+      this.searching = false
+      this.searchNotFound = Boolean(instances.length === 0 && searchText)
+    }).catch(() => {
+      this.reloading = false
+      this.searchNotFound = true
+    })
   }
   }
 
 
-  handleLoadInstancesDetails({ count, fromCache }) {
-    if (fromCache) {
-      return
+  @action loadInstancesDetails(endpointId: string, instances: Instance[]): Promise<void> {
+    instances.sort((a, b) => a.instance_name.localeCompare(b.instance_name))
+    let hash = i => `${i.instance_name}-${i.id}`
+    if (this.instancesDetails.map(hash).join('_') === instances.map(hash).join('_')) {
+      return Promise.resolve()
     }
     }
 
 
     this.loadingInstancesDetails = true
     this.loadingInstancesDetails = true
-    this.instancesDetailsCount = count
     this.instancesDetails = []
     this.instancesDetails = []
-  }
-
-  handleLoadInstanceDetailsSuccess(instance) {
-    this.instancesDetailsCount -= 1
-    this.loadingInstancesDetails = this.instancesDetailsCount > 0
-
-    if (this.instancesDetails.find(i => i.id === instance.id)) {
-      this.instancesDetails = this.instancesDetails.filter(i => i.id !== instance.id)
-    }
-
-    this.instancesDetails = [
-      ...this.instancesDetails,
-      instance,
-    ]
-    this.instancesDetails.sort((a, b) => a.instance_name.localeCompare(b.instance_name))
-  }
-
-  handleLoadInstanceDetailsFailed() {
-    this.instancesDetailsCount -= 1
-    this.loadingInstancesDetails = this.instancesDetailsCount > 0
+    let count = instances.length
+    return new Promise((resolve) => {
+      instances.forEach(instance => {
+        InstanceSource.loadInstanceDetails(endpointId, instance.instance_name).then(instance => {
+          count -= 1
+          this.loadingInstancesDetails = count > 0
+
+          if (this.instancesDetails.find(i => i.id === instance.id)) {
+            this.instancesDetails = this.instancesDetails.filter(i => i.id !== instance.id)
+          }
+
+          this.instancesDetails = [
+            ...this.instancesDetails,
+            instance,
+          ]
+          this.instancesDetails.sort((a, b) => a.instance_name.localeCompare(b.instance_name))
+
+          if (count === 0) {
+            resolve()
+          }
+        }).catch(() => {
+          count -= 1
+          this.loadingInstancesDetails = count > 0
+          if (count === 0) {
+            resolve()
+          }
+        })
+      })
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(InstanceStore)
+export default new InstanceStore()

+ 54 - 71
src/stores/MigrationStore.js

@@ -12,81 +12,76 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import MigrationActions from '../actions/MigrationActions'
-import NotificationActions from '../actions/NotificationActions'
+// @flow
 
 
-class MigrationStore {
-  constructor() {
-    this.migrations = []
-    this.migrationDetails = {}
-    this.loading = true
-    this.canceling = true
-    this.detailsLoading = true
+import { observable, action } from 'mobx'
 
 
-    this.bindListeners({
-      handleGetMigrations: MigrationActions.GET_MIGRATIONS,
-      handleGetMigrationsSuccess: MigrationActions.GET_MIGRATIONS_SUCCESS,
-      handleGetMigrationsFailed: MigrationActions.GET_MIGRATIONS_FAILED,
-      handleGetMigration: MigrationActions.GET_MIGRATION,
-      handleGetMigrationSuccess: MigrationActions.GET_MIGRATION_SUCCESS,
-      handleGetMigrationFailed: MigrationActions.GET_MIGRATION_FAILED,
-      handleDeleteSuccess: MigrationActions.DELETE_SUCCESS,
-      handleMigrateReplicaSuccess: MigrationActions.MIGRATE_REPLICA_SUCCESS,
-      handleCancel: MigrationActions.CANCEL,
-      handleCancelSuccess: MigrationActions.CANCEL_SUCCESS,
-      handleCancelFailed: MigrationActions.CANCEL_FAILED,
-      handleClearDetails: MigrationActions.CLEAR_DETAILS,
-    })
-  }
+import type { MainItem } from '../types/MainItem'
+import type { Field } from '../types/Field'
+import NotificationStore from '../stores/NotificationStore'
+import MigrationSource from '../sources/MigrationSource'
 
 
-  handleGetMigrations({ showLoading }) {
-    if (showLoading || this.migrations.length === 0) {
+class MigrationStore {
+  @observable migrations: MainItem[] = []
+  @observable migrationDetails: ?MainItem = null
+  @observable loading: boolean = true
+  @observable canceling: boolean | { failed: boolean } = true
+  @observable detailsLoading: boolean = true
+
+  @action getMigrations(options?: { showLoading: boolean }) {
+    if ((options && options.showLoading) || this.migrations.length === 0) {
       this.loading = true
       this.loading = true
     }
     }
-  }
 
 
-  handleGetMigrationsSuccess(migrations) {
-    this.migrations = migrations.map(migration => {
-      let oldMigration = this.migrations.find(r => r.id === migration.id)
-      if (oldMigration) {
-        migration.executions = oldMigration.executions
-      }
+    return MigrationSource.getMigrations().then(migrations => {
+      this.migrations = migrations.map(migration => {
+        let oldMigration = this.migrations.find(r => r.id === migration.id)
+        if (oldMigration) {
+          migration.executions = oldMigration.executions
+        }
 
 
-      return migration
+        return migration
+      })
+      this.loading = false
+    }).catch(() => {
+      this.loading = false
     })
     })
-    this.loading = false
-  }
-
-  handleGetMigrationsFailed() {
-    this.loading = false
   }
   }
 
 
-  handleGetMigration({ showLoading }) {
+  @action getMigration(migrationId: string, showLoading: boolean) {
     this.detailsLoading = showLoading
     this.detailsLoading = showLoading
-  }
 
 
-  handleGetMigrationSuccess(migration) {
-    this.detailsLoading = false
-    this.migrationDetails = migration
+    return MigrationSource.getMigration(migrationId).then(migration => {
+      this.detailsLoading = false
+      this.migrationDetails = migration
+    }).catch(() => {
+      this.detailsLoading = false
+    })
   }
   }
 
 
-  handleGetMigrationFailed() {
-    this.detailsLoading = false
+  @action cancel(migrationId: string) {
+    this.canceling = true
+    return MigrationSource.cancel(migrationId).then(() => {
+      this.canceling = false
+    }).catch(() => {
+      this.canceling = { failed: true }
+    })
   }
   }
 
 
-  handleDeleteSuccess(migrationId) {
-    this.migrations = this.migrations.filter(r => r.id !== migrationId)
+  @action delete(migrationId: string) {
+    return MigrationSource.delete(migrationId).then(() => {
+      this.migrations = this.migrations.filter(r => r.id !== migrationId)
+    })
   }
   }
 
 
-  handleMigrateReplicaSuccess(migration) {
-    this.migrations = [
-      migration,
-      ...this.migrations,
-    ]
+  @action migrateReplica(replicaId: string, options: Field[]) {
+    return MigrationSource.migrateReplica(replicaId, options).then(migration => {
+      this.migrations = [
+        migration,
+        ...this.migrations,
+      ]
 
 
-    setTimeout(() => {
-      NotificationActions.notify('Migration successfully created from replica.', 'success', {
+      NotificationStore.notify('Migration successfully created from replica.', 'success', {
         action: {
         action: {
           label: 'View Migration Status',
           label: 'View Migration Status',
           callback: () => {
           callback: () => {
@@ -96,24 +91,12 @@ class MigrationStore {
         persist: true,
         persist: true,
         persistInfo: { title: 'Migration created' },
         persistInfo: { title: 'Migration created' },
       })
       })
-    }, 0)
-  }
-
-  handleCancel() {
-    this.canceling = true
-  }
-
-  handleCancelSuccess() {
-    this.canceling = false
-  }
-
-  handleCancelFailed() {
-    this.canceling = { failed: true }
+    })
   }
   }
 
 
-  handleClearDetails() {
+  @action clearDetails() {
     this.detailsLoading = true
     this.detailsLoading = true
   }
   }
 }
 }
 
 
-export default alt.createStore(MigrationStore)
+export default new MigrationStore()

+ 20 - 26
src/stores/NetworkStore.js

@@ -12,39 +12,33 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import NetworkActions from '../actions/NetworkActions'
+// @flow
+
+import { observable, action } from 'mobx'
+import type { Network } from '../types/Network'
+import NetworkSource from '../sources/NetworkSource'
 
 
 class NetworkStore {
 class NetworkStore {
-  constructor() {
-    this.networks = []
-    this.loading = false
-    this.cacheId = null
+  @observable networks: Network[] = []
+  @observable loading: boolean = false
 
 
-    this.bindListeners({
-      handleLoadNetworks: NetworkActions.LOAD_NETWORKS,
-      handleLoadNetworksSuccess: NetworkActions.LOAD_NETWORKS_SUCCESS,
-      handleLoadNetworksFailed: NetworkActions.LOAD_NETWORKS_FAILED,
-    })
-  }
+  cachedId: string = ''
 
 
-  handleLoadNetworks({ fromCache }) {
-    if (fromCache) {
-      return
+  @action loadNetworks(endpointId: string, environment: ?{ [string]: mixed }): Promise<void> {
+    let id = `${endpointId}-${btoa(JSON.stringify(environment))}`
+    if (this.cachedId === id) {
+      return Promise.resolve()
     }
     }
 
 
     this.loading = true
     this.loading = true
-  }
-
-  handleLoadNetworksSuccess({ networks, cacheId }) {
-    this.loading = false
-    this.networks = networks
-    this.cacheId = cacheId
-  }
-
-  handleLoadNetworksFailed() {
-    this.loading = false
+    return NetworkSource.loadNetworks(endpointId, environment).then((networks: Network[]) => {
+      this.loading = false
+      this.networks = networks
+      this.cachedId = id
+    }).catch(() => {
+      this.loading = false
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(NetworkStore)
+export default new NetworkStore()

+ 24 - 25
src/stores/NotificationStore.js

@@ -12,41 +12,40 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import NotificationActions from '../actions/NotificationActions'
+// @flow
+
+import { observable, action } from 'mobx'
+
+import type { NotificationItem } from '../types/NotificationItem'
+import NotificationSource from '../sources/NotificationSource'
 
 
 class NotificationStore {
 class NotificationStore {
-  constructor() {
-    this.bindListeners({
-      notify: NotificationActions.NOTIFY,
-      notifySuccess: NotificationActions.NOTIFY_SUCCESS,
-      loadNotificationsSuccess: NotificationActions.LOAD_NOTIFICATIONS_SUCCESS,
-      clearNotificationsSuccess: NotificationActions.CLEAR_NOTIFICATIONS_SUCCESS,
-    })
+  @observable notifications: NotificationItem[] = []
+  @observable persistedNotifications: NotificationItem[] = []
 
 
-    this.notifications = []
-    this.persistedNotifications = []
-  }
+  @action notify(message: string, level?: $PropertyType<NotificationItem, 'level'>, options?: $PropertyType<NotificationItem, 'options'>): Promise<void> {
+    this.notifications.push({ message, level, options })
 
 
-  notify(options) {
-    let newItem = {
-      ...options,
+    if (options && options.persist) {
+      return NotificationSource.notify(message, level, options).then((notification: NotificationItem) => {
+        this.persistedNotifications.push(notification)
+      })
     }
     }
 
 
-    this.notifications = this.notifications.concat(newItem)
+    return Promise.resolve()
   }
   }
 
 
-  notifySuccess(notification) {
-    this.persistedNotifications = this.persistedNotifications.concat([notification])
-  }
-
-  loadNotificationsSuccess(notifications) {
-    this.persistedNotifications = notifications
+  @action loadNotifications(): Promise<void> {
+    return NotificationSource.loadNotifications().then((notifications: NotificationItem[]) => {
+      this.persistedNotifications = notifications
+    })
   }
   }
 
 
-  clearNotificationsSuccess() {
-    this.persistedNotifications = []
+  @action clearNotifications(): Promise<void> {
+    return NotificationSource.clearNotifications().then(() => {
+      this.persistedNotifications = []
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(NotificationStore)
+export default new NotificationStore()

+ 16 - 26
src/stores/ProjectStore.js

@@ -12,37 +12,27 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import ProjectActions from '../actions/ProjectActions'
+// @flow
 
 
-class ProjectStore {
-  constructor() {
-    this.projects = []
-    this.loading = false
+import { observable, action } from 'mobx'
 
 
-    if (!ProjectActions) {
-      return
-    }
+import type { Project } from '../types/Project'
+import ProjectSource from '../sources/ProjectSource'
 
 
-    this.bindListeners({
-      handleGetProjects: ProjectActions.GET_PROJECTS,
-      handleGetProjectsCompleted: ProjectActions.GET_PROJECTS_COMPLETED,
-      handleGetProjectsFailed: ProjectActions.GET_PROJECTS_FAILED,
-    })
-  }
-
-  handleGetProjects() {
-    this.loading = true
-  }
 
 
-  handleGetProjectsCompleted(projects) {
-    this.projects = projects
-    this.loading = false
-  }
+class ProjectStore {
+  @observable projects: Project[] = []
+  @observable loading: boolean = false
 
 
-  handleGetProjectsFailed() {
-    this.loading = false
+  @action getProjects(): Promise<void> {
+    this.loading = true
+    return ProjectSource.getProjects().then((projects: Project[]) => {
+      this.loading = false
+      this.projects = projects
+    }).catch(() => {
+      this.loading = false
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(ProjectStore)
+export default new ProjectStore()

+ 36 - 47
src/stores/ProviderStore.js

@@ -12,70 +12,59 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import ProviderActions from '../actions/ProviderActions'
+// @flow
 
 
-class ProviderStore {
-  constructor() {
-    this.connectionInfoSchema = []
-    this.connectionSchemaLoading = false
-    this.providers = null
-    this.providersLoading = false
-    this.optionsSchema = []
-    this.optionsSchemaLoading = false
+import { observable, action } from 'mobx'
 
 
-    this.bindListeners({
-      handleGetConnectionInfoSchema: ProviderActions.GET_CONNECTION_INFO_SCHEMA,
-      handleGetConnectionInfoSchemaSuccess: ProviderActions.GET_CONNECTION_INFO_SCHEMA_SUCCESS,
-      handleGetConnectionInfoSchemaFailed: ProviderActions.GET_CONNECTION_INFO_SCHEMA_FAILED,
-      handleClearConnectionInfoSchema: ProviderActions.CLEAR_CONNECTION_INFO_SCHEMA,
-      handleLoadProviders: ProviderActions.LOAD_PROVIDERS,
-      handleLoadProvidersSuccess: ProviderActions.LOAD_PROVIDERS_SUCCESS,
-      handleLoadOptionsSchema: ProviderActions.LOAD_OPTIONS_SCHEMA,
-      handleLoadOptionsSchemaSuccess: ProviderActions.LOAD_OPTIONS_SCHEMA_SUCCESS,
-      handleLoadOptionsSchemaFailed: ProviderActions.LOAD_OPTIONS_SCHEMA_FAILED,
-    })
-  }
+import ProviderSource from '../sources/ProviderSource'
+import type { Field } from '../types/Field'
+import type { Providers } from '../types/Providers'
 
 
-  handleGetConnectionInfoSchema() {
-    this.connectionSchemaLoading = true
-  }
+class ProviderStore {
+  @observable connectionInfoSchema: Field[] = []
+  @observable connectionSchemaLoading: boolean = false
+  @observable providers: ?Providers
+  @observable providersLoading: boolean = false
+  @observable optionsSchema: Field[] = []
+  @observable optionsSchemaLoading: boolean = false
 
 
-  handleGetConnectionInfoSchemaSuccess(schema) {
-    this.connectionSchemaLoading = false
-    this.connectionInfoSchema = schema
-  }
+  @action getConnectionInfoSchema(providerName: string): Promise<void> {
+    this.connectionSchemaLoading = true
 
 
-  handleGetConnectionInfoSchemaFailed() {
-    this.connectionSchemaLoading = false
+    return ProviderSource.getConnectionInfoSchema(providerName).then((fields: Field[]) => {
+      this.connectionSchemaLoading = false
+      this.connectionInfoSchema = fields
+    }).catch(() => {
+      this.connectionSchemaLoading = false
+    })
   }
   }
 
 
-  handleClearConnectionInfoSchema() {
+  @action clearConnectionInfoSchema() {
     this.connectionInfoSchema = []
     this.connectionInfoSchema = []
   }
   }
 
 
-  handleLoadProviders() {
+  @action loadProviders(): Promise<void> {
     this.providers = null
     this.providers = null
     this.providersLoading = true
     this.providersLoading = true
-  }
 
 
-  handleLoadProvidersSuccess(providers) {
-    this.providers = providers
-    this.providersLoading = false
+    return ProviderSource.loadProviders().then((providers: Providers) => {
+      this.providers = providers
+      this.providersLoading = false
+    }).catch(() => {
+      this.providersLoading = false
+    })
   }
   }
 
 
-  handleLoadOptionsSchema() {
+  @action loadOptionsSchema(providerName: string, schemaType: string): Promise<void> {
     this.optionsSchemaLoading = true
     this.optionsSchemaLoading = true
-  }
-
-  handleLoadOptionsSchemaSuccess(schema) {
-    this.optionsSchemaLoading = false
-    this.optionsSchema = schema
-  }
 
 
-  handleLoadOptionsSchemaFailed() {
-    this.optionsSchemaLoading = false
+    return ProviderSource.loadOptionsSchema(providerName, schemaType).then((fields: Field[]) => {
+      this.optionsSchemaLoading = false
+      this.optionsSchema = fields
+    }).catch(() => {
+      this.optionsSchemaLoading = false
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(ProviderStore)
+export default new ProviderStore()

+ 78 - 105
src/stores/ReplicaStore.js

@@ -12,21 +12,27 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import ReplicaActions from '../actions/ReplicaActions'
-import NotificationActions from '../actions/NotificationActions'
+// @flow
+
+import { observable, action } from 'mobx'
+
+import NotificationStore from '../stores/NotificationStore'
+import ReplicaSource from '../sources/ReplicaSource'
+import type { MainItem } from '../types/MainItem'
+import type { Execution } from '../types/Execution'
+import type { Field } from '../types/Field'
 
 
 class ReplicaStoreUtils {
 class ReplicaStoreUtils {
-  static addExecutionToReplica({ replicaStore, replicaId, execution }) {
-    let executions = [execution]
+  static addExecutionToReplica(opts: { replicaStore: any, replicaId: string, execution: Execution }) {
+    let executions = [opts.execution]
 
 
-    if (replicaStore.replicaDetails.id === replicaId) {
-      if (replicaStore.replicaDetails.executions) {
-        executions = [...replicaStore.replicaDetails.executions, execution]
+    if (opts.replicaStore.replicaDetails.id === opts.replicaId) {
+      if (opts.replicaStore.replicaDetails.executions) {
+        executions = [...opts.replicaStore.replicaDetails.executions, opts.execution]
       }
       }
 
 
-      replicaStore.replicaDetails = {
-        ...replicaStore.replicaDetails,
+      opts.replicaStore.replicaDetails = {
+        ...opts.replicaStore.replicaDetails,
         executions,
         executions,
       }
       }
     }
     }
@@ -34,135 +40,102 @@ class ReplicaStoreUtils {
 }
 }
 
 
 class ReplicaStore {
 class ReplicaStore {
-  constructor() {
-    this.replicas = []
-    this.replicaDetails = {}
-    this.loading = true
-    this.backgroundLoading = false
-    this.detailsLoading = true
-    this.replicasExecutionsLoading = false
-
-    this.bindListeners({
-      handleGetReplicas: ReplicaActions.GET_REPLICAS,
-      handleGetReplicasSuccess: ReplicaActions.GET_REPLICAS_SUCCESS,
-      handleGetReplicasFailed: ReplicaActions.GET_REPLICAS_FAILED,
-      handleGetReplicasExecutions: ReplicaActions.GET_REPLICAS_EXECUTIONS,
-      handleGetReplicasExecutionsSuccess: ReplicaActions.GET_REPLICAS_EXECUTIONS_SUCCESS,
-      handleGetReplicasExecutionsFailed: ReplicaActions.GET_REPLICAS_EXECUTIONS_FAILED,
-      handleGetReplicaExecutionsSuccess: ReplicaActions.GET_REPLICA_EXECUTIONS_SUCCESS,
-      handleGetReplica: ReplicaActions.GET_REPLICA,
-      handleGetReplicaSuccess: ReplicaActions.GET_REPLICA_SUCCESS,
-      handleGetReplicaFailed: ReplicaActions.GET_REPLICA_FAILED,
-      handleExecuteSuccess: ReplicaActions.EXECUTE_SUCCESS,
-      handleDeleteExecutionSuccess: ReplicaActions.DELETE_EXECUTION_SUCCESS,
-      handleDeleteSuccess: ReplicaActions.DELETE_SUCCESS,
-      handleDeleteDisksSuccess: ReplicaActions.DELETE_DISKS_SUCCESS,
-      handleCancelExecutionSuccess: ReplicaActions.CANCEL_EXECUTION_SUCCESS,
-      handleClearDetails: ReplicaActions.CLEAR_DETAILS,
-    })
-  }
+  @observable replicas: MainItem[] = []
+  @observable replicaDetails: ?MainItem = null
+  @observable loading: boolean = true
+  @observable backgroundLoading: boolean = false
+  @observable detailsLoading: boolean = true
 
 
-  handleGetReplicas({ showLoading }) {
+  @action getReplicas(options?: { showLoading: boolean }): Promise<MainItem[]> {
     this.backgroundLoading = true
     this.backgroundLoading = true
 
 
-    if (showLoading || this.replicas.length === 0) {
+    if ((options && options.showLoading) || this.replicas.length === 0) {
       this.loading = true
       this.loading = true
     }
     }
-  }
-
-  handleGetReplicasSuccess(replicas) {
-    this.replicas = replicas
-    this.loading = false
-    this.backgroundLoading = false
-  }
-
-  handleGetReplicasFailed() {
-    this.loading = false
-    this.backgroundLoading = false
-  }
 
 
-  handleGetReplicasExecutions() {
-    this.replicasExecutionsLoading = true
+    return ReplicaSource.getReplicas().then(replicas => {
+      this.replicas = replicas
+      this.loading = false
+      this.backgroundLoading = false
+    }).catch(() => {
+      this.loading = false
+      this.backgroundLoading = false
+    })
   }
   }
 
 
-  handleGetReplicasExecutionsSuccess(replicasExecutions) {
-    replicasExecutions.forEach(({ replicaId, executions }) => {
+  @action getReplicaExecutions(replicaId: string): Promise<Execution[]> {
+    return ReplicaSource.getReplicaExecutions(replicaId).then(executions => {
       let replica = this.replicas.find(replica => replica.id === replicaId)
       let replica = this.replicas.find(replica => replica.id === replicaId)
+
       if (replica) {
       if (replica) {
         replica.executions = executions
         replica.executions = executions
       }
       }
-    })
-
-    this.replicasExecutionsLoading = false
-  }
-
-  handleGetReplicasExecutionsFailed() {
-    this.replicasExecutionsLoading = false
-  }
-
-  handleGetReplicaExecutionsSuccess({ replicaId, executions }) {
-    let replica = this.replicas.find(replica => replica.id === replicaId)
 
 
-    if (replica) {
-      replica.executions = executions
-    }
-
-    if (this.replicaDetails.id === replicaId) {
-      this.replicaDetails = {
-        ...this.replicaDetails,
-        executions,
+      if (this.replicaDetails && this.replicaDetails.id === replicaId) {
+        this.replicaDetails = {
+          ...this.replicaDetails,
+          executions,
+        }
       }
       }
-    }
+    })
   }
   }
 
 
-  handleGetReplica() {
+  @action getReplica(replicaId: string): Promise<MainItem> {
     this.detailsLoading = true
     this.detailsLoading = true
-  }
-
-  handleGetReplicaSuccess(replica) {
-    this.detailsLoading = false
-    this.replicaDetails = replica
-  }
 
 
-  handleGetReplicaFailed() {
-    this.detailsLoading = false
+    return ReplicaSource.getReplica(replicaId).then(replica => {
+      this.detailsLoading = false
+      this.replicaDetails = replica
+    }).catch(() => {
+      this.detailsLoading = false
+    })
   }
   }
 
 
-  handleExecuteSuccess({ replicaId, execution }) {
-    ReplicaStoreUtils.addExecutionToReplica({ replicaStore: this, replicaId, execution })
+  @action execute(replicaId: string, fields?: Field[]): Promise<void> {
+    return ReplicaSource.execute(replicaId, fields).then(execution => {
+      ReplicaStoreUtils.addExecutionToReplica({ replicaStore: this, replicaId, execution })
+    })
   }
   }
 
 
-  handleDeleteDisksSuccess({ replicaId, execution }) {
-    ReplicaStoreUtils.addExecutionToReplica({ replicaStore: this, replicaId, execution })
+  @action cancelExecution(replicaId: string, executionId: string): Promise<void> {
+    return ReplicaSource.cancelExecution(replicaId, executionId).then(() => {
+      NotificationStore.notify('Cancelled', 'success')
+    })
   }
   }
 
 
-  handleDeleteExecutionSuccess({ replicaId, executionId }) {
-    let executions = []
+  @action deleteExecution(replicaId: string, executionId: string): Promise<void> {
+    return ReplicaSource.deleteExecution(replicaId, executionId).then(() => {
+      let executions = []
 
 
-    if (this.replicaDetails.id === replicaId) {
-      if (this.replicaDetails.executions) {
-        executions = [...this.replicaDetails.executions.filter(e => e.id !== executionId)]
-      }
+      if (this.replicaDetails && this.replicaDetails.id === replicaId) {
+        if (this.replicaDetails.executions) {
+          executions = [...this.replicaDetails.executions.filter(e => e.id !== executionId)]
+        }
 
 
-      this.replicaDetails = {
-        ...this.replicaDetails,
-        executions,
+        this.replicaDetails = {
+          ...this.replicaDetails,
+          executions,
+        }
       }
       }
-    }
+    })
   }
   }
 
 
-  handleDeleteSuccess(replicaId) {
-    this.replicas = this.replicas.filter(r => r.id !== replicaId)
+  @action delete(replicaId: string) {
+    return ReplicaSource.delete(replicaId).then(() => {
+      this.replicas = this.replicas.filter(r => r.id !== replicaId)
+    })
   }
   }
 
 
-  handleCancelExecutionSuccess() {
-    setTimeout(() => { NotificationActions.notify('Cancelled', 'success') }, 0)
+  @action deleteDisks(replicaId: string) {
+    return ReplicaSource.deleteDisks(replicaId).then(execution => {
+      ReplicaStoreUtils.addExecutionToReplica({ replicaStore: this, replicaId, execution })
+    })
   }
   }
 
 
-  handleClearDetails() {
+  @action clearDetails() {
     this.detailsLoading = true
     this.detailsLoading = true
-    this.replicaDetails = {}
+    this.replicaDetails = null
   }
   }
 }
 }
 
 
-export default alt.createStore(ReplicaStore)
+export default new ReplicaStore()

+ 54 - 65
src/stores/ScheduleStore.js

@@ -12,8 +12,12 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import ScheduleActions from '../actions/ScheduleActions'
+// @flow
+
+import { observable, action } from 'mobx'
+
+import type { Schedule } from '../types/Schedule'
+import Source from '../sources/ScheduleSource'
 
 
 const updateSchedule = (schedules, id, data) => {
 const updateSchedule = (schedules, id, data) => {
   return schedules.map(schedule => {
   return schedules.map(schedule => {
@@ -30,74 +34,60 @@ const updateSchedule = (schedules, id, data) => {
 }
 }
 
 
 class ScheduleStore {
 class ScheduleStore {
-  constructor() {
-    this.loading = false
-    this.schedules = []
-    this.unsavedSchedules = []
-    this.scheduling = false
-    this.adding = false
-
-    this.bindListeners({
-      handleScheduleMultiple: ScheduleActions.SCHEDULE_MULTIPLE,
-      handleScheduleMultipleSuccess: ScheduleActions.SCHEDULE_MULTIPLE_SUCCESS,
-      handleScheduleMultipleFailed: ScheduleActions.SCHEDULE_MULTIPLE_FAILED,
-      handleGetSchedules: ScheduleActions.GET_SCHEDULES,
-      handleGetSchedulesSuccess: ScheduleActions.GET_SCHEDULES_SUCCESS,
-      handleGetSchedulesFailed: ScheduleActions.GET_SCHEDULES_FAILED,
-      handleAddSchedule: ScheduleActions.ADD_SCHEDULE,
-      handleAddScheduleSuccess: ScheduleActions.ADD_SCHEDULE_SUCCESS,
-      handleAddScheduleFailed: ScheduleActions.ADD_SCHEDULE_FAILED,
-      handleRemoveSchedule: ScheduleActions.REMOVE_SCHEDULE,
-      handleUpdateSchedule: ScheduleActions.UPDATE_SCHEDULE,
-      handleUpdateScheduleSuccess: ScheduleActions.UPDATE_SCHEDULE_SUCCESS,
-      handleClearUnsavedSchedules: ScheduleActions.CLEAR_UNSAVED_SCHEDULES,
-    })
-  }
+  @observable loading: boolean = false
+  @observable schedules: Schedule[] = []
+  @observable unsavedSchedules: Schedule[] = []
+  @observable scheduling: boolean = false
+  @observable adding: boolean = false
 
 
-  handleScheduleMultiple() {
+  @action scheduleMultiple(replicaId: string, schedules: Schedule[]): Promise<void> {
     this.scheduling = true
     this.scheduling = true
-  }
 
 
-  handleScheduleMultipleSuccess() {
-    this.scheduling = false
-  }
-
-  handleScheduleMultipleFailed() {
-    this.scheduling = false
+    return Source.scheduleMultiple(replicaId, schedules).then((schedules: Schedule[]) => {
+      this.scheduling = false
+      this.schedules = schedules
+    }).catch(() => {
+      this.scheduling = false
+    })
   }
   }
 
 
-  handleGetSchedules() {
+  @action getSchedules(replicaId: string): Promise<void> {
     this.loading = true
     this.loading = true
-  }
 
 
-  handleGetSchedulesSuccess(schedules) {
-    this.loading = false
-    this.schedules = schedules
-  }
-
-  handleGetSchedulesFailed() {
-    this.loading = false
+    return Source.getSchedules(replicaId).then((schedules: Schedule[]) => {
+      this.loading = false
+      this.schedules = schedules
+    }).catch(() => {
+      this.loading = false
+    })
   }
   }
 
 
-  handleAddSchedule() {
+  @action addSchedule(replicaId: string, schedule: Schedule): Promise<void> {
     this.adding = true
     this.adding = true
-  }
-
-  handleAddScheduleSuccess(schedule) {
-    this.adding = false
-    this.schedules = [...this.schedules, schedule]
-  }
 
 
-  handleAddScheduleFailed() {
-    this.adding = false
+    return Source.addSchedule(replicaId, schedule).then((schedule: Schedule) => {
+      this.adding = false
+      this.schedules = [...this.schedules, schedule]
+    }).catch(() => {
+      this.adding = false
+    })
   }
   }
 
 
-  handleRemoveSchedule({ scheduleId }) {
+  @action removeSchedule(replicaId: string, scheduleId: string): Promise<void> {
     this.schedules = this.schedules.filter(s => s.id !== scheduleId)
     this.schedules = this.schedules.filter(s => s.id !== scheduleId)
     this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== scheduleId)
     this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== scheduleId)
+
+    return Source.removeSchedule(replicaId, scheduleId)
   }
   }
 
 
-  handleUpdateSchedule({ scheduleId, data, forceSave }) {
+  @action updateSchedule(
+    replicaId: string,
+    scheduleId: string,
+    data: Schedule,
+    oldData: ?Schedule,
+    unsavedData: ?Schedule,
+    forceSave?: boolean
+  ): Promise<void> {
     this.schedules = updateSchedule(this.schedules, scheduleId, data)
     this.schedules = updateSchedule(this.schedules, scheduleId, data)
 
 
     if (!forceSave) {
     if (!forceSave) {
@@ -107,24 +97,23 @@ class ScheduleStore {
       } else {
       } else {
         this.unsavedSchedules.push({ id: scheduleId, ...data })
         this.unsavedSchedules.push({ id: scheduleId, ...data })
       }
       }
+      return Promise.resolve()
     }
     }
-  }
-
-  handleUpdateScheduleSuccess(schedule) {
-    this.schedules = this.schedules.map(s => {
-      if (s.id === schedule.id) {
-        return { ...schedule }
-      }
 
 
-      return { ...s }
+    return Source.updateSchedule(replicaId, scheduleId, data, oldData, unsavedData).then((schedule: Schedule) => {
+      this.schedules = this.schedules.map(s => {
+        if (s.id === schedule.id) {
+          return { ...schedule }
+        }
+        return { ...s }
+      })
+      this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== schedule.id)
     })
     })
-    this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== schedule.id)
   }
   }
 
 
-  handleClearUnsavedSchedules() {
+  @action clearUnsavedSchedules() {
     this.unsavedSchedules = []
     this.unsavedSchedules = []
-    this.saving = false
   }
   }
 }
 }
 
 
-export default alt.createStore(ScheduleStore)
+export default new ScheduleStore()

+ 76 - 41
src/stores/UserStore.js

@@ -12,67 +12,102 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import UserActions from '../actions/UserActions'
+// @flow
 
 
-class UserStore {
-  constructor() {
-    this.user = null
-    this.loading = false
-    this.loginFailedResponse = null
+import { observable, action } from 'mobx'
+import type { User, Credentials } from '../types/User'
+import UserSource from '../sources/UserSource'
+import ProjectStore from './ProjectStore'
+import NotificationStore from '../stores/NotificationStore'
 
 
-    this.bindListeners({
-      handleLogin: UserActions.LOGIN,
-      handleLoginFailed: UserActions.LOGIN_FAILED,
-      handleLoginScopedSuccess: UserActions.LOGIN_SCOPED_SUCCESS,
-      handleLoginScopedFailed: UserActions.LOGIN_SCOPED_FAILED,
-      handleTokenLogin: UserActions.TOKEN_LOGIN,
-      handleTokenLoginSuccess: UserActions.TOKEN_LOGIN_SUCCESS,
-      handleTokenLoginFailed: UserActions.TOKEN_LOGIN_FAILED,
-      handleGetUserInfoSuccess: UserActions.GET_USER_INFO_SUCCESS,
-    })
-  }
+/**
+ * This is the authentication / authorization flow:
+ * 1. Post username and password unscoped login. Set unscoped token in cookies.
+ * 2. Post unscoped token with project id. Set scoped token and project id in cookies.
+ * 3. Get token login on subsequent app reloads to retrieve the user info.
+ * 
+ * After token expiration, the app is redirected to login page.
+ */
+class UserStore {
+  @observable user: ?User = null
+  @observable loading: boolean = false
+  @observable loginFailedResponse: any = null
 
 
-  handleLogin() {
+  @action login(creds: Credentials): Promise<void> {
     this.loading = true
     this.loading = true
     this.user = null
     this.user = null
     this.loginFailedResponse = null
     this.loginFailedResponse = null
-  }
 
 
-  handleLoginFailed(response) {
-    this.loading = false
-    this.loginFailedResponse = response
+    return UserSource.login(creds).then(() => {
+      return this.loginScoped()
+    }).then((user: User) => {
+      this.loading = false
+      NotificationStore.notify('Signed in', 'success')
+      this.user = user
+      this.getUserInfo(user)
+    }).catch((reason) => {
+      this.loading = false
+      this.loginFailedResponse = reason
+    })
   }
   }
 
 
-  handleLoginScopedSuccess(data) {
-    this.user = { ...data, scoped: true }
-    this.loading = false
+  @action loginScoped(projectId?: string): Promise<User> {
+    return new Promise((resolve) => {
+      if (ProjectStore.projects && ProjectStore.projects.length) {
+        UserSource.loginScoped(projectId || ProjectStore.projects[0].id).then((user: User) => {
+          this.user = { ...user, scoped: true }
+          resolve(user)
+        })
+      }
+      ProjectStore.getProjects().then(() => {
+        UserSource.loginScoped(projectId || ProjectStore.projects[0].id).then((user: User) => {
+          this.user = { ...user, scoped: true }
+          resolve(user)
+        })
+      })
+    })
   }
   }
 
 
-  handleLoginScopedFailed(response) {
-    this.user = null
-    this.loading = false
-    this.loginFailedResponse = response
+  @action getUserInfo(user: User): Promise<User> {
+    return UserSource.getUserInfo(user).then((userData: User) => {
+      this.user = { ...this.user, ...userData }
+    }).catch(reason => {
+      console.error('Error while getting user data', reason)
+      NotificationStore.notify('Error while getting user data', 'error')
+    })
   }
   }
 
 
-  handleTokenLogin() {
+  @action tokenLogin(): Promise<User> {
     this.user = null
     this.user = null
     this.loading = true
     this.loading = true
-  }
 
 
-  handleTokenLoginSuccess(data) {
-    this.user = { ...data, scoped: true }
-    this.loading = false
+    return UserSource.tokenLogin().then(user => {
+      this.loading = false
+      this.user = { ...this.user, ...user }
+      NotificationStore.notify('Signed in', 'success')
+      this.getUserInfo(user)
+    }).catch(() => {
+      this.loading = false
+    })
   }
   }
 
 
-  handleTokenLoginFailed() {
-    this.user = null
-    this.loading = false
+  @action switchProject(projectId: string): Promise<User> {
+    NotificationStore.notify('Switching projects')
+    return UserSource.switchProject().then(() => {
+      return this.loginScoped(projectId)
+    }).catch(reason => {
+      console.error('Error switching projects', reason)
+      NotificationStore.notify('Error switching projects')
+      this.logout()
+    })
   }
   }
 
 
-  handleGetUserInfoSuccess(user) {
-    this.user = { ...this.user, ...user }
+  @action logout(): Promise<void> {
+    return UserSource.logout().catch(reason => {
+      console.log('Error logging out', reason)
+      NotificationStore.notify('Error logging out')
+    })
   }
   }
 }
 }
 
 
-export default alt.createStore(UserStore)
+export default new UserStore()

+ 80 - 86
src/stores/WizardStore.js

@@ -12,143 +12,137 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import alt from '../alt'
-import WizardActions from '../actions/WizardActions'
+// @flow
 
 
+import { observable, action } from 'mobx'
+
+import type { WizardData, WizardPage } from '../types/WizardData'
+import type { MainItem } from '../types/MainItem'
+import type { Instance } from '../types/Instance'
+import type { Field } from '../types/Field'
+import type { NetworkMap } from '../types/Network'
+import type { Schedule } from '../types/Schedule'
 import { wizardConfig } from '../config'
 import { wizardConfig } from '../config'
+import Source from '../sources/WizardSource'
 
 
 class WizardStore {
 class WizardStore {
-  constructor() {
-    this.data = {}
-    this.currentPage = wizardConfig.pages[0]
-    this.createdItem = null
-    this.creatingItem = false
-    this.createdItems = null
-    this.creatingItems = false
-
-    this.bindListeners({
-      handleUpdateData: WizardActions.UPDATE_DATA,
-      handleClearData: WizardActions.CLEAR_DATA,
-      handleSetCurrentPage: WizardActions.SET_CURRENT_PAGE,
-      handleToggleInstanceSelection: WizardActions.TOGGLE_INSTANCE_SELECTION,
-      handleUpdateOptions: WizardActions.UPDATE_OPTIONS,
-      handleUpdateNetworks: WizardActions.UPDATE_NETWORKS,
-      handleAddSchedule: WizardActions.ADD_SCHEDULE,
-      handleUpdateSchedule: WizardActions.UPDATE_SCHEDULE,
-      handleRemoveSchedule: WizardActions.REMOVE_SCHEDULE,
-      handleCreate: WizardActions.CREATE,
-      handleCreateSuccess: WizardActions.CREATE_SUCCESS,
-      handleCreateFailed: WizardActions.CREATE_FAILED,
-      handleCreateMultiple: WizardActions.CREATE_MULTIPLE,
-      handleCreateMultipleSuccess: WizardActions.CREATE_MULTIPLE_SUCCESS,
-      handleCreateMultipleFailed: WizardActions.CREATE_MULTIPLE_FAILED,
-      handleGetDataFromPermalink: WizardActions.GET_DATA_FROM_PERMALINK,
-    })
-  }
-
-  handleUpdateData(data) {
-    this.data = {
-      ...this.data,
-      ...data,
-    }
-  }
-
-  handleClearData() {
-    this.data = {}
-    this.currentPage = wizardConfig.pages[0]
-  }
+  @observable data: WizardData = { schedules: [] }
+  @observable currentPage: WizardPage = wizardConfig.pages[0]
+  @observable createdItem: ?MainItem = null
+  @observable creatingItem: boolean = false
+  @observable createdItems: ?MainItem[] = null
+  @observable creatingItems: boolean = false
 
 
-  handleSetCurrentPage(page) {
-    this.currentPage = page
+  @action updateData(data: WizardData) {
+    this.data = { ...this.data, ...data }
   }
   }
 
 
-  handleToggleInstanceSelection(instance) {
+  @action toggleInstanceSelection(instance: Instance) {
     if (!this.data.selectedInstances) {
     if (!this.data.selectedInstances) {
       this.data.selectedInstances = [instance]
       this.data.selectedInstances = [instance]
       return
       return
     }
     }
 
 
     if (this.data.selectedInstances.find(i => i.id === instance.id)) {
     if (this.data.selectedInstances.find(i => i.id === instance.id)) {
+      // $FlowIssue
       this.data.selectedInstances = this.data.selectedInstances.filter(i => i.id !== instance.id)
       this.data.selectedInstances = this.data.selectedInstances.filter(i => i.id !== instance.id)
     } else {
     } else {
+      // $FlowIssue
       this.data.selectedInstances = [...this.data.selectedInstances, instance]
       this.data.selectedInstances = [...this.data.selectedInstances, instance]
     }
     }
   }
   }
 
 
-  handleUpdateOptions({ field, value }) {
+  @action clearData() {
+    this.data = {}
+    this.currentPage = wizardConfig.pages[0]
+  }
+
+  @action setCurrentPage(page: WizardPage) {
+    this.currentPage = page
+  }
+
+  @action updateOptions(data: { field: Field, value: any }) {
     this.data.options = {
     this.data.options = {
       ...this.data.options,
       ...this.data.options,
     }
     }
-    this.data.options[field.name] = value
+    this.data.options[data.field.name] = data.value
   }
   }
 
 
-  handleUpdateNetworks({ sourceNic, targetNetwork }) {
+  @action updateNetworks(network: NetworkMap) {
     if (!this.data.networks) {
     if (!this.data.networks) {
       this.data.networks = []
       this.data.networks = []
     }
     }
 
 
-    this.data.networks = this.data.networks.filter(n => n.sourceNic.network_name !== sourceNic.network_name)
-    this.data.networks.push({ sourceNic, targetNetwork })
+    this.data.networks = this.data.networks.filter(n => n.sourceNic.network_name !== network.sourceNic.network_name)
+    this.data.networks.push(network)
   }
   }
 
 
-  handleAddSchedule(schedule) {
+  @action addSchedule(schedule: Schedule) {
     if (!this.data.schedules) {
     if (!this.data.schedules) {
       this.data.schedules = []
       this.data.schedules = []
     }
     }
-    this.data.schedules.push({ id: new Date().getTime(), schedule: schedule.schedule })
+    this.data.schedules.push({ id: new Date().getTime().toString(), schedule: schedule.schedule })
   }
   }
 
 
-  handleUpdateSchedule({ scheduleId, data }) {
-    let schedule = this.data.schedules.find(s => s.id === scheduleId)
-    if (data.schedule) {
-      schedule.schedule = {
-        ...schedule.schedule,
-        ...data.schedule,
+  @action updateSchedule(scheduleId: string, data: Schedule) {
+    if (!this.data.schedules) {
+      return
+    }
+    this.data.schedules = this.data.schedules.map(schedule => {
+      if (schedule.id !== scheduleId) {
+        return schedule
       }
       }
-    } else {
-      schedule = {
-        ...schedule,
-        ...data,
+      if (data.schedule) {
+        schedule.schedule = {
+          ...schedule.schedule,
+          ...data.schedule,
+        }
+      } else {
+        schedule = {
+          ...schedule,
+          ...data,
+        }
       }
       }
-    }
-
-    this.data.schedules = this.data.schedules.filter(s => s.id !== scheduleId)
-    this.data.schedules.push(schedule)
-    this.data.schedules.sort((a, b) => a.id > b.id)
+      return schedule
+    })
   }
   }
 
 
-  handleRemoveSchedule(scheduleId) {
+  @action removeSchedule(scheduleId: string) {
+    if (!this.data.schedules) {
+      return
+    }
     this.data.schedules = this.data.schedules.filter(s => s.id !== scheduleId)
     this.data.schedules = this.data.schedules.filter(s => s.id !== scheduleId)
   }
   }
 
 
-  handleCreate() {
+  @action create(type: string, data: WizardData): Promise<void> {
     this.creatingItem = true
     this.creatingItem = true
-  }
 
 
-  handleCreateSuccess(item) {
-    this.createdItem = item
-    this.creatingItem = false
-  }
-
-  handleCreateFailed() {
-    this.creatingItem = false
+    return Source.create(type, data).then((item: MainItem) => {
+      this.createdItem = item
+      this.creatingItem = false
+    }).catch(() => {
+      this.creatingItem = false
+    })
   }
   }
 
 
-  handleCreateMultiple() {
+  @action createMultiple(type: string, data: WizardData): Promise<void> {
     this.creatingItems = true
     this.creatingItems = true
-  }
 
 
-  handleCreateMultipleSuccess(items) {
-    this.createdItems = items
-    this.creatingItems = false
+    return Source.createMultiple(type, data).then((items: MainItem[]) => {
+      this.createdItems = items
+      this.creatingItems = false
+    }).catch(() => {
+      this.creatingItems = false
+    })
   }
   }
 
 
-  handleCreateMultipleFailed() {
-    this.creatingItems = false
+  @action setPermalink(data: WizardData) {
+    Source.setPermalink(data)
   }
   }
 
 
-  handleGetDataFromPermalink(data) {
-    if (data === true) {
+  @action getDataFromPermalink() {
+    let data = Source.getDataFromPermalink()
+    if (!data) {
       return
       return
     }
     }
 
 
@@ -159,4 +153,4 @@ class WizardStore {
   }
   }
 }
 }
 
 
-export default alt.createStore(WizardStore)
+export default new WizardStore()

+ 5 - 0
src/types/Endpoint.js

@@ -14,6 +14,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 // @flow
 // @flow
 
 
+export type Validation = {
+  valid: boolean,
+  message: string,
+}
+
 export type Endpoint = {
 export type Endpoint = {
   id: string,
   id: string,
   name: string,
   name: string,

+ 8 - 6
src/types/Network.js

@@ -14,11 +14,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 // @flow
 // @flow
 
 
+import type { Nic } from './Instance'
+
 export type Network = {
 export type Network = {
-  sourceNic: {
-    network_name: string,
-  },
-  targetNetwork: {
-    name: string,
-  },
+  name: string,
+}
+
+export type NetworkMap = {
+  sourceNic: Nic,
+  targetNetwork: Network,
 }
 }

+ 11 - 4
src/types/NotificationItem.js

@@ -15,8 +15,15 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 // @flow
 
 
 export type NotificationItem = {
 export type NotificationItem = {
-  options: { persistInfo?: { title: string } },
-  message?: string,
-  id: string,
-  level: 'success' | 'error' | 'info',
+  options?: {
+    persist?: boolean,
+    persistInfo?: { title: string },
+    action?: {
+      label: string,
+      callback: () => void,
+    }
+  },
+  message: string,
+  id?: string,
+  level?: 'success' | 'error' | 'info',
 }
 }

+ 6 - 2
src/alt.js → src/types/Providers.js

@@ -12,6 +12,10 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import Alt from 'alt/lib'
+// @flow
 
 
-export default new Alt()
+export type Providers = {
+  [string]: {
+    types: number[],
+  },
+}

+ 10 - 21
src/actions/ProjectActions.js → src/types/User.js

@@ -14,27 +14,16 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 // @flow
 // @flow
 
 
-import alt from '../alt'
+import type { Project } from './Project'
 
 
-import PojectSource from '../sources/ProjectSource'
-
-class ProjectActions {
-  getProjects() {
-    return {
-      promise: PojectSource.getProjects().then(
-        this.getProjectsCompleted.bind(this),
-        this.getProjectsFailed.bind(this)
-      ).catch(this.getProjectsFailed.bind(this)),
-    }
-  }
-
-  getProjectsCompleted(response) {
-    return response || true
-  }
-
-  getProjectsFailed(response) {
-    return response || true
-  }
+export type User = {
+  scoped: boolean,
+  project: Project,
+  email: string,
+  name: string,
 }
 }
 
 
-export default alt.createActions(ProjectActions)
+export type Credentials = {
+  name: string,
+  password: string,
+}

+ 14 - 7
src/types/WizardData.js

@@ -16,14 +16,21 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import type { Schedule } from './Schedule'
 import type { Schedule } from './Schedule'
 import type { Instance } from './Instance'
 import type { Instance } from './Instance'
-import type { Network } from './Network'
+import type { NetworkMap } from './Network'
 import type { Endpoint } from './Endpoint'
 import type { Endpoint } from './Endpoint'
 
 
 export type WizardData = {
 export type WizardData = {
-  options: { [string]: mixed },
-  schedules: Schedule[],
-  selectedInstances: Instance[],
-  networks: Network[],
-  source: Endpoint,
-  target: Endpoint,
+  options?: ?{ [string]: mixed },
+  schedules?: Schedule[],
+  selectedInstances?: ?Instance[],
+  networks?: ?NetworkMap[],
+  source?: Endpoint,
+  target?: Endpoint,
+}
+
+export type WizardPage = {
+  id: string,
+  title: string,
+  breadcrumb: string,
+  excludeFrom?: 'replica' | 'migration',
 }
 }

+ 3 - 3
src/utils/ApiCaller.js

@@ -12,7 +12,7 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import NotificationActions from '../actions/NotificationActions'
+import NotificationStore from '../stores/NotificationStore'
 
 
 let apiInstance = null
 let apiInstance = null
 
 
@@ -90,7 +90,7 @@ class ApiCaller {
 
 
           if (result.data && result.data.error && result.data.error.message &&
           if (result.data && result.data.error && result.data.error.message &&
             (result.status !== 401 || window.location.hash !== loginUrl)) {
             (result.status !== 401 || window.location.hash !== loginUrl)) {
-            NotificationActions.notify(result.data.error.message, 'error')
+            NotificationStore.notify(result.data.error.message, 'error')
           }
           }
 
 
           if (result.status === 401 && window.location.hash !== loginUrl) {
           if (result.status === 401 && window.location.hash !== loginUrl) {
@@ -104,7 +104,7 @@ class ApiCaller {
       request.onerror = (result) => {
       request.onerror = (result) => {
         let loginUrl = '#/'
         let loginUrl = '#/'
         if (window.location.hash !== loginUrl) {
         if (window.location.hash !== loginUrl) {
-          NotificationActions.notify(`Request failed, there might be a problem with the 
+          NotificationStore.notify(`Request failed, there might be a problem with the 
           connection to the server.`, 'error')
           connection to the server.`, 'error')
         }
         }
 
 

+ 29 - 51
yarn.lock

@@ -291,20 +291,6 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
 
 
-alt-utils@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/alt-utils/-/alt-utils-2.0.0.tgz#4bbd562d20a9a295ac813a9e32b03539465a40cc"
-  dependencies:
-    prop-types "^15.5.10"
-
-alt@^0.18.6:
-  version "0.18.6"
-  resolved "https://registry.yarnpkg.com/alt/-/alt-0.18.6.tgz#d84c6c85e0179cb6c2fc7b9f9acec8c1faabd606"
-  dependencies:
-    flux "2.1.1"
-    is-promise "2.1.0"
-    transmitter "3.0.1"
-
 amdefine@>=0.0.4:
 amdefine@>=0.0.4:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
   resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
@@ -906,7 +892,7 @@ babel-plugin-syntax-class-properties@^6.8.0:
   version "6.13.0"
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
 
 
-babel-plugin-syntax-decorators@^6.13.0:
+babel-plugin-syntax-decorators@^6.1.18, babel-plugin-syntax-decorators@^6.13.0:
   version "6.13.0"
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
 
 
@@ -979,6 +965,14 @@ babel-plugin-transform-class-properties@6.24.1, babel-plugin-transform-class-pro
     babel-runtime "^6.22.0"
     babel-runtime "^6.22.0"
     babel-template "^6.24.1"
     babel-template "^6.24.1"
 
 
+babel-plugin-transform-decorators-legacy@^1.3.4:
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz#741b58f6c5bce9e6027e0882d9c994f04f366925"
+  dependencies:
+    babel-plugin-syntax-decorators "^6.1.18"
+    babel-runtime "^6.2.0"
+    babel-template "^6.3.0"
+
 babel-plugin-transform-decorators@^6.24.1:
 babel-plugin-transform-decorators@^6.24.1:
   version "6.24.1"
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
@@ -1499,7 +1493,7 @@ babel-register@^6.26.0:
     mkdirp "^0.5.1"
     mkdirp "^0.5.1"
     source-map-support "^0.4.15"
     source-map-support "^0.4.15"
 
 
-babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2:
+babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2:
   version "6.26.0"
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   dependencies:
   dependencies:
@@ -1515,7 +1509,7 @@ babel-template@7.0.0-beta.0:
     babylon "7.0.0-beta.22"
     babylon "7.0.0-beta.22"
     lodash "^4.2.0"
     lodash "^4.2.0"
 
 
-babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.7.0:
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-template@^6.7.0:
   version "6.26.0"
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
   dependencies:
   dependencies:
@@ -3203,21 +3197,7 @@ fb-watchman@^2.0.0:
   dependencies:
   dependencies:
     bser "^2.0.0"
     bser "^2.0.0"
 
 
-fbemitter@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865"
-  dependencies:
-    fbjs "^0.8.4"
-
-fbjs@0.1.0-alpha.7:
-  version "0.1.0-alpha.7"
-  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.1.0-alpha.7.tgz#ad4308b8f232fb3c73603349ea725d1e9c39323c"
-  dependencies:
-    core-js "^1.0.0"
-    promise "^7.0.3"
-    whatwg-fetch "^0.9.0"
-
-fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.5, fbjs@^0.8.9:
+fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9:
   version "0.8.16"
   version "0.8.16"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
   dependencies:
   dependencies:
@@ -3346,14 +3326,6 @@ flow-typed@^2.3.0:
     which "^1.3.0"
     which "^1.3.0"
     yargs "^4.2.0"
     yargs "^4.2.0"
 
 
-flux@2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/flux/-/flux-2.1.1.tgz#2c6ac652d4337488968489c6586f3aff26a38ea4"
-  dependencies:
-    fbemitter "^2.0.0"
-    fbjs "0.1.0-alpha.7"
-    immutable "^3.7.4"
-
 for-in@^1.0.1:
 for-in@^1.0.1:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -3768,6 +3740,10 @@ hoist-non-react-statics@^2.3.0:
   version "2.3.1"
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
 
 
+hoist-non-react-statics@^2.3.1:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
+
 home-or-tmp@^2.0.0:
 home-or-tmp@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -3900,7 +3876,7 @@ ignore@^3.3.3:
   version "3.3.5"
   version "3.3.5"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6"
 
 
-immutable@^3.7.4, immutable@^3.8.1:
+immutable@^3.8.1:
   version "3.8.2"
   version "3.8.2"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
 
 
@@ -4130,7 +4106,7 @@ is-primitive@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
   resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
 
 
-is-promise@2.1.0, is-promise@^2.1.0:
+is-promise@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
 
 
@@ -5056,10 +5032,20 @@ mkdirp@0.5, mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp
   dependencies:
   dependencies:
     minimist "0.0.8"
     minimist "0.0.8"
 
 
+mobx-react@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.4.2.tgz#22d710974883de1763cdafce3025570a79c64336"
+  dependencies:
+    hoist-non-react-statics "^2.3.1"
+
 mobx@^2.3.4:
 mobx@^2.3.4:
   version "2.7.0"
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/mobx/-/mobx-2.7.0.tgz#cf3d82d18c0ca7f458d8f2a240817b3dc7e54a01"
   resolved "https://registry.yarnpkg.com/mobx/-/mobx-2.7.0.tgz#cf3d82d18c0ca7f458d8f2a240817b3dc7e54a01"
 
 
+mobx@^3.6.1:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.6.1.tgz#ae63a8f00e1485a740d0f91ae2f6a5f68e303bea"
+
 moment@^2.18.1:
 moment@^2.18.1:
   version "2.18.1"
   version "2.18.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
@@ -5961,7 +5947,7 @@ promise.prototype.finally@^3.0.0:
     es-abstract "^1.9.0"
     es-abstract "^1.9.0"
     function-bind "^1.1.1"
     function-bind "^1.1.1"
 
 
-promise@^7.0.3, promise@^7.1.1:
+promise@^7.1.1:
   version "7.3.1"
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
   dependencies:
   dependencies:
@@ -7263,10 +7249,6 @@ tr46@~0.0.3:
   version "0.0.3"
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
 
 
-transmitter@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/transmitter/-/transmitter-3.0.1.tgz#32e99e43d1321e49dc2e194fa75df4fe84a8b918"
-
 "traverse@>=0.3.0 <0.4":
 "traverse@>=0.3.0 <0.4":
   version "0.3.9"
   version "0.3.9"
   resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
   resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
@@ -7686,10 +7668,6 @@ whatwg-fetch@>=0.10.0:
   version "2.0.3"
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
 
 
-whatwg-fetch@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0"
-
 whatwg-url@^4.3.0:
 whatwg-url@^4.3.0:
   version "4.8.0"
   version "4.8.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"