Pārlūkot izejas kodu

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 gadi atpakaļ
vecāks
revīzija
f176b6aa3d

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

@@ -22,7 +22,6 @@ import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 
 import errorImage from './images/error.svg'
-import progressWithBackgroundImage from './images/progress-background.svg'
 import progressImage from './images/progress.js'
 import successImage from './images/success.svg'
 import warningImage from './images/warning.js'
@@ -37,17 +36,13 @@ type Props = {
   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) => {
-  if (props.useBackground) {
-    return css`url('${progressWithBackgroundImage}')`
-  }
-
   const smallCircleColor = props.secondary ? Palette.grayscale[0] : Palette.primary
-  return getSpinnerUrl(smallCircleColor)
+  return getSpinnerUrl(smallCircleColor, props.useBackground)
 }
 
 const getWarningUrl = (background: string) => {
@@ -69,7 +64,7 @@ const statuses = (status, props) => {
     case 'CANCELLING':
     case 'CANCELLING_AFTER_COMPLETION':
       return css`
-        background-image: ${getSpinnerUrl(Palette.warning)};
+        background-image: ${getSpinnerUrl(Palette.warning, props.useBackground)};
         ${StyleProps.animations.rotation}
       `
     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/>.
 */
 
-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">
     <g>
       <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" />
+      ${useWhiteBackground ? '<circle fill="white" cx="8" cy="8" r="6"></circle>' : ''}
     </g>
   </svg>
 `

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

@@ -62,3 +62,9 @@ storiesOf('StatusIcon', module)
   .add('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) {
       this.props.onModalClose()
     }
-    this.setState({ showChooseProviderModal: false })
+    this.setState({ showChooseProviderModal: false }, () => { this.pollData() })
   }
 
   handleProviderClick(providerType: string) {
@@ -213,17 +213,22 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
       this.props.onModalClose()
     }
-    this.setState({ showEndpointModal: false })
+    this.setState({ showEndpointModal: false }, () => { this.pollData() })
   }
 
   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) {
     await userStore.switchProject(project.id)
     projectStore.getProjects()
-    notificationStore.loadData()
+    notificationStore.loadData(true)
 
     if (this.props.onProjectChange) {
       this.props.onProjectChange(project)
@@ -234,7 +239,7 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
       this.props.onModalClose()
     }
-    this.setState({ showUserModal: false })
+    this.setState({ showUserModal: false }, () => { this.pollData() })
   }
 
   async handleUserUpdateClick(user: User) {
@@ -242,14 +247,14 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
       this.props.onModalClose()
     }
-    this.setState({ showUserModal: false })
+    this.setState({ showUserModal: false }, () => { this.pollData() })
   }
 
   handleProjectModalClose() {
     if (this.props.onModalClose) {
       this.props.onModalClose()
     }
-    this.setState({ showProjectModal: false })
+    this.setState({ showProjectModal: false }, () => { this.pollData() })
   }
 
   async handleProjectModalUpdateClick(project: Project) {
@@ -257,7 +262,7 @@ class PageHeader extends React.Component<Props, State> {
     if (this.props.onModalClose) {
       this.props.onModalClose()
     }
-    this.setState({ showProjectModal: false })
+    this.setState({ showProjectModal: false }, () => { this.pollData() })
   }
 
   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 notificationStore from '../../../stores/NotificationStore'
 import providerStore from '../../../stores/ProviderStore'
+
 import configLoader from '../../../utils/Config'
 import utils from '../../../utils/ObjectUtils'
 import { providerTypes } from '../../../constants'
@@ -55,7 +56,7 @@ import Palette from '../../styleUtils/Palette'
 const Wrapper = styled.div``
 
 type Props = {
-  match: any,
+  match: { params: { id: string, page: ?string } },
   history: any,
 }
 type State = {
@@ -89,18 +90,29 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
   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() {
     document.title = 'Replica Details'
 
     let loadReplica = async () => {
       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
       }
-      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) {
         return
       }
@@ -115,7 +127,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         })
         let getOptionsValuesConfig = {
           optionsType,
-          endpointId: optionsType === 'source' ? details.origin_endpoint_id : details.destination_endpoint_id,
+          endpointId: optionsType === 'source' ? replica.origin_endpoint_id : replica.destination_endpoint_id,
           providerName,
           useCache: true,
           quietError: true,
@@ -127,7 +139,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         await providerStore.getOptionsValues(getOptionsValuesConfig)
         await providerStore.getOptionsValues({
           ...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()
 
-    scheduleStore.getSchedules(this.props.match.params.id)
+    scheduleStore.getSchedules(this.replicaId)
     this.pollData(true)
   }
 
@@ -148,7 +160,6 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   componentWillUnmount() {
-    replicaStore.clearDetails()
     scheduleStore.clearUnsavedSchedules()
     this.stopPolling = true
   }
@@ -174,34 +185,35 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   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,
       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({
-      endpointId: details.origin_endpoint_id,
+      endpointId: replica.origin_endpoint_id,
       // $FlowIgnore
-      instancesInfo: details.instances.map(n => ({ instance_name: n })),
+      instancesInfo: replica.instances.map(n => ({ instance_name: n })),
       cache,
       quietError: false,
-      env: details.source_environment,
+      env: replica.source_environment,
       targetProvider: targetEndpoint ? targetEndpoint.type : '',
     })
+    return replica
   }
 
   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
   }
 
@@ -211,8 +223,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   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')
   }
@@ -235,10 +251,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   handleDeleteExecutionConfirmation() {
-    if (!this.state.confirmationItem) {
+    let replica = this.replica
+    if (!this.state.confirmationItem || !replica) {
       return
     }
-    replicaStore.deleteExecution(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '', this.state.confirmationItem.id)
+    replicaStore.deleteExecution(replica.id, this.state.confirmationItem.id)
     this.handleCloseExecutionConfirmation()
   }
 
@@ -266,8 +283,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
   handleDeleteReplicaConfirmation() {
     this.setState({ showDeleteReplicaConfirmation: false })
+    let replica = this.replica
+    if (!replica) {
+      return
+    }
     this.props.history.push('/replicas')
-    replicaStore.delete(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '')
+    replicaStore.delete(replica.id)
   }
 
   handleCloseDeleteReplicaConfirmation() {
@@ -276,8 +297,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
   handleDeleteReplicaDisksConfirmation() {
     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() {
@@ -297,7 +322,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   handleAddScheduleClick(schedule: Schedule) {
-    scheduleStore.addSchedule(this.props.match.params.id, schedule)
+    scheduleStore.addSchedule(this.replicaId, schedule)
   }
 
   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)
 
     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) {
     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) {
     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) {
-    if (!this.state.confirmationItem) {
+    let replica = this.replica
+    if (!this.state.confirmationItem || !replica) {
       return
     }
     replicaStore.cancelExecution(
-      replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '',
+      replica.id,
       this.state.confirmationItem.id,
       force
     )
@@ -361,8 +387,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   async migrate(options: Field[], uploadedScripts: InstanceScript[]) {
+    let replica = this.replica
+    if (!replica) {
+      return
+    }
     let migration = await migrationStore.migrateReplica(
-      replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '',
+      replica.id,
       options,
       uploadedScripts
     )
@@ -377,9 +407,13 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   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.props.history.push(`/replica/executions/${replicaStore.replicaDetails ? replicaStore.replicaDetails.id : ''}`)
+    this.props.history.push(`/replica/executions/${replica.id}`)
   }
 
   async pollData(showLoading: boolean) {
@@ -387,11 +421,8 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       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)
   }
 
@@ -402,25 +433,25 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   handleEditReplicaReload() {
-    this.loadReplicaWithInstances(this.props.match.params.id, false)
+    this.loadReplicaWithInstances(this.replicaId, false)
   }
 
   handleUpdateComplete(redirectTo: string) {
-    if (!replicaStore.replicaDetails) {
-      return
-    }
-
     this.props.history.push(redirectTo)
     this.closeEditModal()
   }
 
   renderEditReplica() {
+    let replica = this.replica
+    if (!replica) {
+      return null
+    }
     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
-      .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
     }
 
@@ -431,7 +462,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         sourceEndpoint={sourceEndpoint}
         onUpdateComplete={url => { this.handleUpdateComplete(url) }}
         onRequestClose={() => { this.closeEditModal() }}
-        replica={replicaStore.replicaDetails}
+        replica={replica}
         destinationEndpoint={destinationEndpoint}
         instancesDetails={instanceStore.instancesDetails}
         instancesDetailsLoading={instanceStore.loadingInstancesDetails}
@@ -480,6 +511,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         action: () => { this.handleDeleteReplicaClick() },
       },
     ]
+    let replica = this.replica
 
     return (
       <Wrapper>
@@ -489,20 +521,20 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           contentHeaderComponent={<DetailsContentHeader
-            item={replicaStore.replicaDetails}
+            item={replica}
             dropdownActions={dropdownActions}
             backLink="/replicas"
             typeImage={replicaImage}
             alertInfoPill
           />}
           contentComponent={<ReplicaDetailsContent
-            item={replicaStore.replicaDetails}
+            item={replica}
             instancesDetails={instanceStore.instancesDetails}
             instancesDetailsLoading={instanceStore.loadingInstancesDetails}
             endpoints={endpointStore.endpoints}
             scheduleStore={scheduleStore}
             networks={networkStore.networks}
-            detailsLoading={replicaStore.detailsLoading || endpointStore.loading}
+            detailsLoading={replicaStore.loading || endpointStore.loading}
             sourceSchema={providerStore.sourceSchema}
             sourceSchemaLoading={providerStore.sourceSchemaLoading
               || providerStore.sourceOptionsPrimaryLoading
@@ -511,7 +543,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
             destinationSchemaLoading={providerStore.destinationSchemaLoading
               || providerStore.destinationOptionsPrimaryLoading
               || providerStore.destinationOptionsSecondaryLoading}
-            executionsLoading={replicaStore.executionsLoading}
+            executionsLoading={replicaStore.startingExecution}
             page={this.props.match.params.page || ''}
             onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
@@ -558,7 +590,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         />
         {this.state.showDeleteReplicaConfirmation ? (
           <DeleteReplicaModal
-            hasDisks={replicaStore.hasReplicaDisks(replicaStore.replicaDetails)}
+            hasDisks={replicaStore.hasReplicaDisks(this.replica)}
             onRequestClose={() => this.handleCloseDeleteReplicaConfirmation()}
             onDeleteReplica={() => { this.handleDeleteReplicaConfirmation() }}
             onDeleteDisks={() => { this.handleDeleteReplicaDisksConfirmation() }}

+ 4 - 25
src/sources/ReplicaSource.js

@@ -85,8 +85,8 @@ class ReplicaSourceUtils {
     }
 
     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 bLastExecution = b.executions && b.executions.length ? b.executions[b.executions.length - 1] : null
       let aLastTime = aLastExecution ? aLastExecution.updated_at || aLastExecution.created_at : null
@@ -119,10 +119,11 @@ class ReplicaSourceUtils {
 }
 
 class ReplicaSource {
-  async getReplicas(skipLog?: boolean): Promise<MainItem[]> {
+  async getReplicas(skipLog?: boolean, quietError?: boolean): Promise<MainItem[]> {
     let response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/detail`,
       skipLog,
+      quietError,
     })
     let replicas = response.data.replicas
     replicas = ReplicaSourceUtils.filterDeletedExecutionsInReplicas(replicas)
