Jelajahi Sumber

Update `unit tests & integration tests`

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
Mihaela Balutoiu 1 tahun lalu
induk
melakukan
642b9128ab
26 mengubah file dengan 395 tambahan dan 384 penghapusan
  1. 7 6
      cypress/e2e/dashboard/dashboard.cy.ts
  2. 4 2
      cypress/e2e/deployments/deployments-list.cy.ts
  3. 10 34
      cypress/fixtures/transfers/migrations.json
  4. 1 3
      cypress/fixtures/transfers/replica-unexecuted.json
  5. 3 9
      cypress/fixtures/transfers/replicas.json
  6. 2 0
      src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.spec.tsx
  7. 1 0
      src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.spec.tsx
  8. 9 5
      src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.tsx
  9. 1 1
      src/components/modules/TransferModule/Schedule/Schedule.tsx
  10. 7 10
      src/components/modules/TransferModule/TransferItemModal/TransferItemModal.tsx
  11. 272 266
      src/components/modules/WizardModule/WizardExecuteOptions/WizardExecuteOptions.tsx
  12. 5 5
      src/components/modules/WizardModule/WizardExecuteOptions/package.json
  13. 1 1
      src/components/modules/WizardModule/WizardOptions/WizardOptions.spec.tsx
  14. 1 6
      src/components/modules/WizardModule/WizardOptions/WizardOptions.tsx
  15. 12 7
      src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx
  16. 1 0
      src/components/modules/WizardModule/WizardSummary/WizardSummary.spec.tsx
  17. 13 5
      src/components/modules/WizardModule/WizardSummary/WizardSummary.tsx
  18. 11 2
      src/components/smart/WizardPage/WizardPage.tsx
  19. 7 5
      src/constants.ts
  20. 1 5
      src/plugins/default/OptionsSchemaPlugin.ts
  21. 2 5
      src/sources/ScheduleSource.ts
  22. 3 1
      src/sources/TransferSource.ts
  23. 4 1
      src/sources/WizardSource.ts
  24. 1 4
      src/stores/WizardStore.ts
  25. 1 1
      src/utils/LabelDictionary.ts
  26. 15 0
      tests/mocks/TransferMock.ts

+ 7 - 6
cypress/e2e/dashboard/dashboard.cy.ts

@@ -79,14 +79,15 @@ describe("Dashboard", () => {
     cy.wait(["@transfers", "@endpoints"]);
 
     cy.loadFixtures(
-      [
-        "transfers/replicas.json",
-        "endpoints/endpoints.json",
-      ],
+      ["transfers/replicas.json", "endpoints/endpoints.json"],
       results => {
         const [transfersFixture, endpointsFixture] = results;
-        const replicasCount = transfersFixture.transfers.filter(transfer => transfer.scenario === "replica").length;
-        const migrationsCount = transfersFixture.transfers.filter(transfer => transfer.scenario === "live_migration").length;
+        const replicasCount = transfersFixture.transfers.filter(
+          transfer => transfer.scenario === "replica"
+        ).length;
+        const migrationsCount = transfersFixture.transfers.filter(
+          transfer => transfer.scenario === "live_migration"
+        ).length;
 
         cy.get("div[class^='DashboardInfoCount__CountBlock']").should(
           "contain.text",

+ 4 - 2
cypress/e2e/deployments/deployments-list.cy.ts

@@ -121,8 +121,10 @@ describe("Deployments list", () => {
       );
 
       cy.intercept("POST", routeSelectors.DEPLOYMENTS, req => {
-        expect(req.body.deployment.transfer_id, 
-          "Transfer ID should be present in the request body").to.exist;
+        expect(
+          req.body.deployment.transfer_id,
+          "Transfer ID should be present in the request body"
+        ).to.exist;
       }).as("deployments-recreate");
 
       cy.get("button").contains("Yes").click();

+ 10 - 34
cypress/fixtures/transfers/migrations.json

@@ -33,9 +33,7 @@
       },
       "type": "deployment",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": null,
       "notes": "",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -113,9 +111,7 @@
       },
       "type": "deployment",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-uefi"
-      ],
+      "instances": ["Datacenter/ol88-uefi"],
       "reservation_id": null,
       "notes": null,
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -154,10 +150,7 @@
                 "id": "eth0",
                 "name": "eth0",
                 "mac_address": "00:x:x:x:x:b0",
-                "ip_addresses": [
-                  "10.x.x.6",
-                  "fd42:x:x:x:x:x:x:c1b0"
-                ],
+                "ip_addresses": ["10.x.x.6", "fd42:x:x:x:x:x:x:c1b0"],
                 "network_name": "tapff00339e",
                 "network_id": "tapff00339e"
               }
@@ -238,9 +231,7 @@
       },
       "type": "deployment",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": null,
       "notes": "",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -318,9 +309,7 @@
       },
       "type": "deployment",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": null,
       "notes": "",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -359,10 +348,7 @@
                 "id": "eth0",
                 "name": "eth0",
                 "mac_address": "00:x:x:x:x:b0",
-                "ip_addresses": [
-                  "10.x.x.6",
-                  "fd42:x:x:x:x:x:x:c1b0"
-                ],
+                "ip_addresses": ["10.x.x.6", "fd42:x:x:x:x:x:x:c1b0"],
                 "network_name": "tapff00339e",
                 "network_id": "tapff00339e"
               }
@@ -443,9 +429,7 @@
       },
       "type": "deployment",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": null,
       "notes": "",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -484,10 +468,7 @@
                 "id": "eth0",
                 "name": "eth0",
                 "mac_address": "00:x:x:x:x:b0",
