Jelajahi Sumber

Add ability to remove the uploaded user scripts

Removing the uploaded scripts translates to sending `null` value to the
server where the script content string would be.

This applies to editing the user scripts when editing a replica,
creating and recreating a migration, if a script was previously added.
Sergiu Miclea 5 tahun lalu
induk
melakukan
53ec81233e

+ 2 - 2
src/@types/Instance.ts

@@ -54,8 +54,8 @@ export type InstanceBase = {
 export type InstanceScript = {
   global?: 'windows' | 'linux' | null,
   instanceId?: string | null,
-  scriptContent: string,
-  fileName: string,
+  scriptContent: string | null,
+  fileName: string | null,
 }
 
 export const shortenId = (id: string) => id.replace(/(^.*?)-.*-(.*$)/, '$1-...-$2')

+ 1 - 0
src/@types/MainItem.ts

@@ -34,6 +34,7 @@ export type UpdateData = {
   network: NetworkMap[],
   storage: StorageMap[],
   uploadedScripts: InstanceScript[],
+  removedScripts: InstanceScript[],
 }
 type NetworkMapSecurityGroups = { id: string, security_groups?: string[] }
 type NetworkMapSourceDest = {

+ 14 - 0
src/components/organisms/EditReplica/EditReplica.tsx

@@ -110,6 +110,7 @@ type State = {
   sourceFailed: boolean,
   destinationFailedMessage: string | null,
   uploadedScripts: InstanceScript[],
+  removedScripts: InstanceScript[],
 }
 
 @observer
@@ -125,6 +126,7 @@ class EditReplica extends React.Component<Props, State> {
     uploadedScripts: [],
     sourceFailed: false,
     destinationFailedMessage: null,
+    removedScripts: [],
   }
 
   scrollableRef: HTMLElement | null | undefined
@@ -469,6 +471,7 @@ class EditReplica extends React.Component<Props, State> {
       network: this.state.selectedNetworks.length > 0 ? this.getSelectedNetworks() : [],
       storage: this.state.storageMap,
       uploadedScripts: this.state.uploadedScripts,
+      removedScripts: this.state.removedScripts,
     }
     if (this.props.type === 'replica') {
       try {
@@ -534,6 +537,15 @@ class EditReplica extends React.Component<Props, State> {
     }))
   }
 
+  handleScriptDataRemove(script: InstanceScript) {
+    this.setState(prevState => ({
+      removedScripts: [
+        ...prevState.removedScripts,
+        script,
+      ],
+    }))
+  }
+
   handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
     this.setState(prevState => {
       const diskFieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
@@ -658,8 +670,10 @@ class EditReplica extends React.Component<Props, State> {
         instances={this.props.instancesDetails}
         loadingInstances={this.props.instancesDetailsLoading}
         onScriptUpload={s => { this.handleScriptUpload(s) }}
+        onScriptDataRemove={s => { this.handleScriptDataRemove(s) }}
         onCancelScript={(g, i) => { this.handleCancelScript(g, i) }}
         uploadedScripts={this.state.uploadedScripts}
+        removedScripts={this.state.removedScripts}
         userScriptData={this.props.replica?.user_scripts}
         scrollableRef={(r: HTMLElement) => { this.scrollableRef = r }}
         style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}

+ 15 - 0
src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx

@@ -87,6 +87,7 @@ type Props = {
   onMigrateClick: (
     fields: Field[],
     uploadedScripts: InstanceScript[],
+    removedScripts: InstanceScript[],
     minionPoolMappings: { [instance: string]: string }
   ) => void,
   onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
@@ -95,6 +96,7 @@ type State = {
   fields: Field[],
   selectedBarButton: string,
   uploadedScripts: InstanceScript[],
+  removedScripts: InstanceScript[],
   minionPoolMappings: {[instance: string]: string}
 }
 
@@ -104,6 +106,7 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
     fields: [],
     selectedBarButton: 'options',
     uploadedScripts: [],
+    removedScripts: [],
     minionPoolMappings: {},
   }
 
@@ -140,6 +143,7 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
     this.props.onMigrateClick(
       this.state.fields,
       this.state.uploadedScripts,
+      this.state.removedScripts,
       this.state.minionPoolMappings,
     )
   }
@@ -174,6 +178,15 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
     }))
   }
 
