Просмотр исходного кода

Merge pull request #522 from smiclea/force-cancel

Add replica and migration force cancel support CORWEB-228
Nashwan Azhari 6 лет назад
Родитель
Сommit
104aec0ff4

+ 17 - 2
src/components/organisms/Executions/Executions.jsx

@@ -89,7 +89,7 @@ const NoExecutionText = styled.div`
 type Props = {
   item: ?MainItem,
   loading: boolean,
-  onCancelExecutionClick: (execution: ?Execution) => void,
+  onCancelExecutionClick: (execution: ?Execution, force?: boolean) => void,
   onDeleteExecutionClick: (execution: ?Execution) => void,
   onExecuteClick: () => void,
 }
@@ -192,6 +192,10 @@ class Executions extends React.Component<Props, State> {
     this.props.onCancelExecutionClick(this.state.selectedExecution)
   }
 
+  handleForceCancelExecutionClick() {
+    this.props.onCancelExecutionClick(this.state.selectedExecution, true)
+  }
+
   renderLoading() {
     if (!this.props.loading) {
       return null
@@ -234,7 +238,18 @@ class Executions extends React.Component<Props, State> {
           hollow
           onClick={() => { this.handleCancelExecutionClick() }}
           data-test-id="executions-cancelButton"
-        >Cancel Execution</Button>)
+        >Cancel Execution</Button>
+      )
+    }
+
+    if (this.state.selectedExecution.status === 'CANCELLING') {
+      return (
+        <Button
+          secondary
+          hollow
+          onClick={() => { this.handleForceCancelExecutionClick() }}
+        >Force Cancel Execution</Button>
+      )
     }
 
     return (

+ 1 - 1
src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.jsx

@@ -83,7 +83,7 @@ type Props = {
   page: string,
   detailsLoading: boolean,
   executionsLoading: boolean,
-  onCancelExecutionClick: (execution: ?Execution) => void,
+  onCancelExecutionClick: (execution: ?Execution, force?: boolean) => void,
   onDeleteExecutionClick: (execution: ?Execution) => void,
   onExecuteClick: () => void,
   onCreateMigrationClick: () => void,

+ 51 - 20
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.jsx

@@ -51,6 +51,7 @@ type Props = {
 type State = {
   showDeleteMigrationConfirmation: boolean,
   showCancelConfirmation: boolean,
+  showForceCancelConfirmation: boolean,
   showEditModal: boolean,
   showFromReplicaModal: boolean,
   pausePolling: boolean,
@@ -60,6 +61,7 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   state = {
     showDeleteMigrationConfirmation: false,
     showCancelConfirmation: false,
+    showForceCancelConfirmation: false,
     showEditModal: false,
     showFromReplicaModal: false,
     pausePolling: false,
@@ -167,8 +169,12 @@ class MigrationDetailsPage extends React.Component<Props, State> {
     this.setState({ showDeleteMigrationConfirmation: false })
   }
 
-  handleCancelMigrationClick() {
-    this.setState({ showCancelConfirmation: true })
+  handleCancelMigrationClick(force?: boolean) {
+    if (force) {
+      this.setState({ showForceCancelConfirmation: true })
+    } else {
+      this.setState({ showCancelConfirmation: true })
+    }
   }
 
   handleRecreateClick() {
@@ -185,16 +191,20 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   }
 
   handleCloseCancelConfirmation() {
-    this.setState({ showCancelConfirmation: false })
+    this.setState({ showCancelConfirmation: false, showForceCancelConfirmation: false })
   }
 
-  async handleCancelConfirmation() {
-    this.setState({ showCancelConfirmation: false })
+  async handleCancelConfirmation(force?: boolean) {
+    this.setState({ showCancelConfirmation: false, showForceCancelConfirmation: false })
     if (!migrationStore.migrationDetails) {
       return
     }
-    await migrationStore.cancel(migrationStore.migrationDetails.id)
-    notificationStore.alert('Canceled', 'success')
+    await migrationStore.cancel(migrationStore.migrationDetails.id, force)
+    if (force) {
+      notificationStore.alert('Force Canceled', 'success')
+    } else {
+      notificationStore.alert('Canceled', 'success')
+    }
   }
 
   async recreateFromReplica(options: Field[], userScripts: InstanceScript[]) {
@@ -267,18 +277,28 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   }
 
   render() {
-    let dropdownActions = [{
-      label: 'Cancel',
-      disabled: this.getStatus() !== 'RUNNING',
-      action: () => { this.handleCancelMigrationClick() },
-    }, {
-      label: 'Recreate Migration',
-      action: () => { this.handleRecreateClick() },
-    }, {
-      label: 'Delete Migration',
-      color: Palette.alert,
-      action: () => { this.handleDeleteMigrationClick() },
-    }]
+    let dropdownActions = [
+      {
+        label: 'Cancel',
+        disabled: this.getStatus() !== 'RUNNING',
+        hidden: this.getStatus() === 'CANCELLING',
+        action: () => { this.handleCancelMigrationClick() },
+      },
+      {
+        label: 'Force Cancel',
+        hidden: this.getStatus() !== 'CANCELLING',
+        action: () => { this.handleCancelMigrationClick(true) },
+      },
+      {
+        label: 'Recreate Migration',
+        action: () => { this.handleRecreateClick() },
+      },
+      {
+        label: 'Delete Migration',
+        color: Palette.alert,
+        action: () => { this.handleDeleteMigrationClick() },
+      },
+    ]
 
     return (
       <Wrapper>
@@ -316,7 +336,6 @@ class MigrationDetailsPage extends React.Component<Props, State> {
           onConfirmation={() => { this.handleDeleteMigrationConfirmation() }}
           onRequestClose={() => { this.handleCloseDeleteMigrationConfirmation() }}
         />
-
         <AlertModal
           isOpen={this.state.showCancelConfirmation}
           title="Cancel Migration?"
@@ -325,6 +344,18 @@ class MigrationDetailsPage extends React.Component<Props, State> {
           onConfirmation={() => { this.handleCancelConfirmation() }}
           onRequestClose={() => { this.handleCloseCancelConfirmation() }}
         />
+        <AlertModal
+          isOpen={this.state.showForceCancelConfirmation}
+          title="Force Cancel Migration?"
+          message="Are you sure you want to force cancel the migration?"
+          extraMessage={`
+The migration is currently being cancelled.
+Would you like to force its cancellation?
+Note that this may lead to scheduled cleanup tasks being forcibly skipped, and thus manual cleanup of temporary resources on the source/destination platforms may be required.`
+          }
+          onConfirmation={() => { this.handleCancelConfirmation(true) }}
+          onRequestClose={() => { this.handleCloseCancelConfirmation() }}
+        />
         {this.state.showFromReplicaModal ? (
           <Modal
             isOpen

+ 75 - 35
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.jsx

@@ -62,6 +62,7 @@ type State = {
   showMigrationModal: boolean,
   showEditModal: boolean,
   showDeleteExecutionConfirmation: boolean,
+  showForceCancelConfirmation: boolean,
   showDeleteReplicaConfirmation: boolean,
   showDeleteReplicaDisksConfirmation: boolean,
   confirmationItem: ?MainItem | ?Execution,
@@ -80,6 +81,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     showDeleteReplicaDisksConfirmation: false,
     confirmationItem: null,
     showCancelConfirmation: false,
+    showForceCancelConfirmation: false,
     isEditable: false,
     pausePolling: false,
   }
@@ -196,7 +198,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     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)
 
-    return Boolean(!originEndpoint || !targetEndpoint || this.getStatus() === 'RUNNING')
+    return Boolean(!originEndpoint || !targetEndpoint || this.getStatus() === 'RUNNING' || this.getStatus() === 'CANCELLING')
   }
 
   handleUserItemClick(item: { value: string }) {
@@ -303,24 +305,38 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     }
   }
 
-  handleCancelLastExecutionClick() {
-    this.handleCancelExecution(this.getLastExecution())
+  handleCancelLastExecutionClick(force?: boolean) {
+    this.handleCancelExecution(this.getLastExecution(), force)
   }
 
-  handleCancelExecution(confirmationItem: ?Execution) {
-    this.setState({ confirmationItem, showCancelConfirmation: true })
+  handleCancelExecution(confirmationItem: ?Execution, force: ?boolean) {
+    if (force) {
+      this.setState({ confirmationItem, showForceCancelConfirmation: true })
+    } else {
+      this.setState({ confirmationItem, showCancelConfirmation: true })
+    }
   }
 
   handleCloseCancelConfirmation() {
-    this.setState({ showCancelConfirmation: false })
+    this.setState({
+      showForceCancelConfirmation: false,
+      showCancelConfirmation: false,
+    })
   }
 
-  handleCancelConfirmation() {
+  handleCancelConfirmation(force?: boolean) {
     if (!this.state.confirmationItem) {
       return
     }
-    replicaStore.cancelExecution(replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '', this.state.confirmationItem.id)
-    this.setState({ showCancelConfirmation: false })
+    replicaStore.cancelExecution(
+      replicaStore.replicaDetails ? replicaStore.replicaDetails.id : '',
+      this.state.confirmationItem.id,
+      force
+    )
+    this.setState({
+      showForceCancelConfirmation: false,
+      showCancelConfirmation: false,
+    })
   }
 
   migrateReplica(options: Field[], uploadedScripts: InstanceScript[]) {
@@ -411,31 +427,43 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   render() {
-    let dropdownActions: DropdownAction[] = [{
-      label: 'Execute',
-      action: () => { this.handleExecuteClick() },
-      hidden: this.isExecuteDisabled(),
-    }, {
-      label: 'Cancel',
-      hidden: this.getStatus() !== 'RUNNING',
-      action: () => { this.handleCancelLastExecutionClick() },
-    }, {
-      label: 'Create Migration',
-      color: Palette.primary,
-      action: () => { this.handleCreateMigrationClick() },
-    }, {
-      label: 'Edit',
-      title: !this.state.isEditable ? 'At least one of the providers doesn\'t support editing' : null,
-      action: () => { this.handleReplicaEditClick() },
-      disabled: !this.state.isEditable,
-    }, {
-      label: 'Delete Disks',
-      action: () => { this.handleDeleteReplicaDisksClick() },
-    }, {
-      label: 'Delete Replica',
-      color: Palette.alert,
-      action: () => { this.handleDeleteReplicaClick() },
-    }]
+    let dropdownActions: DropdownAction[] = [
+      {
+        label: 'Execute',
+        action: () => { this.handleExecuteClick() },
+        hidden: this.isExecuteDisabled(),
+      },
+      {
+        label: 'Cancel',
+        hidden: this.getStatus() !== 'RUNNING',
+        action: () => { this.handleCancelLastExecutionClick() },
+      },
+      {
+        label: 'Force Cancel',
+        hidden: this.getStatus() !== 'CANCELLING',
+        action: () => { this.handleCancelLastExecutionClick(true) },
+      },
+      {
+        label: 'Create Migration',
+        color: Palette.primary,
+        action: () => { this.handleCreateMigrationClick() },
+      },
+      {
+        label: 'Edit',
+        title: !this.state.isEditable ? 'At least one of the providers doesn\'t support editing' : null,
+        action: () => { this.handleReplicaEditClick() },
+        disabled: !this.state.isEditable,
+      },
+      {
+        label: 'Delete Disks',
+        action: () => { this.handleDeleteReplicaDisksClick() },
+      },
+      {
+        label: 'Delete Replica',
+        color: Palette.alert,
+        action: () => { this.handleDeleteReplicaClick() },
+      },
+    ]
 
     return (
       <Wrapper>
@@ -465,7 +493,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               || providerStore.destinationOptionsSecondaryLoading}
             executionsLoading={replicaStore.executionsLoading}
             page={this.props.match.params.page || ''}
-            onCancelExecutionClick={execution => { this.handleCancelExecution(execution) }}
+            onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
             onExecuteClick={() => { this.handleExecuteClick() }}
             onCreateMigrationClick={() => { this.handleCreateMigrationClick() }}
@@ -532,6 +560,18 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
           onConfirmation={() => { this.handleCancelConfirmation() }}
           onRequestClose={() => { this.handleCloseCancelConfirmation() }}
         />
+        <AlertModal
+          isOpen={this.state.showForceCancelConfirmation}
+          title="Force Cancel Execution?"
+          message="Are you sure you want to force cancel the current execution?"
+          extraMessage={`
+The execution is currently being cancelled.
+Would you like to force its cancellation?
+Note that this may lead to scheduled cleanup tasks being forcibly skipped, and thus manual cleanup of temporary resources on the source/destination platforms may be required.`
+          }
+          onConfirmation={() => { this.handleCancelConfirmation(true) }}
+          onRequestClose={() => { this.handleCloseCancelConfirmation() }}
+        />
         {this.renderEditReplica()}
       </Wrapper>
     )

+ 6 - 2
src/sources/MigrationSource.js

@@ -143,11 +143,15 @@ class MigrationSource {
     return response.data.migration
   }
 
-  async cancel(migrationId: string): Promise<string> {
+  async cancel(migrationId: string, force: ?boolean): Promise<string> {
+    let data: any = { cancel: null }
+    if (force) {
+      data.cancel = { force: true }
+    }
     await Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/migrations/${migrationId}/actions`,
       method: 'POST',
