2
0
Эх сурвалжийг харах

Merge pull request #157 from smiclea/mobx

Replace alt.js with MobX for state management
Dorin Paslaru 8 жил өмнө
parent
commit
1262f633a5
73 өөрчлөгдсөн 1342 нэмэгдсэн , 2666 устгасан
  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"
   ],
   "plugins": [
-    "react-hot-loader/babel"
+    "react-hot-loader/babel",
+    "transform-decorators-legacy"
   ],
   "env": {
     "development": {

+ 1 - 0
.eslintrc

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

+ 1 - 0
.flowconfig

@@ -8,6 +8,7 @@
 esproposal.class_static_fields=enable
 esproposal.class_instance_fields=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='styl' -> '<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.exports: any;
 }
+
+declare module 'mobx' {
+  declare module.exports: any;
+}

+ 3 - 2
package.json

@@ -54,11 +54,10 @@
   },
   "dependencies": {
     "@webpack-blocks/webpack2": "^0.4.0",
-    "alt": "^0.18.6",
-    "alt-utils": "^2.0.0",
     "babel-core": "^6.26.0",
     "babel-loader": "^7.1.2",
     "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-react-remove-prop-types": "^0.4.9",
     "babel-preset-env": "^1.6.0",
@@ -75,6 +74,8 @@
     "html-webpack-plugin": "^2.30.1",
     "js-cookie": "^2.1.4",
     "lodash": "^4.17.4",
+    "mobx": "^3.6.1",
+    "mobx-react": "^4.4.2",
     "moment": "^2.18.1",
     "path": "^0.12.7",
     "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 EndpointDetailsPage from './pages/EndpointDetailsPage'
 import WizardPage from './pages/WizardPage'
+import UserStore from '../stores/UserStore'
 
 import Palette from './styleUtils/Palette'
 import StyleProps from './styleUtils/StyleProps'
-import UserActions from '../actions/UserActions'
 
 injectGlobal`
   ${Fonts}
@@ -50,7 +50,7 @@ const Wrapper = styled.div``
 
 class App extends React.Component<{}> {
   componentWillMount() {
-    UserActions.tokenLogin()
+    UserStore.tokenLogin()
   }
 
   render() {

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

@@ -18,8 +18,8 @@ import React from 'react'
 import styled from 'styled-components'
 
 import CopyButton from '../CopyButton'
-import NotificationActions from '../../../actions/NotificationActions'
 import DomUtils from '../../../utils/DomUtils'
+import NotificationStore from '../../../stores/NotificationStore'
 
 const Wrapper = styled.div`
   cursor: pointer;
@@ -50,9 +50,9 @@ class CopyValue extends React.Component<Props> {
     let succesful = DomUtils.copyTextToClipboard(this.props.value)
 
     if (succesful) {
-      NotificationActions.notify('The value has been copied to clipboard.')
+      NotificationStore.notify('The value has been copied to clipboard.')
     } 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 styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 import type { Endpoint } from '../../../types/Endpoint'
 import Checkbox from '../../atoms/Checkbox'
@@ -99,6 +100,7 @@ type Props = {
   onSelectedChange: (value: boolean) => void,
   getUsage: (item: Endpoint) => { replicasCount: number, migrationsCount: number },
 }
+@observer
 class EndpointListItem extends React.Component<Props> {
   render() {
     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 styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 import Checkbox from '../../atoms/Checkbox'
 import StatusPill from '../../atoms/StatusPill'
@@ -113,6 +114,7 @@ type Props = {
   endpointType: (endpointId: string) => string,
   onSelectedChange: (value: boolean) => void,
 }
+@observer
 class MainListItem extends React.Component<Props> {
   getLastExecution(): ?Execution | ?MainItem {
     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 = (
       <List>
         {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
 
           return (

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

@@ -28,7 +28,7 @@ import { executionOptions } from '../../../config'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import DateUtils from '../../../utils/DateUtils'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import deleteImage from './images/delete.svg'
 import deleteHoverImage from './images/delete-hover.svg'
 import saveImage from './images/save.svg'
@@ -145,7 +145,7 @@ class ScheduleItem extends React.Component<Props> {
   handleExpirationDateChange(date: Date) {
     let newDate = moment(date)
     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
     }
 

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

@@ -25,7 +25,7 @@ import StatusPill from '../../atoms/StatusPill'
 import CopyValue from '../../atoms/CopyValue'
 import ProgressBar from '../../atoms/ProgressBar'
 import CopyButton from '../../atoms/CopyButton'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import DomUtils from '../../../utils/DomUtils'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -159,7 +159,7 @@ class TaskItem extends React.Component<Props> {
     let succesful = DomUtils.copyTextToClipboard(exceptionText)
 
     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() {
-    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 (
         <Value
           width="calc(100% - 16px)"

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

@@ -82,7 +82,7 @@ const ItemLabel = styled.div`
 `
 
 type Props = {
-  items: Execution[],
+  items: ?Execution[],
   selectedItem: ?Execution,
   onPreviousClick: () => void,
   onNextClick: () => void,
@@ -113,7 +113,7 @@ class Timeline extends React.Component<Props> {
   }
 
   moveToSelectedItem() {
-    if (!this.progressLineRef || !this.endLineRef) {
+    if (!this.progressLineRef || !this.endLineRef || !this.props.items) {
       return
     }
 
@@ -132,6 +132,7 @@ class Timeline extends React.Component<Props> {
 
     this.itemsRef.style.marginLeft = `${offset}px`
 
+    // $FlowIssue
     let lastItemPos = (itemGap * (this.props.items.length - 1)) + offset + itemHalfWidth
     this.progressLineRef.style.width = `${lastItemPos}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 Props = {
   onItemClick: (item: DictItem) => void,
-  user: User,
+  user: ?User,
   white?: boolean,
 }
 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 Button from '../../atoms/Button'
 import StatusImage from '../../atoms/StatusImage'
+import type { Providers as ProvidersType } from '../../../types/Providers'
 
 import StyleProps from '../../styleUtils/StyleProps'
 
@@ -53,7 +54,7 @@ const LoadingText = styled.div`
 `
 
 type Props = {
-  providers: { [string]: any },
+  providers: ?ProvidersType,
   onCancelClick: () => void,
   onProviderClick: (provider: string) => void,
   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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import SideMenu from '../../molecules/SideMenu'
 import NotificationDropdown from '../../molecules/NotificationDropdown'
 import UserDropdown from '../../molecules/UserDropdown'
+import type { User as UserType } from '../../../types/User'
 
-import NotificationActions from '../../../actions/NotificationActions'
 import NotificationStore from '../../../stores/NotificationStore'
 
 import backgroundImage from './images/star-bg.jpg'
@@ -55,27 +55,17 @@ const User = styled.div`
 `
 
 type Props = {
-  user: { username: string, email: string },
+  user?: ?UserType,
   onUserItemClick: (userItem: { label: string, value: string }) => void,
-  notificationStore?: any,
 }
+@observer
 export class DetailsPageHeader extends React.Component<Props> {
-  static getStores() {
-    return [NotificationStore]
-  }
-
-  static getPropsFromStores(): $Shape<Props> {
-    return {
-      notificationStore: NotificationStore.getState(),
-    }
-  }
-
   componentDidMount() {
-    NotificationActions.loadNotifications()
+    NotificationStore.loadNotifications()
   }
 
   handleNotificationsClose() {
-    NotificationActions.clearNotifications()
+    NotificationStore.clearNotifications()
   }
 
   render() {
@@ -86,7 +76,7 @@ export class DetailsPageHeader extends React.Component<Props> {
           <Logo href="/#/replicas" />
         </Menu>
         <User>
-          <NotificationDropdown white items={this.props.notificationStore ? this.props.notificationStore.persistedNotifications : []} onClose={() => this.handleNotificationsClose()} />
+          <NotificationDropdown white items={NotificationStore.persistedNotifications} onClose={() => this.handleNotificationsClose()} />
           <UserDropdownStyled
             white
             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 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 StatusIcon from '../../atoms/StatusIcon'
@@ -27,11 +28,10 @@ import Button from '../../atoms/Button'
 import LoadingButton from '../../molecules/LoadingButton'
 
 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 EndpointActions from '../../../actions/EndpointActions'
 import ProviderStore from '../../../stores/ProviderStore'
-import ProviderActions from '../../../actions/ProviderActions'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import Palette from '../../styleUtils/Palette'
 import DomUtils from '../../../utils/DomUtils'
@@ -105,43 +105,32 @@ const Buttons = styled.div`
 `
 
 type Props = {
-  type: string,
+  type: ?string,
   cancelButtonText: string,
   deleteOnCancel: boolean,
-  endpoint: EndpointType,
-  connectionInfo: { [string]: mixed },
+  endpoint: ?EndpointType,
   onCancelClick: (opts?: { autoClose?: boolean }) => void,
   onResizeUpdate: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
-  endpointStore: any,
-  providerStore: any,
 }
 type State = {
   invalidFields: any[],
   validating: boolean,
   showErrorMessage: boolean,
-  endpoint: EndpointType | {},
+  endpoint: ?EndpointType,
   isNew: ?boolean,
 }
+@observer
 class Endpoint extends React.Component<Props, State> {
   static defaultProps: $Shape<Props> = {
     cancelButtonText: 'Cancel',
   }
 
-  static getStores() {
-    return [EndpointStore, ProviderStore]
-  }
-
-  static getPropsFromStores() {
-    return {
-      endpointStore: EndpointStore.getState(),
-      providerStore: ProviderStore.getState(),
-    }
-  }
-
   scrollableRef: HTMLElement
   closeTimeout: TimeoutID
   contentPluginRef: DefaultContentPlugin
   isValidateButtonEnabled: boolean
+  providerStoreObserver: any
+  endpointStoreObserver: any
 
   constructor() {
     super()
@@ -150,28 +139,38 @@ class Endpoint extends React.Component<Props, State> {
       invalidFields: [],
       validating: false,
       showErrorMessage: false,
-      endpoint: {},
+      endpoint: 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() {
-    ProviderActions.getConnectionInfoSchema(this.getEndpointType())
+    ProviderStore.getConnectionInfoSchema(this.getEndpointType())
     KeyboardManager.onEnter('endpoint', () => { if (this.isValidateButtonEnabled) this.handleValidateClick() }, 2)
   }
 
-  componentWillReceiveProps(props) {
+  componentWillReceiveProps(props: Props) {
     if (this.state.validating) {
-      if (props.endpointStore.validation && !props.endpointStore.validation.valid) {
+      if (EndpointStore.validation && !EndpointStore.validation.valid) {
         this.setState({ validating: false })
       }
     }
 
-    if (props.endpoint && props.endpointStore.connectionInfo) {
+    if (props.endpoint && EndpointStore.connectionInfo) {
       this.setState({
         endpoint: {
-          ...ObjectUtils.flatten(props.endpoint),
-          ...ObjectUtils.flatten(props.endpointStore.connectionInfo),
+          ...ObjectUtils.flatten(props.endpoint || {}),
+          ...ObjectUtils.flatten(EndpointStore.connectionInfo || {}),
         },
       })
     } else {
@@ -179,19 +178,21 @@ class Endpoint extends React.Component<Props, State> {
         isNew: this.state.isNew === null || this.state.isNew,
         endpoint: {
           type: props.type,
-          ...ObjectUtils.flatten(this.state.endpoint),
+          ...ObjectUtils.flatten(this.state.endpoint || {}),
         },
       })
     }
 
-    this.props.onResizeUpdate(this.scrollableRef)
+    props.onResizeUpdate(this.scrollableRef)
   }
 
   componentWillUnmount() {
-    EndpointActions.clearValidation()
-    ProviderActions.clearConnectionInfoSchema()
+    EndpointStore.clearValidation()
+    ProviderStore.clearConnectionInfoSchema()
     clearTimeout(this.closeTimeout)
     KeyboardManager.removeKeyDown('endpoint')
+    this.providerStoreObserver()
+    this.endpointStoreObserver()
   }
 
   getEndpointType() {
@@ -199,11 +200,11 @@ class Endpoint extends React.Component<Props, State> {
       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 ''
     }
     if (this.state.endpoint[field.name]) {
@@ -217,11 +218,7 @@ class Endpoint extends React.Component<Props, State> {
     return ''
   }
 
-  isValidating() {
-    return this.state.validating
-  }
-
-  handleFieldsChange(items) {
+  handleFieldsChange(items: { field: Field, value: any }[]) {
     let endpoint: EndpointType = { ...this.state.endpoint }
 
     items.forEach(item => {
@@ -235,8 +232,8 @@ class Endpoint extends React.Component<Props, State> {
     if (!this.highlightRequired()) {
       this.setState({ validating: true })
 
-      NotificationActions.notify('Saving endpoint ...')
-      EndpointActions.clearValidation()
+      NotificationStore.notify('Saving endpoint ...')
+      EndpointStore.clearValidation()
 
       if (this.state.isNew) {
         this.add()
@@ -244,25 +241,31 @@ class Endpoint extends React.Component<Props, State> {
         this.update()
       }
     } else {
-      NotificationActions.notify('Please fill all the required fields', 'error')
+      NotificationStore.notify('Please fill all the required fields', 'error')
     }
   }
 
   handleShowErrorMessageClick() {
-    this.setState({ showErrorMessage: !this.state.showErrorMessage })
+    this.setState({ showErrorMessage: !this.state.showErrorMessage }, () => {
+      this.props.onResizeUpdate(this.scrollableRef)
+    })
   }
 
   handleCopyErrorMessageClick() {
-    let succesful = DomUtils.copyTextToClipboard(this.props.endpointStore.validation.message)
+    if (!EndpointStore.validation) {
+      return
+    }
+    // $FlowIssue
+    let succesful = DomUtils.copyTextToClipboard(EndpointStore.validation.message)
 
     if (succesful) {
-      NotificationActions.notify('The message has been copied to clipboard.')
+      NotificationStore.notify('The message has been copied to clipboard.')
     }
   }
 
   handleCancelClick() {
     if (this.props.deleteOnCancel && this.state.isNew === false) {
-      EndpointActions.delete(EndpointStore.getState().endpoints[0])
+      EndpointStore.delete(EndpointStore.endpoints[0])
     }
     this.props.onCancelClick()
   }
@@ -274,24 +277,33 @@ class Endpoint extends React.Component<Props, State> {
   }
 
   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() {
-    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 })
-      NotificationActions.notify('Validating endpoint ...')
-      EndpointActions.validate(endpoint)
+      NotificationStore.notify('Validating endpoint ...')
+      EndpointStore.validate(endpoint)
     })
   }
 
   renderEndpointStatus() {
-    let validation = this.props.endpointStore.validation
-    if (!this.isValidating() && !validation) {
+    let validation = EndpointStore.validation
+    if (!this.state.validating && !validation) {
       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 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 ...'
       }
 
@@ -352,19 +364,20 @@ class Endpoint extends React.Component<Props, State> {
   }
 
   renderContent() {
-    if (this.props.providerStore.connectionSchemaLoading) {
+    const endpointType = this.getEndpointType()
+    if (ProviderStore.connectionSchemaLoading || !endpointType) {
       return null
     }
-
     return (
       <Content>
         {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,
           validating: this.state.validating,
-          disabled: this.isValidating() || (this.props.endpointStore.validation && this.props.endpointStore.validation.valid),
+          disabled: this.state.validating,
           cancelButtonText: this.props.cancelButtonText,
           getFieldValue: field => this.getFieldValue(field),
           highlightRequired: () => this.highlightRequired(),
@@ -384,7 +397,7 @@ class Endpoint extends React.Component<Props, State> {
   }
 
   renderLoading() {
-    if (!this.props.providerStore.connectionSchemaLoading) {
+    if (!ProviderStore.connectionSchemaLoading) {
       return null
     }
 
@@ -397,7 +410,7 @@ class Endpoint extends React.Component<Props, State> {
   }
 
   render() {
-    if (this.props.endpointStore.validation && this.props.endpointStore.validation.valid
+    if (EndpointStore.validation && EndpointStore.validation.valid
       && !this.closeTimeout) {
       this.closeTimeout = setTimeout(() => {
         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 = {
   item: ?Endpoint,
-  connectionInfo: { [string]: mixed },
+  connectionInfo: ?$PropertyType<Endpoint, 'connection_info'>,
   loading: boolean,
   onDeleteClick: () => 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 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'
 
 const Wrapper = styled.div`
@@ -71,7 +72,7 @@ const Error = styled.div`
 
 type Props = {
   loading: boolean,
-  validation?: { valid: boolean, message: string },
+  validation?: ?ValidationType,
   onCancelClick: () => void,
   onRetryClick: () => void,
 }
@@ -80,9 +81,9 @@ class EndpointValidation extends React.Component<Props> {
     let succesful = DomUtils.copyTextToClipboard(message)
 
     if (succesful) {
-      NotificationActions.notify('The value has been copied to clipboard.')
+      NotificationStore.notify('The value has been copied to clipboard.')
     } 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 = {
-  item: MainItem,
+  item: ?MainItem,
   onCancelExecutionClick: (execution: ?Execution) => void,
   onDeleteExecutionClick: (execution: ?Execution) => void,
   onExecuteClick: () => void,
@@ -105,9 +105,9 @@ class Executions extends React.Component<Props, State> {
     let lastExecution = this.getLastExecution(props)
     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
-        && lastExecution.status === 'RUNNING') {
+        && lastExecution && lastExecution.status === 'RUNNING') {
         selectExecution = lastExecution
       }
 
@@ -117,9 +117,9 @@ class Executions extends React.Component<Props, State> {
         if (!isSelectedAvailable) {
           // $FlowIssue
           let lastIndex = this.props.item.executions.findIndex(e => e.id === this.state.selectedExecution.id)
-
+          // $FlowIssue
           if (props.item.executions.length) {
-            if (props.item.executions[lastIndex]) {
+            if (props.item.executions.length - 1 >= lastIndex) {
               selectExecution = props.item.executions[lastIndex]
             } else {
               selectExecution = props.item.executions[lastIndex - 1]
@@ -149,11 +149,11 @@ class Executions extends React.Component<Props, State> {
   }
 
   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) {
-    return props.item.executions && props.item.executions.length
+    return props.item && props.item.executions && props.item.executions.length
   }
 
   handlePreviousExecutionClick() {
@@ -164,14 +164,14 @@ class Executions extends React.Component<Props, State> {
       return
     }
 
-    this.setState({ selectedExecution: this.props.item.executions[selectedIndex - 1] })
+    this.setState({ selectedExecution: this.props.item ? this.props.item.executions[selectedIndex - 1] : null })
   }
 
   handleNextExecutionClick() {
     // $FlowIssue
     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
     }
 
@@ -189,7 +189,7 @@ class Executions extends React.Component<Props, State> {
   renderTimeline() {
     return (
       <Timeline
-        items={this.props.item.executions || null}
+        items={this.props.item ? this.props.item.executions : null}
         selectedItem={this.state.selectedExecution}
         onPreviousClick={() => { this.handlePreviousExecutionClick() }}
         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 { loginButtons } from '../../../config'
-import NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 
 const Form = styled.form`
   background: rgba(221, 224, 229, 0.5);
@@ -118,7 +118,7 @@ class LoginForm extends React.Component<Props, State> {
     e.preventDefault()
 
     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 {
       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 = {
-  item: MainItem,
+  item: ?MainItem,
   endpoints: Endpoint[],
   bottomControls: React.Node,
   loading: boolean,
 }
 class MainDetails extends React.Component<Props> {
   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
   }
 
   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
   }
 
   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]
     }
 
@@ -132,7 +132,11 @@ class MainDetails extends React.Component<Props> {
 
   getConnectedVms(networkId: string) {
     let vms = []
+    if (!this.props.item) {
+      return '-'
+    }
     Object.keys(this.props.item.info).forEach(key => {
+      // $FlowIssue
       let instance = this.props.item.info[key]
       if (instance.export_info && instance.export_info.devices.nics.length) {
         instance.export_info.devices.nics.forEach(nic => {
@@ -153,7 +157,7 @@ class MainDetails extends React.Component<Props> {
     let networks = []
     Object.keys(this.props.item.destination_environment.network_map).forEach(key => {
       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 = [
           this.props.item.destination_environment.network_map[key].source_network,
           this.getConnectedVms(key),
@@ -165,7 +169,7 @@ class MainDetails extends React.Component<Props> {
         newItem = [
           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',
         ]
       }
@@ -237,7 +241,7 @@ class MainDetails extends React.Component<Props> {
 
     return (
       <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']
           if (skipProps.find(p => p === propName)) {
             return null
@@ -245,10 +249,10 @@ class MainDetails extends React.Component<Props> {
           return (
             <PropertyRow key={propName}>
               <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>
           )
-        })}
+        }) : null}
       </PropertiesTable>
     )
   }
@@ -275,25 +279,25 @@ class MainDetails extends React.Component<Props> {
           <Row>
             <Field>
               <Label>Id</Label>
-              <CopyValue value={this.props.item.id} width="192px" />
+              <CopyValue value={this.props.item ? this.props.item.id : '-'} width="192px" />
             </Field>
           </Row>
           <Row>
             <Field>
               <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>
           </Row>
           <Row>
             <Field>
               <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>
           </Row>
           <Row>
             <Field>
               <Label>Type</Label>
-              <Value capitalize>Coriolis {this.props.item.type}</Value>
+              <Value capitalize>Coriolis {this.props.item && this.props.item.type}</Value>
             </Field>
           </Row>
           <Row>
@@ -325,7 +329,7 @@ class MainDetails extends React.Component<Props> {
           <Row>
             <Field>
               <Label>Instances</Label>
-              <Value>{this.props.item.instances.join(', ')}</Value>
+              <Value>{this.props.item && this.props.item.instances.join(', ')}</Value>
             </Field>
           </Row>
         </Column>

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

@@ -51,7 +51,7 @@ const NavigationItems = [
 ]
 
 type Props = {
-  item: MainItem,
+  item: ?MainItem,
   detailsLoading: boolean,
   endpoints: Endpoint[],
   page: string,
@@ -86,7 +86,7 @@ class MigrationDetailsContent extends React.Component<Props> {
   }
 
   renderTasks() {
-    if (this.props.page !== 'tasks' || !this.props.item.tasks) {
+    if (this.props.page !== 'tasks' || !this.props.item || !this.props.item.tasks) {
       return null
     }
 
@@ -103,7 +103,7 @@ class MigrationDetailsContent extends React.Component<Props> {
         <DetailsNavigation
           items={NavigationItems}
           selectedValue={this.props.page}
-          itemId={this.props.item.id}
+          itemId={this.props.item ? this.props.item.id : ''}
           itemType="migration"
         />
         <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 styled, { injectGlobal } from 'styled-components'
 import NotificationSystem from 'react-notification-system'
+import { observe } from 'mobx'
 
 import NotificationStore from '../../../stores/NotificationStore'
+import type { NotificationItem } from '../../../types/NotificationItem'
 
 import NotificationsStyle from './style.js'
 
@@ -28,9 +30,6 @@ injectGlobal`
 
 const Wrapper = styled.div``
 
-type StoreState = {
-  notifications: { message: string, level: string, action: string }[],
-}
 class Notifications extends React.Component<{}> {
   notificationsCount: number
   notificationSystem: NotificationSystem
@@ -41,29 +40,27 @@ class Notifications extends React.Component<{}> {
   }
 
   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
     }
 
-    let lastNotification = state.notifications[state.notifications.length - 1]
+    let lastNotification = notifications[notifications.length - 1]
     this.notificationSystem.addNotification({
       title: lastNotification.title || lastNotification.message,
       message: lastNotification.title ? lastNotification.message : null,
       level: lastNotification.level || 'info',
       position: 'br',
       autoDismiss: 10,
-      action: lastNotification.action,
+      action: lastNotification.options ? lastNotification.options.action : null,
     })
 
-    this.notificationsCount = state.notifications.length
+    this.notificationsCount = notifications.length
   }
 
   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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import Dropdown from '../../molecules/Dropdown'
 import NewItemDropdown from '../../molecules/NewItemDropdown'
@@ -29,10 +29,7 @@ import Endpoint from '../../organisms/Endpoint'
 
 import ProjectStore from '../../../stores/ProjectStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import NotificationActions from '../../../actions/NotificationActions'
 import NotificationStore from '../../../stores/NotificationStore'
-import ProviderActions from '../../../actions/ProviderActions'
 import ProviderStore from '../../../stores/ProviderStore'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -64,32 +61,16 @@ const Controls = styled.div`
 type Props = {
   title: string,
   onProjectChange: (project: Project) => void,
-  onModalOpen: () => void,
-  onModalClose: () => void,
-  projectStore: any,
-  userStore: any,
-  providerStore: any,
-  notificationStore: any,
+  onModalOpen?: () => void,
+  onModalClose?: () => void,
 }
 type State = {
   showChooseProviderModal: boolean,
   showEndpointModal: boolean,
   providerType?: string,
 }
+@observer
 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() {
     super()
 
@@ -100,21 +81,22 @@ class PageHeader extends React.Component<Props, State> {
   }
 
   componentDidMount() {
-    NotificationActions.loadNotifications()
+    NotificationStore.loadNotifications()
   }
 
   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
   }
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
       case 'profile':
         window.location.href = '/#/profile'
@@ -126,7 +108,7 @@ class PageHeader extends React.Component<Props, State> {
   handleNewItem(item: ItemType) {
     switch (item.value) {
       case 'endpoint':
-        ProviderActions.loadProviders()
+        ProviderStore.loadProviders()
         if (this.props.onModalOpen) {
           this.props.onModalOpen()
         }
@@ -137,7 +119,7 @@ class PageHeader extends React.Component<Props, State> {
   }
 
   handleNotificationsClose() {
-    NotificationActions.clearNotifications()
+    NotificationStore.clearNotifications()
   }
 
   handleCloseChooseProviderModal() {
@@ -162,7 +144,7 @@ class PageHeader extends React.Component<Props, State> {
     this.setState({ showEndpointModal: false })
   }
 
-  handleBackEndpointModal(options) {
+  handleBackEndpointModal(options?: { autoClose?: boolean }) {
     this.setState({ showChooseProviderModal: !options || !options.autoClose, showEndpointModal: false })
   }
 
@@ -173,14 +155,14 @@ class PageHeader extends React.Component<Props, State> {
         <Controls>
           <Dropdown
             selectedItem={this.getCurrentProject()}
-            items={this.props.projectStore.projects}
+            items={ProjectStore.projects}
             onChange={this.props.onProjectChange}
             noItemsMessage="Loading..."
             labelField="name"
           />
           <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>
         <Modal
           isOpen={this.state.showChooseProviderModal}
@@ -189,8 +171,8 @@ class PageHeader extends React.Component<Props, State> {
         >
           <ChooseProvider
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
-            providers={this.props.providerStore.providers}
-            loading={this.props.providerStore.providersLoading}
+            providers={ProviderStore.providers}
+            loading={ProviderStore.providersLoading}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
           />
         </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 styled from 'styled-components'
+import { observer } from 'mobx-react'
 
+import ScheduleStore from '../../../stores/ScheduleStore'
 import Button from '../../atoms/Button'
 import DetailsNavigation from '../../molecules/DetailsNavigation'
 import MainDetails from '../../organisms/MainDetails'
@@ -67,9 +69,9 @@ const NavigationItems = [
 
 type TimezoneValue = 'utc' | 'local'
 type Props = {
-  item: MainItem,
+  item: ?MainItem,
   endpoints: Endpoint[],
-  scheduleStore: any,
+  scheduleStore: typeof ScheduleStore,
   page: string,
   detailsLoading: boolean,
   onCancelExecutionClick: (execution: ?Execution) => void,
@@ -78,14 +80,15 @@ type Props = {
   onCreateMigrationClick: () => void,
   onDeleteReplicaClick: () => 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,
 }
 type State = {
   timezone: TimezoneValue,
 }
+@observer
 class ReplicaDetailsContent extends React.Component<Props, State> {
   constructor() {
     super()
@@ -96,7 +99,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
   }
 
   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]
   }
 
@@ -106,8 +109,8 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
   }
 
   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)
   }
@@ -132,7 +135,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
             hollow
             secondary
             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>
           <Button
             alert
@@ -201,7 +204,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
         <DetailsNavigation
           items={NavigationItems}
           selectedValue={this.props.page}
-          itemId={this.props.item.id}
+          itemId={this.props.item ? this.props.item.id : ''}
           itemType="replica"
         />
         <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 styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 import Button from '../../atoms/Button'
 import StatusImage from '../../atoms/StatusImage'
@@ -106,13 +107,13 @@ const Buttons = styled.div`
 
 type TimeZoneValue = 'local' | 'utc'
 type Props = {
-  schedules: ScheduleType[],
+  schedules: ?ScheduleType[],
   unsavedSchedules: ScheduleType[],
   timezone: TimeZoneValue,
   onTimezoneChange: (timezone: TimeZoneValue) => 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,
   adding?: boolean,
   loading?: boolean,
@@ -126,6 +127,7 @@ type State = {
 }
 
 const colWidths = ['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']
+@observer
 class Schedule extends React.Component<Props, State> {
   static defaultProps: $Shape<Props> = {
     unsavedSchedules: [],
@@ -156,7 +158,9 @@ class Schedule extends React.Component<Props, State> {
 
   handleDeleteConfirmation() {
     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>) {
@@ -174,7 +178,9 @@ class Schedule extends React.Component<Props, State> {
       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) {
@@ -256,6 +262,10 @@ class Schedule extends React.Component<Props, State> {
   }
 
   renderBody() {
+    if (!this.props.schedules) {
+      return null
+    }
+
     return (
       <Body>
         {this.props.schedules.map(schedule => (
@@ -265,7 +275,7 @@ class Schedule extends React.Component<Props, State> {
             item={schedule}
             unsavedSchedules={this.props.unsavedSchedules}
             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) }}
             onShowOptionsClick={() => { this.handleShowOptions(schedule) }}
             onDeleteClick={() => { this.handleDeleteClick(schedule) }}

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

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

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

@@ -180,7 +180,7 @@ const BigInstanceImage = styled.div`
 
 type Props = {
   instances: InstanceType[],
-  selectedInstances: InstanceType[],
+  selectedInstances: ?InstanceType[],
   currentPage: number,
   loading: 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 StyleProps from '../../styleUtils/StyleProps'
 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 bigNetworkImage from './images/network-big.svg'
@@ -109,7 +109,7 @@ type Props = {
   loadingInstancesDetails: boolean,
   networks: Network[],
   instancesDetails: Instance[],
-  selectedNetworks: Network[],
+  selectedNetworks: ?NetworkMap[],
   onChange: (nic: NicType, network: Network) => void,
 }
 class WizardNetworks extends React.Component<Props> {

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

@@ -42,8 +42,8 @@ const WizardOptionsFieldStyled = styled(WizardOptionsField) `
 
 type Props = {
   fields: Field[],
-  selectedInstances: Instance[],
-  data: { [string]: mixed },
+  selectedInstances: ?Instance[],
+  data: ?{ [string]: mixed },
   onChange: (field: Field, value: any) => void,
   useAdvancedOptions: boolean,
   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 })
     }
 
-    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 })
     }
 

+ 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 styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 import EndpointLogos from '../../atoms/EndpointLogos'
 import WizardType from '../../molecules/WizardType'
@@ -33,8 +34,15 @@ import Palette from '../../styleUtils/Palette'
 import { providerTypes, wizardConfig } from '../../../config'
 import type { WizardData } from '../../../types/WizardData'
 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 NetworkStore from '../../../stores/NetworkStore'
 
 const bodyWidth = 800
 const Wrapper = styled.div`
@@ -86,9 +94,9 @@ type Props = {
   page: { id: string, title: string },
   type: 'replica' | 'migration',
   nextButtonDisabled: boolean,
-  providerStore: any,
-  instanceStore: any,
-  networkStore: any,
+  providerStore: typeof ProviderStore,
+  instanceStore: typeof InstanceStore,
+  networkStore: typeof NetworkStore,
   wizardData: WizardData,
   endpoints: Endpoint[],
   onTypeChange: (isReplicaChecked: ?boolean) => void,
@@ -97,16 +105,16 @@ type Props = {
   onSourceEndpointChange: (endpoint: Endpoint) => void,
   onTargetEndpointChange: (endpoint: Endpoint) => void,
   onAddEndpoint: (provider: string, fromSource: boolean) => void,
-  onInstancesSearchInputChange: () => void,
-  onInstancesNextPageClick: () => void,
+  onInstancesSearchInputChange: (searchText: string) => void,
+  onInstancesNextPageClick: (searchText: string) => 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,
 }
 type TimezoneValue = 'local' | 'utc'
@@ -114,6 +122,8 @@ type State = {
   useAdvancedOptions: boolean,
   timezone: TimezoneValue,
 }
+
+@observer
 class WizardPageContent extends React.Component<Props, State> {
   constructor() {
     super()
@@ -149,9 +159,14 @@ class WizardPageContent extends React.Component<Props, State> {
   getProviders(type: string) {
     let providers = []
     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)
       }
     })

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

@@ -236,9 +236,10 @@ class WizardSummary extends React.Component<Props> {
         <SectionTitle>{type} Options</SectionTitle>
         <OptionsList>
           {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 => {
             if (optionName === 'execute_now' || optionName === 'separate_vm'
+              // $FlowIssue  
               || data.options[optionName] === null || data.options[optionName] === undefined) {
               return null
             }
@@ -246,7 +247,10 @@ class WizardSummary extends React.Component<Props> {
             return (
               <Option key={optionName}>
                 <OptionLabel>{LabelDictionary.get(optionName)}</OptionLabel>
-                <OptionValue>{this.renderOptionValue(data.options[optionName])}</OptionValue>
+                <OptionValue>{
+                  // $FlowIssue
+                  this.renderOptionValue(data.options[optionName])
+                }</OptionValue>
               </Option>
             )
           }) : null}
@@ -287,7 +291,7 @@ class WizardSummary extends React.Component<Props> {
       <Section>
         <SectionTitle>Instances</SectionTitle>
         <Table>
-          {data.selectedInstances.map(instance => {
+          {data.selectedInstances ? data.selectedInstances.map(instance => {
             let flavorName = instance.flavor_name ? `/${instance.flavor_name}` : ''
             return (
               <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>
               </Row>
             )
-          })}
+          }) : null}
         </Table>
       </Section>
     )
@@ -311,15 +315,15 @@ class WizardSummary extends React.Component<Props> {
           <OverviewRow>
             <OverviewLabel>Source</OverviewLabel>
             <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>
           </OverviewRow>
           <OverviewRow>
             <OverviewLabel>Target</OverviewLabel>
             <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>
           </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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -28,13 +28,9 @@ import EndpointValidation from '../../organisms/EndpointValidation'
 import Endpoint from '../../organisms/Endpoint'
 
 import EndpointStore from '../../../stores/EndpointStore'
-import EndpointActions from '../../../actions/EndpointActions'
 import MigrationStore from '../../../stores/MigrationStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
-import MigrationActions from '../../../actions/MigrationActions'
-import ReplicaActions from '../../../actions/ReplicaActions'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 
 import endpointImage from './images/endpoint.svg'
@@ -43,10 +39,6 @@ const Wrapper = styled.div``
 
 type Props = {
   match: any,
-  endpointStore: any,
-  userStore: any,
-  migrationStore: any,
-  replicaStore: any,
 }
 type State = {
   showDeleteEndpointConfirmation: boolean,
@@ -55,20 +47,8 @@ type State = {
   showEndpointInUseModal: boolean,
   showEndpointInUseLoadingModal: boolean,
 }
+@observer
 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() {
     super()
 
@@ -88,18 +68,18 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
 
   componentWillUnmount() {
-    EndpointActions.clearConnectionInfo()
+    EndpointStore.clearConnectionInfo()
   }
 
   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() {
     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
-    let migrationsCount = this.props.migrationStore.migrations.filter(
+    let migrationsCount = MigrationStore.migrations.filter(
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId).length
 
     return { migrationsCount, replicasCount }
@@ -108,7 +88,7 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
       case 'profile':
         window.location.href = '/#/profile'
@@ -124,7 +104,7 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   handleDeleteEndpointClick() {
     this.setState({ showEndpointInUseLoadingModal: true })
 
-    Promise.all([ReplicaActions.getReplicas().promise, MigrationActions.getMigrations().promise]).then(() => {
+    Promise.all([ReplicaStore.getReplicas(), MigrationStore.getMigrations()]).then(() => {
       let endpointUsage = this.getEndpointUsage()
 
       if (endpointUsage.migrationsCount === 0 && endpointUsage.replicasCount === 0) {
@@ -138,7 +118,10 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   handleDeleteEndpointConfirmation() {
     this.setState({ showDeleteEndpointConfirmation: false })
     window.location.href = '/#/endpoints'
-    EndpointActions.delete(this.getEndpoint())
+    let endpoint = this.getEndpoint()
+    if (endpoint) {
+      EndpointStore.delete(endpoint)
+    }
   }
 
   handleCloseDeleteEndpointConfirmation() {
@@ -146,16 +129,22 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
 
   handleValidateClick() {
-    EndpointActions.validate(this.getEndpoint())
+    let endpoint = this.getEndpoint()
+    if (endpoint) {
+      EndpointStore.validate(endpoint)
+    }
     this.setState({ showValidationModal: true })
   }
 
   handleRetryValidation() {
-    EndpointActions.validate(this.getEndpoint())
+    let endpoint = this.getEndpoint()
+    if (endpoint) {
+      EndpointStore.validate(endpoint)
+    }
   }
 
   handleCloseValidationModal() {
-    EndpointActions.clearValidation()
+    EndpointStore.clearValidation()
     this.setState({ showValidationModal: false })
   }
 
@@ -163,8 +152,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
     this.setState({ showEndpointModal: true })
   }
 
-  handleEditValidateClick(endpoint) {
-    EndpointActions.validate(endpoint)
+  handleEditValidateClick(endpoint: EndpointType) {
+    EndpointStore.validate(endpoint)
   }
 
   handleCloseEndpointModal() {
@@ -176,13 +165,13 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
 
   loadData() {
-    EndpointActions.getEndpoints().promise.then(() => {
+    EndpointStore.getEndpoints().then(() => {
       let endpoint = this.getEndpoint()
 
       if (endpoint && endpoint.connection_info && endpoint.connection_info.secret_ref) {
-        EndpointActions.getConnectionInfo(endpoint)
+        EndpointStore.getConnectionInfo(endpoint)
       } 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>
         <DetailsTemplate
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           contentHeaderComponent={<DetailsContentHeader
@@ -206,8 +195,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
           />}
           contentComponent={<EndpointDetailsContent
             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() }}
             onValidateClick={() => { this.handleValidateClick() }}
             onEditClick={() => { this.handleEditClick() }}
@@ -240,8 +229,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
           onRequestClose={() => { this.handleCloseValidationModal() }}
         >
           <EndpointValidation
-            validation={this.props.endpointStore.validation}
-            loading={this.props.endpointStore.validating}
+            validation={EndpointStore.validation}
+            loading={EndpointStore.validating}
             onCancelClick={() => { this.handleCloseValidationModal() }}
             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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import MainTemplate from '../../templates/MainTemplate'
 import Navigation from '../../organisms/Navigation'
@@ -28,6 +28,7 @@ import Modal from '../../molecules/Modal'
 import ChooseProvider from '../../organisms/ChooseProvider'
 import Endpoint from '../../organisms/Endpoint'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
+import type { Project } from '../../../types/Project'
 
 import endpointImage from './images/endpoint-large.svg'
 
@@ -36,13 +37,7 @@ import UserStore from '../../../stores/UserStore'
 import EndpointStore from '../../../stores/EndpointStore'
 import MigrationStore from '../../../stores/MigrationStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
-import ProjectActions from '../../../actions/ProjectActions'
 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 LabelDictionary from '../../../utils/LabelDictionary'
 
@@ -52,14 +47,6 @@ const BulkActions = [
   { label: 'Delete', value: 'delete' },
 ]
 
-type Props = {
-  projectStore: any,
-  userStore: any,
-  endpointStore: any,
-  migrationStore: any,
-  replicaStore: any,
-  providerStore: any,
-}
 type State = {
   showDeleteEndpointsConfirmation: boolean,
   confirmationItems: ?EndpointType[],
@@ -67,22 +54,8 @@ type State = {
   showEndpointModal: boolean,
   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
 
   constructor() {
@@ -100,10 +73,10 @@ class EndpointsPage extends React.Component<Props, State> {
   componentDidMount() {
     document.title = 'Coriolis Endpoints'
 
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints()
-    MigrationActions.getMigrations()
-    ReplicaActions.getReplicas()
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints()
+    MigrationStore.getMigrations()
+    ReplicaStore.getReplicas()
   }
 
   componentWillUnmount() {
@@ -112,7 +85,7 @@ class EndpointsPage extends React.Component<Props, State> {
 
   getFilterItems() {
     let types = [{ label: 'All', value: 'all' }]
-    this.props.endpointStore.endpoints.forEach(endpoint => {
+    EndpointStore.endpoints.forEach(endpoint => {
       if (!types.find(t => t.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
   }
 
-  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
-    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
 
     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() {
-    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}`
   }
 
-  handleActionChange(items, action) {
+  handleActionChange(items: EndpointType[], action: string) {
     if (action === 'delete') {
       this.setState({
         showDeleteEndpointsConfirmation: true,
@@ -172,14 +146,14 @@ class EndpointsPage extends React.Component<Props, State> {
   handleDeleteEndpointsConfirmation() {
     if (this.state.confirmationItems) {
       this.state.confirmationItems.forEach(endpoint => {
-        EndpointActions.delete(endpoint)
+        EndpointStore.delete(endpoint)
       })
     }
     this.handleCloseDeleteEndpointsConfirmation()
   }
 
   handleEmptyListButtonClick() {
-    ProviderActions.loadProviders()
+    ProviderStore.loadProviders()
     this.setState({ showChooseProviderModal: true })
   }
 
@@ -187,7 +161,7 @@ class EndpointsPage extends React.Component<Props, State> {
     this.setState({ showChooseProviderModal: false })
   }
 
-  handleProviderClick(providerType) {
+  handleProviderClick(providerType: string) {
     this.setState({
       showChooseProviderModal: false,
       showEndpointModal: true,
@@ -199,11 +173,12 @@ class EndpointsPage extends React.Component<Props, State> {
     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
-      item.description.toLowerCase().indexOf(filterText) === -1)
+      endpoint.description.toLowerCase().indexOf(filterText) === -1)
     ) {
       return false
     }
@@ -212,6 +187,7 @@ class EndpointsPage extends React.Component<Props, State> {
   }
 
   render() {
+    let items: any = EndpointStore.endpoints
     return (
       <Wrapper>
         <MainTemplate
@@ -220,12 +196,20 @@ class EndpointsPage extends React.Component<Props, State> {
             <FilterList
               filterItems={this.getFilterItems()}
               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() }}
               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)}
               renderItemComponent={options =>
                 // $FlowIssue
@@ -263,8 +247,8 @@ class EndpointsPage extends React.Component<Props, State> {
         >
           <ChooseProvider
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
-            providers={this.props.providerStore.providers}
-            loading={this.props.providerStore.providersLoading}
+            providers={ProviderStore.providers}
+            loading={ProviderStore.providersLoading}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
           />
         </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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import EmptyTemplate from '../../templates/EmptyTemplate'
 import Logo from '../../atoms/Logo'
 import LoginForm from '../../organisms/LoginForm'
 
 import StyleProps from '../../styleUtils/StyleProps'
-import UserActions from '../../../actions/UserActions'
 import UserStore from '../../../stores/UserStore'
 
 import backgroundImage from './images/star-bg.jpg'
@@ -76,51 +75,24 @@ const CbsLogo = styled.a`
   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() {
     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,
       password: data.password,
     })
   }
 
   render() {
+    if (UserStore.user) {
+      window.location.href = '/#/replicas'
+    }
+
     return (
       <EmptyTemplate>
         <Wrapper>
@@ -128,8 +100,8 @@ class LoginPage extends React.Component<Props, State> {
             <Logo />
             <StyledLoginForm
               onFormSubmit={data => this.handleFormSubmit(data)}
-              loading={this.props.loading}
-              loginFailedResponse={this.props.loginFailedResponse}
+              loading={UserStore.loading}
+              loginFailedResponse={UserStore.loginFailedResponse}
             />
             <Footer>
               <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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -26,11 +26,8 @@ import AlertModal from '../../organisms/AlertModal'
 
 import MigrationStore from '../../../stores/MigrationStore'
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import MigrationActions from '../../../actions/MigrationActions'
 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 migrationImage from './images/migration.svg'
@@ -39,27 +36,13 @@ const Wrapper = styled.div``
 
 type Props = {
   match: any,
-  migrationStore: any,
-  endpointStore: any,
-  userStore: any,
 }
 type State = {
   showDeleteMigrationConfirmation: boolean,
   showCancelConfirmation: boolean,
 }
+@observer
 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
 
   constructor() {
@@ -74,20 +57,20 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   componentDidMount() {
     document.title = 'Migration Details'
 
-    EndpointActions.getEndpoints()
+    EndpointStore.getEndpoints()
     this.pollData(true)
     this.pollInterval = setInterval(() => { this.pollData() }, requestPollTimeout)
   }
 
   componentWillUnmount() {
-    MigrationActions.clearDetails()
+    MigrationStore.clearDetails()
     clearInterval(this.pollInterval)
   }
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
       case 'profile':
         window.location.href = '/#/profile'
@@ -107,7 +90,9 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   handleDeleteMigrationConfirmation() {
     this.setState({ showDeleteMigrationConfirmation: false })
     window.location.href = '/#/migrations'
-    MigrationActions.delete(this.props.migrationStore.migrationDetails.id)
+    if (MigrationStore.migrationDetails) {
+      MigrationStore.delete(MigrationStore.migrationDetails.id)
+    }
   }
 
   handleCloseDeleteMigrationConfirmation() {
@@ -124,17 +109,20 @@ class MigrationDetailsPage extends React.Component<Props, State> {
 
   handleCancelConfirmation() {
     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 {
-        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() {
@@ -142,21 +130,21 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       <Wrapper>
         <DetailsTemplate
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           contentHeaderComponent={<DetailsContentHeader
-            item={this.props.migrationStore.migrationDetails}
+            item={MigrationStore.migrationDetails}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             typeImage={migrationImage}
             primaryInfoPill
             onCancelClick={() => { this.handleCancelMigrationClick() }}
           />}
           contentComponent={<MigrationDetailsContent
-            item={this.props.migrationStore.migrationDetails}
-            endpoints={this.props.endpointStore.endpoints}
+            item={MigrationStore.migrationDetails}
+            endpoints={EndpointStore.endpoints}
             page={this.props.match.params.page || ''}
-            detailsLoading={this.props.endpointStore.loading || this.props.migrationStore.detailsLoading}
+            detailsLoading={EndpointStore.loading || MigrationStore.detailsLoading}
             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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import MainTemplate from '../../templates/MainTemplate'
 import Navigation from '../../organisms/Navigation'
@@ -25,6 +25,7 @@ import PageHeader from '../../organisms/PageHeader'
 import AlertModal from '../../organisms/AlertModal'
 import MainListItem from '../../molecules/MainListItem'
 import type { MainItem } from '../../../types/MainItem'
+import type { Project } from '../../../types/Project'
 
 import migrationItemImage from './images/migration.svg'
 import migrationLargeImage from './images/migration-large.svg'
@@ -33,12 +34,8 @@ import ProjectStore from '../../../stores/ProjectStore'
 import UserStore from '../../../stores/UserStore'
 import MigrationStore from '../../../stores/MigrationStore'
 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 NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 
 const Wrapper = styled.div``
 
@@ -47,31 +44,13 @@ const BulkActions = [
   { label: 'Delete', value: 'delete' },
 ]
 
-type Props = {
-  projectStore: any,
-  migrationStore: any,
-  userStore: any,
-  endpointStore: any,
-}
 type State = {
   showDeleteMigrationConfirmation: boolean,
   showCancelMigrationConfirmation: boolean,
   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() {
     super()
 
@@ -85,17 +64,13 @@ class MigrationsPage extends React.Component<Props, State> {
   componentDidMount() {
     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() {
@@ -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() {
-    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') {
       window.location.href = `/#/migration/tasks/${item.id}`
     } else {
@@ -131,7 +107,7 @@ class MigrationsPage extends React.Component<Props, State> {
     }
   }
 
-  handleActionChange(confirmationItems, action) {
+  handleActionChange(confirmationItems: MainItem[], action: string) {
     if (action === 'cancel') {
       this.setState({
         showCancelMigrationConfirmation: true,
@@ -150,9 +126,9 @@ class MigrationsPage extends React.Component<Props, State> {
       return
     }
     this.state.confirmationItems.forEach(migration => {
-      MigrationActions.cancel(migration.id)
+      MigrationStore.cancel(migration.id)
     })
-    NotificationActions.notify('Canceling migrations')
+    NotificationStore.notify('Canceling migrations')
     this.handleCloseCancelMigration()
   }
 
@@ -175,7 +151,7 @@ class MigrationsPage extends React.Component<Props, State> {
       return
     }
     this.state.confirmationItems.forEach(migration => {
-      MigrationActions.delete(migration.id)
+      MigrationStore.delete(migration.id)
     })
     this.handleCloseDeleteMigrationConfirmation()
   }
@@ -184,7 +160,7 @@ class MigrationsPage extends React.Component<Props, State> {
     window.location.href = '/#/wizard/migration'
   }
 
-  searchText(item, text) {
+  searchText(item: MainItem, text?: string) {
     let result = false
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
       return true
@@ -201,7 +177,7 @@ class MigrationsPage extends React.Component<Props, State> {
     return result
   }
 
-  itemFilterFunction(item, filterStatus, filterText) {
+  itemFilterFunction(item: MainItem, filterStatus?: ?string, filterText?: string) {
     if ((filterStatus !== 'all' && (item.status !== filterStatus)) ||
       !this.searchText(item, filterText)
     ) {
@@ -234,8 +210,8 @@ class MigrationsPage extends React.Component<Props, State> {
             <FilterList
               filterItems={this.getFilterItems()}
               selectionLabel="migration"
-              loading={this.props.migrationStore.loading}
-              items={this.props.migrationStore.migrations}
+              loading={MigrationStore.loading}
+              items={MigrationStore.migrations}
               onItemClick={item => { this.handleItemClick(item) }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               actions={BulkActions}
@@ -245,7 +221,13 @@ class MigrationsPage extends React.Component<Props, State> {
                 (<MainListItem
                   {...options}
                   image={migrationItemImage}
-                  endpointType={id => this.getEndpoint(id).type}
+                  endpointType={id => {
+                    let endpoint = this.getEndpoint(id)
+                    if (endpoint) {
+                      return endpoint.type
+                    }
+                    return ''
+                  }}
                   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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -28,15 +28,13 @@ import AlertModal from '../../organisms/AlertModal'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import type { MainItem } from '../../../types/MainItem'
 import type { Execution } from '../../../types/Execution'
+import type { Schedule } from '../../../types/Schedule'
+import type { Field } from '../../../types/Field'
 
 import ReplicaStore from '../../../stores/ReplicaStore'
+import MigrationStore from '../../../stores/MigrationStore'
 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 EndpointActions from '../../../actions/EndpointActions'
-import ScheduleActions from '../../../actions/ScheduleActions'
 import ScheduleStore from '../../../stores/ScheduleStore'
 import { requestPollTimeout } from '../../../config'
 
@@ -46,10 +44,6 @@ const Wrapper = styled.div``
 
 type Props = {
   match: any,
-  replicaStore: any,
-  endpointStore: any,
-  userStore: any,
-  scheduleStore: any,
 }
 type State = {
   showOptionsModal: boolean,
@@ -60,20 +54,8 @@ type State = {
   confirmationItem: ?MainItem | ?Execution,
   showCancelConfirmation: boolean,
 }
+@observer
 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
 
   constructor() {
@@ -93,32 +75,32 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   componentDidMount() {
     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()
   }
 
   componentWillUnmount() {
-    ReplicaActions.clearDetails()
-    ScheduleActions.clearUnsavedSchedules()
+    ReplicaStore.clearDetails()
+    ScheduleStore.clearUnsavedSchedules()
     clearTimeout(this.pollTimeout)
   }
 
   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
 
     return Boolean(!originEndpoint || !targetEndpoint || status === 'RUNNING')
   }
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
       case 'profile':
         window.location.href = '/#/profile'
@@ -143,11 +125,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     if (!this.state.confirmationItem) {
       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()
   }
 
-  handleDeleteExecutionClick(execution) {
+  handleDeleteExecutionClick(execution: ?Execution) {
     this.setState({
       showDeleteExecutionConfirmation: true,
       confirmationItem: execution,
@@ -172,7 +154,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   handleDeleteReplicaConfirmation() {
     this.setState({ showDeleteReplicaConfirmation: false })
     window.location.href = '/#/replicas'
-    ReplicaActions.delete(this.props.replicaStore.replicaDetails.id)
+    ReplicaStore.delete(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '')
   }
 
   handleCloseDeleteReplicaConfirmation() {
@@ -181,8 +163,8 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
   handleDeleteReplicaDisksConfirmation() {
     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() {
@@ -197,25 +179,32 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     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 })
   }
 
@@ -227,23 +216,23 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     if (!this.state.confirmationItem) {
       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 })
   }
 
-  migrateReplica(options) {
-    MigrationActions.migrateReplica(this.props.replicaStore.replicaDetails.id, options)
+  migrateReplica(options: Field[]) {
+    MigrationStore.migrateReplica(ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : '', options)
     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()
-    window.location.href = `/#/replica/executions/${this.props.replicaStore.replicaDetails.id}`
+    window.location.href = `/#/replica/executions/${ReplicaStore.replicaDetails ? ReplicaStore.replicaDetails.id : ''}`
   }
 
   pollData() {
-    ReplicaActions.getReplicaExecutions(this.props.match.params.id).promise.then(() => {
+    ReplicaStore.getReplicaExecutions(this.props.match.params.id).then(() => {
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
     })
   }
@@ -253,24 +242,28 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       <Wrapper>
         <DetailsTemplate
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           contentHeaderComponent={<DetailsContentHeader
-            item={this.props.replicaStore.replicaDetails}
+            item={ReplicaStore.replicaDetails}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onActionButtonClick={() => { this.handleActionButtonClick() }}
-            onCancelClick={execution => { this.handleCancelExecutionClick(execution) }}
+            onCancelClick={item => {
+              let any: any = item
+              let execution: Execution = any
+              this.handleCancelExecutionClick(execution)
+            }}
             actionButtonDisabled={this.isActionButtonDisabled()}
             typeImage={replicaImage}
             alertInfoPill
             buttonLabel="Execute Now"
           />}
           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 || ''}
             onCancelExecutionClick={execution => { this.handleCancelExecutionClick(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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import MainTemplate from '../../templates/MainTemplate'
 import Navigation from '../../organisms/Navigation'
@@ -25,6 +25,7 @@ import PageHeader from '../../organisms/PageHeader'
 import AlertModal from '../../organisms/AlertModal'
 import MainListItem from '../../molecules/MainListItem'
 import type { MainItem } from '../../../types/MainItem'
+import type { Project } from '../../../types/Project'
 
 import replicaItemImage from './images/replica.svg'
 import replicaLargeImage from './images/replica-large.svg'
@@ -33,12 +34,8 @@ import ProjectStore from '../../../stores/ProjectStore'
 import UserStore from '../../../stores/UserStore'
 import ReplicaStore from '../../../stores/ReplicaStore'
 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 NotificationActions from '../../../actions/NotificationActions'
+import NotificationStore from '../../../stores/NotificationStore'
 import { requestPollTimeout } from '../../../config'
 
 const Wrapper = styled.div``
@@ -48,31 +45,13 @@ const BulkActions = [
   { label: 'Delete', value: 'delete' },
 ]
 
-type Props = {
-  projectStore: any,
-  replicaStore: any,
-  userStore: any,
-  endpointStore: any,
-}
 type State = {
   showDeleteReplicaConfirmation: boolean,
   confirmationItems: ?MainItem[],
   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
 
   constructor() {
@@ -88,8 +67,8 @@ class ReplicasPage extends React.Component<Props, State> {
   componentDidMount() {
     document.title = 'Coriolis Replicas'
 
-    ProjectActions.getProjects()
-    EndpointActions.getEndpoints()
+    ProjectStore.getProjects()
+    EndpointStore.getEndpoints()
 
     this.pollData()
   }
@@ -98,12 +77,8 @@ class ReplicasPage extends React.Component<Props, State> {
     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() {
@@ -115,30 +90,31 @@ class ReplicasPage extends React.Component<Props, State> {
     ]
   }
 
-  getLastExecution(item) {
+  getLastExecution(item: MainItem) {
     let lastExecution = item.executions && item.executions.length ?
       item.executions[item.executions.length - 1] : null
 
     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() {
-    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)
     if (lastExecution && lastExecution.status === 'RUNNING') {
       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') {
       items.forEach(replica => {
-        ReplicaActions.execute(replica.id)
+        ReplicaStore.execute(replica.id)
       })
-      NotificationActions.notify('Executing replicas')
+      NotificationStore.notify('Executing replicas')
     } else if (action === 'delete') {
       this.setState({
         showDeleteReplicaConfirmation: true,
@@ -173,7 +149,7 @@ class ReplicasPage extends React.Component<Props, State> {
       return
     }
     this.state.confirmationItems.forEach(replica => {
-      ReplicaActions.delete(replica.id)
+      ReplicaStore.delete(replica.id)
     })
     this.handleCloseDeleteReplicaConfirmation()
   }
@@ -196,12 +172,12 @@ class ReplicasPage extends React.Component<Props, State> {
     if (this.state.modalIsOpen) {
       return
     }
-    ReplicaActions.getReplicas().promise.then(() => {
+    ReplicaStore.getReplicas().then(() => {
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
     })
   }
 
-  searchText(item, text) {
+  searchText(item: MainItem, text: ?string) {
     let result = false
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
       return true
@@ -218,7 +194,7 @@ class ReplicasPage extends React.Component<Props, State> {
     return result
   }
 
-  itemFilterFunction(item, filterStatus, filterText) {
+  itemFilterFunction(item: MainItem, filterStatus?: ?string, filterText?: string) {
     let lastExecution = this.getLastExecution(item)
     if ((filterStatus !== 'all' && (!lastExecution || lastExecution.status !== filterStatus)) ||
       !this.searchText(item, filterText)
@@ -238,8 +214,8 @@ class ReplicasPage extends React.Component<Props, State> {
             <FilterList
               filterItems={this.getFilterItems()}
               selectionLabel="replica"
-              loading={this.props.replicaStore.loading}
-              items={this.props.replicaStore.replicas}
+              loading={ReplicaStore.loading}
+              items={ReplicaStore.replicas}
               onItemClick={item => { this.handleItemClick(item) }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               actions={BulkActions}
@@ -249,7 +225,13 @@ class ReplicasPage extends React.Component<Props, State> {
                 (<MainListItem
                   {...options}
                   image={replicaItemImage}
-                  endpointType={id => this.getEndpoint(id).type}
+                  endpointType={id => {
+                    let endpoint = this.getEndpoint(id)
+                    if (endpoint) {
+                      return endpoint.type
+                    }
+                    return ''
+                  }}
                 />)
               }
               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 styled from 'styled-components'
-import connectToStores from 'alt-utils/lib/connectToStores'
+import { observer } from 'mobx-react'
 
 import WizardTemplate from '../../templates/WizardTemplate'
 import { DetailsPageHeader } from '../../organisms/DetailsPageHeader'
@@ -25,34 +25,28 @@ import Modal from '../../molecules/Modal'
 import Endpoint from '../../organisms/Endpoint'
 
 import UserStore from '../../../stores/UserStore'
-import UserActions from '../../../actions/UserActions'
-import ProviderActions from '../../../actions/ProviderActions'
 import ProviderStore from '../../../stores/ProviderStore'
-import EndpointActions from '../../../actions/EndpointActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import WizardStore from '../../../stores/WizardStore'
-import WizardActions from '../../../actions/WizardActions'
 import InstanceStore from '../../../stores/InstanceStore'
-import InstanceActions from '../../../actions/InstanceActions'
 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 ReplicaStore from '../../../stores/ReplicaStore'
 import Wait from '../../../utils/Wait'
 import KeyboardManager from '../../../utils/KeyboardManager'
 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``
 
 type Props = {
-  userStore: any,
-  wizardStore: any,
-  providerStore: any,
-  endpointStore: any,
-  instanceStore: any,
-  networkStore: any,
   match: any,
 }
 type WizardType = 'migration' | 'replica'
@@ -63,22 +57,8 @@ type State = {
   newEndpointType?: string,
   newEndpointFromSource?: boolean,
 }
+@observer
 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
 
   constructor() {
@@ -92,7 +72,7 @@ class WizardPage extends React.Component<Props, State> {
   }
 
   componentWillMount() {
-    WizardActions.getDataFromPermalink()
+    WizardStore.getDataFromPermalink()
     let type = this.props.match && this.props.match.params.type
     if (type === 'migration' || type === 'replica') {
       this.setState({ type })
@@ -106,7 +86,7 @@ class WizardPage extends React.Component<Props, State> {
   }
 
   componentWillUnmount() {
-    WizardActions.clearData()
+    WizardStore.clearData()
     KeyboardManager.removeKeyDown('wizard')
   }
 
@@ -120,9 +100,9 @@ class WizardPage extends React.Component<Props, State> {
     this.handleBackClick()
   }
 
-  handleCreationSuccess(items) {
+  handleCreationSuccess(items: MainItem[]) {
     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') {
       items.forEach(replica => {
@@ -139,7 +119,7 @@ class WizardPage extends React.Component<Props, State> {
         location += 'tasks/'
       }
 
-      Wait.for(() => !ScheduleStore.getState().scheduling, () => {
+      Wait.for(() => !ScheduleStore.scheduling, () => {
         window.location.href = location + items[0].id
       })
     } else {
@@ -147,10 +127,10 @@ class WizardPage extends React.Component<Props, State> {
     }
   }
 
-  handleUserItemClick(item) {
+  handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'signout':
-        UserActions.logout()
+        UserStore.logout()
         return
       case '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' })
   }
 
   handleBackClick() {
     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) {
       window.history.back()
@@ -174,12 +154,12 @@ class WizardPage extends React.Component<Props, State> {
 
     let page = pages[currentPageIndex - 1]
     this.loadDataForPage(page)
-    WizardActions.setCurrentPage(page)
+    WizardStore.setCurrentPage(page)
   }
 
   handleNextClick() {
     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) {
       this.create()
@@ -188,22 +168,22 @@ class WizardPage extends React.Component<Props, State> {
 
     let page = pages[currentPageIndex + 1]
     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
-    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({
       showNewEndpointModal: true,
       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) {
-        WizardActions.updateData({ source: this.props.endpointStore.endpoints[0] })
+        WizardStore.updateData({ source: EndpointStore.endpoints[0] })
       } 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 })
   }
 
-  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() {
-    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) {
       case 'source': {
-        ProviderActions.loadProviders()
-        EndpointActions.getEndpoints()
+        ProviderStore.loadProviders()
+        EndpointStore.getEndpoints()
         // 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
       }
       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
       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
       default:
     }
@@ -296,11 +288,11 @@ class WizardPage extends React.Component<Props, State> {
 
   createMultiple() {
     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) {
-        NotificationActions.notify(`${typeLabel}s couldn't be created`, 'error')
+        NotificationStore.notify(`${typeLabel}s couldn't be created`, 'error')
         this.setState({ nextButtonDisabled: false })
         return
       }
@@ -310,11 +302,11 @@ class WizardPage extends React.Component<Props, State> {
 
   createSingle() {
     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) {
-        NotificationActions.notify(`${typeLabel} couldn't be created`, 'error')
+        NotificationStore.notify(`${typeLabel} couldn't be created`, 'error')
         this.setState({ nextButtonDisabled: false })
         return
       }
@@ -323,14 +315,14 @@ class WizardPage extends React.Component<Props, State> {
   }
 
   separateVms() {
-    let data = WizardStore.getState().data
+    let data = WizardStore.data
     let separateVms = true
 
     if (data.options && data.options.separate_vm !== null && data.options.separate_vm !== undefined) {
       separateVms = data.options.separate_vm
     }
 
-    if (data.selectedInstances.length === 1) {
+    if (data.selectedInstances && data.selectedInstances.length === 1) {
       separateVms = false
     }
 
@@ -346,18 +338,18 @@ class WizardPage extends React.Component<Props, State> {
     this.separateVms()
   }
 
-  scheduleReplica(replica) {
-    let data = WizardStore.getState().data
+  scheduleReplica(replica: MainItem) {
+    let data = WizardStore.data
 
     if (!data.schedules || data.schedules.length === 0) {
       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
     if (options && options.execute_now !== null && options.execute_now !== undefined) {
       executeNow = options.execute_now
@@ -373,7 +365,7 @@ class WizardPage extends React.Component<Props, State> {
       return field
     })
 
-    ReplicaActions.execute(replica.id, executeNowOptions)
+    ReplicaStore.execute(replica.id, executeNowOptions)
   }
 
   render() {
@@ -381,16 +373,16 @@ class WizardPage extends React.Component<Props, State> {
       <Wrapper>
         <WizardTemplate
           pageHeaderComponent={<DetailsPageHeader
-            user={this.props.userStore.user}
+            user={UserStore.user}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           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}
             type={this.state.type}
             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 { SchemaParser } from './Schemas'
 import ObjectUtils from '../utils/ObjectUtils'
-import type { Endpoint } from '../types/Endpoint'
+import type { Endpoint, Validation } from '../types/Endpoint'
 
 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) => {
       let projectId = cookie.get('projectId')
       Api.sendAjaxRequest({

+ 2 - 2
src/sources/ReplicaSource.js

@@ -104,7 +104,7 @@ class ReplicaSource {
         let executions = response.data.executions
         ReplicaSourceUtils.sortExecutionsAndTaskUpdates(executions)
 
-        resolve({ replicaId, executions })
+        resolve(executions)
       }, reject).catch(reject)
     })
   }
@@ -142,7 +142,7 @@ class ReplicaSource {
       }).then((response) => {
         let execution = response.data.execution
         ReplicaSourceUtils.sortTaskUpdates(execution)
-        resolve({ replicaId, execution })
+        resolve(execution)
       }, 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 Api from '../utils/ApiCaller'
-import NotificationActions from '../actions/NotificationActions'
+import NotificationStore from '../stores/NotificationStore'
 import { servicesUrl, executionOptions } from '../config'
 
 class WizardSourceUtils {
@@ -93,7 +93,7 @@ class WizardSource {
           }
         }, () => {
           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,
             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/>.
 */
 
-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 => {
   if (e.id === endpoint.id) {
@@ -23,121 +25,98 @@ const updateEndpoint = (endpoint, endpoints) => endpoints.map(e => {
 })
 
 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
     }
-  }
-
-  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
-  }
 
-  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
   }
 
-  handleValidate() {
+  @action validate(endpoint: Endpoint) {
     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.validation = null
   }
 
-  handleUpdate({ endpoint }) {
+  @action update(endpoint: Endpoint) {
     this.endpoints = updateEndpoint(endpoint, this.endpoints)
     this.connectionInfo = { ...endpoint.connection_info }
     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
   }
 
-  handleAdd() {
+  @action add(endpoint: Endpoint) {
     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/>.
 */
 
-import alt from '../alt'
-import InstanceActions from '../actions/InstanceActions'
+// @flow
+
+import { observable, action } from 'mobx'
 
 import { wizardConfig } from '../config'
+import type { Instance } from '../types/Instance'
+import InstanceSource from '../sources/InstanceSource'
 
 class InstanceStoreUtils {
   static hasNextPage(instances) {
@@ -42,167 +45,151 @@ class InstanceStoreUtils {
 }
 
 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.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
+    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.currentPage = this.currentPage - 1
     this.instances = InstanceStoreUtils.loadFromCache(this.cachedInstances, this.currentPage)
   }
 
-  handleReloadInstances() {
+  @action reloadInstances(endpointId: string, searchText: string) {
     this.reloading = true
     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.instancesDetailsCount = count
     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/>.
 */
 
-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
     }
-  }
 
-  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
-  }
 
-  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: {
           label: 'View Migration Status',
           callback: () => {
@@ -96,24 +91,12 @@ class MigrationStore {
         persist: true,
         persistInfo: { title: 'Migration created' },
       })
-    }, 0)
-  }
-
-  handleCancel() {
-    this.canceling = true
-  }
-
-  handleCancelSuccess() {
-    this.canceling = false
-  }
-
-  handleCancelFailed() {
-    this.canceling = { failed: true }
+    })
   }
 
-  handleClearDetails() {
+  @action clearDetails() {
     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/>.
 */
 
-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 {
-  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
-  }
-
-  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/>.
 */
 
-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 {
-  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/>.
 */
 
-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/>.
 */
 
-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 = []
   }
 
-  handleLoadProviders() {
+  @action loadProviders(): Promise<void> {
     this.providers = null
     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
-  }
-
-  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/>.
 */
 
-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 {
-  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,
       }
     }
@@ -34,135 +40,102 @@ class ReplicaStoreUtils {
 }
 
 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
 
-    if (showLoading || this.replicas.length === 0) {
+    if ((options && options.showLoading) || this.replicas.length === 0) {
       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)
+
       if (replica) {
         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
-  }
-
-  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.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/>.
 */
 
-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) => {
   return schedules.map(schedule => {
@@ -30,74 +34,60 @@ const updateSchedule = (schedules, id, data) => {
 }
 
 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
-  }
 
-  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
-  }
 
-  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
-  }
-
-  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.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)
 
     if (!forceSave) {
@@ -107,24 +97,23 @@ class ScheduleStore {
       } else {
         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.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/>.
 */
 
-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.user = 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.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/>.
 */
 
-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 Source from '../sources/WizardSource'
 
 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) {
       this.data.selectedInstances = [instance]
       return
     }
 
     if (this.data.selectedInstances.find(i => i.id === instance.id)) {
+      // $FlowIssue
       this.data.selectedInstances = this.data.selectedInstances.filter(i => i.id !== instance.id)
     } else {
+      // $FlowIssue
       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[field.name] = value
+    this.data.options[data.field.name] = data.value
   }
 
-  handleUpdateNetworks({ sourceNic, targetNetwork }) {
+  @action updateNetworks(network: NetworkMap) {
     if (!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) {
       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)
   }
 
-  handleCreate() {
+  @action create(type: string, data: WizardData): Promise<void> {
     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
-  }
 
-  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
     }
 
@@ -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
 
+export type Validation = {
+  valid: boolean,
+  message: string,
+}
+
 export type Endpoint = {
   id: 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
 
+import type { Nic } from './Instance'
+
 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
 
 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/>.
 */
 
-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
 
-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 { Instance } from './Instance'
-import type { Network } from './Network'
+import type { NetworkMap } from './Network'
 import type { Endpoint } from './Endpoint'
 
 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/>.
 */
 
-import NotificationActions from '../actions/NotificationActions'
+import NotificationStore from '../stores/NotificationStore'
 
 let apiInstance = null
 
@@ -90,7 +90,7 @@ class ApiCaller {
 
           if (result.data && result.data.error && result.data.error.message &&
             (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) {
@@ -104,7 +104,7 @@ class ApiCaller {
       request.onerror = (result) => {
         let 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')
         }
 

+ 29 - 51
yarn.lock

@@ -291,20 +291,6 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
   version "1.0.2"
   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:
   version "1.0.1"
   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"
   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"
   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-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:
   version "6.24.1"
   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"
     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"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   dependencies:
@@ -1515,7 +1509,7 @@ babel-template@7.0.0-beta.0:
     babylon "7.0.0-beta.22"
     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"
   resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
   dependencies:
@@ -3203,21 +3197,7 @@ fb-watchman@^2.0.0:
   dependencies:
     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"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
   dependencies:
@@ -3346,14 +3326,6 @@ flow-typed@^2.3.0:
     which "^1.3.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:
   version "1.0.2"
   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"
   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:
   version "2.0.0"
   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"
   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"
   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"
   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"
   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:
     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:
   version "2.7.0"
   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:
   version "2.18.1"
   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"
     function-bind "^1.1.1"
 
-promise@^7.0.3, promise@^7.1.1:
+promise@^7.1.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
   dependencies:
@@ -7263,10 +7249,6 @@ tr46@~0.0.3:
   version "0.0.3"
   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":
   version "0.3.9"
   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"
   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:
   version "4.8.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"