Kaynağa Gözat

Add naive copy of Migration components as Deployments.

Signed-off-by: Nashwan Azhari <nazhari@cloudbasesolutions.com>
Nashwan Azhari 2 yıl önce
ebeveyn
işleme
e9f4585de3
21 değiştirilmiş dosya ile 2523 ekleme ve 2 silme
  1. 16 2
      src/@types/MainItem.ts
  2. 74 0
      src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.spec.tsx
  3. 142 0
      src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.tsx
  4. 6 0
      src/components/modules/TransferModule/DeploymentDetailsContent/package.json
  5. 95 0
      src/components/modules/TransferModule/DeploymentDetailsContent/story.tsx
  6. 154 0
      src/components/modules/TransferModule/ReplicaDeploymentOptions/ReplicaDeploymentOptions.spec.tsx
  7. 343 0
      src/components/modules/TransferModule/ReplicaDeploymentOptions/ReplicaDeploymentOptions.tsx
  8. 15 0
      src/components/modules/TransferModule/ReplicaDeploymentOptions/images/replica-deployment.svg
  9. 6 0
      src/components/modules/TransferModule/ReplicaDeploymentOptions/package.json
  10. 33 0
      src/components/modules/TransferModule/ReplicaDeploymentOptions/replicaDeploymentFields.ts
  11. 25 0
      src/components/modules/TransferModule/ReplicaDeploymentOptions/story.tsx
  12. 600 0
      src/components/smart/DeploymentDetailsPage/DeploymentDetailsPage.tsx
  13. 10 0
      src/components/smart/DeploymentDetailsPage/images/deployment.svg
  14. 6 0
      src/components/smart/DeploymentDetailsPage/package.json
  15. 377 0
      src/components/smart/DeploymentsPage/DeploymentsPage.tsx
  16. 10 0
      src/components/smart/DeploymentsPage/images/deployment-large.svg
  17. 10 0
      src/components/smart/DeploymentsPage/images/deployment.svg
  18. 6 0
      src/components/smart/DeploymentsPage/package.json
  19. 369 0
      src/sources/DeploymentSource.ts
  20. 198 0
      src/stores/DeploymentStore.ts
  21. 28 0
      tests/mocks/TransferMock.ts

+ 16 - 2
src/@types/MainItem.ts

@@ -116,7 +116,17 @@ export type MigrationItemOptions = MigrationItem & {
   shutdown_instances: boolean;
 };
 