-                "ip_addresses": [
-                  "10.x.x.6",
-                  "fd42:x:x:x:x:x:x:c1b0"
-                ],
+                "ip_addresses": ["10.x.x.6", "fd42:x:x:x:x:x:x:c1b0"],
                 "network_name": "tapff00339e",
                 "network_id": "tapff00339e"
               }
@@ -568,9 +549,7 @@
       },
       "type": "deployment",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": null,
       "notes": "",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -609,10 +588,7 @@
                 "id": "eth0",
                 "name": "eth0",
                 "mac_address": "00:x:x:x:x:f4",
-                "ip_addresses": [
-                  "10.x.x.x",
-                  "fd42:x:x:x:x:x:x:79f4"
-                ],
+                "ip_addresses": ["10.x.x.x", "fd42:x:x:x:x:x:x:79f4"],
                 "network_name": "tapx10",
                 "network_id": "tapx10"
               }

+ 1 - 3
cypress/fixtures/transfers/replica-unexecuted.json

@@ -18,9 +18,7 @@
     },
     "type": "transfer",
     "executions": [],
-    "instances": [
-      "Datacenter/dvincze-ol9-bios"
-    ],
+    "instances": ["Datacenter/dvincze-ol9-bios"],
     "reservation_id": "3e147d9c-7259-48a6-bf73-db399e972bae",
     "notes": "dvincze-ol9-bios",
     "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",

+ 3 - 9
cypress/fixtures/transfers/replicas.json

@@ -19,9 +19,7 @@
       },
       "type": "transfer",
       "executions": [],
-      "instances": [
-        "Datacenter/dvincze-ol9-bios"
-      ],
+      "instances": ["Datacenter/dvincze-ol9-bios"],
       "reservation_id": "3e147d9c-7259-48a6-bf73-db399e972bae",
       "notes": "dvincze-ol9-bios",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -82,9 +80,7 @@
       },
       "type": "transfer",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": "c45dc2fc-a222-4f0b-b8bd-b527ac32c695",
       "notes": "",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
@@ -160,9 +156,7 @@
       },
       "type": "transfer",
       "executions": [],
-      "instances": [
-        "Datacenter/ol88-bios"
-      ],
+      "instances": ["Datacenter/ol88-bios"],
       "reservation_id": "09ee12d0-f0d0-4603-949e-6312f17430a8",
       "notes": "ol88-bios",
       "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",

+ 2 - 0
src/components/modules/DashboardModule/DashboardExecutions/DashboardExecutions.spec.tsx

@@ -41,6 +41,8 @@ const transferItem = (scenario: string, date: string): TransferItem => {
     transfer_result: null,
     last_execution_status: "",
     user_id: "",
+    clone_disks: false,
+    skip_os_morphing: false,
     scenario,
   };
 };

+ 1 - 0
src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.spec.tsx

@@ -53,6 +53,7 @@ describe("DeploymentDetailsContent", () => {
       endpoints: [OPENSTACK_ENDPOINT_MOCK, VMWARE_ENDPOINT_MOCK],
       page: "",
       onDeleteDeploymentClick: jest.fn(),
+      onShowExecutionClick: jest.fn(),
     };
   });
 

+ 9 - 5
src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.tsx

@@ -168,7 +168,9 @@ class DeploymentDetailsContent extends React.Component<Props> {
           <NoPendingDeploymentTitle>
             Current deployment is waiting for its deployer execution to finish.
           </NoPendingDeploymentTitle>
-          <StyledButton onClick={this.props.onShowExecutionClick}>Show Execution</StyledButton>
+          <StyledButton onClick={this.props.onShowExecutionClick}>
+            Show Execution
+          </StyledButton>
         </PendingMessage>
       );
     }
@@ -180,7 +182,9 @@ class DeploymentDetailsContent extends React.Component<Props> {
           <ErrorMessage>
             The deployer execution was not able to complete.
           </ErrorMessage>
-          <StyledButton onClick={this.props.onShowExecutionClick}>Show Execution</StyledButton>
+          <StyledButton onClick={this.props.onShowExecutionClick}>
+            Show Execution
+          </StyledButton>
         </PendingMessage>
       );
     }
@@ -200,9 +204,9 @@ class DeploymentDetailsContent extends React.Component<Props> {
         <DetailsBody>
           {this.renderMainDetails()}
           {this.renderTasks()}
-          {this.props.page === "tasks" && !this.props.item?.tasks ? (
-            this.renderPendingMessage()
-          ) : null}
+          {this.props.page === "tasks" && !this.props.item?.tasks
+            ? this.renderPendingMessage()
+            : null}
         </DetailsBody>
       </Wrapper>
     );

+ 1 - 1
src/components/modules/TransferModule/Schedule/Schedule.tsx