+  handleScriptRemove(script: InstanceScript) {
+    this.setState(prevState => ({
+      removedScripts: [
+        ...prevState.removedScripts,
+        script,
+      ],
+    }))
+  }
+
   renderField(field: Field) {
     return (
       <FieldInputStyled
@@ -249,8 +262,10 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
         instances={this.props.instances}
         loadingInstances={this.props.loadingInstances}
         onScriptUpload={s => { this.handleScriptUpload(s) }}
+        onScriptDataRemove={s => { this.handleScriptRemove(s) }}
         onCancelScript={(g, i) => { this.handleCanceScript(g, i) }}
         uploadedScripts={this.state.uploadedScripts}
+        removedScripts={this.state.removedScripts}
         userScriptData={this.props.transferItem?.user_scripts}
         scrollableRef={(r: HTMLElement) => { this.scrollableRef = r }}
         layout="modal"

+ 2 - 0
src/components/organisms/WizardPageContent/WizardPageContent.tsx

@@ -509,6 +509,8 @@ class WizardPageContent extends React.Component<Props, State> {
             onCancelScript={this.props.onCancelUploadedScript}
             uploadedScripts={this.props.uploadedUserScripts}
             userScriptData={null}
+            removedScripts={[]}
+            onScriptDataRemove={() => {}}
           />
         )
         break

+ 42 - 9
src/components/organisms/WizardScripts/WizardScripts.tsx

@@ -14,7 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import styled, { css } from 'styled-components'
 
 import InfoIcon from '../../atoms/InfoIcon'
 import { Close as InputClose } from '../../atoms/TextInput'
@@ -127,16 +127,28 @@ const FakeFileInput = styled.input`
   opacity: 0;
   top: -99999px;
 `
-const DownloadScriptData = styled.div`
-  color: ${Palette.primary};
+const ScriptDataActions = styled.div`
+  display: flex;
+  margin-left: -8px;
+  margin-top: 8px;
+  > div {
+    margin-left: 8px;
+  }
+`
+const ScriptDataAction = styled.div<{ red?: boolean, disabled?: boolean}>`
+  color: ${props => (props.red ? Palette.alert : Palette.primary)};
   cursor: pointer;
+  ${props => (props.disabled ? css`
+    opacity: 0.6;
+    cursor: default;
+  ` : '')}
   font-size: 12px;
-  margin-top: 8px;
 `
 
 type Props = {
   instances: Instance[],
   uploadedScripts: InstanceScript[],
+  removedScripts: InstanceScript[],
   layout?: 'modal' | 'page',
   loadingInstances?: boolean,
   userScriptData: UserScriptData | null | undefined
@@ -145,6 +157,7 @@ type Props = {
   onCancelScript: (global: 'windows' | 'linux' | null, instanceName: string | null) => void,
   onScrollableRef?: (ref: HTMLElement) => void,
   scrollableRef?: (r: HTMLElement) => void
+  onScriptDataRemove: (script: InstanceScript) => void
 }
 type FileInputRefs = {
   [prop: string]: {
@@ -193,6 +206,8 @@ class WizardScripts extends React.Component<Props> {
     } else if (instanceId) {
       scriptData = this.props.userScriptData?.instances?.[instanceId]
     }
+    const isRemoved: boolean = Boolean(this.props.removedScripts
+      .find(s => (global ? s.global === global : s.instanceId === instanceId)))
 
     return (
       <Script key={title}>
@@ -202,11 +217,29 @@ class WizardScripts extends React.Component<Props> {
             <NameLabelTitle>{title}</NameLabelTitle>
             {subtitle ? <NameLabelSubtitle>{subtitle}</NameLabelSubtitle> : null}
             {scriptData ? (
-              <DownloadScriptData onClick={() => {
-                this.handleScriptDataDownload(scriptData as string, title.toLowerCase().replaceAll(' ', '_'))
-              }}
-              >Download the current script
-              </DownloadScriptData>
+              <ScriptDataActions>
+                <ScriptDataAction
+                  title="Downloads the currently uploaded script"
+                  onClick={() => {
+                    this.handleScriptDataDownload(scriptData as string, title.toLowerCase().replaceAll(' ', '_'))
+                  }}
+                >Download
+                </ScriptDataAction>
+                <ScriptDataAction
+                  title={isRemoved ? 'The currently uploaded script will be removed' : 'Removes the currently uploaded script'}
+                  red
+                  disabled={isRemoved}
+                  onClick={() => {
+                    if (isRemoved) {
+                      return
+                    }
+                    this.props.onScriptDataRemove({
+                      global, instanceId, scriptContent: null, fileName: null,
+                    })
+                  }}
+                >{isRemoved ? 'To be removed' : 'Remove'}
+                </ScriptDataAction>
+              </ScriptDataActions>
             ) : null}
           </NameLabel>
         </Name>

+ 8 - 5
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx

@@ -232,7 +232,8 @@ class MigrationDetailsPage extends React.Component<Props, State> {
 
   async recreateFromReplica(
     options: Field[],
-    userScripts: InstanceScript[],
+    uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     minionPoolMappings: { [instance: string]: string },
   ) {
     const replicaId = migrationStore.migrationDetails && migrationStore.migrationDetails.replica_id
@@ -240,20 +241,22 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       return
     }
 
-    this.migrate(replicaId, options, userScripts, minionPoolMappings)
+    this.migrate(replicaId, options, uploadedUserScripts, removedUserScripts, minionPoolMappings)
     this.handleCloseFromReplicaModal()
   }
 
   async migrate(
     replicaId: string,
     options: Field[],
-    userScripts: InstanceScript[],
+    uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     minionPoolMappings: { [instance: string]: string },
   ) {
     const migration = await migrationStore.migrateReplica(
       replicaId,
       options,
-      userScripts,
+      uploadedUserScripts,
+      removedUserScripts,
       migrationStore.migrationDetails?.user_scripts,
       minionPoolMappings,
     )
@@ -433,7 +436,7 @@ Note that this may lead to scheduled cleanup tasks being forcibly skipped, and t
               transferItem={migrationStore.migrationDetails}
               minionPools={minionPoolStore.minionPools}
               onCancelClick={() => { this.handleCloseFromReplicaModal() }}
-              onMigrateClick={(o, s, m) => { this.recreateFromReplica(o, s, m) }}
+              onMigrateClick={(o, s, r, m) => { this.recreateFromReplica(o, s, r, m) }}
               instances={instanceStore.instancesDetails}
               loadingInstances={instanceStore.loadingInstancesDetails}
               defaultSkipOsMorphing={migrationStore

+ 1 - 0
src/components/pages/MigrationsPage/MigrationsPage.tsx

@@ -146,6 +146,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
           migration.replica_id,
           replicaMigrationFields,
           [],
+          [],
           migration.user_scripts,
           migration.instance_osmorphing_minion_pool_mappings || {},
         )

+ 7 - 3
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx

@@ -392,15 +392,17 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   migrateReplica(
     options: Field[],
     uploadedScripts: InstanceScript[],
+    removedScripts: InstanceScript[],
     minionPoolMappings: { [instance: string]: string },
   ) {
-    this.migrate(options, uploadedScripts, minionPoolMappings)
+    this.migrate(options, uploadedScripts, removedScripts, minionPoolMappings)
     this.handleCloseMigrationModal()
   }
 
   async migrate(
     options: Field[],
     uploadedScripts: InstanceScript[],
+    removedScripts: InstanceScript[],
     minionPoolMappings: { [instance: string]: string },
   ) {
     const replica = this.replica
@@ -411,6 +413,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       replica.id,
       options,
       uploadedScripts,
+      removedScripts,
       replica.user_scripts,
       minionPoolMappings,
     )
@@ -509,6 +512,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   render() {
+    const editTitle = providerStore.providersLoading ? 'Loading providers data' : !this.state.isEditable ? 'At least one of the providers doesn\'t support editing' : null
     const dropdownActions: DropdownAction[] = [
       {
         label: 'Execute',
@@ -532,7 +536,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       },
       {
         label: 'Edit',
-        title: !this.state.isEditable ? 'At least one of the providers doesn\'t support editing' : null,
+        title: editTitle,
         action: () => { this.handleReplicaEditClick() },
         disabled: !this.state.isEditable,
       },
@@ -644,7 +648,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               loadingInstances={instanceStore.loadingInstancesDetails}
               instances={instanceStore.instancesDetails}
               onCancelClick={() => { this.handleCloseMigrationModal() }}
-              onMigrateClick={(o, s, m) => { this.migrateReplica(o, s, m) }}
+              onMigrateClick={(o, s, r, m) => { this.migrateReplica(o, s, r, m) }}
             />
           </Modal>
         ) : null}

+ 1 - 0
src/components/pages/ReplicasPage/ReplicasPage.tsx

@@ -161,6 +161,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
         fields,
         uploadedScripts.filter(s => !s.instanceId
           || replica.instances.find(i => i === s.instanceId)),
+        [],
         replica.user_scripts,
         replica.instance_osmorphing_minion_pool_mappings || {},
       )))

+ 19 - 12
src/plugins/endpoint/default/OptionsSchemaPlugin.ts

@@ -260,23 +260,30 @@ export default class OptionsSchemaParser {
 
   static getUserScripts(
     uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     userScriptData: UserScriptData | null | undefined,
   ) {
     const payload: any = userScriptData || {}
-    const globalScripts = uploadedUserScripts.filter(s => s.global)
-    if (globalScripts.length) {
-      payload.global = payload.global || {}
-      globalScripts.forEach(script => {
-        payload.global[script.global || ''] = script.scriptContent
-      })
-    }
-    const instanceScripts = uploadedUserScripts.filter(s => s.instanceId)
-    if (instanceScripts.length) {
-      payload.instances = payload.instances || {}
-      instanceScripts.forEach(script => {
-        payload.instances[script.instanceId || ''] = script.scriptContent
+
+    const setPayload = (scripts: InstanceScript[], scriptProp: 'global' | 'instanceId', payloadProp: 'global' | 'instances') => {
+      if (!scripts.length) {
+        return
+      }
+      payload[payloadProp] = payload[payloadProp] || {}
+      scripts.forEach(script => {
+        const scriptValue = script[scriptProp]
+        if (!scriptValue) {
+          throw new Error(`The uploaded script structure is missing the '${scriptProp}' property`)
+        }
+        payload[payloadProp][scriptValue] = script.scriptContent
       })
     }
+
+    setPayload(removedUserScripts.filter(s => s.global), 'global', 'global')
+    setPayload(removedUserScripts.filter(s => s.instanceId), 'instanceId', 'instances')
+    setPayload(uploadedUserScripts.filter(s => s.global), 'global', 'global')
+    setPayload(uploadedUserScripts.filter(s => s.instanceId), 'instanceId', 'instances')
+
     return payload
   }
 }

+ 3 - 1
src/plugins/endpoint/openstack/OptionsSchemaPlugin.ts

@@ -105,8 +105,10 @@ export default class OptionsSchemaParser {
 
   static getUserScripts(
     uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     userScriptData: UserScriptData | null | undefined,
   ) {
-    return DefaultOptionsSchemaPlugin.getUserScripts(uploadedUserScripts, userScriptData)
+    return DefaultOptionsSchemaPlugin
+      .getUserScripts(uploadedUserScripts, removedUserScripts, userScriptData)
   }
 }

+ 3 - 1
src/plugins/endpoint/ovm/OptionsSchemaPlugin.ts

@@ -103,8 +103,10 @@ export default class OptionsSchemaParser {
 
   static getUserScripts(
     uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     userScriptData: UserScriptData | null | undefined,
   ) {
-    return DefaultOptionsSchemaPlugin.getUserScripts(uploadedUserScripts, userScriptData)
+    return DefaultOptionsSchemaPlugin
+      .getUserScripts(uploadedUserScripts, removedUserScripts, userScriptData)
   }
 }

+ 11 - 5
src/sources/MigrationSource.ts

@@ -135,6 +135,7 @@ class MigrationSource {
     replicationCount?: number | null,
     migration: MigrationItemDetails,
     uploadedScripts: InstanceScript[]
+    removedScripts: InstanceScript[]
   }): Promise<MigrationItemDetails> {
     const getValue = (fieldName: string): string | null => {
       const updatedDestEnv = opts.updatedDestEnv && opts.updatedDestEnv[fieldName]
@@ -227,9 +228,13 @@ class MigrationSource {
       ...updatedDestEnv,
     }
 
-    if (opts.uploadedScripts?.length || migration.user_scripts) {
+    if (opts.uploadedScripts?.length || opts.removedScripts?.length || migration.user_scripts) {
       payload.migration.user_scripts = DefaultOptionsSchemaPlugin
-        .getUserScripts(opts.uploadedScripts, migration.user_scripts)
+        .getUserScripts(
+          opts.uploadedScripts || [],
+          opts.removedScripts || [],
+          migration.user_scripts,
+        )
     }
 
     const response = await Api.send({
@@ -264,7 +269,8 @@ class MigrationSource {
   async migrateReplica(
     replicaId: string,
     options: Field[],
-    userScripts: InstanceScript[],
+    uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     userScriptData: UserScriptData | null | undefined,
     minionPoolMappings: { [instance: string]: string },
   ): Promise<MigrationItem> {
@@ -277,9 +283,9 @@ class MigrationSource {
       payload.migration[o.name] = o.value || o.default || false
     })
 
-    if (userScripts.length || userScriptData) {
+    if (uploadedUserScripts.length || removedUserScripts.length || userScriptData) {
       payload.migration.user_scripts = DefaultOptionsSchemaPlugin
-        .getUserScripts(userScripts, userScriptData)
+        .getUserScripts(uploadedUserScripts, removedUserScripts, userScriptData)
     }
 
     if (Object.keys(minionPoolMappings).length) {

+ 11 - 3
src/sources/ReplicaSource.ts

@@ -218,7 +218,11 @@ class ReplicaSource {
     storageConfigDefault: string,
   }): Promise<Execution> {
     const {
-      replica, destinationEndpoint, updateData, defaultStorage, storageConfigDefault,
+      replica,
+      destinationEndpoint,
+      updateData,
+      defaultStorage,
+      storageConfigDefault,
     } = options
 
     const parser = OptionsSchemaPlugin.for(destinationEndpoint.type)
@@ -262,9 +266,13 @@ class ReplicaSource {
         .getStorageMap(defaultStorage, updateData.storage, storageConfigDefault)
     }
 
-    if (updateData.uploadedScripts?.length) {
+    if (updateData.uploadedScripts?.length || updateData.removedScripts?.length) {
       payload.replica.user_scripts = DefaultOptionsSchemaPlugin
-        .getUserScripts(updateData.uploadedScripts, replica.user_scripts)
+        .getUserScripts(
+          updateData.uploadedScripts || [],
+          updateData.removedScripts || [],
+          replica.user_scripts,
+        )
     }
 
     const response = await Api.send({

+ 1 - 1
src/sources/WizardSource.ts

@@ -92,7 +92,7 @@ class WizardSource {
     )
 
     if (uploadedUserScripts.length) {
-      payload[type].user_scripts = destParser.getUserScripts(uploadedUserScripts, {})
+      payload[type].user_scripts = destParser.getUserScripts(uploadedUserScripts, [], {})
     }
 
     if (type === 'migration') {

+ 5 - 2
src/stores/MigrationStore.ts

@@ -90,6 +90,7 @@ class MigrationStore {
       defaultSkipOsMorphing: this.getDefaultSkipOsMorphing(migration),
       replicationCount,
       uploadedScripts: updateData.uploadedScripts,
+      removedScripts: updateData.removedScripts,
     })
     return migrationResult
   }
@@ -124,14 +125,16 @@ class MigrationStore {
   @action async migrateReplica(
     replicaId: string,
     options: Field[],
-    userScripts: InstanceScript[],
+    uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
     userScriptData: UserScriptData | null | undefined,
     minionPoolMappings: { [instance: string]: string },
   ) {
     const migration = await MigrationSource.migrateReplica(
       replicaId,
       options,
-      userScripts,
+      uploadedUserScripts,
+      removedUserScripts,
       userScriptData,
       minionPoolMappings,
     )