-export type TransferItem = ReplicaItem | MigrationItem;
+export type DeploymentItem = BaseItem & {
+  type: "deployment";
+  replica_id?: string;
+};
+
+export type DeploymentItemOptions = DeploymentItem & {
+  skip_os_morphing: boolean;
+  shutdown_instances: boolean;
+};
+
+export type TransferItem = ReplicaItem | MigrationItem | DeploymentItem;
 
 export type ReplicaItemDetails = ReplicaItem & {
   executions: Execution[];
@@ -126,7 +136,11 @@ export type MigrationItemDetails = MigrationItem & {
   tasks: Task[];
 };
 
-export type TransferItemDetails = ReplicaItemDetails | MigrationItemDetails;
+export type DeploymentItemDetails = DeploymentItem & {
+  tasks: Task[];
+};
+
+export type TransferItemDetails = ReplicaItemDetails | MigrationItemDetails | DeploymentItemDetails;
 
 export const getTransferItemTitle = (item: TransferItem | null) => {
   if (!item) {

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

@@ -0,0 +1,74 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import React from "react";
+
+import { render } from "@testing-library/react";
+
+import DeploymentDetailsContent from ".";
+import { DEPLOYMENT_ITEM_DETAILS_MOCK } from "@tests/mocks/TransferMock";
+import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock";
+import { STORAGE_BACKEND_MOCK } from "@tests/mocks/StoragesMock";
+import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock";
+import { NETWORK_MOCK } from "@tests/mocks/NetworksMock";
+import {
+  OPENSTACK_ENDPOINT_MOCK,
+  VMWARE_ENDPOINT_MOCK,
+} from "@tests/mocks/EndpointsMock";
+
+jest.mock("@src/components/modules/EndpointModule/EndpointLogos", () => ({
+  __esModule: true,
+  default: (props: any) => <div>{props.endpoint}</div>,
+}));
+jest.mock("react-router-dom", () => ({ Link: "a" }));
+
+describe("DeploymentDetailsContent", () => {
+  let defaultProps: DeploymentDetailsContent["props"];
+
+  beforeEach(() => {
+    defaultProps = {
+      item: DEPLOYMENT_ITEM_DETAILS_MOCK,
+      itemId: DEPLOYMENT_ITEM_DETAILS_MOCK.id,
+      minionPools: [MINION_POOL_MOCK],
+      detailsLoading: false,
+      storageBackends: [STORAGE_BACKEND_MOCK],
+      instancesDetails: [INSTANCE_MOCK],
+      instancesDetailsLoading: false,
+      networks: [NETWORK_MOCK],
+      sourceSchema: [],
+      sourceSchemaLoading: false,
+      destinationSchema: [],
+      destinationSchemaLoading: false,
+      endpoints: [OPENSTACK_ENDPOINT_MOCK, VMWARE_ENDPOINT_MOCK],
+      page: "",
+      onDeleteDeploymentClick: jest.fn(),
+    };
+  });
+
+  it("renders without crashing", () => {
+    const { getByText } = render(<DeploymentDetailsContent {...defaultProps} />);
+    expect(getByText(DEPLOYMENT_ITEM_DETAILS_MOCK.id)).toBeTruthy();
+  });
+
+  it("renders tasks page", () => {
+    const { getByText } = render(
+      <DeploymentDetailsContent {...defaultProps} page="tasks" />
+    );
+    expect(
+      getByText(
+        DEPLOYMENT_ITEM_DETAILS_MOCK.tasks[0].task_type.replace("_", " ")
+      )
+    ).toBeTruthy();
+  });
+});

+ 142 - 0
src/components/modules/TransferModule/DeploymentDetailsContent/DeploymentDetailsContent.tsx

@@ -0,0 +1,142 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import { observer } from "mobx-react";
+import React from "react";
+import styled from "styled-components";
+
+import { DeploymentItemDetails } from "@src/@types/MainItem";
+import { MinionPool } from "@src/@types/MinionPool";
+import { Network } from "@src/@types/Network";
+import DetailsNavigation from "@src/components/modules/NavigationModule/DetailsNavigation";
+import MainDetails from "@src/components/modules/TransferModule/MainDetails";
+import Tasks from "@src/components/modules/TransferModule/Tasks";
+import { ThemeProps } from "@src/components/Theme";
+import Button from "@src/components/ui/Button";
+
+import type { Instance } from "@src/@types/Instance";
+import type { Endpoint, StorageBackend } from "@src/@types/Endpoint";
+import type { Field } from "@src/@types/Field";
+const Wrapper = styled.div<any>`
+  display: flex;
+  justify-content: center;
+`;
+
+const Buttons = styled.div<any>`
+  margin-top: 24px;
+  & > button:last-child {
+    float: right;
+  }
+`;
+const DetailsBody = styled.div<any>`
+  ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
+`;
+
+const NavigationItems = [
+  {
+    label: "Deployment",
+    value: "",
+  },
+  {
+    label: "Tasks",
+    value: "tasks",
+  },
+];
+
+type Props = {
+  item: DeploymentItemDetails | null;
+  itemId: string;
+  minionPools: MinionPool[];
+  detailsLoading: boolean;
+  storageBackends: StorageBackend[];
+  instancesDetails: Instance[];
+  instancesDetailsLoading: boolean;
+  networks: Network[];
+  sourceSchema: Field[];
+  sourceSchemaLoading: boolean;
+  destinationSchema: Field[];
+  destinationSchemaLoading: boolean;
+  endpoints: Endpoint[];
+  page: string;
+  onDeleteDeploymentClick: () => void;
+};
+@observer
+class DeploymentDetailsContent extends React.Component<Props> {
+  renderBottomControls() {
+    return (
+      <Buttons>
+        <Button alert hollow onClick={this.props.onDeleteDeploymentClick}>
+          Delete Deployment
+        </Button>
+      </Buttons>
+    );
+  }
+
+  renderMainDetails() {
+    if (this.props.page !== "") {
+      return null;
+    }
+
+    return (
+      <MainDetails
+        item={this.props.item}
+        storageBackends={this.props.storageBackends}
+        minionPools={this.props.minionPools}
+        instancesDetails={this.props.instancesDetails}
+        instancesDetailsLoading={this.props.instancesDetailsLoading}
+        networks={this.props.networks}
+        sourceSchema={this.props.sourceSchema}
+        sourceSchemaLoading={this.props.sourceSchemaLoading}
+        destinationSchema={this.props.destinationSchema}
+        destinationSchemaLoading={this.props.destinationSchemaLoading}
+        endpoints={this.props.endpoints}
+        bottomControls={this.renderBottomControls()}
+        loading={this.props.detailsLoading}
+      />
+    );
+  }
+
+  renderTasks() {
+    if (this.props.page !== "tasks" || !this.props.item?.tasks) {
+      return null;
+    }
+
+    return (
+      <Tasks
+        items={this.props.item.tasks}
+        loading={this.props.detailsLoading}
+        instancesDetails={this.props.instancesDetails}
+      />
+    );
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        <DetailsNavigation
+          items={NavigationItems}
+          selectedValue={this.props.page}
+          itemId={this.props.itemId}
+          itemType="deployment"
+        />
+        <DetailsBody>
+          {this.renderMainDetails()}
+          {this.renderTasks()}
+        </DetailsBody>
+      </Wrapper>
+    );
+  }
+}
+
+export default DeploymentDetailsContent;

+ 6 - 0
src/components/modules/TransferModule/DeploymentDetailsContent/package.json

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

+ 95 - 0
src/components/modules/TransferModule/DeploymentDetailsContent/story.tsx

@@ -0,0 +1,95 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+/* eslint-disable react/jsx-props-no-spreading */
+
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import DeploymentDetailsContent from ".";
+
+const tasks: any = [
+  {
+    progress_updates: [
+      { message: "the task has a progress of 10%", created_at: new Date() },
+    ],
+    exception_details: "Exception details",
+    status: "COMPLETED",
+    created_at: new Date(),
+    depends_on: ["depends on id"],
+    id: "task-1",
+    task_type: "Task name 1",
+  },
+  {
+    progress_updates: [
+      { message: "the task has a progress of 50%", created_at: new Date() },
+      { message: "the task is almost done", created_at: new Date() },
+    ],
+    exception_details: "Exception details",
+    status: "RUNNING",
+    created_at: new Date(),
+    depends_on: ["depends on id"],
+    id: "task-2",
+    task_type: "Task name 2",
+  },
+];
+const endpoints: any = [
+  { id: "endpoint-1", name: "Endpoint OPS", type: "openstack" },
+  { id: "endpoint-2", name: "Endpoint AZURE", type: "azure" },
+];
+const item: any = {
+  origin_endpoint_id: "endpoint-1",
+  destination_endpoint_id: "endpoint-2",
+  id: "item-id",
+  created_at: new Date(2017, 10, 24, 16, 15),
+  info: {
+    instance: {
+      export_info: { devices: { nics: [{ network_name: "map_1" }] } },
+    },
+  },
+  tasks,
+  destination_environment: {
+    description: "A description",
+    network_map: {
+      map_1: "Mapping 1",
+    },
+  },
+  type: "Deployment",
+};
+const props: any = {};
+storiesOf("DeploymentDetailsContent", module)
+  .add("default", () => (
+    <DeploymentDetailsContent
+      item={item}
+      endpoints={endpoints}
+      page=""
+      {...props}
+    />
+  ))
+  .add("details loading", () => (
+    <DeploymentDetailsContent
+      item={item}
+      endpoints={endpoints}
+      page=""
+      detailsLoading
+      {...props}
+    />
+  ))
+  .add("tasks", () => (
+    <DeploymentDetailsContent
+      item={item}
+      endpoints={endpoints}
+      page="tasks"
+      {...props}
+    />
+  ));

+ 154 - 0
src/components/modules/TransferModule/ReplicaDeploymentOptions/ReplicaDeploymentOptions.spec.tsx

@@ -0,0 +1,154 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import React from "react";
+
+import WizardScripts from "@src/components/modules/WizardModule/WizardScripts";
+import { fireEvent, render } from "@testing-library/react";
+import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock";
+import { MINION_POOL_MOCK } from "@tests/mocks/MinionPoolMock";
+import { REPLICA_ITEM_DETAILS_MOCK } from "@tests/mocks/TransferMock";
+import TestUtils from "@tests/TestUtils";
+
+import ReplicaDeploymentOptions from "./";
+
+jest.mock("@src/plugins/default/ContentPlugin", () => jest.fn(() => null));
+jest.mock("@src/components/modules/WizardModule/WizardScripts", () => ({
+  __esModule: true,
+  default: (props: WizardScripts["props"]) => (
+    <div data-testid="ScriptsComponent">
+      <div data-testid="ScriptsUploaded">
+        {props.uploadedScripts.map(s => s.scriptContent).join(", ")}
+      </div>
+      <div
+        data-testid="ScriptsRemove"
+        onClick={() => {
+          props.onScriptDataRemove(props.uploadedScripts[0]);
+        }}
+      />
+      <div data-testid="ScriptsRemoved">
+        {props.removedScripts.map(s => s.scriptContent).join(", ")}
+      </div>
+      <div
+        data-testid="ScriptsCancel"
+        onClick={() => {
+          props.onCancelScript("windows", null);
+          props.scrollableRef &&
+            props.scrollableRef(null as any as HTMLElement);
+        }}
+      />
+      <div
+        data-testid="ScriptsUpload"
+        onClick={() => {
+          props.onScriptUpload({
+            scriptContent: `script-content-${Math.random()}`,
+            fileName: `script-name.ps1`,
+            global: "windows",
+          });
+        }}
+      />
+    </div>
+  ),
+}));
+
+describe("ReplicaDeploymentOptions", () => {
+  let defaultProps: ReplicaDeploymentOptions["props"];
+
+  beforeEach(() => {
+    defaultProps = {
+      instances: [INSTANCE_MOCK],
+      transferItem: REPLICA_ITEM_DETAILS_MOCK,
+      minionPools: [
+        MINION_POOL_MOCK,
+        { ...MINION_POOL_MOCK, id: "pool2", name: "Pool2" },
+      ],
+      loadingInstances: false,
+      onCancelClick: jest.fn(),
+      onDeployClick: jest.fn(),
+      onResizeUpdate: jest.fn(),
+    };
+  });
+
+  it("renders without crashing", () => {
+    const { getByText } = render(<ReplicaDeploymentOptions {...defaultProps} />);
+    expect(getByText("Deploy")).toBeTruthy();
+  });
+
+  it("executes on Enter", () => {
+    render(<ReplicaDeploymentOptions {...defaultProps} />);
+    fireEvent.keyDown(document.body, { key: "Enter" });
+    expect(defaultProps.onDeployClick).toHaveBeenCalled();
+  });
+
+  it("calls onResizeUpdate on selectedBarButton state change", () => {
+    render(<ReplicaDeploymentOptions {...defaultProps} />);
+    fireEvent.click(TestUtils.selectAll("ToggleButtonBar__Item-")[1]);
+    expect(defaultProps.onResizeUpdate).toHaveBeenCalled();
+  });
+
+  it("handles value change", () => {
+    render(<ReplicaDeploymentOptions {...defaultProps} />);
+    expect(TestUtils.select("Switch__Wrapper")?.textContent).toBe("Yes");
+    fireEvent.click(TestUtils.select("Switch__InputWrapper")!);
+    expect(TestUtils.select("Switch__Wrapper")?.textContent).toBe("No");
+  });
+
+  it("handles script operations", () => {
+    const { getByTestId } = render(
+      <ReplicaDeploymentOptions {...defaultProps} />
+    );
+    fireEvent.click(TestUtils.selectAll("ToggleButtonBar__Item-")[1]);
+    fireEvent.click(getByTestId("ScriptsUpload"));
+    expect(getByTestId("ScriptsUploaded").textContent).toContain(
+      "script-content"
+    );
+    fireEvent.click(getByTestId("ScriptsCancel"));
+    expect(getByTestId("ScriptsUploaded").textContent).toBe("");
+
+    fireEvent.click(getByTestId("ScriptsUpload"));
+    expect(getByTestId("ScriptsUploaded").textContent).toContain(
+      "script-content"
+    );
+    expect(getByTestId("ScriptsRemoved").textContent).toBe("");
+    fireEvent.click(getByTestId("ScriptsRemove"));
+    expect(getByTestId("ScriptsRemoved").textContent).toContain(
+      "script-content"
+    );
+  });
+
+  it("doesn't render minion pool mappings", () => {
+    const { rerender } = render(<ReplicaDeploymentOptions {...defaultProps} />);
+    expect(document.body.textContent).toContain("Minion Pool Mappings");
+
+    rerender(<ReplicaDeploymentOptions {...defaultProps} minionPools={[]} />);
+    expect(document.body.textContent).not.toContain("Minion Pool Mappings");
+  });
+
+  it("changes minion pool mappings value", () => {
+    render(<ReplicaDeploymentOptions {...defaultProps} />);
+    fireEvent.click(TestUtils.select("DropdownButton__Wrapper-")!);
+    const dropdownItem = TestUtils.selectAll("Dropdown__ListItem-")[2];
+    expect(dropdownItem.textContent).toBe("Pool2");
+    fireEvent.click(dropdownItem);
+    expect(TestUtils.select("DropdownButton__Label-")?.textContent).toBe(
+      "Pool2"
+    );
+  });
+
+  it("handles migrate click", () => {
+    const { getByText } = render(<ReplicaDeploymentOptions {...defaultProps} />);
+    fireEvent.click(getByText("Deploy"));
+    expect(defaultProps.onDeployClick).toHaveBeenCalled();
+  });
+});

+ 343 - 0
src/components/modules/TransferModule/ReplicaDeploymentOptions/ReplicaDeploymentOptions.tsx

@@ -0,0 +1,343 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import { observer } from "mobx-react";
+import React from "react";
+import styled from "styled-components";
+
+import { TransferItemDetails } from "@src/@types/MainItem";
+import { MinionPool } from "@src/@types/MinionPool";
+import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from "@src/components/modules/WizardModule/WizardOptions";
+import WizardScripts from "@src/components/modules/WizardModule/WizardScripts";
+import { ThemeProps } from "@src/components/Theme";
+import Button from "@src/components/ui/Button";
+import FieldInput from "@src/components/ui/FieldInput";
+import LoadingButton from "@src/components/ui/LoadingButton";
+import ToggleButtonBar from "@src/components/ui/ToggleButtonBar";
+import KeyboardManager from "@src/utils/KeyboardManager";
+import LabelDictionary from "@src/utils/LabelDictionary";
+
+import replicaDeploymentImage from "./images/replica-deployment.svg";
+import replicaDeploymentFields from "./replicaDeploymentFields";
+
+import type { Field } from "@src/@types/Field";
+import type { Instance, InstanceScript } from "@src/@types/Instance";
+const Wrapper = styled.div<any>`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 32px 32px 32px;
+  min-height: 0;
+`;
+const Image = styled.div<any>`
+  ${ThemeProps.exactWidth("288px")}
+  ${ThemeProps.exactHeight("96px")}
+  background: url('${replicaDeploymentImage}') center no-repeat;
+  margin: 80px 0;
+`;
+const OptionsBody = styled.div<any>`
+  display: flex;
+  flex-direction: column;
+`;
+const ScriptsBody = styled.div<any>`
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  overflow: auto;
+  min-height: 0;
+  margin-bottom: 32px;
+`;
+const Form = styled.div<any>`
+  display: flex;
+  flex-wrap: wrap;
+  margin-left: -64px;
+  justify-content: space-between;
+  margin: 0 auto;
+`;
+const Buttons = styled.div<any>`
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+`;
+const FieldInputStyled = styled(FieldInput)`
+  width: 224px;
+  justify-content: space-between;
+  margin-bottom: 32px;
+`;
+
+type Props = {
+  instances: Instance[];
+  transferItem: TransferItemDetails | null;
+  minionPools: MinionPool[];
+  loadingInstances: boolean;
+  defaultSkipOsMorphing?: boolean | null;
+  deploying?: boolean;
+  onCancelClick: () => void;
+  onDeployClick: (opts: {
+    fields: Field[];
+    uploadedUserScripts: InstanceScript[];
+    removedUserScripts: InstanceScript[];
+    minionPoolMappings: { [instance: string]: string };
+  }) => void;
+  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void;
+};
+type State = {
+  fields: Field[];
+  selectedBarButton: string;
+  uploadedScripts: InstanceScript[];
+  removedScripts: InstanceScript[];
+  minionPoolMappings: { [instance: string]: string };
+};
+
+@observer
+class ReplicaDeploymentOptions extends React.Component<Props, State> {
+  state: State = {
+    fields: [],
+    selectedBarButton: "options",
+    uploadedScripts: [],
+    removedScripts: [],
+    minionPoolMappings: {},
+  };
+
+  scrollableRef!: HTMLElement;
+
+  UNSAFE_componentWillMount() {
+    const mappings =
+      this.props.transferItem?.instance_osmorphing_minion_pool_mappings || {};
+
+    this.setState({
+      fields: replicaDeploymentFields.map(f =>
+        f.name === "skip_os_morphing"
+          ? { ...f, value: this.props.defaultSkipOsMorphing || null }
+          : f
+      ),
+      minionPoolMappings: { ...mappings },
+    });
+  }
+
+  componentDidMount() {
+    KeyboardManager.onEnter(
+      "deployment-options",
+      () => {
+        this.deploy();
+      },
+      2
+    );
+  }
+
+  componentDidUpdate(_: Props, prevState: State) {
+    if (prevState.selectedBarButton !== this.state.selectedBarButton) {
+      if (this.props.onResizeUpdate) {
+        this.props.onResizeUpdate(this.scrollableRef, 0);
+      }
+    }
+  }
+
+  componentWillUnmount() {
+    KeyboardManager.removeKeyDown("deployment-options");
+  }
+
+  deploy() {
+    this.props.onDeployClick({
+      fields: this.state.fields,
+      uploadedUserScripts: this.state.uploadedScripts,
+      removedUserScripts: this.state.removedScripts,
+      minionPoolMappings: this.state.minionPoolMappings,
+    });
+  }
+
+  handleValueChange(field: Field, value: boolean) {
+    this.setState(prevState => {
+      const fields = prevState.fields.map(f => {
+        const newField = { ...f };
+        if (f.name === field.name) {
+          newField.value = value;
+        }
+        return newField;
+      });
+
+      return { fields };
+    });
+  }
+
+  handleCancelScript(global: string | null, instanceName: string | null) {
+    this.setState(prevState => ({
+      uploadedScripts: prevState.uploadedScripts.filter(s =>
+        global ? s.global !== global : s.instanceId !== instanceName
+      ),
+    }));
+  }
+
+  handleScriptUpload(script: InstanceScript) {
+    this.setState(prevState => ({
+      uploadedScripts: [...prevState.uploadedScripts, script],
+    }));
+  }
+
+  handleScriptRemove(script: InstanceScript) {
+    this.setState(prevState => ({
+      removedScripts: [...prevState.removedScripts, script],
+    }));
+  }
+
+  renderField(field: Field) {
+    return (
+      <FieldInputStyled
+        width={224}
+        key={field.name}
+        name={field.name}
+        type={field.type}
+        value={field.value || field.default}
+        minimum={field.minimum}
+        maximum={field.maximum}
+        layout="page"
+        label={field.label || LabelDictionary.get(field.name)}
+        onChange={value => this.handleValueChange(field, value)}
+        description={LabelDictionary.getDescription(field.name)}
+      />
+    );
+  }
+
+  renderMinionPoolMappings() {
+    const minionPools = this.props.minionPools.filter(
+      m => m.endpoint_id === this.props.transferItem?.destination_endpoint_id
+    );
+    if (!minionPools.length) {
+      return null;
+    }
+
+    const properties: Field[] = this.props.instances.map(instance => ({
+      name: instance.instance_name || instance.id,
+      label: instance.name,
+      type: "string",
+      enum: minionPools.map(minionPool => ({
+        name: minionPool.name,
+        id: minionPool.id,
+      })),
+    }));
+
+    return (
+      <FieldInputStyled
+        width={500}
+        style={{ marginBottom: "64px" }}
+        name={INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS}
+        type="object"
+        valueCallback={field =>
+          this.state.minionPoolMappings &&
+          this.state.minionPoolMappings[field.name]
+        }
+        layout="page"
+        label="Instance OSMorphing Minion Pool Mappings"
+        onChange={(value, field) =>
+          this.setState(prevState => {
+            const minionPoolMappings = { ...prevState.minionPoolMappings };
+            minionPoolMappings[field!.name] = value;
+            return { minionPoolMappings };
+          })
+        }
+        properties={properties}
+        labelRenderer={(propName: string) =>
+          propName.indexOf("/") > -1
+            ? propName.split("/")[propName.split("/").length - 1]
+            : propName
+        }
+      />
+    );
+  }
+
+  renderOptions() {
+    return (
+      <>
+        <Form>{this.state.fields.map(field => this.renderField(field))}</Form>
+        {this.renderMinionPoolMappings()}
+      </>
+    );
+  }
+
+  renderScripts() {
+    return (
+      <WizardScripts
+        instances={this.props.instances}
+        loadingInstances={this.props.loadingInstances}
+        onScriptUpload={s => {
+          this.handleScriptUpload(s);
+        }}
+        onScriptDataRemove={s => {
+          this.handleScriptRemove(s);
+        }}
+        onCancelScript={(g, i) => {
+          this.handleCancelScript(g, i);
+        }}
+        uploadedScripts={this.state.uploadedScripts}
+        removedScripts={this.state.removedScripts}
+        userScriptData={this.props.transferItem?.user_scripts}
+        scrollableRef={(r: HTMLElement) => {
+          this.scrollableRef = r;
+        }}
+        layout="modal"
+      />
+    );
+  }
+
+  renderBody() {
+    const Body =
+      this.state.selectedBarButton === "options" ? OptionsBody : ScriptsBody;
+
+    return (
+      <Body>
+        <ToggleButtonBar
+          items={[
+            { label: "Options", value: "options" },
+            { label: "User Scripts", value: "script" },
+          ]}
+          selectedValue={this.state.selectedBarButton}
+          onChange={item => {
+            this.setState({ selectedBarButton: item.value });
+          }}
+          style={{ marginBottom: "32px" }}
+        />
+        {this.state.selectedBarButton === "options"
+          ? this.renderOptions()
+          : this.renderScripts()}
+      </Body>
+    );
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        <Image />
+        {this.renderBody()}
+        <Buttons>
+          <Button secondary onClick={this.props.onCancelClick}>
+            Cancel
+          </Button>
+          {this.props.deploying ? (
+            <LoadingButton>Deploying ...</LoadingButton>
+          ) : (
+            <Button
+              onClick={() => {
+                this.deploy();
+              }}
+            >
+              Deploy
+            </Button>
+          )}
+        </Buttons>
+      </Wrapper>
+    );
+  }
+}
+
+export default ReplicaDeploymentOptions;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 15 - 0
src/components/modules/TransferModule/ReplicaDeploymentOptions/images/replica-deployment.svg


+ 6 - 0
src/components/modules/TransferModule/ReplicaDeploymentOptions/package.json

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

+ 33 - 0
src/components/modules/TransferModule/ReplicaDeploymentOptions/replicaDeploymentFields.ts

@@ -0,0 +1,33 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import { Field } from "@src/@types/Field";
+
+const replicaDeploymentFields: Field[] = [
+  {
+    name: "clone_disks",
+    type: "boolean",
+    value: true,
+  },
+  {
+    name: "force",
+    type: "boolean",
+  },
+  {
+    name: "skip_os_morphing",
+    type: "boolean",
+  },
+];
+
+export default replicaDeploymentFields;

+ 25 - 0
src/components/modules/TransferModule/ReplicaDeploymentOptions/story.tsx

@@ -0,0 +1,25 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import React from "react";
+import { storiesOf } from "@storybook/react";
+import ReplicaDeploymentOptions from ".";
+
+const props: any = {};
+storiesOf("ReplicaDeploymentOptions", module).add("default", () => (
+  // eslint-disable-next-line react/jsx-props-no-spreading
+  <div style={{ width: "562px" }}>
+    <ReplicaDeploymentOptions {...props} />
+  </div>
+));

+ 600 - 0
src/components/smart/DeploymentDetailsPage/DeploymentDetailsPage.tsx

@@ -0,0 +1,600 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import { observer } from "mobx-react";
+import React from "react";
+import styled from "styled-components";
+
+import { getTransferItemTitle } from "@src/@types/MainItem";
+import DetailsContentHeader from "@src/components/modules/DetailsModule/DetailsContentHeader";
+import DetailsPageHeader from "@src/components/modules/DetailsModule/DetailsPageHeader";
+import DetailsTemplate from "@src/components/modules/TemplateModule/DetailsTemplate";
+import DeploymentDetailsContent from "@src/components/modules/TransferModule/DeploymentDetailsContent";
+import ReplicaDeploymentOptions from "@src/components/modules/TransferModule/ReplicaDeploymentOptions";
+import TransferItemModal from "@src/components/modules/TransferModule/TransferItemModal";
+import { ThemePalette } from "@src/components/Theme";
+import AlertModal from "@src/components/ui/AlertModal";
+import Modal from "@src/components/ui/Modal";
+import { providerTypes } from "@src/constants";
+import endpointStore from "@src/stores/EndpointStore";
+import instanceStore from "@src/stores/InstanceStore";
+import deploymentStore from "@src/stores/DeploymentStore";
+import minionPoolStore from "@src/stores/MinionPoolStore";
+import networkStore from "@src/stores/NetworkStore";
+import notificationStore from "@src/stores/NotificationStore";
+import providerStore from "@src/stores/ProviderStore";
+import userStore from "@src/stores/UserStore";
+import configLoader from "@src/utils/Config";
+
+import deploymentImage from "./images/deployment.svg";
+
+import type { Field } from "@src/@types/Field";
+import type { InstanceScript } from "@src/@types/Instance";
+const Wrapper = styled.div<any>``;
+
+type Props = {
+  match: any;
+  history: any;
+};
+type State = {
+  showDeleteDeploymentConfirmation: boolean;
+  showCancelConfirmation: boolean;
+  showForceCancelConfirmation: boolean;
+  showEditModal: boolean;
+  showFromReplicaModal: boolean;
+  pausePolling: boolean;
+  initialLoading: boolean;
+  deploying: boolean;
+};
+@observer
+class DeploymentDetailsPage extends React.Component<Props, State> {
+  state: State = {
+    showDeleteDeploymentConfirmation: false,
+    showCancelConfirmation: false,
+    showForceCancelConfirmation: false,
+    showEditModal: false,
+    showFromReplicaModal: false,
+    pausePolling: false,
+    initialLoading: true,
+    deploying: false,
+  };
+
+  stopPolling: boolean | null = null;
+
+  timeoutRef: any = null;
+
+  componentDidMount() {
+    document.title = "Deployment Details";
+
+    this.loadDeploymentAndPollData();
+  }
+
+  UNSAFE_componentWillReceiveProps(newProps: any) {
+    if (newProps.match.params.id === this.props.match.params.id) {
+      return;
+    }
+    this.timeoutRef && clearTimeout(this.timeoutRef);
+    deploymentStore.cancelDeploymentDetails();
+    deploymentStore.clearDetails();
+    endpointStore.getEndpoints();
+    this.loadDeploymentAndPollData();
+  }
+
+  componentWillUnmount() {
+    deploymentStore.cancelDeploymentDetails();
+    deploymentStore.clearDetails();
+    this.stopPolling = true;
+  }
+
+  getStatus() {
+    return deploymentStore.deploymentDetails?.last_execution_status;
+  }
+
+  async loadDeploymentAndPollData() {
+    const loadDeployment = async () => {
+      await endpointStore.getEndpoints({ showLoading: true });
+      this.setState({ initialLoading: false });
+      await this.loadDeploymentWithInstances({
+        deploymentId: this.props.match.params.id,
+        cache: true,
+        onDetailsLoaded: async () => {
+          const details = deploymentStore.deploymentDetails;
+          if (!details) {
+            return;
+          }
+          const sourceEndpoint = endpointStore.endpoints.find(
+            e => e.id === details.origin_endpoint_id
+          );
+          const destinationEndpoint = endpointStore.endpoints.find(
+            e => e.id === details.destination_endpoint_id
+          );
+          if (!sourceEndpoint || !destinationEndpoint) {
+            return;
+          }
+          const loadOptions = async (optionsType: "source" | "destination") => {
+            const providerName =
+              optionsType === "source"
+                ? sourceEndpoint.type
+                : destinationEndpoint.type;
+            // This allows the values to be displayed with their allocated names instead of their IDs
+            await providerStore.loadOptionsSchema({
+              providerName,
+              optionsType,
+              useCache: true,
+              quietError: true,
+            });
+            const getOptionsValuesConfig = {
+              optionsType,
+              endpointId:
+                optionsType === "source"
+                  ? details.origin_endpoint_id
+                  : details.destination_endpoint_id,
+              providerName,
+              useCache: true,
+              quietError: true,
+              allowMultiple: true,
+            };
+            // For some providers, the API doesn't return the required fields values
+            // if those required fields are sent in env data,
+            // so to retrieve those values a request without env data must be made
+            await providerStore.getOptionsValues(getOptionsValuesConfig);
+            await providerStore.getOptionsValues({
+              ...getOptionsValuesConfig,
+              envData:
+                optionsType === "source"
+                  ? details.source_environment
+                  : details.destination_environment,
+            });
+          };
+
+          await Promise.all([
+            loadOptions("source"),
+            loadOptions("destination"),
+          ]);
+        },
+      });
+    };
+    await loadDeployment();
+    this.pollData();
+  }
+
+  async loadDeploymentWithInstances(options: {
+    deploymentId: string;
+    cache: boolean;
+    onDetailsLoaded?: () => void;
+  }) {
+    await deploymentStore.getDeployment(options.deploymentId, {
+      showLoading: true,
+    });
+    const details = deploymentStore.deploymentDetails;
+    if (!details) {
+      return;
+    }
+    if (options.onDetailsLoaded) {
+      options.onDetailsLoaded();
+    }
+    if (details.origin_minion_pool_id || details.destination_minion_pool_id) {
+      minionPoolStore.loadMinionPools();
+    }
+
+    await providerStore.loadProviders();
+
+    const targetEndpoint = endpointStore.endpoints.find(
+      e => e.id === details.destination_endpoint_id
+    );
+    const hasStorageMap = targetEndpoint
+      ? providerStore.providers && providerStore.providers[targetEndpoint.type]
+        ? !!providerStore.providers[targetEndpoint.type].types.find(
+            t => t === providerTypes.STORAGE
+          )
+        : false
+      : false;
+    if (hasStorageMap) {
+      endpointStore.loadStorage(
+        details.destination_endpoint_id,
+        details.destination_environment
+      );
+    }
+
+    networkStore.loadNetworks(
+      details.destination_endpoint_id,
+      details.destination_environment,
+      {
+        quietError: true,
+        cache: options.cache,
+      }
+    );
+
+    instanceStore.loadInstancesDetails({
+      endpointId: details.origin_endpoint_id,
+      instances: details.instances.map(n => ({ id: n })),
+      cache: options.cache,
+      quietError: false,
+      env: details.source_environment,
+      targetProvider: targetEndpoint?.type,
+    });
+  }
+
+  handleUserItemClick(item: { value: string }) {
+    switch (item.value) {
+      case "signout":
+        userStore.logout();
+        break;
+      default:
+    }
+  }
+
+  handleDeleteDeploymentClick() {
+    this.setState({ showDeleteDeploymentConfirmation: true });
+  }
+
+  handleDeleteDeploymentConfirmation() {
+    this.setState({ showDeleteDeploymentConfirmation: false });
+    this.props.history.push("/deployments");
+    if (deploymentStore.deploymentDetails) {
+      deploymentStore.delete(deploymentStore.deploymentDetails.id);
+    }
+  }
+
+  handleCloseDeleteDeploymentConfirmation() {
+    this.setState({ showDeleteDeploymentConfirmation: false });
+  }
+
+  handleCancelDeploymentClick(force?: boolean) {
+    if (force) {
+      this.setState({ showForceCancelConfirmation: true });
+    } else {
+      this.setState({ showCancelConfirmation: true });
+    }
+  }
+
+  handleRecreateClick() {
+    if (!deploymentStore.deploymentDetails?.replica_id) {
+      this.setState({ showEditModal: true, pausePolling: true });
+      return;
+    }
+    this.setState({ showFromReplicaModal: true, pausePolling: true });
+  }
+
+  handleCloseFromReplicaModal() {
+    this.setState({ showFromReplicaModal: false, pausePolling: false });
+  }
+
+  handleCloseCancelConfirmation() {
+    this.setState({
+      showCancelConfirmation: false,
+      showForceCancelConfirmation: false,
+    });
+  }
+
+  async handleCancelConfirmation(force?: boolean) {
+    this.setState({
+      showCancelConfirmation: false,
+      showForceCancelConfirmation: false,
+    });
+    if (!deploymentStore.deploymentDetails) {
+      return;
+    }
+    await deploymentStore.cancel(deploymentStore.deploymentDetails.id, force);
+    if (force) {
+      notificationStore.alert("Force Canceled", "success");
+    } else {
+      notificationStore.alert("Canceled", "success");
+    }
+  }
+
+  async recreateFromReplica(opts: {
+    fields: Field[];
+    uploadedUserScripts: InstanceScript[];
+    removedUserScripts: InstanceScript[];
+    minionPoolMappings: { [instance: string]: string };
+  }) {
+    const {
+      fields,
+      uploadedUserScripts,
+      removedUserScripts,
+      minionPoolMappings,
+    } = opts;
+    const replicaId = deploymentStore.deploymentDetails?.replica_id;
+    if (!replicaId) {
+      return;
+    }
+
+    this.setState({ deploying: true });
+    try {
+      const deployment = await this.deploy({
+        replicaId,
+        fields,
+        uploadedUserScripts,
+        removedUserScripts,
+        minionPoolMappings,
+      });
+      this.props.history.push(`/deployments/${deployment.id}/tasks`);
+    } finally {
+      this.setState({ deploying: false });
+    }
+    this.handleCloseFromReplicaModal();
+  }
+
+  async deploy(opts: {
+    replicaId: string;
+    fields: Field[];
+    uploadedUserScripts: InstanceScript[];
+    removedUserScripts: InstanceScript[];
+    minionPoolMappings: { [instance: string]: string };
+  }) {
+    const {
+      replicaId,
+      fields,
+      uploadedUserScripts,
+      removedUserScripts,
+      minionPoolMappings,
+    } = opts;
+    const deployment = await deploymentStore.deployReplica({
+      replicaId,
+      fields,
+      uploadedUserScripts,
+      removedUserScripts,
+      userScriptData: deploymentStore.deploymentDetails?.user_scripts,
+      minionPoolMappings,
+    });
+    return deployment;
+  }
+
+  async pollData() {
+    if (this.state.pausePolling || this.stopPolling) {
+      return;
+    }
+    await deploymentStore.getDeployment(this.props.match.params.id, {
+      showLoading: false,
+      skipLog: true,
+    });
+    this.timeoutRef = setTimeout(() => {
+      this.pollData();
+    }, configLoader.config.requestPollTimeout);
+  }
+
+  closeEditModal() {
+    this.setState({ showEditModal: false, pausePolling: false }, () => {
+      this.pollData();
+    });
+  }
+
+  handleEditReplicaReload() {
+    this.loadDeploymentWithInstances({
+      deploymentId: this.props.match.params.id,
+      cache: false,
+    });
+  }
+
+  handleUpdateComplete(redirectTo: string) {
+    this.props.history.push(redirectTo);
+  }
+
+  renderEditModal() {
+    const sourceEndpoint = endpointStore.endpoints.find(
+      e =>
+        deploymentStore.deploymentDetails &&
+        e.id === deploymentStore.deploymentDetails.origin_endpoint_id
+    );
+    const destinationEndpoint = endpointStore.endpoints.find(
+      e =>
+        deploymentStore.deploymentDetails &&
+        e.id === deploymentStore.deploymentDetails.destination_endpoint_id
+    );
+
+    if (
+      !this.state.showEditModal ||
+      !deploymentStore.deploymentDetails ||
+      !destinationEndpoint ||
+      !sourceEndpoint
+    ) {
+      return null;
+    }
+
+    return (
+      <TransferItemModal
+        type="deployment"
+        isOpen
+        onRequestClose={() => {
+          this.closeEditModal();
+        }}
+        onUpdateComplete={url => {
+          this.handleUpdateComplete(url);
+        }}
+        sourceEndpoint={sourceEndpoint}
+        replica={deploymentStore.deploymentDetails}
+        destinationEndpoint={destinationEndpoint}
+        instancesDetails={instanceStore.instancesDetails}
+        instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+        networks={networkStore.networks}
+        networksLoading={networkStore.loading}
+        onReloadClick={() => {
+          this.handleEditReplicaReload();
+        }}
+      />
+    );
+  }
+
+  render() {
+    const dropdownActions = [
+      {
+        label: "Cancel",
+        disabled:
+          this.getStatus() !== "RUNNING" &&
+          this.getStatus() !== "AWAITING_MINION_ALLOCATIONS",
+        hidden: this.getStatus() === "CANCELLING",
+        action: () => {
+          this.handleCancelDeploymentClick();
+        },
+      },
+      {
+        label: "Force Cancel",
+        hidden: this.getStatus() !== "CANCELLING",
+        action: () => {
+          this.handleCancelDeploymentClick(true);
+        },
+      },
+      {
+        label: "Recreate Deployment",
+        color: ThemePalette.primary,
+        action: () => {
+          this.handleRecreateClick();
+        },
+      },
+      {
+        label: "Delete Deployment",
+        color: ThemePalette.alert,
+        action: () => {
+          this.handleDeleteDeploymentClick();
+        },
+      },
+    ];
+
+    return (
+      <Wrapper>
+        <DetailsTemplate
+          pageHeaderComponent={
+            <DetailsPageHeader
+              user={userStore.loggedUser}
+              onUserItemClick={item => {
+                this.handleUserItemClick(item);
+              }}
+            />
+          }
+          contentHeaderComponent={
+            <DetailsContentHeader
+              statusPill={
+                deploymentStore.deploymentDetails?.last_execution_status
+              }
+              itemTitle={getTransferItemTitle(deploymentStore.deploymentDetails)}
+              itemType="deployment"
+              itemDescription={deploymentStore.deploymentDetails?.description}
+              backLink="/deployments"
+              typeImage={deploymentImage}
+              dropdownActions={dropdownActions}
+              primaryInfoPill
+            />
+          }
+          contentComponent={
+            <DeploymentDetailsContent
+              item={deploymentStore.deploymentDetails}
+              itemId={this.props.match.params.id}
+              instancesDetails={instanceStore.instancesDetails}
+              instancesDetailsLoading={
+                instanceStore.loadingInstancesDetails ||
+                endpointStore.storageLoading ||
+                providerStore.providersLoading
+              }
+              storageBackends={endpointStore.storageBackends}
+              networks={networkStore.networks}
+              sourceSchema={providerStore.sourceSchema}
+              sourceSchemaLoading={
+                providerStore.sourceSchemaLoading ||
+                providerStore.sourceOptionsPrimaryLoading ||
+                providerStore.sourceOptionsSecondaryLoading
+              }
+              destinationSchema={providerStore.destinationSchema}
+              destinationSchemaLoading={
+                providerStore.destinationSchemaLoading ||
+                providerStore.destinationOptionsPrimaryLoading ||
+                providerStore.destinationOptionsSecondaryLoading
+              }
+              endpoints={endpointStore.endpoints}
+              page={this.props.match.params.page || ""}
+              minionPools={minionPoolStore.minionPools}
+              detailsLoading={
+                deploymentStore.detailsLoading ||
+                endpointStore.loading ||
+                minionPoolStore.loadingMinionPools ||
+                this.state.initialLoading
+              }
+              onDeleteDeploymentClick={() => {
+                this.handleDeleteDeploymentClick();
+              }}
+            />
+          }
+        />
+        <AlertModal
+          isOpen={this.state.showDeleteDeploymentConfirmation}
+          title="Delete Deployment?"
+          message="Are you sure you want to delete this deployment?"
+          extraMessage="Deleting a Coriolis Deployment is permanent!"
+          onConfirmation={() => {
+            this.handleDeleteDeploymentConfirmation();
+          }}
+          onRequestClose={() => {
+            this.handleCloseDeleteDeploymentConfirmation();
+          }}
+        />
+        <AlertModal
+          isOpen={this.state.showCancelConfirmation}
+          title="Cancel Deployment?"
+          message="Are you sure you want to cancel the deployment?"
+          extraMessage=" "
+          onConfirmation={() => {
+            this.handleCancelConfirmation();
+          }}
+          onRequestClose={() => {
+            this.handleCloseCancelConfirmation();
+          }}
+        />
+        <AlertModal
+          isOpen={this.state.showForceCancelConfirmation}
+          title="Force Cancel Deployment?"
+          message="Are you sure you want to force cancel the deployment?"
+          extraMessage={`
+The deployment 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
+            title="Recreate Deployment from Replica"
+            onRequestClose={() => {
+              this.handleCloseFromReplicaModal();
+            }}
+          >
+            <ReplicaDeploymentOptions
+              transferItem={deploymentStore.deploymentDetails}
+              minionPools={minionPoolStore.minionPools}
+              onCancelClick={() => {
+                this.handleCloseFromReplicaModal();
+              }}
+              onDeployClick={opts => {
+                this.recreateFromReplica(opts);
+              }}
+              instances={instanceStore.instancesDetails}
+              loadingInstances={instanceStore.loadingInstancesDetails}
+              defaultSkipOsMorphing={deploymentStore.getDefaultSkipOsMorphing(
+                deploymentStore.deploymentDetails
+              )}
+              deploying={this.state.deploying}
+            />
+          </Modal>
+        ) : null}
+        {this.renderEditModal()}
+      </Wrapper>
+    );
+  }
+}
+
+export default DeploymentDetailsPage;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 10 - 0
src/components/smart/DeploymentDetailsPage/images/deployment.svg


+ 6 - 0
src/components/smart/DeploymentDetailsPage/package.json

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

+ 377 - 0
src/components/smart/DeploymentsPage/DeploymentsPage.tsx

@@ -0,0 +1,377 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import React from "react";
+import styled from "styled-components";
+import { observer } from "mobx-react";
+
+import MainTemplate from "@src/components/modules/TemplateModule/MainTemplate";
+import Navigation from "@src/components/modules/NavigationModule/Navigation";
+import FilterList from "@src/components/ui/Lists/FilterList";
+import PageHeader from "@src/components/smart/PageHeader";
+import AlertModal from "@src/components/ui/AlertModal";
+
+import projectStore from "@src/stores/ProjectStore";
+import deploymentStore from "@src/stores/DeploymentStore";
+import endpointStore from "@src/stores/EndpointStore";
+import notificationStore from "@src/stores/NotificationStore";
+import configLoader from "@src/utils/Config";
+
+import { ThemePalette } from "@src/components/Theme";
+import replicaDeploymentFields from "@src/components/modules/TransferModule/ReplicaDeploymentOptions/replicaDeploymentFields";
+import { DeploymentItem } from "@src/@types/MainItem";
+import userStore from "@src/stores/UserStore";
+import TransferListItem from "@src/components/modules/TransferModule/TransferListItem";
+import deploymentLargeImage from "./images/deployment-large.svg";
+import deploymentItemImage from "./images/deployment.svg";
+
+const Wrapper = styled.div<any>``;
+
+type State = {
+  selectedDeployments: DeploymentItem[];
+  modalIsOpen: boolean;
+  showDeleteDeploymentModal: boolean;
+  showCancelDeploymentModal: boolean;
+  showRecreateDeploymentsModal: boolean;
+};
+@observer
+class DeploymentsPage extends React.Component<{ history: any }, State> {
+  state: State = {
+    showDeleteDeploymentModal: false,
+    showCancelDeploymentModal: false,
+    showRecreateDeploymentsModal: false,
+    selectedDeployments: [],
+    modalIsOpen: false,
+  };
+
+  pollTimeout = 0;
+
+  stopPolling = false;
+
+  componentDidMount() {
+    document.title = "Coriolis Deployments";
+
+    projectStore.getProjects();
+    endpointStore.getEndpoints({ showLoading: true });
+    userStore.getAllUsers({
+      showLoading: userStore.users.length === 0,
+      quietError: true,
+    });
+
+    this.stopPolling = false;
+    this.pollData();
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this.pollTimeout);
+    this.stopPolling = true;
+  }
+
+  getEndpoint(endpointId: string) {
+    return endpointStore.endpoints.find(endpoint => endpoint.id === endpointId);
+  }
+
+  getFilterItems() {
+    return [
+      { label: "All", value: "all" },
+      { label: "Running", value: "RUNNING" },
+      { label: "Error", value: "ERROR" },
+      { label: "Completed", value: "COMPLETED" },
+      { label: "Canceled", value: "CANCELED" },
+    ];
+  }
+
+  getStatus(deploymentId: string): string {
+    const deployment = deploymentStore.deployments.find(m => m.id === deploymentId);
+    return deployment ? deployment.last_execution_status : "";
+  }
+
+  handleProjectChange() {
+    endpointStore.getEndpoints({ showLoading: true });
+    deploymentStore.getDeployments({ showLoading: true });
+  }
+
+  handleReloadButtonClick() {
+    projectStore.getProjects();
+    endpointStore.getEndpoints({ showLoading: true });
+    deploymentStore.getDeployments({ showLoading: true });
+    userStore.getAllUsers({ showLoading: true, quietError: true });
+  }
+
+  handleItemClick(item: DeploymentItem) {
+    if (item.last_execution_status === "RUNNING") {
+      this.props.history.push(`/deployments/${item.id}/tasks`);
+    } else {
+      this.props.history.push(`/deployments/${item.id}`);
+    }
+  }
+
+  deleteSelectedDeployments() {
+    this.state.selectedDeployments.forEach(deployment => {
+      deploymentStore.delete(deployment.id);
+    });
+    this.setState({ showDeleteDeploymentModal: false });
+  }
+
+  cancelSelectedDeployments() {
+    this.state.selectedDeployments.forEach(deployment => {
+      const status = this.getStatus(deployment.id);
+      if (status === "RUNNING" || status === "AWAITING_MINION_ALLOCATIONS") {
+        deploymentStore.cancel(deployment.id);
+      }
+    });
+    notificationStore.alert("Canceling deployments");
+    this.setState({ showCancelDeploymentModal: false });
+  }
+
+  async recreateDeployments() {
+    notificationStore.alert("Recreating deployments");
+    this.setState({ showRecreateDeploymentsModal: false });
+
+    await Promise.all(
+      this.state.selectedDeployments.map(async deployment => {
+        if (deployment.replica_id) {
+          await deploymentStore.deployReplica({
+            replicaId: deployment.replica_id,
+            fields: replicaDeploymentFields,
+            uploadedUserScripts: [],
+            removedUserScripts: [],
+            userScriptData: deployment.user_scripts,
+            minionPoolMappings:
+              deployment.instance_osmorphing_minion_pool_mappings || {},
+          });
+        } else {
+          await deploymentStore.recreateFullCopy(deployment as any);
+        }
+      })
+    );
+
+    deploymentStore.getDeployments();
+  }
+
+  handleEmptyListButtonClick() {
+    this.props.history.push("/wizard/deployment");
+  }
+
+  handleModalOpen() {
+    this.setState({ modalIsOpen: true });
+  }
+
+  handleModalClose() {
+    this.setState({ modalIsOpen: false }, () => {
+      this.pollData();
+    });
+  }
+
+  searchText(item: DeploymentItem, text: string) {
+    let result = false;
+    if (item.instances[0].toLowerCase().indexOf(text) > -1) {
+      return true;
+    }
+    if (item.notes && item.notes.toLowerCase().indexOf(text) > -1) {
+      return true;
+    }
+    if (item.destination_environment) {
+      Object.keys(item.destination_environment).forEach(prop => {
+        if (
+          item.destination_environment[prop]?.toLowerCase &&
+          item.destination_environment[prop].toLowerCase().indexOf(text) > -1
+        ) {
+          result = true;
+        }
+      });
+    }
+    return result;
+  }
+
+  itemFilterFunction(
+    item: DeploymentItem,
+    filterStatus?: string | null,
+    filterText?: string
+  ) {
+    if (
+      (filterStatus !== "all" && item.last_execution_status !== filterStatus) ||
+      !this.searchText(
+        item,
+        (filterText?.toLowerCase && filterText.toLowerCase()) || ""
+      )
+    ) {
+      return false;
+    }
+
+    return true;
+  }
+
+  async pollData() {
+    if (this.state.modalIsOpen || this.stopPolling) {
+      return;
+    }
+
+    await Promise.all([
+      deploymentStore.getDeployments({ skipLog: true }),
+      endpointStore.getEndpoints({ skipLog: true }),
+      userStore.getAllUsers({ skipLog: true, quietError: true }),
+    ]);
+    this.pollTimeout = window.setTimeout(() => {
+      this.pollData();
+    }, configLoader.config.requestPollTimeout);
+  }
+
+  render() {
+    let atLeaseOneIsRunning = false;
+    this.state.selectedDeployments.forEach(deployment => {
+      const status = this.getStatus(deployment.id);
+      atLeaseOneIsRunning =
+        atLeaseOneIsRunning ||
+        status === "RUNNING" ||
+        status === "AWAITING_MINION_ALLOCATIONS";
+    });
+    const BulkActions = [
+      {
+        label: "Cancel",
+        disabled: !atLeaseOneIsRunning,
+        action: () => {
+          this.setState({ showCancelDeploymentModal: true });
+        },
+      },
+      {
+        label: "Recreate Deployments",
+        disabled: atLeaseOneIsRunning,
+        color: ThemePalette.primary,
+        action: () => {
+          this.setState({ showRecreateDeploymentsModal: true });
+        },
+      },
+      {
+        label: "Delete Deployments",
+        color: ThemePalette.alert,
+        action: () => {
+          this.setState({ showDeleteDeploymentModal: true });
+        },
+      },
+    ];
+
+    return (
+      <Wrapper>
+        <MainTemplate
+          navigationComponent={<Navigation currentPage="deployments" />}
+          listComponent={
+            <FilterList
+              filterItems={this.getFilterItems()}
+              selectionLabel="deployment"
+              loading={deploymentStore.loading}
+              items={deploymentStore.deployments}
+              onItemClick={item => {
+                this.handleItemClick(item);
+              }}
+              onReloadButtonClick={() => {
+                this.handleReloadButtonClick();
+              }}
+              itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
+              onSelectedItemsChange={selectedDeployments => {
+                this.setState({ selectedDeployments });
+              }}
+              dropdownActions={BulkActions}
+              renderItemComponent={options => (
+                <TransferListItem
+                  {...options}
+                  image={deploymentItemImage}
+                  endpointType={id => {
+                    const endpoint = this.getEndpoint(id);
+                    if (endpoint) {
+                      return endpoint.type;
+                    }
+                    if (endpointStore.loading) {
+                      return "Loading...";
+                    }
+                    return "Not Found";
+                  }}
+                  getUserName={id =>
+                    userStore.users.find(u => u.id === id)?.name
+                  }
+                  userNameLoading={userStore.allUsersLoading}
+                />
+              )}
+              emptyListImage={deploymentLargeImage}
+              emptyListMessage="It seems like you don't have any Deployments in this project."
+              emptyListExtraMessage="A Coriolis Deployment is a full virtual machine deployment between two cloud endpoints."
+              emptyListButtonLabel="Create a Deployment"
+              onEmptyListButtonClick={() => {
+                this.handleEmptyListButtonClick();
+              }}
+            />
+          }
+          headerComponent={
+            <PageHeader
+              title="Coriolis Deployments"
+              onProjectChange={() => {
+                this.handleProjectChange();
+              }}
+              onModalOpen={() => {
+                this.handleModalOpen();
+              }}
+              onModalClose={() => {
+                this.handleModalClose();
+              }}
+            />
+          }
+        />
+        {this.state.showDeleteDeploymentModal ? (
+          <AlertModal
+            isOpen
+            title="Delete Selected Deployments?"
+            message="Are you sure you want to delete the selected deployments?"
+            extraMessage="Deleting a Coriolis Deployment is permanent!"
+            onConfirmation={() => {
+              this.deleteSelectedDeployments();
+            }}
+            onRequestClose={() => {
+              this.setState({ showDeleteDeploymentModal: false });
+            }}
+          />
+        ) : null}
+        {this.state.showCancelDeploymentModal ? (
+          <AlertModal
+            isOpen
+            title="Cancel Selected Deployments?"
+            message="Are you sure you want to cancel the selected deployments?"
+            extraMessage="Canceling a Coriolis Deployment is permanent!"
+            onConfirmation={() => {
+              this.cancelSelectedDeployments();
+            }}
+            onRequestClose={() => {
+              this.setState({ showCancelDeploymentModal: false });
+            }}
+          />
+        ) : null}
+        {this.state.showRecreateDeploymentsModal ? (
+          <AlertModal
+            isOpen
+            title="Recreate Selected Deployments?"
+            message="Are you sure you want to recreate the selected deployments?"
+            extraMessage="Deployments created from replicas will be recreated using default options and regular deployments will be recreated using their original source and destination environment options."
+            onConfirmation={() => {
+              this.recreateDeployments();
+            }}
+            onRequestClose={() => {
+              this.setState({ showRecreateDeploymentsModal: false });
+            }}
+          />
+        ) : null}
+      </Wrapper>
+    );
+  }
+}
+
+export default DeploymentsPage;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 10 - 0
src/components/smart/DeploymentsPage/images/deployment-large.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 10 - 0
src/components/smart/DeploymentsPage/images/deployment.svg


+ 6 - 0
src/components/smart/DeploymentsPage/package.json

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

+ 369 - 0
src/sources/DeploymentSource.ts

@@ -0,0 +1,369 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import {
+  DeploymentItem,
+  DeploymentItemDetails,
+  DeploymentItemOptions,
+  UserScriptData,
+} from "@src/@types/MainItem";
+import { ProgressUpdate, Task } from "@src/@types/Task";
+import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from "@src/components/modules/WizardModule/WizardOptions";
+import { OptionsSchemaPlugin } from "@src/plugins";
+import DefaultOptionsSchemaPlugin from "@src/plugins/default/OptionsSchemaPlugin";
+import Api from "@src/utils/ApiCaller";
+import configLoader from "@src/utils/Config";
+
+import { sortTasks } from "./ReplicaSource";
+
+import type { InstanceScript } from "@src/@types/Instance";
+import type { Field } from "@src/@types/Field";
+import type { NetworkMap } from "@src/@types/Network";
+import type { Endpoint, StorageMap } from "@src/@types/Endpoint";
+
+class DeploymentSourceUtils {
+  static sortTaskUpdates(updates: ProgressUpdate[]) {
+    if (!updates) {
+      return;
+    }
+    updates.sort((a, b) => {
+      const sortNull = !a && b ? 1 : a && !b ? -1 : !a && !b ? 0 : false;
+      if (sortNull !== false) {
+        return sortNull;
+      }
+      return a.index - b.index;
+    });
+  }
+
+  static sortDeployments(deployments: any[]) {
+    deployments.sort(
+      (a: any, b: any) =>
+        new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+    );
+
+    deployments.forEach((deployment: { tasks: Task[] }) => {
+      sortTasks(deployment.tasks, DeploymentSourceUtils.sortTaskUpdates);
+    });
+  }
+}
+
+class DeploymentSource {
+  async getDeployments(skipLog?: boolean): Promise<DeploymentItem[]> {
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments`,
+      skipLog,
+    });
+    const deployments = response.data.deployments;
+    DeploymentSourceUtils.sortDeployments(deployments);
+    return deployments;
+  }
+
+  async getDeployment(
+    deploymentId: string,
+    skipLog?: boolean
+  ): Promise<DeploymentItemDetails> {
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments/${deploymentId}`,
+      skipLog,
+      cancelId: deploymentId,
+    });
+    const deployment = response.data.deployment;
+    sortTasks(deployment.tasks, DeploymentSourceUtils.sortTaskUpdates);
+    return deployment;
+  }
+
+  async recreateFullCopy(
+    deployment: DeploymentItemOptions
+  ): Promise<DeploymentItem> {
+    const {
+      // eslint-disable-next-line @typescript-eslint/naming-convention
+      origin_endpoint_id,
+      destination_endpoint_id,
+      destination_environment,
+      // eslint-disable-next-line @typescript-eslint/naming-convention
+      network_map,
+      instances,
+      storage_mappings,
+      notes,
+      destination_minion_pool_id,
+      // eslint-disable-next-line @typescript-eslint/naming-convention
+      origin_minion_pool_id,
+      instance_osmorphing_minion_pool_mappings,
+    } = deployment;
+
+    const payload: any = {
+      deployment: {
+        origin_endpoint_id,
+        destination_endpoint_id,
+        destination_environment,
+        network_map,
+        instances,
+        storage_mappings,
+        notes,
+        destination_minion_pool_id,
+        origin_minion_pool_id,
+        instance_osmorphing_minion_pool_mappings,
+      },
+    };
+
+    if (deployment.skip_os_morphing != null) {
+      payload.deployment.skip_os_morphing = deployment.skip_os_morphing;
+    }
+
+    if (deployment.source_environment) {
+      payload.deployment.source_environment = deployment.source_environment;
+    }
+
+    payload.deployment.shutdown_instances = Boolean(
+      deployment.shutdown_instances
+    );
+    payload.deployment.replication_count = deployment.replication_count || 2;
+
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments`,
+      method: "POST",
+      data: payload,
+    });
+    return response.data.deployment;
+  }
+
+  async recreate(opts: {
+    sourceEndpoint: Endpoint;
+    destEndpoint: Endpoint;
+    instanceNames: string[];
+    destEnv: { [prop: string]: any } | null;
+    updatedDestEnv: { [prop: string]: any } | null;
+    sourceEnv?: { [prop: string]: any } | null;
+    updatedSourceEnv?: { [prop: string]: any } | null;
+    storageMappings?: { [prop: string]: any } | null;
+    updatedStorageMappings: StorageMap[] | null;
+    defaultStorage?: { value: string | null; busType?: string | null };
+    updatedDefaultStorage?: { value: string | null; busType?: string | null };
+    networkMappings?: any;
+    updatedNetworkMappings: NetworkMap[] | null;
+    defaultSkipOsMorphing: boolean | null;
+    replicationCount?: number | null;
+    deployment: DeploymentItemDetails;
+    uploadedScripts: InstanceScript[];
+    removedScripts: InstanceScript[];
+  }): Promise<DeploymentItemDetails> {
+    const getValue = (fieldName: string): string | null => {
+      const updatedDestEnv =
+        opts.updatedDestEnv && opts.updatedDestEnv[fieldName];
+      return updatedDestEnv != null
+        ? updatedDestEnv
+        : opts.destEnv && opts.destEnv[fieldName];
+    };
+
+    const sourceParser = OptionsSchemaPlugin.for(opts.sourceEndpoint.type);
+    const destParser = OptionsSchemaPlugin.for(opts.destEndpoint.type);
+    const payload: any = {};
+
+    payload.deployment = {
+      origin_endpoint_id: opts.sourceEndpoint.id,
+      destination_endpoint_id: opts.destEndpoint.id,
+      shutdown_instances: Boolean(
+        opts.updatedDestEnv && opts.updatedDestEnv.shutdown_instances
+      ),
+      replication_count:
+        (opts.updatedDestEnv && opts.updatedDestEnv.replication_count) ||
+        opts.replicationCount ||
+        2,
+      instances: opts.instanceNames,
+      notes: opts.updatedDestEnv?.title || opts.deployment.notes || "",
+    };
+
+    const skipOsMorphingValue = getValue("skip_os_morphing");
+    if (skipOsMorphingValue != null) {
+      payload.deployment.skip_os_morphing = skipOsMorphingValue;
+    } else if (opts.defaultSkipOsMorphing != null) {
+      payload.deployment.skip_os_morphing = opts.defaultSkipOsMorphing;
+    }
+
+    if (
+      opts.networkMappings ||
+      (opts.updatedNetworkMappings && opts.updatedNetworkMappings.length)
+    ) {
+      payload.deployment.network_map = {
+        ...opts.networkMappings,
+        ...destParser.getNetworkMap(opts.updatedNetworkMappings),
+      };
+    }
+
+    if (
+      (opts.storageMappings && Object.keys(opts.storageMappings).length) ||
+      (opts.updatedStorageMappings && opts.updatedStorageMappings.length)
+    ) {
+      payload.deployment.storage_mappings = {
+        ...opts.storageMappings,
+        ...destParser.getStorageMap(
+          opts.updatedDefaultStorage || opts.defaultStorage,
+          opts.updatedStorageMappings
+        ),
+      };
+    }
+    const { deployment } = opts;
+    const sourceEnv: any = {
+      ...opts.sourceEnv,
+    };
+    const updatedSourceEnv = opts.updatedSourceEnv
+      ? sourceParser.getDestinationEnv(opts.updatedSourceEnv)
+      : {};
+    const sourceMinionPoolId =
+      opts?.updatedSourceEnv?.minion_pool_id || deployment.origin_minion_pool_id;
+    if (sourceMinionPoolId) {
+      payload.deployment.origin_minion_pool_id = sourceMinionPoolId;
+    }
+    payload.deployment.source_environment = {
+      ...sourceEnv,
+      ...updatedSourceEnv,
+    };
+
+    const destEnv: any = {
+      ...opts.destEnv,
+    };
+    const updatedDestEnv = opts.updatedDestEnv
+      ? sourceParser.getDestinationEnv(opts.updatedDestEnv)
+      : {};
+    const destMinionPoolId =
+      opts?.updatedDestEnv?.minion_pool_id ||
+      deployment.destination_minion_pool_id;
+    if (destMinionPoolId) {
+      payload.deployment.destination_minion_pool_id = destMinionPoolId;
+    }
+
+    const updatedDestEnvMappings =
+      updatedDestEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] || {};
+    const oldMappings =
+      deployment[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] || {};
+    const mergedMappings = { ...oldMappings, ...updatedDestEnvMappings };
+    if (Object.keys(mergedMappings).length) {
+      const newMappings: any = {};
+      Object.keys(mergedMappings).forEach(k => {
+        if (mergedMappings[k] !== null) {
+          newMappings[k] = mergedMappings[k];
+        }
+      });
+      payload.deployment[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] = newMappings;
+    }
+
+    delete updatedDestEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS];
+
+    payload.deployment.destination_environment = {
+      ...destEnv,
+      ...updatedDestEnv,
+    };
+
+    if (
+      opts.uploadedScripts?.length ||
+      opts.removedScripts?.length ||
+      deployment.user_scripts
+    ) {
+      payload.deployment.user_scripts =
+        new DefaultOptionsSchemaPlugin().getUserScripts(
+          opts.uploadedScripts || [],
+          opts.removedScripts || [],
+          deployment.user_scripts
+        );
+    }
+
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments`,
+      method: "POST",
+      data: payload,
+    });
+    return response.data.deployment;
+  }
+
+  async cancel(deploymentId: string, force?: boolean | null): Promise<string> {
+    const data: any = { cancel: null };
+    if (force) {
+      data.cancel = { force: true };
+    }
+    await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments/${deploymentId}/actions`,
+      method: "POST",
+      data,
+    });
+    return deploymentId;
+  }
+
+  async delete(deploymentId: string): Promise<string> {
+    await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments/${deploymentId}`,
+      method: "DELETE",
+    });
+    return deploymentId;
+  }
+
+  async migrateReplica(opts: {
+    replicaId: string;
+    options: Field[];
+    uploadedUserScripts: InstanceScript[];
+    removedUserScripts: InstanceScript[];
+    userScriptData: UserScriptData | null | undefined;
+    minionPoolMappings: { [instance: string]: string };
+  }): Promise<DeploymentItem> {
+    const {
+      replicaId,
+      options,
+      uploadedUserScripts,
+      removedUserScripts,
+      userScriptData,
+      minionPoolMappings,
+    } = opts;
+    const payload: any = {
+      deployment: {
+        replica_id: replicaId,
+      },
+    };
+    options.forEach(o => {
+      payload.deployment[o.name] = o.value || o.default || false;
+    });
+
+    if (
+      uploadedUserScripts.length ||
+      removedUserScripts.length ||
+      userScriptData
+    ) {
+      payload.deployment.user_scripts =
+        new DefaultOptionsSchemaPlugin().getUserScripts(
+          uploadedUserScripts,
+          removedUserScripts,
+          userScriptData
+        );
+    }
+
+    if (Object.keys(minionPoolMappings).length) {
+      const newMappings: any = {};
+      Object.keys(minionPoolMappings).forEach(k => {
+        if (minionPoolMappings[k] !== null) {
+          newMappings[k] = minionPoolMappings[k];
+        }
+      });
+      payload.deployment[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] = newMappings;
+    } else {
+      payload.deployment[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] = null;
+    }
+
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/deployments`,
+      method: "POST",
+      data: payload,
+    });
+    return response.data.deployment;
+  }
+}
+
+export default new DeploymentSource();