@@ -130,28 +131,6 @@ class ReplicaSource {
     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> {
     let payload = { execution: { shutdown_instances: false } }
     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
 
 import { observable, action, runInAction } from 'mobx'
+import moment from 'moment'
 
 import notificationStore from '../stores/NotificationStore'
 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 {
   @observable replicas: MainItem[] = []
-  @observable replicaDetails: ?MainItem = null
-  @observable loading: boolean = true
+  @observable loading: boolean = false
   @observable backgroundLoading: boolean = false
-  @observable detailsLoading: boolean = true
-  @observable executionsLoading: boolean = false
+  @observable startingExecution: 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
 
     if ((options && options.showLoading) || !this.replicasLoaded) {
@@ -57,7 +78,8 @@ class ReplicaStore {
     }
 
     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)
     } finally {
       this.getReplicasDone()
@@ -74,62 +96,24 @@ class ReplicaStore {
     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> {
+    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)
     this.executeSuccess(replicaId, 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) {
       const updatedReplica = ReplicaStoreUtils.getNewReplica(this.replicas[replicasItemIndex], execution)
       this.replicas[replicasItemIndex] = updatedReplica
     }
+    this.startingExecution = false
   }
 
   async cancelExecution(replicaId: string, executionId: string, force: ?boolean): Promise<void> {
@@ -149,15 +133,11 @@ class ReplicaStore {
   @action deleteExecutionSuccess(replicaId: string, executionId: string) {
     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) {
-    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) {
       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) {
     await ReplicaSource.update(replica, destinationEndpoint, updateData, defaultStorage, storageConfigDefault)
   }