Browse Source

Refactor replica tasks loading and polling

There were some redundant calls being made to load and to poll replica
execution tasks.

Those redundancies were removed.

This also fixes an issue were sometime the tasks didn't appear until the
first tasks polling was rescheduled (i.e. with a delay of about 2
seconds).

This includes a small UI fix where the 'CANCELLING' spinner would appear
on a transparent background in executions timeline component.
Sergiu Miclea 6 years ago
parent
commit
f176b6aa3d

+ 4 - 9
src/components/atoms/StatusIcon/StatusIcon.jsx

@@ -22,7 +22,6 @@ import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 
 
 import errorImage from './images/error.svg'
 import errorImage from './images/error.svg'
-import progressWithBackgroundImage from './images/progress-background.svg'
 import progressImage from './images/progress.js'
 import progressImage from './images/progress.js'
 import successImage from './images/success.svg'
 import successImage from './images/success.svg'
 import warningImage from './images/warning.js'
 import warningImage from './images/warning.js'
@@ -37,17 +36,13 @@ type Props = {
   secondary?: boolean,
   secondary?: boolean,
 }
 }
 
 
-const getSpinnerUrl = (smallCircleColor: string) => {
-  return css`url('data:image/svg+xml;utf8,${encodeURIComponent(progressImage(Palette.grayscale[3], smallCircleColor))}')`
+const getSpinnerUrl = (smallCircleColor: string, useWhiteBackground: ?boolean) => {
+  return css`url('data:image/svg+xml;utf8,${encodeURIComponent(progressImage(Palette.grayscale[3], smallCircleColor, useWhiteBackground))}')`
 }
 }
 
 
 const getRunningImageUrl = (props: Props) => {
 const getRunningImageUrl = (props: Props) => {
-  if (props.useBackground) {
-    return css`url('${progressWithBackgroundImage}')`
-  }
-
   const smallCircleColor = props.secondary ? Palette.grayscale[0] : Palette.primary
   const smallCircleColor = props.secondary ? Palette.grayscale[0] : Palette.primary
-  return getSpinnerUrl(smallCircleColor)
+  return getSpinnerUrl(smallCircleColor, props.useBackground)
 }
 }
 
 
 const getWarningUrl = (background: string) => {
 const getWarningUrl = (background: string) => {
@@ -69,7 +64,7 @@ const statuses = (status, props) => {
     case 'CANCELLING':
     case 'CANCELLING':
     case 'CANCELLING_AFTER_COMPLETION':
     case 'CANCELLING_AFTER_COMPLETION':
       return css`
       return css`
-        background-image: ${getSpinnerUrl(Palette.warning)};
+        background-image: ${getSpinnerUrl(Palette.warning, props.useBackground)};
         ${StyleProps.animations.rotation}
         ${StyleProps.animations.rotation}
       `
       `
     case 'SCHEDULED':
     case 'SCHEDULED':

+ 0 - 16
src/components/atoms/StatusIcon/images/progress-background.svg

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
-
-    <desc>Created with Sketch.</desc>
-    <defs></defs>
-    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="Icon/Progress/Default">
-            <g id="Group-2">
-                <circle id="Oval-2-Copy" fill="#C8CCD7" cx="8" cy="8" r="8"></circle>
-                <path d="M16,8 C16,3.581722 12.418278,0 8,0 L8,8 L16,8 Z" id="Combined-Shape" fill="#0044CA"></path>
-                <circle id="Oval-2-Copy" fill="#FFFFFF" cx="8" cy="8" r="6"></circle>
-            </g>
-        </g>
-    </g>
-</svg>

+ 4 - 1
src/components/atoms/StatusIcon/images/progress.js

@@ -12,11 +12,14 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-const image = (bigColor, smallColor) => `
+// @flow
+
+const image = (bigColor: string, smallColor: string, useWhiteBackground: ?boolean) => `
   <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
   <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
     <g>
       <circle fill="none" stroke="${bigColor}" stroke-width="2"  cx="8" cy="8" r="7"></circle>
       <circle fill="none" stroke="${bigColor}" stroke-width="2"  cx="8" cy="8" r="7"></circle>
       <path d="M 15 8 A 7 7 0 0 0 8 1" fill="none" stroke="${smallColor}" stroke-width="2" />
       <path d="M 15 8 A 7 7 0 0 0 8 1" fill="none" stroke="${smallColor}" stroke-width="2" />
+      ${useWhiteBackground ? '<circle fill="white" cx="8" cy="8" r="6"></circle>' : ''}
     </g>
     </g>
   </svg>
   </svg>
 `
 `

+ 6 - 0
src/components/atoms/StatusIcon/story.jsx

@@ -62,3 +62,9 @@ storiesOf('StatusIcon', module)
   .add('error hollow', () => (
   .add('error hollow', () => (
     <StatusIcon status="ERROR" hollow />
     <StatusIcon status="ERROR" hollow />
   ))
   ))
+  .add('running white background', () => (
+    <Wrapper>
+      <StatusIcon status="RUNNING" useBackground />
+      <StatusIcon status="CANCELLING" useBackground />
+    </Wrapper>
+  ))

+ 13 - 8
src/components/organisms/PageHeader/PageHeader.jsx

@@ -172,7 +172,7 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
     if (this.props.onModalClose) {
       this.props.onModalClose()
       this.props.onModalClose()
     }
     }
-    this.setState({ showChooseProviderModal: false })
+    this.setState({ showChooseProviderModal: false }, () => { this.pollData() })
   }
   }
 
 
   handleProviderClick(providerType: string) {
   handleProviderClick(providerType: string) {
@@ -213,17 +213,22 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
     if (this.props.onModalClose) {
       this.props.onModalClose()
       this.props.onModalClose()
     }
     }
-    this.setState({ showEndpointModal: false })
+    this.setState({ showEndpointModal: false }, () => { this.pollData() })
   }
   }
 
 
   handleBackEndpointModal(options?: { autoClose?: boolean }) {
   handleBackEndpointModal(options?: { autoClose?: boolean }) {
-    this.setState({ showChooseProviderModal: !options || !options.autoClose, showEndpointModal: false })
+    let showChooseProviderModal = !options || !options.autoClose
+    this.setState({ showChooseProviderModal, showEndpointModal: false }, () => {
+      if (!showChooseProviderModal) {
+        this.pollData()
+      }
+    })
   }
   }
 
 
   async handleProjectChange(project: Project) {
   async handleProjectChange(project: Project) {
     await userStore.switchProject(project.id)
     await userStore.switchProject(project.id)
     projectStore.getProjects()
     projectStore.getProjects()
-    notificationStore.loadData()
+    notificationStore.loadData(true)
 
 
     if (this.props.onProjectChange) {
     if (this.props.onProjectChange) {
       this.props.onProjectChange(project)
       this.props.onProjectChange(project)
@@ -234,7 +239,7 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
     if (this.props.onModalClose) {
       this.props.onModalClose()
       this.props.onModalClose()
     }
     }
-    this.setState({ showUserModal: false })
+    this.setState({ showUserModal: false }, () => { this.pollData() })
   }
   }
 
 
   async handleUserUpdateClick(user: User) {
   async handleUserUpdateClick(user: User) {
@@ -242,14 +247,14 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
     if (this.props.onModalClose) {
       this.props.onModalClose()
       this.props.onModalClose()
     }
     }
-    this.setState({ showUserModal: false })
+    this.setState({ showUserModal: false }, () => { this.pollData() })
   }
   }
 
 
   handleProjectModalClose() {
   handleProjectModalClose() {
     if (this.props.onModalClose) {
     if (this.props.onModalClose) {
       this.props.onModalClose()
       this.props.onModalClose()
     }
     }
-    this.setState({ showProjectModal: false })
+    this.setState({ showProjectModal: false }, () => { this.pollData() })
   }
   }
 
 
   async handleProjectModalUpdateClick(project: Project) {
   async handleProjectModalUpdateClick(project: Project) {
@@ -257,7 +262,7 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
     if (this.props.onModalClose) {
       this.props.onModalClose()
       this.props.onModalClose()
     }
     }
-    this.setState({ showProjectModal: false })
+    this.setState({ showProjectModal: false }, () => { this.pollData() })
   }
   }
 
 
   async pollData(showLoading?: boolean) {
   async pollData(showLoading?: boolean) {

+ 89 - 57
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.jsx

@@ -45,6 +45,7 @@ import instanceStore from '../../../stores/InstanceStore'
 import networkStore from '../../../stores/NetworkStore'
 import networkStore from '../../../stores/NetworkStore'
 import notificationStore from '../../../stores/NotificationStore'
 import notificationStore from '../../../stores/NotificationStore'
 import providerStore from '../../../stores/ProviderStore'
 import providerStore from '../../../stores/ProviderStore'
+
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
 import utils from '../../../utils/ObjectUtils'
 import utils from '../../../utils/ObjectUtils'
 import { providerTypes } from '../../../constants'
 import { providerTypes } from '../../../constants'
@@ -55,7 +56,7 @@ import Palette from '../../styleUtils/Palette'
 const Wrapper = styled.div``
 const Wrapper = styled.div``
 
 
 type Props = {
 type Props = {
-  match: any,
+  match: { params: { id: string, page: ?string } },
   history: any,
   history: any,
 }
 }
 type State = {
 type State = {
@@ -89,18 +90,29 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
   stopPolling: ?boolean
   stopPolling: ?boolean
 
 
+  get replicaId() {
+    if (!this.props.match || !this.props.match.params || !this.props.match.params.id) {
+      throw new Error('Invalid replica id')
+    }
+    return this.props.match.params.id
+  }
+
+  get replica() {
+    let replica = replicaStore.replicas.find(r => r.id === this.replicaId)
+    return replica
+  }
+
   componentWillMount() {
   componentWillMount() {
     document.title = 'Replica Details'
     document.title = 'Replica Details'
 
 
     let loadReplica = async () => {
     let loadReplica = async () => {
       await endpointStore.getEndpoints({ showLoading: true })
       await endpointStore.getEndpoints({ showLoading: true })
-      await this.loadReplicaWithInstances(this.props.match.params.id, true)
-      let details = replicaStore.replicaDetails
-      if (!details) {
+      let replica = await this.loadReplicaWithInstances(this.replicaId, true)
+      if (!replica) {
         return
         return
       }
       }
-      let sourceEndpoint = endpointStore.endpoints.find(e => e.id === details.origin_endpoint_id)
-      let destinationEndpoint = endpointStore.endpoints.find(e => e.id === details.destination_endpoint_id)
+      let sourceEndpoint = endpointStore.endpoints.find(e => e.id === replica.origin_endpoint_id)
+      let destinationEndpoint = endpointStore.endpoints.find(e => e.id === replica.destination_endpoint_id)
       if (!sourceEndpoint || !destinationEndpoint) {
       if (!sourceEndpoint || !destinationEndpoint) {
         return
         return
       }
       }
@@ -115,7 +127,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         })
         })
         let getOptionsValuesConfig = {
         let getOptionsValuesConfig = {
           optionsType,
           optionsType,
-          endpointId: optionsType === 'source' ? details.origin_endpoint_id : details.destination_endpoint_id,
+          endpointId: optionsType === 'source' ? replica.origin_endpoint_id : replica.destination_endpoint_id,
           providerName,
           providerName,
           useCache: true,
           useCache: true,
           quietError: true,
           quietError: true,
@@ -127,7 +139,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         await providerStore.getOptionsValues(getOptionsValuesConfig)
         await providerStore.getOptionsValues(getOptionsValuesConfig)
         await providerStore.getOptionsValues({
         await providerStore.getOptionsValues({
           ...getOptionsValuesConfig,
           ...getOptionsValuesConfig,
-          envData: optionsType === 'source' ? details.source_environment : details.destination_environment,
+          envData: optionsType === 'source' ? replica.source_environment : replica.destination_environment,
         })
         })
       }
       }
 
 
@@ -136,7 +148,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     }
     }
     loadReplica()
     loadReplica()
 
 
-    scheduleStore.getSchedules(this.props.match.params.id)
+    scheduleStore.getSchedules(this.replicaId)
     this.pollData(true)
     this.pollData(true)
   }
   }
 
 