@@ -182,7 +182,7 @@ class Schedule extends React.Component<Props, State> {
     }
     const options: any = {};
     fields.forEach(f => {
-      options[f.name] = f.value || execOptions[f.name] || false;
+      options[f.name] = f.value || execOptions![f.name] || false;
     });
 
     if (this.state.selectedSchedule && this.state.selectedSchedule.id) {

+ 7 - 10
src/components/modules/TransferModule/TransferItemModal/TransferItemModal.tsx

@@ -35,6 +35,8 @@ import WizardOptions, {
   INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS,
 } from "@src/components/modules/WizardModule/WizardOptions";
 import WizardStorage from "@src/components/modules/WizardModule/WizardStorage";
+import WizardScripts from "@src/components/modules/WizardModule/WizardScripts";
+import WizardExecuteOptions from "@src/components/modules/WizardModule/WizardExecuteOptions";
 
 import type { UpdateData, ActionItemDetails } from "@src/@types/MainItem";
 import {
@@ -56,11 +58,9 @@ import { deploymentFields, providerTypes } from "@src/constants";
 import configLoader from "@src/utils/Config";
 import LoadingButton from "@src/components/ui/LoadingButton";
 import minionPoolStore from "@src/stores/MinionPoolStore";
-import WizardScripts from "@src/components/modules/WizardModule/WizardScripts";
 import networkStore from "@src/stores/NetworkStore";
 import { ThemeProps } from "@src/components/Theme";
 import ObjectUtils from "@src/utils/ObjectUtils";
-import WizardExecuteOptions from "@src/components/modules/WizardModule/WizardExecuteOptions/WizardExecuteOptions";
 
 const PanelContent = styled.div<any>`
   display: flex;
@@ -707,7 +707,7 @@ class TransferItemModal extends React.Component<Props, State> {
   handleDeployFieldChange(field: Field, value: any) {
     const data = this.state.deployData;
     data[field.name] = value;
-    this.setState({ deployData: { ...this.state.deployData, ...data }});
+    this.setState({ deployData: { ...this.state.deployData, ...data } });
   }
 
   async handleUpdateClick() {
@@ -902,10 +902,7 @@ class TransferItemModal extends React.Component<Props, State> {
         layout="modal"
         isSource={type === "source"}
         optionsLoading={optionsLoading}
-        optionsLoadingSkipFields={[
-          ...optionsLoadingSkipFields,
-          "description"
-        ]}
+        optionsLoadingSkipFields={[...optionsLoadingSkipFields, "description"]}
         dictionaryKey={dictionaryKey}
         executeNowOptionsDisabled={
           !providerStore.hasExecuteNowOptions(this.props.sourceEndpoint.type)
@@ -996,7 +993,7 @@ class TransferItemModal extends React.Component<Props, State> {
         data={this.state.deployData}
         getFieldValue={(f, d) => this.getDeployFieldValue(f, d)}
       />
-    )
+    );
   }
 
   renderContent() {
@@ -1101,8 +1098,8 @@ class TransferItemModal extends React.Component<Props, State> {
 
     navigationItems.push({
       value: "deploy_options",
-      label: "Deploy Options"
-    })
+      label: "Deploy Options",
+    });
 
     return (
       <Modal

+ 272 - 266
src/components/modules/WizardModule/WizardExecuteOptions/WizardExecuteOptions.tsx

@@ -66,7 +66,8 @@ const GroupNameBar = styled.div<any>`
 const GroupFields = styled.div<any>`
   display: flex;
   justify-content: space-between;
-  margin-top: ${props => (props.name === "Deployment options" ? "32px" : "16px")};
+  margin-top: ${props =>
+    props.name === "Deployment options" ? "32px" : "16px"};
 `;
 const Column = styled.div<any>`
   margin-top: -16px;
@@ -78,299 +79,304 @@ const FieldInputStyled = styled(FieldInput)`
 `;
 
 export const shouldRenderField = (field: Field) =>
-    (field.type !== "array" ||
-        (field.enum && field.enum.length && field.enum.length > 0)) &&
-    (field.type !== "object" || field.properties);
+  (field.type !== "array" ||
+    (field.enum && field.enum.length && field.enum.length > 0)) &&
+  (field.type !== "object" || field.properties);
 
 type FieldRender = {
-    field: Field;
-    component: React.ReactNode;
-    column: number;
+  field: Field;
+  component: React.ReactNode;
+  column: number;
 };
 
 type Props = {
-    options: Field[];
-    wizardType: string;
-    layout?: "page" | "modal";
-    data?: { [prop: string]: any } | null;
-    fieldWidth?: number;
-    getFieldValue?: (
-        fieldName: string,
-        defaultValue: any,
-        parentFieldName: string | undefined
-    ) => any;
-    onChange: (field: Field, value: any, parentFieldName?: string) => void;
-    onScrollableRef?: (ref: HTMLElement) => void;
+  options: Field[];
+  wizardType: string;
+  layout?: "page" | "modal";
+  data?: { [prop: string]: any } | null;
+  fieldWidth?: number;
+  getFieldValue?: (
+    fieldName: string,
+    defaultValue: any,
+    parentFieldName: string | undefined
+  ) => any;
+  onChange: (field: Field, value: any, parentFieldName?: string) => void;
+  onScrollableRef?: (ref: HTMLElement) => void;
 };
 type State = {
-    executionOptions: { [prop: string]: any } | null;
+  executionOptions: { [prop: string]: any } | null;
 };
 
 @observer
 class WizardExecuteOptions extends React.Component<Props, State> {
-    state: State = {
-        executionOptions: null,
-    };
-
-    getDefaultSimpleFieldsSchema() {
-        const fieldsSchema: Field[] = [];
-        if (this.props.wizardType === "replica-execute" || this.props.wizardType === "migration-execute") {
-            fieldsSchema.push({
-                name: "execute_now",
-                type: "boolean",
-                default: true,
-                nullableBoolean: false,
-                description:
-                    "When enabled, the transfer will be executed immediately after the options are configured.",
-
-            });
-            fieldsSchema.push({
-                name: "auto_deploy",
-                type: "boolean",
-                default: false,
-                nullableBoolean: false,
-                description:
-                    "When enabled, the transfer will automatically deploy the instances on the destination cloud after the transfer is complete.",
-            });
-
-            fieldsSchema.push({
-                name: "shutdown_instances",
-                type: "boolean",
-                default: false,
-                nullableBoolean: false,
-                description:
-                    "When enabled, the instances will be shut down before the transfer is executed.",
-            });
-        }
-        else if (this.props.wizardType === "edit-deploy") {
-            fieldsSchema.push({
-                name: "clone_disks",
-                type: "boolean",
-                label: "Clone Disks",
-                nullableBoolean: false,
-                default: true,
-                description: "When enabled, the disks will be cloned during the deployment."
-            })
-
-            fieldsSchema.push({
-                name: "skip_os_morphing",
-                type: "boolean",
-                label: "Skip OS Morphing",
-                nullableBoolean: false,
-                default: false,
-                description: "When enabled, OS Morphing will be skipped during the deployment."
-            })
-        }
-
-        return fieldsSchema;
+  state: State = {
+    executionOptions: null,
+  };
+
+  getDefaultSimpleFieldsSchema() {
+    const fieldsSchema: Field[] = [];
+    if (
+      this.props.wizardType === "replica-execute" ||
+      this.props.wizardType === "migration-execute"
+    ) {
+      fieldsSchema.push({
+        name: "execute_now",
+        type: "boolean",
+        default: true,
+        nullableBoolean: false,
+        description:
+          "When enabled, the transfer will be executed immediately after the options are configured.",
+      });
+      fieldsSchema.push({
+        name: "auto_deploy",
+        type: "boolean",
+        default: false,
+        nullableBoolean: false,
+        description:
+          "When enabled, the transfer will automatically deploy the instances on the destination cloud after the transfer is complete.",
+      });
+
+      fieldsSchema.push({
+        name: "shutdown_instances",
+        type: "boolean",
+        default: false,
+        nullableBoolean: false,
+        description:
+          "When enabled, the instances will be shut down before the transfer is executed.",
+      });
+    } else if (this.props.wizardType === "edit-deploy") {
+      fieldsSchema.push({
+        name: "clone_disks",
+        type: "boolean",
+        label: "Clone Disks",
+        nullableBoolean: false,
+        default: true,
+        description:
+          "When enabled, the disks will be cloned during the deployment.",
+      });
+
+      fieldsSchema.push({
+        name: "skip_os_morphing",
+        type: "boolean",
+        label: "Skip OS Morphing",
+        nullableBoolean: false,
+        default: false,
+        description:
+          "When enabled, OS Morphing will be skipped during the deployment.",
+      });
     }
 
-    generateGroups(fields: FieldRender[]) {
-        let groups: Array<{ fields: FieldRender[]; name?: string }> = [{ fields }];
-
-        if (this.props.wizardType === "replica-execute" || this.props.wizardType === "migration-execute") {
-            const deploymentFieldNames = deploymentFields.map(f => f.name);
-            const deploymentFieldsInUse = fields.filter(f =>
-                deploymentFieldNames.includes(f.field.name)
-            );
-            const additionalDeploymentFields = deploymentFields.filter(
-                f => !fields.some(field => field.field.name === f.name)
-            ).map((field) => ({
-                column: fields.length % 2,
-                component: this.renderOptionsField({
-                    ...field,
-                    default: field.defaultValue,
-                }),
-                field: {
-                    ...field,
-                    default: field.defaultValue,
-                },
-            }));
-            if (deploymentFieldsInUse.length > 0 || additionalDeploymentFields.length > 0) {
-                groups.push({
-                    name: "Deployment options",
-                    fields: [
-                        ...deploymentFieldsInUse.map((f, i) => ({ ...f, column: i % 2 })),
-                        ...additionalDeploymentFields
-                    ],
-                });
-            }
-        }
+    return fieldsSchema;
+  }
 
-        fields.forEach(f => {
-            if (f.field.groupName) {
-                groups[0].fields = groups[0].fields
-                    ? groups[0].fields.filter(gf => gf.field.name !== f.field.name)
-                    : [];
-
-                const group = groups.find(g => g.name && g.name === f.field.groupName);
-                if (!group) {
-                    groups.push({
-                        name: f.field.groupName,
-                        fields: [f],
-                    });
-                } else {
-                    group.fields.push(f);
-                }
-            }
-        });
+  generateGroups(fields: FieldRender[]) {
+    const groups: Array<{ fields: FieldRender[]; name?: string }> = [
+      { fields },
+    ];
 
-        return groups;
-    }
-
-    getFieldValue(
-        fieldName: string,
-        defaultValue: any,
-        parentFieldName?: string
+    if (
+      this.props.wizardType === "replica-execute" ||
+      this.props.wizardType === "migration-execute"
     ) {
-        if (this.props.getFieldValue) {
-            return this.props.getFieldValue(fieldName, defaultValue, parentFieldName);
-        }
+      const deploymentFieldNames = deploymentFields.map(f => f.name);
+      const deploymentFieldsInUse = fields.filter(f =>
+        deploymentFieldNames.includes(f.field.name)
+      );
+      const additionalDeploymentFields = deploymentFields
+        .filter(f => !fields.some(field => field.field.name === f.name))
+        .map(field => ({
+          column: fields.length % 2,
+          component: this.renderOptionsField({
+            ...field,
+            default: field.defaultValue,
+          }),
+          field: {
+            ...field,
+            default: field.defaultValue,
+          },
+        }));
+      if (
+        deploymentFieldsInUse.length > 0 ||
+        additionalDeploymentFields.length > 0
+      ) {
+        groups.push({
+          name: "Deployment options",
+          fields: [
+            ...deploymentFieldsInUse.map((f, i) => ({ ...f, column: i % 2 })),
+            ...additionalDeploymentFields,
+          ],
+        });
+      }
+    }
 
-        if (!this.props.data) {
-            return defaultValue;
+    fields.forEach(f => {
+      if (f.field.groupName) {
+        groups[0].fields = groups[0].fields
+          ? groups[0].fields.filter(gf => gf.field.name !== f.field.name)
+          : [];
+
+        const group = groups.find(g => g.name && g.name === f.field.groupName);
+        if (!group) {
+          groups.push({
+            name: f.field.groupName,
+            fields: [f],
+          });
+        } else {
+          group.fields.push(f);
         }
+      }
+    });
 
-        if (parentFieldName) {
-            if (
-                this.props.data[parentFieldName] &&
-                this.props.data[parentFieldName][fieldName] !== undefined
-            ) {
-                return this.props.data[parentFieldName][fieldName];
-            }
-            return defaultValue;
-        }
+    return groups;
+  }
 
-        if (!this.props.data || this.props.data[fieldName] === undefined) {
-            return defaultValue;
-        }
+  getFieldValue(
+    fieldName: string,
+    defaultValue: any,
+    parentFieldName?: string
+  ) {
+    if (this.props.getFieldValue) {
+      return this.props.getFieldValue(fieldName, defaultValue, parentFieldName);
+    }
 
-        return this.props.data[fieldName];
+    if (!this.props.data) {
+      return defaultValue;
     }
 
-    renderOptionsField(field: Field) {
-        let additionalProps;
-        if (field.type === "object" && field.properties) {
-            additionalProps = {
-                valueCallback: (f: any) =>
-                    this.getFieldValue(f.name, f.default, field.name),
-                onChange: (value: any, f: any) => {
-                    this.props.onChange(f, value, field.name);
-                },
-                properties: field.properties,
-            };
-        } else {
-            additionalProps = {
-                value: this.getFieldValue(field.name, field.default, field.groupName),
-                onChange: (value: any) => {
-                    this.props.onChange(field, value);
-                },
-            };
-        }
-        return (
-            <FieldInputStyled
-                layout={this.props.layout || "page"}
-                key={field.name}
-                name={field.name}
-                type={field.type}
-                minimum={field.minimum}
-                maximum={field.maximum}
-                label={field.label || LabelDictionary.get(field.name)}
-                description={field.description || LabelDictionary.getDescription(field.name)}
-                password={field.name.toLowerCase().includes("password")}
-                enum={field.enum}
-                addNullValue
-                required={field.required}
-                width={this.props.fieldWidth || ThemeProps.inputSizes.wizard.width}
-                nullableBoolean={field.nullableBoolean}
-                disabled={field.disabled}
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...additionalProps}
-            />
-        );
+    if (parentFieldName) {
+      if (
+        this.props.data[parentFieldName] &&
+        this.props.data[parentFieldName][fieldName] !== undefined
+      ) {
+        return this.props.data[parentFieldName][fieldName];
+      }
+      return defaultValue;
     }
 
-    renderOptionsFields() {
-        let fieldsSchema: Field[] = this.getDefaultSimpleFieldsSchema();
-
-        const isRequired = (f: Field) =>
-            f.required || f.properties?.some(p => p.required);
-
-        const defaultFieldNames = fieldsSchema.map(f => f.name);
-        const filteredOptions = this.props.options.filter(
-            f => !defaultFieldNames.includes(f.name) && isRequired(f)
-        );
-
-        fieldsSchema = fieldsSchema.concat(filteredOptions);
-
-        const nonNullableBooleans: string[] = fieldsSchema
-            .filter(f => f.type === "boolean" && f.nullableBoolean === false)
-            .map(f => f.name);
-
-        let executeNowColumn: number;
-        const fields: FieldRender[] = fieldsSchema
-            .filter(f => shouldRenderField(f))
-            .map((field, i) => {
-                let column: number = i % 2;
-                if (field.name === "execute_now") {
-                    executeNowColumn = column;
-                }
-                const usableField = toJS(field);
-                if (
-                    field.type === "boolean" &&
-                    !nonNullableBooleans.find(name => name === field.name)
-                ) {
-                    usableField.nullableBoolean = true;
-                }
-
-                return {
-                    column,
-                    component: this.renderOptionsField(usableField),
-                    field: usableField,
-                };
-            });
-
-        const groups = this.generateGroups(fields);
-        return (
-            <Fields ref={this.props.onScrollableRef} layout={this.props.layout}>
-                {groups.map((g, i) => {
-                    const getColumnInGroup = (field: any, fieldIndex: number) =>
-                        g.name ? fieldIndex % 2 : field.column;
-                    return (
-                        <Group key={g.name || 0}>
-                            {g.name ? (
-                                <GroupName>
-                                    <GroupNameBar />
-                                    <GroupNameText>{LabelDictionary.get(g.name)}</GroupNameText>
-                                    <GroupNameBar />
-                                </GroupName>
-                            ) : null}
-                            <GroupFields>
-                                <Column left>
-                                    {g.fields.map(
-                                        (f, j) => getColumnInGroup(f, j) === 0 && f.component
-                                    )}
-                                </Column>
-                                <Column right>
-                                    {g.fields.map(
-                                        (f, j) => getColumnInGroup(f, j) === 1 && f.component
-                                    )}
-                                </Column>
-                            </GroupFields>
-                        </Group>
-                    );
-                })}
-            </Fields>
-        );
+    if (!this.props.data || this.props.data[fieldName] === undefined) {
+      return defaultValue;
     }
 
-    render() {
-        return (
-            <Wrapper>
-                {this.renderOptionsFields()}
-            </Wrapper>
-        );
+    return this.props.data[fieldName];
+  }
+
+  renderOptionsField(field: Field) {
+    let additionalProps;
+    if (field.type === "object" && field.properties) {
+      additionalProps = {
+        valueCallback: (f: any) =>
+          this.getFieldValue(f.name, f.default, field.name),
+        onChange: (value: any, f: any) => {
+          this.props.onChange(f, value, field.name);
+        },
+        properties: field.properties,
+      };
+    } else {
+      additionalProps = {
+        value: this.getFieldValue(field.name, field.default, field.groupName),
+        onChange: (value: any) => {
+          this.props.onChange(field, value);
+        },
+      };
     }
+    return (
+      <FieldInputStyled
+        layout={this.props.layout || "page"}
+        key={field.name}
+        name={field.name}
+        type={field.type}
+        minimum={field.minimum}
+        maximum={field.maximum}
+        label={field.label || LabelDictionary.get(field.name)}
+        description={
+          field.description || LabelDictionary.getDescription(field.name)
+        }
+        password={field.name.toLowerCase().includes("password")}
+        enum={field.enum}
+        addNullValue
+        required={field.required}
+        width={this.props.fieldWidth || ThemeProps.inputSizes.wizard.width}
+        nullableBoolean={field.nullableBoolean}
+        disabled={field.disabled}
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        {...additionalProps}
+      />
+    );
+  }
+
+  renderOptionsFields() {
+    let fieldsSchema: Field[] = this.getDefaultSimpleFieldsSchema();
+
+    const isRequired = (f: Field) =>
+      f.required || f.properties?.some(p => p.required);
+
+    const defaultFieldNames = fieldsSchema.map(f => f.name);
+    const filteredOptions = this.props.options.filter(
+      f => !defaultFieldNames.includes(f.name) && isRequired(f)
+    );
+
+    fieldsSchema = fieldsSchema.concat(filteredOptions);
+
+    const nonNullableBooleans: string[] = fieldsSchema
+      .filter(f => f.type === "boolean" && f.nullableBoolean === false)
+      .map(f => f.name);
+
+    const fields: FieldRender[] = fieldsSchema
+      .filter(f => shouldRenderField(f))
+      .map((field, i) => {
+        const column: number = i % 2;
+        const usableField = toJS(field);
+        if (
+          field.type === "boolean" &&
+          !nonNullableBooleans.find(name => name === field.name)
+        ) {
+          usableField.nullableBoolean = true;
+        }
+
+        return {
+          column,
+          component: this.renderOptionsField(usableField),
+          field: usableField,
+        };
+      });
+
+    const groups = this.generateGroups(fields);
+    return (
+      <Fields ref={this.props.onScrollableRef} layout={this.props.layout}>
+        {groups.map(g => {
+          const getColumnInGroup = (field: any, fieldIndex: number) =>
+            g.name ? fieldIndex % 2 : field.column;
+          return (
+            <Group key={g.name || 0}>
+              {g.name ? (
+                <GroupName>
+                  <GroupNameBar />
+                  <GroupNameText>{LabelDictionary.get(g.name)}</GroupNameText>
+                  <GroupNameBar />
+                </GroupName>
+              ) : null}
+              <GroupFields>
+                <Column left>
+                  {g.fields.map(
+                    (f, j) => getColumnInGroup(f, j) === 0 && f.component
+                  )}
+                </Column>
+                <Column right>
+                  {g.fields.map(
+                    (f, j) => getColumnInGroup(f, j) === 1 && f.component
+                  )}
+                </Column>
+              </GroupFields>
+            </Group>
+          );
+        })}
+      </Fields>
+    );
+  }
+
+  render() {
+    return <Wrapper>{this.renderOptionsFields()}</Wrapper>;
+  }
 }
 
 export default WizardExecuteOptions;

+ 5 - 5
src/components/modules/WizardModule/WizardExecuteOptions/package.json

@@ -1,6 +1,6 @@
 {
-    "name": "WizardExecuteOptions",
-    "version": "0.0.0",
-    "private": true,
-    "main": "./WizardExecuteOptions.tsx"
-  }
+  "name": "WizardExecuteOptions",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./WizardExecuteOptions.tsx"
+}

+ 1 - 1
src/components/modules/WizardModule/WizardOptions/WizardOptions.spec.tsx

@@ -45,6 +45,6 @@ describe("WizardOptions", () => {
 
   it("renders without crashing", () => {
     const { getByText } = render(<WizardOptions {...defaultProps} />);
-    expect(getByText("Execute Now")).toBeTruthy();
+    expect(getByText("Target Minion Pool")).toBeTruthy();
   });
 });

+ 1 - 6
src/components/modules/WizardModule/WizardOptions/WizardOptions.tsx

@@ -24,7 +24,6 @@ import { ThemePalette, ThemeProps } from "@src/components/Theme";
 import FieldInput from "@src/components/ui/FieldInput";
 import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
 import ToggleButtonBar from "@src/components/ui/ToggleButtonBar";
-import { executionOptions } from "@src/constants";
 import { MinionPoolStoreUtils } from "@src/stores/MinionPoolStore";
 import configLoader from "@src/utils/Config";
 import LabelDictionary from "@src/utils/LabelDictionary";
@@ -550,14 +549,10 @@ class WizardOptions extends React.Component<Props> {
     });
     fieldsSchema = [...fieldsSchema, ...subFields];
 
-    let executeNowColumn: number;
     const fields: FieldRender[] = fieldsSchema
       .filter(f => shouldRenderField(f))
       .map((field, i) => {
-        let column: number = i % 2;
-        if (field.name === "execute_now") {
-          executeNowColumn = column;
-        }
+        const column: number = i % 2;
         const usableField = toJS(field);
         if (
           field.type === "boolean" &&

+ 12 - 7
src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx

@@ -30,13 +30,18 @@ import WizardOptions from "@src/components/modules/WizardModule/WizardOptions";
 import WizardScripts from "@src/components/modules/WizardModule/WizardScripts";
 import WizardStorage from "@src/components/modules/WizardModule/WizardStorage";
 import WizardSummary from "@src/components/modules/WizardModule/WizardSummary";
-import WizardExecuteOptions from "@src/components/modules/WizardModule/WizardExecuteOptions/WizardExecuteOptions";
+import WizardExecuteOptions from "@src/components/modules/WizardModule/WizardExecuteOptions";
 import WizardType from "@src/components/modules/WizardModule/WizardType";
 import { ThemePalette, ThemeProps } from "@src/components/Theme";
 import Button from "@src/components/ui/Button";
 import InfoIcon from "@src/components/ui/InfoIcon";
 import LoadingButton from "@src/components/ui/LoadingButton";
-import { deploymentFields, executeOptionsWithExecuteNow, providerTypes, wizardPages } from "@src/constants";
+import {
+  deploymentFields,
+  executeOptionsWithExecuteNow,
+  providerTypes,
+  wizardPages,
+} from "@src/constants";
 import endpointStore from "@src/stores/EndpointStore";
 import instanceStore from "@src/stores/InstanceStore";
 import minionPoolStore from "@src/stores/MinionPoolStore";
@@ -174,10 +179,7 @@ type Props = {
     global: string | null,
     instanceName: string | null
   ) => void;
-  onTransferExecuteOptionsChange: (
-    field: Field,
-    value: any,
-  ) => void;
+  onTransferExecuteOptionsChange: (field: Field, value: any) => void;
 };
 type TimezoneValue = "local" | "utc";
 type State = {
@@ -642,7 +644,10 @@ class WizardPageContent extends React.Component<Props, State> {
             destinationSchema={this.props.providerStore.destinationSchema}
             uploadedUserScripts={this.props.uploadedUserScripts}
             minionPools={this.props.minionPoolStore.minionPools}
-            executionOptions={[...executeOptionsWithExecuteNow, ...deploymentFields]}
+            executionOptions={[
+              ...executeOptionsWithExecuteNow,
+              ...deploymentFields,
+            ]}
           />
         );
         break;

+ 1 - 0
src/components/modules/WizardModule/WizardSummary/WizardSummary.spec.tsx

@@ -58,6 +58,7 @@ describe("WizardSummary", () => {
         { name: "option_2", label: "Option 2", type: "string" },
       ],
       uploadedUserScripts: [],
+      executionOptions: [],
     };
   });
 

+ 13 - 5
src/components/modules/WizardModule/WizardSummary/WizardSummary.tsx

@@ -325,7 +325,7 @@ class WizardSummary extends React.Component<Props> {
   }
 
   hasDefaultValue(option: any): option is { defaultValue: boolean } {
-    return option && typeof option.defaultValue !== 'undefined';
+    return option && typeof option.defaultValue !== "undefined";
   }
 
   renderTransferExecuteOptions() {
@@ -334,7 +334,10 @@ class WizardSummary extends React.Component<Props> {
       this.props.wizardType.substr(1);
     const data = this.props.data;
 
-    if (!this.props.executionOptions || this.props.executionOptions.length === 0) {
+    if (
+      !this.props.executionOptions ||
+      this.props.executionOptions.length === 0
+    ) {
       return null;
     }
 
@@ -353,9 +356,14 @@ class WizardSummary extends React.Component<Props> {
             <Option key={option.name}>
               <OptionLabel>{option.label}</OptionLabel>
               <OptionValue>
-                {data.executeOptions && data.executeOptions[option.name] !== undefined
-                  ? data.executeOptions[option.name] ? "Yes" : "No"
-                  : this.hasDefaultValue(option) && option.defaultValue ? "Yes" : "No"}
+                {data.executeOptions &&
+                data.executeOptions[option.name] !== undefined
+                  ? data.executeOptions[option.name]
+                    ? "Yes"
+                    : "No"
+                  : this.hasDefaultValue(option) && option.defaultValue
+                  ? "Yes"
+                  : "No"}
               </OptionValue>
             </Option>
           ))}

+ 11 - 2
src/components/smart/WizardPage/WizardPage.tsx

@@ -26,7 +26,13 @@ import { WizardNetworksChangeObject } from "@src/components/modules/WizardModule
 import WizardPageContent from "@src/components/modules/WizardModule/WizardPageContent";
 import Modal from "@src/components/ui/Modal";
 import endpointStore from "@src/stores/EndpointStore";
-import { executeOptionsWithExecuteNow, deploymentFields, providerTypes, wizardPages, executionOptions } from "@src/constants";
+import {
+  executeOptionsWithExecuteNow,
+  deploymentFields,
+  providerTypes,
+  wizardPages,
+  executionOptions,
+} from "@src/constants";
 import instanceStore from "@src/stores/InstanceStore";
 import minionPoolStore from "@src/stores/MinionPoolStore";
 import networkStore from "@src/stores/NetworkStore";
@@ -515,7 +521,10 @@ class WizardPage extends React.Component<Props, State> {
 
     if (!wizardStore.data.executeOptions) {
       wizardStore.updateData({
-        executeOptions: [...executeOptionsWithExecuteNow, ...deploymentFields].reduce((acc, option) => {
+        executeOptions: [
+          ...executeOptionsWithExecuteNow,
+          ...deploymentFields,
+        ].reduce((acc, option) => {
           acc[option.name] = option.defaultValue;
           return acc;
         }, {} as { [key: string]: any }),

+ 7 - 5
src/constants.ts

@@ -77,7 +77,7 @@ export const executionOptions = [
     nullableBoolean: false,
     description:
       "When enabled, the transfer will automatically deploy the instances on the destination cloud after the transfer is complete.",
-  }
+  },
 ];
 
 export const executeOptionsWithExecuteNow = [
@@ -90,7 +90,7 @@ export const executeOptionsWithExecuteNow = [
     nullableBoolean: false,
     description:
       "When enabled, the transfer will be executed immediately after the options are configured.",
-  }
+  },
 ];
 
 export const deploymentFields = [
@@ -100,7 +100,8 @@ export const deploymentFields = [
     label: "Clone Disks",
     defaultValue: true,
     nullableBoolean: false,
-    description: "When enabled, the disks will be cloned during the deployment.",
+    description:
+      "When enabled, the disks will be cloned during the deployment.",
   },
   {
     name: "skip_os_morphing",
@@ -108,8 +109,9 @@ export const deploymentFields = [
     label: "Skip OS Morphing",
     defaultValue: false,
     nullableBoolean: false,
-    description: "When enabled, OS morphing will be skipped during the deployment.",
-  }
+    description:
+      "When enabled, OS morphing will be skipped during the deployment.",
+  },
 ];
 
 export const wizardPages: WizardPage[] = [

+ 1 - 5
src/plugins/default/OptionsSchemaPlugin.ts

@@ -127,11 +127,7 @@ export const defaultGetDestinationEnv = (
   oldOptions?: { [prop: string]: any } | null
 ): any => {
   const env: any = {};
-  const specialOptions = [
-    "separate_vm",
-    "title",
-    "minion_pool_id",
-  ]
+  const specialOptions = ["separate_vm", "title", "minion_pool_id"]
     .concat(executionOptions.map(o => o.name))
     .concat(migrationImageOsTypes);
 

+ 2 - 5
src/sources/ScheduleSource.ts

@@ -31,9 +31,7 @@ class ScheduleSource {
           ? false
           : scheduleData.shutdown_instances,
       auto_deploy:
-        scheduleData.auto_deploy == null 
-          ? false
-          : scheduleData.auto_deploy,
+        scheduleData.auto_deploy == null ? false : scheduleData.auto_deploy,
     };
 
     if (scheduleData.expiration_date) {
@@ -94,8 +92,7 @@ class ScheduleSource {
         : undefined,
       shutdown_instances:
         s.shutdown_instance != null ? s.shutdown_instance : undefined,
-      auto_deploy:
-        s.auto_deploy != null ? s.auto_deploy : undefined,
+      auto_deploy: s.auto_deploy != null ? s.auto_deploy : undefined,
     }));
     schedules.sort(
       (a, b) =>

+ 3 - 1
src/sources/TransferSource.ts

@@ -155,7 +155,9 @@ class TransferSource {
   }
 
   async execute(transferId: string, fields?: Field[]): Promise<ExecutionTasks> {
-    const payload: any = { execution: { shutdown_instances: false, auto_deploy: false } };
+    const payload: any = {
+      execution: { shutdown_instances: false, auto_deploy: false },
+    };
     if (fields) {
       fields.forEach(f => {
         payload.execution[f.name] = f.value || false;

+ 4 - 1
src/sources/WizardSource.ts

@@ -103,7 +103,10 @@ class WizardSource {
     }
 
     deploymentFields.forEach(option => {
-      if (data.executeOptions && data.executeOptions[option.name] !== undefined) {
+      if (
+        data.executeOptions &&
+        data.executeOptions[option.name] !== undefined
+      ) {
         payload[option.name] = data.executeOptions[option.name];
       }
     });

+ 1 - 4
src/stores/WizardStore.ts

@@ -235,10 +235,7 @@ class WizardStore {
     parentFieldName: string | undefined;
   }) {
     this.data = { ...this.data };
-    this.data.executeOptions = updateOptions(
-      this.data.executeOptions,
-      data
-    );
+    this.data.executeOptions = updateOptions(this.data.executeOptions, data);
   }
 
   @action updateNetworks(network: NetworkMap) {

+ 1 - 1
src/utils/LabelDictionary.ts

@@ -96,7 +96,7 @@ const dictionary = {
     label: "Auto Deploy",
     description:
       "When enabled, the transfer will automatically deploy the instances on the destination cloud after the transfer is complete.",
-  }
+  },
 };
 
 const cache: {

+ 15 - 0
tests/mocks/TransferMock.ts

@@ -77,6 +77,7 @@ export const DEPLOYMENT_MOCK: DeploymentItem = {
   type: "deployment",
   transfer_id: "deployment-transfer-id",
   transfer_scenario_type: "replica",
+  deployer_id: "deployer-id",
   description: "deployment-description",
   notes: "deployment-notes",
   created_at: "2023-11-26T12:00:00Z",
@@ -92,6 +93,20 @@ export const DEPLOYMENT_MOCK: DeploymentItem = {
   transfer_result: {},
   last_execution_status: "COMPLETED",
   user_id: "user-id",
+  instance_osmorphing_minion_pool_mappings: {
+    "instance-id": "minion-pool-id",
+  },
+  user_scripts: {
+    global: {
+      linux: "linux-script",
+      windows: "windows-script",
+    },
+    instances: {
+      "instance-id": "instance-script",
+    },
+  },
+  clone_disks: true,
+  skip_os_morphing: false,
 };
 
 export const DEPLOYMENT_ITEM_DETAILS_MOCK: DeploymentItemDetails = {