-      data: { cancel: null },
+      data,
     })
     return migrationId
   }

+ 6 - 2
src/sources/ReplicaSource.js

@@ -169,11 +169,15 @@ class ReplicaSource {
     return execution
   }
 
-  async cancelExecution(replicaId: string, executionId: string): Promise<string> {
+  async cancelExecution(replicaId: string, executionId: string, force: ?boolean): Promise<string> {
+    let data: any = { cancel: null }
+    if (force) {
+      data.cancel = { force: true }
+    }
     await Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}/actions`,
       method: 'POST',
-      data: { cancel: null },
+      data,
     })
     return replicaId
   }

+ 2 - 2
src/stores/MigrationStore.js

@@ -97,8 +97,8 @@ class MigrationStore {
     }
   }
 
-  @action async cancel(migrationId: string) {
-    await MigrationSource.cancel(migrationId)
+  @action async cancel(migrationId: string, force: ?boolean) {
+    await MigrationSource.cancel(migrationId, force)
   }
 
   @action async delete(migrationId: string) {

+ 7 - 3
src/stores/ReplicaStore.js

@@ -132,9 +132,13 @@ class ReplicaStore {
     }
   }
 
-  async cancelExecution(replicaId: string, executionId: string): Promise<void> {
-    await ReplicaSource.cancelExecution(replicaId, executionId)
-    notificationStore.alert('Cancelled', 'success')
+  async cancelExecution(replicaId: string, executionId: string, force: ?boolean): Promise<void> {
+    await ReplicaSource.cancelExecution(replicaId, executionId, force)
+    if (force) {
+      notificationStore.alert('Force cancelled', 'success')
+    } else {
+      notificationStore.alert('Cancelled', 'success')
+    }
   }
 
   async deleteExecution(replicaId: string, executionId: string): Promise<void> {