@@ -148,7 +160,6 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    replicaStore.clearDetails()
     scheduleStore.clearUnsavedSchedules()
     scheduleStore.clearUnsavedSchedules()
     this.stopPolling = true
     this.stopPolling = true
   }
   }
@@ -174,34 +185,35 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   async loadReplicaWithInstances(replicaId: string, cache: boolean) {
   async loadReplicaWithInstances(replicaId: string, cache: boolean) {
-    await replicaStore.getReplica(replicaId, { showLoading: true })
-    let details = replicaStore.replicaDetails
-    if (!details) {
-      return
+    await replicaStore.getReplicas({ showLoading: true })
+    let replica = this.replica
+    if (!replica) {
+      return null
     }
     }
-    this.loadIsEditable(details)
-    networkStore.loadNetworks(details.destination_endpoint_id, details.destination_environment, {
+    this.loadIsEditable(replica)
+    networkStore.loadNetworks(replica.destination_endpoint_id, replica.destination_environment, {
       quietError: true,
       quietError: true,
       cache,
       cache,
     })
     })
 
 
-    let targetEndpoint = endpointStore.endpoints.find(e => e.id === details.destination_endpoint_id)
+    let targetEndpoint = endpointStore.endpoints.find(e => e.id === replica.destination_endpoint_id)
     instanceStore.loadInstancesDetails({
     instanceStore.loadInstancesDetails({
-      endpointId: details.origin_endpoint_id,
+      endpointId: replica.origin_endpoint_id,
       // $FlowIgnore
       // $FlowIgnore
-      instancesInfo: details.instances.map(n => ({ instance_name: n })),
+      instancesInfo: replica.instances.map(n => ({ instance_name: n })),
       cache,
       cache,
       quietError: false,
       quietError: false,
-      env: details.source_environment,
+      env: replica.source_environment,
       targetProvider: targetEndpoint ? targetEndpoint.type : '',
       targetProvider: targetEndpoint ? targetEndpoint.type : '',
     })
     })
+    return replica
   }
   }
 
 
   getLastExecution() {
   getLastExecution() {
-    if (replicaStore.replicaDetails && replicaStore.replicaDetails.executions && replicaStore.replicaDetails.executions.length) {
-      return replicaStore.replicaDetails.executions[replicaStore.replicaDetails.executions.length - 1]
+    let replica = this.replica
+    if (replica && replica.executions && replica.executions.length) {
+      return replica.executions[replica.executions.length - 1]
     }
     }
-
     return null
     return null
   }
   }
 
 
@@ -211,8 +223,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   isExecuteDisabled() {
   isExecuteDisabled() {
-    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 replica = this.replica
+    if (!replica) {
+      return true
+    }
+    let originEndpoint = endpointStore.endpoints.find(e => e.id === replica.origin_endpoint_id)
+    let targetEndpoint = endpointStore.endpoints.find(e => e.id === replica.destination_endpoint_id)
 
 
     return Boolean(!originEndpoint || !targetEndpoint || this.getStatus() === 'RUNNING' || this.getStatus() === 'CANCELLING')
     return Boolean(!originEndpoint || !targetEndpoint || this.getStatus() === 'RUNNING' || this.getStatus() === 'CANCELLING')
   }
   }
@@ -235,10 +251,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleDeleteExecutionConfirmation() {
   handleDeleteExecutionConfirmation() {
-    if (!this.state.confirmationItem) {
+    let replica = this.replica
+    if (!this.state.confirmationItem || !replica) {
       return
       return
     }
     }
-    replicaStore.deleteExecution(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '', this.state.confirmationItem.id)
+    replicaStore.deleteExecution(replica.id, this.state.confirmationItem.id)
     this.handleCloseExecutionConfirmation()
     this.handleCloseExecutionConfirmation()
   }
   }
 
 
@@ -266,8 +283,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
   handleDeleteReplicaConfirmation() {
   handleDeleteReplicaConfirmation() {
     this.setState({ showDeleteReplicaConfirmation: false })
     this.setState({ showDeleteReplicaConfirmation: false })
+    let replica = this.replica
+    if (!replica) {
+      return
+    }
     this.props.history.push('/replicas')
     this.props.history.push('/replicas')
-    replicaStore.delete(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '')
+    replicaStore.delete(replica.id)
   }
   }
 
 
   handleCloseDeleteReplicaConfirmation() {
   handleCloseDeleteReplicaConfirmation() {
@@ -276,8 +297,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
   handleDeleteReplicaDisksConfirmation() {
   handleDeleteReplicaDisksConfirmation() {
     this.setState({ showDeleteReplicaDisksConfirmation: false, showDeleteReplicaConfirmation: false })
     this.setState({ showDeleteReplicaDisksConfirmation: false, showDeleteReplicaConfirmation: false })
-    replicaStore.deleteDisks(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '')
-    this.props.history.push(`/replica/executions/${replicaStore.replicaDetails ? replicaStore.replicaDetails.id : ''}`)
+    let replica = this.replica
+    if (!replica) {
+      return
+    }
+    replicaStore.deleteDisks(replica.id)
+    this.props.history.push(`/replica/executions/${replica.id}`)
   }
   }
 
 
   handleCloseDeleteReplicaDisksConfirmation() {
   handleCloseDeleteReplicaDisksConfirmation() {
@@ -297,7 +322,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleAddScheduleClick(schedule: Schedule) {
   handleAddScheduleClick(schedule: Schedule) {
-    scheduleStore.addSchedule(this.props.match.params.id, schedule)
+    scheduleStore.addSchedule(this.replicaId, schedule)
   }
   }
 
 
   handleScheduleChange(scheduleId: ?string, data: Schedule, forceSave?: boolean) {
   handleScheduleChange(scheduleId: ?string, data: Schedule, forceSave?: boolean) {
@@ -305,19 +330,19 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     let unsavedData = scheduleStore.unsavedSchedules.find(s => s.id === scheduleId)
     let unsavedData = scheduleStore.unsavedSchedules.find(s => s.id === scheduleId)
 
 
     if (scheduleId) {
     if (scheduleId) {
-      scheduleStore.updateSchedule(this.props.match.params.id, scheduleId, data, oldData, unsavedData, forceSave)
+      scheduleStore.updateSchedule(this.replicaId, scheduleId, data, oldData, unsavedData, forceSave)
     }
     }
   }
   }
 
 
   handleScheduleSave(schedule: Schedule) {
   handleScheduleSave(schedule: Schedule) {
     if (schedule.id) {
     if (schedule.id) {
-      scheduleStore.updateSchedule(this.props.match.params.id, schedule.id, schedule, schedule, schedule, true)
+      scheduleStore.updateSchedule(this.replicaId, schedule.id, schedule, schedule, schedule, true)
     }
     }
   }
   }
 
 
   handleScheduleRemove(scheduleId: ?string) {
   handleScheduleRemove(scheduleId: ?string) {
     if (scheduleId) {
     if (scheduleId) {
-      scheduleStore.removeSchedule(this.props.match.params.id, scheduleId)
+      scheduleStore.removeSchedule(this.replicaId, scheduleId)
     }
     }
   }
   }
 
 
@@ -341,11 +366,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleCancelConfirmation(force?: boolean) {
   handleCancelConfirmation(force?: boolean) {
-    if (!this.state.confirmationItem) {
+    let replica = this.replica
+    if (!this.state.confirmationItem || !replica) {
       return
       return
     }
     }
     replicaStore.cancelExecution(
     replicaStore.cancelExecution(
-      replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '',
+      replica.id,
       this.state.confirmationItem.id,
       this.state.confirmationItem.id,
       force
       force
     )
     )
@@ -361,8 +387,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   async migrate(options: Field[], uploadedScripts: InstanceScript[]) {
   async migrate(options: Field[], uploadedScripts: InstanceScript[]) {
+    let replica = this.replica
+    if (!replica) {
+      return
+    }
     let migration = await migrationStore.migrateReplica(
     let migration = await migrationStore.migrateReplica(
-      replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '',
+      replica.id,
       options,
       options,
       uploadedScripts
       uploadedScripts
     )
     )
@@ -377,9 +407,13 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   executeReplica(fields: Field[]) {
   executeReplica(fields: Field[]) {
-    replicaStore.execute(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '', fields)
+    let replica = this.replica
+    if (!replica) {
+      return
+    }
+    replicaStore.execute(replica.id, fields)
     this.handleCloseOptionsModal()
     this.handleCloseOptionsModal()
-    this.props.history.push(`/replica/executions/${replicaStore.replicaDetails ? replicaStore.replicaDetails.id : ''}`)
+    this.props.history.push(`/replica/executions/${replica.id}`)
   }
   }
 
 
   async pollData(showLoading: boolean) {
   async pollData(showLoading: boolean) {
@@ -387,11 +421,8 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       return
       return
     }
     }
 
 
-    if (!this.props.match.params.page) {
-      replicaStore.getReplica(this.props.match.params.id, { showLoading, skipLog: true })
-    }
+    await replicaStore.getReplicas({ showLoading, skipLog: true })
 
 
-    await replicaStore.getReplicaExecutions(this.props.match.params.id, { showLoading, skipLog: true })
     setTimeout(() => { this.pollData(false) }, configLoader.config.requestPollTimeout)
     setTimeout(() => { this.pollData(false) }, configLoader.config.requestPollTimeout)
   }
   }
 
 
@@ -402,25 +433,25 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleEditReplicaReload() {
   handleEditReplicaReload() {
-    this.loadReplicaWithInstances(this.props.match.params.id, false)
+    this.loadReplicaWithInstances(this.replicaId, false)
   }
   }
 
 
   handleUpdateComplete(redirectTo: string) {
   handleUpdateComplete(redirectTo: string) {
-    if (!replicaStore.replicaDetails) {
-      return
-    }
-
     this.props.history.push(redirectTo)
     this.props.history.push(redirectTo)
     this.closeEditModal()
     this.closeEditModal()
   }
   }
 
 
   renderEditReplica() {
   renderEditReplica() {
+    let replica = this.replica
+    if (!replica) {
+      return null
+    }
     let sourceEndpoint = endpointStore.endpoints
     let sourceEndpoint = endpointStore.endpoints
-      .find(e => replicaStore.replicaDetails && e.id === replicaStore.replicaDetails.origin_endpoint_id)
+      .find(e => e.id === replica.origin_endpoint_id)
     let destinationEndpoint = endpointStore.endpoints
     let destinationEndpoint = endpointStore.endpoints
-      .find(e => replicaStore.replicaDetails && e.id === replicaStore.replicaDetails.destination_endpoint_id)
+      .find(e => e.id === replica.destination_endpoint_id)
 
 
-    if (!this.state.showEditModal || !replicaStore.replicaDetails || !destinationEndpoint || !sourceEndpoint) {
+    if (!this.state.showEditModal || !destinationEndpoint || !sourceEndpoint) {
       return null
       return null
     }
     }
 
 
@@ -431,7 +462,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         sourceEndpoint={sourceEndpoint}
         sourceEndpoint={sourceEndpoint}
         onUpdateComplete={url => { this.handleUpdateComplete(url) }}
         onUpdateComplete={url => { this.handleUpdateComplete(url) }}
         onRequestClose={() => { this.closeEditModal() }}
         onRequestClose={() => { this.closeEditModal() }}
-        replica={replicaStore.replicaDetails}
+        replica={replica}
         destinationEndpoint={destinationEndpoint}
         destinationEndpoint={destinationEndpoint}
         instancesDetails={instanceStore.instancesDetails}
         instancesDetails={instanceStore.instancesDetails}
         instancesDetailsLoading={instanceStore.loadingInstancesDetails}
         instancesDetailsLoading={instanceStore.loadingInstancesDetails}
@@ -480,6 +511,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         action: () => { this.handleDeleteReplicaClick() },
         action: () => { this.handleDeleteReplicaClick() },
       },
       },
     ]
     ]
+    let replica = this.replica
 
 
     return (
     return (
       <Wrapper>
       <Wrapper>
@@ -489,20 +521,20 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
             onUserItemClick={item => { this.handleUserItemClick(item) }}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           />}
           contentHeaderComponent={<DetailsContentHeader
           contentHeaderComponent={<DetailsContentHeader
-            item={replicaStore.replicaDetails}
+            item={replica}
             dropdownActions={dropdownActions}
             dropdownActions={dropdownActions}
             backLink="/replicas"
             backLink="/replicas"
             typeImage={replicaImage}
             typeImage={replicaImage}
             alertInfoPill
             alertInfoPill
           />}
           />}
           contentComponent={<ReplicaDetailsContent
           contentComponent={<ReplicaDetailsContent
-            item={replicaStore.replicaDetails}
+            item={replica}
             instancesDetails={instanceStore.instancesDetails}
             instancesDetails={instanceStore.instancesDetails}
             instancesDetailsLoading={instanceStore.loadingInstancesDetails}
             instancesDetailsLoading={instanceStore.loadingInstancesDetails}
             endpoints={endpointStore.endpoints}
             endpoints={endpointStore.endpoints}
             scheduleStore={scheduleStore}
             scheduleStore={scheduleStore}
             networks={networkStore.networks}
             networks={networkStore.networks}
-            detailsLoading={replicaStore.detailsLoading || endpointStore.loading}
+            detailsLoading={replicaStore.loading || endpointStore.loading}
             sourceSchema={providerStore.sourceSchema}
             sourceSchema={providerStore.sourceSchema}
             sourceSchemaLoading={providerStore.sourceSchemaLoading
             sourceSchemaLoading={providerStore.sourceSchemaLoading
               || providerStore.sourceOptionsPrimaryLoading
               || providerStore.sourceOptionsPrimaryLoading
@@ -511,7 +543,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
             destinationSchemaLoading={providerStore.destinationSchemaLoading
             destinationSchemaLoading={providerStore.destinationSchemaLoading
               || providerStore.destinationOptionsPrimaryLoading
               || providerStore.destinationOptionsPrimaryLoading
               || providerStore.destinationOptionsSecondaryLoading}
               || providerStore.destinationOptionsSecondaryLoading}
-            executionsLoading={replicaStore.executionsLoading}
+            executionsLoading={replicaStore.startingExecution}
             page={this.props.match.params.page || ''}
             page={this.props.match.params.page || ''}
             onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
             onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
@@ -558,7 +590,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         />
         />
         {this.state.showDeleteReplicaConfirmation ? (
         {this.state.showDeleteReplicaConfirmation ? (
           <DeleteReplicaModal
           <DeleteReplicaModal
-            hasDisks={replicaStore.hasReplicaDisks(replicaStore.replicaDetails)}
+            hasDisks={replicaStore.hasReplicaDisks(this.replica)}
             onRequestClose={() => this.handleCloseDeleteReplicaConfirmation()}
             onRequestClose={() => this.handleCloseDeleteReplicaConfirmation()}
             onDeleteReplica={() => { this.handleDeleteReplicaConfirmation() }}
             onDeleteReplica={() => { this.handleDeleteReplicaConfirmation() }}
             onDeleteDisks={() => { this.handleDeleteReplicaDisksConfirmation() }}
             onDeleteDisks={() => { this.handleDeleteReplicaDisksConfirmation() }}

+ 4 - 25
src/sources/ReplicaSource.js

@@ -85,8 +85,8 @@ class ReplicaSourceUtils {
     }
     }
 
 
     replicas.sort((a, b) => {
     replicas.sort((a, b) => {
-      ReplicaSourceUtils.sortExecutions(a.executions)
-      ReplicaSourceUtils.sortExecutions(b.executions)
+      ReplicaSourceUtils.sortExecutionsAndTasks(a.executions)
+      ReplicaSourceUtils.sortExecutionsAndTasks(b.executions)
       let aLastExecution = a.executions && a.executions.length ? a.executions[a.executions.length - 1] : null
       let aLastExecution = a.executions && a.executions.length ? a.executions[a.executions.length - 1] : null
       let bLastExecution = b.executions && b.executions.length ? b.executions[b.executions.length - 1] : null
       let bLastExecution = b.executions && b.executions.length ? b.executions[b.executions.length - 1] : null
       let aLastTime = aLastExecution ? aLastExecution.updated_at || aLastExecution.created_at : null
       let aLastTime = aLastExecution ? aLastExecution.updated_at || aLastExecution.created_at : null
@@ -119,10 +119,11 @@ class ReplicaSourceUtils {
 }
 }
 
 
 class ReplicaSource {
 class ReplicaSource {
-  async getReplicas(skipLog?: boolean): Promise<MainItem[]> {
+  async getReplicas(skipLog?: boolean, quietError?: boolean): Promise<MainItem[]> {
     let response = await Api.send({
     let response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/detail`,
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/detail`,
       skipLog,
       skipLog,
+      quietError,
     })
     })
     let replicas = response.data.replicas
     let replicas = response.data.replicas
     replicas = ReplicaSourceUtils.filterDeletedExecutionsInReplicas(replicas)
     replicas = ReplicaSourceUtils.filterDeletedExecutionsInReplicas(replicas)
@@ -130,28 +131,6 @@ class ReplicaSource {
     return replicas
     return replicas
   }
   }
 
 
-  async getReplicaExecutions(replicaId: string, skipLog?: boolean): Promise<Execution[]> {
-    let response = await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/detail`,
-      skipLog,
-    })
-    let executions = response.data.executions
-    ReplicaSourceUtils.sortExecutionsAndTasks(executions)
-
-    return executions
-  }
-
-  async getReplica(replicaId: string, skipLog?: boolean): Promise<MainItem> {
-    let response = await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}`,
-      skipLog,
-    })
-    let replica = response.data.replica
-    replica.executions = ReplicaSourceUtils.filterDeletedExecutions(replica.executions)
-    ReplicaSourceUtils.sortExecutions(replica.executions)
-    return replica
-  }
-
   async execute(replicaId: string, fields?: Field[]): Promise<Execution> {
   async execute(replicaId: string, fields?: Field[]): Promise<Execution> {
     let payload = { execution: { shutdown_instances: false } }
     let payload = { execution: { shutdown_instances: false } }
     if (fields) {
     if (fields) {

+ 40 - 69
src/stores/ReplicaStore.js

@@ -15,6 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 // @flow
 
 
 import { observable, action, runInAction } from 'mobx'
 import { observable, action, runInAction } from 'mobx'
+import moment from 'moment'
 
 
 import notificationStore from '../stores/NotificationStore'
 import notificationStore from '../stores/NotificationStore'
 import ReplicaSource from '../sources/ReplicaSource'
 import ReplicaSource from '../sources/ReplicaSource'
@@ -39,17 +40,37 @@ class ReplicaStoreUtils {
   }
   }
 }
 }
 
 
+let checkAddExecution = (replicas: MainItem[], addExecution: ?{ replicaId: string, execution: Execution }) => {
+  let usableAddExecution = addExecution
+  if (!usableAddExecution) {
+    return
+  }
+  let executionTime = moment.utc(usableAddExecution.execution.created_at).local().toDate().getTime()
+  if (new Date().getTime() - executionTime > 5000) {
+    return
+  }
+  let replica = replicas.find(r => r.id === usableAddExecution.replicaId)
+  if (!replica) {
+    return
+  }
+  let execution = replica.executions.find(e => e.id === usableAddExecution.execution.id)
+  if (execution) {
+    return
+  }
+  replica.executions.push(usableAddExecution.execution)
+}
+
 class ReplicaStore {
 class ReplicaStore {
   @observable replicas: MainItem[] = []
   @observable replicas: MainItem[] = []
-  @observable replicaDetails: ?MainItem = null
-  @observable loading: boolean = true
+  @observable loading: boolean = false
   @observable backgroundLoading: boolean = false
   @observable backgroundLoading: boolean = false
-  @observable detailsLoading: boolean = true
-  @observable executionsLoading: boolean = false
+  @observable startingExecution: boolean = false
 
 
   replicasLoaded: boolean = false
   replicasLoaded: boolean = false
 
 
-  @action async getReplicas(options?: { showLoading?: boolean, skipLog?: boolean }): Promise<void> {
+  addExecution: ?{ replicaId: string, execution: Execution } = null
+
+  @action async getReplicas(options?: { showLoading?: boolean, skipLog?: boolean, quietError?: boolean }): Promise<void> {
     this.backgroundLoading = true
     this.backgroundLoading = true
 
 
     if ((options && options.showLoading) || !this.replicasLoaded) {
     if ((options && options.showLoading) || !this.replicasLoaded) {
@@ -57,7 +78,8 @@ class ReplicaStore {
     }
     }
 
 
     try {
     try {
-      let replicas = await ReplicaSource.getReplicas(options && options.skipLog)
+      let replicas = await ReplicaSource.getReplicas(options && options.skipLog, options && options.quietError)
+      checkAddExecution(replicas, this.addExecution)
       this.getReplicasSuccess(replicas)
       this.getReplicasSuccess(replicas)
     } finally {
     } finally {
       this.getReplicasDone()
       this.getReplicasDone()
@@ -74,62 +96,24 @@ class ReplicaStore {
     this.backgroundLoading = false
     this.backgroundLoading = false
   }
   }
 
 
-  @action async getReplicaExecutions(replicaId: string, options?: { showLoading?: boolean, skipLog?: boolean }): Promise<void> {
-    if (options && options.showLoading) this.executionsLoading = true
-
-    try {
-      let executions = await ReplicaSource.getReplicaExecutions(replicaId, options && options.skipLog)
-      this.getReplicaExecutionsSuccess(replicaId, executions)
-    } finally {
-      runInAction(() => { this.executionsLoading = false })
-    }
-  }
-
-  @action getReplicaExecutionsSuccess(replicaId: string, executions: Execution[]) {
-    let replica = this.replicas.find(replica => replica.id === replicaId)
-
-    if (replica) {
-      replica.executions = executions
-    }
-
-    if (this.replicaDetails && this.replicaDetails.id === replicaId) {
-      this.replicaDetails = {
-        ...this.replicaDetails,
-        executions,
-      }
-    }
-  }
-
-  @action async getReplica(replicaId: string, options?: { showLoading?: boolean, skipLog?: boolean }): Promise<void> {
-    this.detailsLoading = Boolean(options && options.showLoading)
-
-    try {
-      let replica = await ReplicaSource.getReplica(replicaId, options && options.skipLog)
-      runInAction(() => {
-        this.replicaDetails = replica
-        this.replicas = this.replicas.map(r => r.id === replica.id ? replica : r)
-      })
-    } finally {
-      runInAction(() => { this.detailsLoading = false })
-    }
-  }
-
   @action async execute(replicaId: string, fields?: Field[]): Promise<void> {
   @action async execute(replicaId: string, fields?: Field[]): Promise<void> {
+    let replica = this.replicas.find(r => r.id === replicaId)
+    if (replica && replica.executions && replica.executions.length === 0) {
+      this.startingExecution = true
+    }
     let execution = await ReplicaSource.execute(replicaId, fields)
     let execution = await ReplicaSource.execute(replicaId, fields)
     this.executeSuccess(replicaId, execution)
     this.executeSuccess(replicaId, execution)
   }
   }
 
 
   @action executeSuccess(replicaId: string, execution: Execution) {
   @action executeSuccess(replicaId: string, execution: Execution) {
-    if (this.replicaDetails && this.replicaDetails.id === replicaId) {
-      this.replicaDetails = ReplicaStoreUtils.getNewReplica(this.replicaDetails, execution)
-    }
-
-    let replicasItemIndex = this.replicas ? this.replicas.findIndex(r => r.id === replicaId) : -1
+    this.addExecution = { replicaId, execution }
+    let replicasItemIndex = this.replicas.findIndex(r => r.id === replicaId)
 
 
     if (replicasItemIndex > -1) {
     if (replicasItemIndex > -1) {
       const updatedReplica = ReplicaStoreUtils.getNewReplica(this.replicas[replicasItemIndex], execution)
       const updatedReplica = ReplicaStoreUtils.getNewReplica(this.replicas[replicasItemIndex], execution)
       this.replicas[replicasItemIndex] = updatedReplica
       this.replicas[replicasItemIndex] = updatedReplica
     }
     }
+    this.startingExecution = false
   }
   }
 
 
   async cancelExecution(replicaId: string, executionId: string, force: ?boolean): Promise<void> {
   async cancelExecution(replicaId: string, executionId: string, force: ?boolean): Promise<void> {
@@ -149,15 +133,11 @@ class ReplicaStore {
   @action deleteExecutionSuccess(replicaId: string, executionId: string) {
   @action deleteExecutionSuccess(replicaId: string, executionId: string) {
     let executions = []
     let executions = []
 
 
-    if (this.replicaDetails && this.replicaDetails.id === replicaId) {
-      if (this.replicaDetails.executions) {
-        executions = [...this.replicaDetails.executions.filter(e => e.id !== executionId)]
-      }
+    let replicasItemIndex = this.replicas ? this.replicas.findIndex(r => r.id === replicaId) : -1
 
 
-      this.replicaDetails = {
-        ...this.replicaDetails,
-        executions,
-      }
+    if (replicasItemIndex > -1) {
+      executions = [...this.replicas[replicasItemIndex].executions.filter(e => e.id !== executionId)]
+      this.replicas[replicasItemIndex].executions = executions
     }
     }
   }
   }
 
 
@@ -172,11 +152,7 @@ class ReplicaStore {
   }
   }
 
 
   @action deleteDisksSuccess(replicaId: string, execution: Execution) {
   @action deleteDisksSuccess(replicaId: string, execution: Execution) {
-    if (this.replicaDetails && this.replicaDetails.id === replicaId) {
-      this.replicaDetails = ReplicaStoreUtils.getNewReplica(this.replicaDetails, execution)
-    }
-
-    let replicasItemIndex = this.replicas ? this.replicas.findIndex(r => r.id === replicaId) : -1
+    let replicasItemIndex = this.replicas.findIndex(r => r.id === replicaId)
 
 
     if (replicasItemIndex > -1) {
     if (replicasItemIndex > -1) {
       const updatedReplica = ReplicaStoreUtils.getNewReplica(this.replicas[replicasItemIndex], execution)
       const updatedReplica = ReplicaStoreUtils.getNewReplica(this.replicas[replicasItemIndex], execution)
@@ -184,11 +160,6 @@ class ReplicaStore {
     }
     }
   }
   }
 
 
-  @action clearDetails() {
-    this.detailsLoading = true
-    this.replicaDetails = null
-  }
-
   async update(replica: MainItem, destinationEndpoint: Endpoint, updateData: UpdateData, defaultStorage: ?string, storageConfigDefault: string) {
   async update(replica: MainItem, destinationEndpoint: Endpoint, updateData: UpdateData, defaultStorage: ?string, storageConfigDefault: string) {
     await ReplicaSource.update(replica, destinationEndpoint, updateData, defaultStorage, storageConfigDefault)
     await ReplicaSource.update(replica, destinationEndpoint, updateData, defaultStorage, storageConfigDefault)
   }
   }