+ 198 - 0
src/stores/DeploymentStore.ts

@@ -0,0 +1,198 @@
+/*
+Copyright (C) 2024 Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+import { observable, action, runInAction } from "mobx";
+
+import type {
+  UpdateData,
+  DeploymentItem,
+  DeploymentItemDetails,
+  DeploymentItemOptions,
+  UserScriptData,
+} from "@src/@types/MainItem";
+import type { Field } from "@src/@types/Field";
+import type { Endpoint } from "@src/@types/Endpoint";
+import type { InstanceScript } from "@src/@types/Instance";
+import DeploymentSource from "@src/sources/DeploymentSource";
+import apiCaller from "@src/utils/ApiCaller";
+
+class DeploymentStore {
+  @observable deployments: DeploymentItem[] = [];
+
+  @observable deploymentDetails: DeploymentItemDetails | null = null;
+
+  @observable loading = true;
+
+  @observable detailsLoading = true;
+
+  deploymentsLoaded = false;
+
+  @action async getDeployments(options?: {
+    showLoading?: boolean;
+    skipLog?: boolean;
+  }) {
+    if ((options && options.showLoading) || !this.deploymentsLoaded) {
+      this.loading = true;
+    }
+
+    try {
+      const deployments = await DeploymentSource.getDeployments(
+        options && options.skipLog
+      );
+      runInAction(() => {
+        this.deployments = deployments;
+        this.loading = false;
+        this.deploymentsLoaded = true;
+      });
+    } catch (ex) {
+      runInAction(() => {
+        this.loading = false;
+      });
+      throw ex;
+    }
+  }
+
+  getDefaultSkipOsMorphing(deployment: DeploymentItemDetails | null) {
+    const tasks = deployment && deployment.tasks;
+    if (tasks && !tasks.find(t => t.task_type === "OS_MORPHING")) {
+      return true;
+    }
+    return null;
+  }
+
+  @action async recreateFullCopy(deployment: DeploymentItemOptions) {
+    return DeploymentSource.recreateFullCopy(deployment);
+  }
+
+  @action async recreate(opts: {
+    deployment: DeploymentItemDetails;
+    sourceEndpoint: Endpoint;
+    destEndpoint: Endpoint;
+    updateData: UpdateData;
+    defaultStorage: { value: string | null; busType?: string | null };
+    updatedDefaultStorage:
+      | { value: string | null; busType?: string | null }
+      | undefined;
+    replicationCount: number | null | undefined;
+  }): Promise<DeploymentItemDetails> {
+    const {
+      deployment,
+      sourceEndpoint,
+      destEndpoint,
+      updateData,
+      defaultStorage,
+      updatedDefaultStorage,
+      replicationCount,
+    } = opts;
+    const deploymentResult = await DeploymentSource.recreate({
+      sourceEndpoint,
+      destEndpoint,
+      deployment,
+      instanceNames: deployment.instances,
+      sourceEnv: deployment.source_environment,
+      updatedSourceEnv: updateData.source,
+      destEnv: deployment.destination_environment,
+      updatedDestEnv: updateData.destination,
+      storageMappings: deployment.storage_mappings,
+      updatedStorageMappings: updateData.storage,
+      defaultStorage,
+      updatedDefaultStorage,
+      networkMappings: deployment.network_map,
+      updatedNetworkMappings: updateData.network,
+      defaultSkipOsMorphing: this.getDefaultSkipOsMorphing(deployment),
+      replicationCount,
+      uploadedScripts: updateData.uploadedScripts,
+      removedScripts: updateData.removedScripts,
+    });
+    return deploymentResult;
+  }
+
+  @action async getDeployment(
+    deploymentId: string,
+    options?: { showLoading?: boolean; skipLog?: boolean }
+  ) {
+    if (options && options.showLoading) {
+      this.detailsLoading = true;
+    }
+
+    try {
+      const deployment = await DeploymentSource.getDeployment(
+        deploymentId,
+        options && options.skipLog
+      );
+      runInAction(() => {
+        this.deploymentDetails = deployment;
+        this.deployments = this.deployments.map(m =>
+          m.id === deployment.id ? deployment : m
+        );
+      });
+    } finally {
+      runInAction(() => {
+        this.detailsLoading = false;
+      });
+    }
+  }
+
+  @action async cancel(deploymentId: string, force?: boolean | null) {
+    await DeploymentSource.cancel(deploymentId, force);
+  }
+
+  @action async delete(deploymentId: string) {
+    await DeploymentSource.delete(deploymentId);
+    runInAction(() => {
+      this.deployments = this.deployments.filter(r => r.id !== deploymentId);
+    });
+  }
+
+  @action async deployReplica(opts: {
+    replicaId: string;
+    fields: Field[];
+    uploadedUserScripts: InstanceScript[];
+    removedUserScripts: InstanceScript[];
+    userScriptData: UserScriptData | null | undefined;
+    minionPoolMappings: { [instance: string]: string };
+  }) {
+    const {
+      replicaId,
+      fields: options,
+      uploadedUserScripts,
+      removedUserScripts,
+      userScriptData,
+      minionPoolMappings,
+    } = opts;
+    const deployment = await DeploymentSource.deployReplica({
+      replicaId,
+      options,
+      uploadedUserScripts,
+      removedUserScripts,
+      userScriptData,
+      minionPoolMappings,
+    });
+    return deployment;
+  }
+
+  @action cancelDeploymentDetails() {
+    if (this.deploymentDetails) {
+      apiCaller.cancelRequests(this.deploymentDetails.id);
+    }
+    this.detailsLoading = false;
+  }
+
+  @action clearDetails() {
+    this.detailsLoading = true;
+    this.deploymentDetails = null;
+  }
+}
+
+export default new DeploymentStore();

+ 28 - 0
tests/mocks/TransferMock.ts

@@ -11,6 +11,7 @@ export const REPLICA_MOCK: ReplicaItem = {
   id: "replica-id",
   name: "replica-name",
   type: "replica",
+  scenario: "replica",
   description: "replica-description",
   notes: "replica-notes",
   created_at: "2023-11-26T12:00:00Z",
@@ -91,7 +92,34 @@ export const MIGRATION_MOCK: MigrationItem = {
   user_id: "user-id",
 };
 
+export const DEPLOYMENT_MOCK: DEPLOYMENT_ITEM = {
+  id: "deployment-id",
+  name: "deployment-name",
+  type: "deployment",
+  replica_scenario_type: "replica",
+  description: "deployment-description",
+  notes: "deployment-notes",
+  created_at: "2023-11-26T12:00:00Z",
+  updated_at: "2023-11-26T12:00:00Z",
+  origin_endpoint_id: "openstack",
+  destination_endpoint_id: "vmware",
+  origin_minion_pool_id: "origin-minion-pool-id",
+  destination_minion_pool_id: "destination-minion-pool-id",
+  instances: ["instance-id"],
+  info: {},
+  destination_environment: {},
+  source_environment: {},
+  transfer_result: {},
+  last_execution_status: "COMPLETED",
+  user_id: "user-id",
+};
+
 export const MIGRATION_ITEM_DETAILS_MOCK: MigrationItemDetails = {
   ...MIGRATION_MOCK,
   tasks: [{ ...TASK_MOCK, task_type: "migration_task" }],
 };
+
+export const DEPLOYMENT_ITEM_DETAILS_MOCK: DeploymentItemDetails = {
+  ...DEPLOYMENT_MOCK,
+  tasks: [{ ...TASK_MOCK, task_type: "deployment_task" }],
+};

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor