From 1b0e3f088aaf3de2272294f9c26910750bc511a1 Mon Sep 17 00:00:00 2001 From: Jones Ogolo <47540149+Jay-Topher@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:53:39 +0100 Subject: [PATCH] feat: move collapsible partition forms to side panel (#5356) --- .../AddLogicalVolumeFields.tsx | 4 +- .../AvailableStorageTable.test.tsx | 94 +++---- .../AvailableStorageTable.tsx | 252 ++---------------- .../CreateBcacheFields/CreateBcacheFields.tsx | 4 +- .../CreateCacheSet/CreateCacheSet.test.tsx | 25 +- .../CreateCacheSet/CreateCacheSet.tsx | 14 +- .../DeletePartition/DeletePartition.test.tsx | 69 +++++ .../DeletePartition/DeletePartition.tsx | 44 +++ .../DeletePartition/index.ts | 1 + .../DeleteVolumeGroup.test.tsx | 60 +++++ .../DeleteVolumeGroup/DeleteVolumeGroup.tsx | 44 +++ .../DeleteVolumeGroup/index.ts | 1 + .../EditPartitionFields.tsx | 4 +- .../StorageDeviceActions.tsx | 7 + .../components/MachineForms/MachineForms.tsx | 63 ++++- src/app/machines/constants.ts | 5 + src/app/machines/types.ts | 1 + src/app/store/utils/node/base.ts | 10 + 18 files changed, 400 insertions(+), 302 deletions(-) create mode 100644 src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.test.tsx create mode 100644 src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.tsx create mode 100644 src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/index.ts create mode 100644 src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.test.tsx create mode 100644 src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.tsx create mode 100644 src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/index.ts diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/AddLogicalVolume/AddLogicalVolumeFields/AddLogicalVolumeFields.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/AddLogicalVolume/AddLogicalVolumeFields/AddLogicalVolumeFields.tsx index 91b6a94e7f..8cd6d5cb65 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/AddLogicalVolume/AddLogicalVolumeFields/AddLogicalVolumeFields.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/AddLogicalVolume/AddLogicalVolumeFields/AddLogicalVolumeFields.tsx @@ -18,7 +18,7 @@ export const AddLogicalVolumeFields = ({ systemId }: Props): JSX.Element => { return ( - + { /> - + diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.test.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.test.tsx index 7e3395bb68..2a4581ba20 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.test.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.test.tsx @@ -7,7 +7,6 @@ import AvailableStorageTable from "./AvailableStorageTable"; import * as sidePanelHooks from "@/app/base/side-panel-context"; import { MachineSidePanelViews } from "@/app/machines/constants"; -import { actions as machineActions } from "@/app/store/machine"; import { MIN_PARTITION_SIZE } from "@/app/store/machine/constants"; import type { RootState } from "@/app/store/root/types"; import { DiskTypes } from "@/app/store/types/enum"; @@ -40,6 +39,15 @@ const getAvailableDisk = (name = "available-disk") => }); const setSidePanelContent = vi.fn(); + +beforeEach(() => { + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + setSidePanelContent, + sidePanelContent: null, + setSidePanelSize: vi.fn(), + sidePanelSize: "regular", + }); +}); afterEach(() => { vi.restoreAllMocks(); }); @@ -258,12 +266,6 @@ describe("performing machine actions", () => { }); it("can open the add partition form if disk can be partitioned", async () => { - vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ - setSidePanelContent, - sidePanelContent: null, - setSidePanelSize: vi.fn(), - sidePanelSize: "regular", - }); const disk = getAvailableDisk(); const machine = machineDetailsFactory({ disks: [disk], @@ -293,7 +295,7 @@ describe("performing machine actions", () => { ); }); - it("can open the edit partition form if partition can be edited", async () => { + it("can trigger the edit partition form if partition can be edited", async () => { const partition = partitionFactory({ filesystem: null }); const disk = diskFactory({ available_size: MIN_PARTITION_SIZE - 1, @@ -322,10 +324,12 @@ describe("performing machine actions", () => { screen.getByRole("button", { name: /Edit partition/ }) ); - expect(screen.getByLabelText("Edit partition form")).toBeInTheDocument(); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.EDIT_PARTITION }) + ); }); - it("can open the add logical volume form if disk can have one added", async () => { + it("can trigger the add logical volume form if disk can have one added", async () => { const disk = diskFactory({ available_size: MIN_PARTITION_SIZE + 1, type: DiskTypes.VOLUME_GROUP, @@ -358,18 +362,14 @@ describe("performing machine actions", () => { screen.getByRole("button", { name: /Add logical volume/ }) ); - expect( - screen.getByLabelText("Add logical volume form") - ).toBeInTheDocument(); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.CREATE_LOGICAL_VOLUME, + }) + ); }); it("can open the edit disk form if the disk is not a volume group", async () => { - vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ - setSidePanelContent, - sidePanelContent: null, - setSidePanelSize: vi.fn(), - sidePanelSize: "regular", - }); const disk = diskFactory({ type: DiskTypes.PHYSICAL }); const machine = machineDetailsFactory({ disks: [disk], @@ -430,10 +430,13 @@ describe("performing machine actions", () => { screen.getByRole("button", { name: /Create bcache/ }) ); - expect(screen.getByLabelText("Create bcache form")).toBeInTheDocument(); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.CREATE_BCACHE }) + ); }); it("disables actions if a bulk action has been selected", async () => { + vi.restoreAllMocks(); const partitions = [ partitionFactory({ filesystem: null, name: "part-1" }), partitionFactory({ filesystem: null, name: "part-2" }), @@ -477,7 +480,7 @@ describe("performing machine actions", () => { ).toBeDisabled(); }); - it("can create a cache set from a partition", async () => { + it("can trigger a create cache set form for a partition", async () => { const partition = partitionFactory({ filesystem: null }); const machine = machineDetailsFactory({ disks: [ @@ -511,20 +514,13 @@ describe("performing machine actions", () => { await userEvent.click( screen.getByRole("button", { name: /Create cache set/ }) ); - await userEvent.click( - screen.getByRole("button", { name: /Create cache set/ }) - ); - const expectedAction = machineActions.createCacheSet({ - partitionId: partition.id, - systemId: machine.system_id, - }); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.CREATE_CACHE_SET }) + ); }); - it("can delete a volume group", async () => { + it("can trigger a form to delete a volume group", async () => { const disk = diskFactory({ available_size: MIN_PARTITION_SIZE + 1, type: DiskTypes.VOLUME_GROUP, @@ -557,20 +553,12 @@ describe("performing machine actions", () => { await userEvent.click( screen.getByRole("button", { name: /Remove volume group/ }) ); - await userEvent.click( - screen.getByRole("button", { name: /Remove volume group/ }) - ); - const expectedAction = machineActions.deleteVolumeGroup({ - systemId: machine.system_id, - volumeGroupId: disk.id, - }); - expect( - screen.getByText("Are you sure you want to remove this volume group?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.DELETE_VOLUME_GROUP, + }) + ); }); it("can delete a partition", async () => { @@ -608,19 +596,9 @@ describe("performing machine actions", () => { await userEvent.click( screen.getByRole("button", { name: /Remove partition/ }) ); - await userEvent.click( - screen.getByRole("button", { name: /Remove partition/ }) - ); - const expectedAction = machineActions.deletePartition({ - partitionId: partition.id, - systemId: machine.system_id, - }); - expect( - screen.getByText("Are you sure you want to remove this partition?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.REMOVE_PARTITION }) + ); }); }); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx index f29e6ff379..f68049a1fa 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx @@ -2,30 +2,23 @@ import { useEffect, useState } from "react"; import { MainTable } from "@canonical/react-components"; import type { MainTableRow } from "@canonical/react-components/dist/components/MainTable/MainTable"; -import { useDispatch } from "react-redux"; -import AddLogicalVolume from "./AddLogicalVolume"; -import AddPartition from "./AddPartition"; import BulkActions from "./BulkActions"; -import CreateBcache from "./CreateBcache"; -import EditDisk from "./EditDisk"; -import EditPartition from "./EditPartition"; import StorageDeviceActions from "./StorageDeviceActions"; import DoubleRow from "@/app/base/components/DoubleRow"; import GroupCheckbox from "@/app/base/components/GroupCheckbox"; import RowCheckbox from "@/app/base/components/RowCheckbox"; import TagLinks from "@/app/base/components/TagLinks"; -import ActionConfirm from "@/app/base/components/node/ActionConfirm"; import DiskBootStatus from "@/app/base/components/node/DiskBootStatus"; import DiskNumaNodes from "@/app/base/components/node/DiskNumaNodes"; import DiskTestStatus from "@/app/base/components/node/DiskTestStatus"; import type { SetSidePanelContent } from "@/app/base/side-panel-context"; import { useSidePanel } from "@/app/base/side-panel-context"; import urls from "@/app/base/urls"; +import { MachineSidePanelViews } from "@/app/machines/constants"; import type { ControllerDetails } from "@/app/store/controller/types"; import { FilterControllers } from "@/app/store/controller/utils"; -import { actions as machineActions } from "@/app/store/machine"; import type { MachineDetails } from "@/app/store/machine/types"; import { FilterMachines } from "@/app/store/machine/utils"; import type { Disk, Node, Partition } from "@/app/store/types/node"; @@ -64,11 +57,6 @@ export enum StorageDeviceAction { SET_BOOT_DISK = "setBootDisk", } -type Expanded = { - content: StorageDeviceAction; - id: string; -}; - type Props = { canEditStorage: boolean; node: ControllerDetails | MachineDetails; @@ -118,10 +106,10 @@ const isSelected = ( * @param systemId - the system_id of the machine * @param storageDevice - the disk or partition to normalise. * @param actionsDisabled - whether actions should be disabled. - * @param expanded - the currently expanded row and content. - * @param setExpanded - function to set the expanded table row and content. * @param selected - the currently selected storage devices. * @param handleRowCheckbox - row checkbox handler function. + * @param setSidePanelContent - function to display the required sidepanel form + * @param parentDisk - the parent disk of the partition for editing a partition * @returns normalised row data */ const normaliseRowData = ( @@ -129,17 +117,14 @@ const normaliseRowData = ( isMachine: boolean, storageDevice: Disk | Partition, actionsDisabled: boolean, - expanded: Expanded | null, - setExpanded: (expanded: Expanded | null) => void, selected: (Disk | Partition)[], handleRowCheckbox: (storageDevice: Disk | Partition) => void, - setSidePanelContent: SetSidePanelContent + setSidePanelContent: SetSidePanelContent, + parentDisk?: Disk ) => { const rowId = uniqueId(storageDevice); - const isExpanded = expanded?.id === rowId && Boolean(expanded?.content); return { - className: isExpanded ? "p-table__row is-active" : null, columns: [ { "aria-label": "Name & Serial", @@ -266,8 +251,19 @@ const normaliseRowData = ( content: ( { + onActionClick={(_: StorageDeviceAction, view) => { if (view) { + if (view === MachineSidePanelViews.EDIT_PARTITION) { + setSidePanelContent({ + view, + extras: { + systemId, + disk: parentDisk, + partition: storageDevice, + }, + }); + return; + } setSidePanelContent({ view, extras: { @@ -275,10 +271,11 @@ const normaliseRowData = ( disk: isDisk(storageDevice) ? storageDevice : undefined, + partition: isPartition(storageDevice) + ? storageDevice + : undefined, }, }); - } else { - setExpanded({ content: action, id: rowId }); } }} storageDevice={storageDevice} @@ -289,7 +286,6 @@ const normaliseRowData = ( ] : []), ], - expanded: isExpanded, key: rowId, }; }; @@ -298,13 +294,10 @@ const AvailableStorageTable = ({ canEditStorage, node, }: Props): JSX.Element => { - const dispatch = useDispatch(); - const [expanded, setExpanded] = useState(null); const [selected, setSelected] = useState<(Disk | Partition)[]>([]); const isMachine = nodeIsMachine(node); const { sidePanelContent, setSidePanelContent } = useSidePanel(); - const closeExpanded = () => setExpanded(null); const handleRowCheckbox = (storageDevice: Disk | Partition) => { const newSelected = isSelected(storageDevice, selected) ? selected.filter((item) => item !== storageDevice) @@ -361,149 +354,16 @@ const AvailableStorageTable = ({ const rows: MainTableRow[] = []; node.disks.forEach((disk) => { if (isAvailable(disk)) { - const diskType = formatType(disk, true); - rows.push({ ...normaliseRowData( node.system_id, isMachine, disk, actionsDisabled, - expanded, - setExpanded, selected, handleRowCheckbox, setSidePanelContent ), - expandedContent: isMachine ? ( -
- {expanded?.content === StorageDeviceAction.CREATE_BCACHE && ( - setExpanded(null)} - storageDevice={disk} - systemId={node.system_id} - /> - )} - {expanded?.content === StorageDeviceAction.CREATE_CACHE_SET && ( - { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.createCacheSet({ - blockId: disk.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Create cache set from disk", - category: "Machine storage", - label: "Create cache set", - }} - statusKey="creatingCacheSet" - submitAppearance="positive" - systemId={node.system_id} - /> - )} - {expanded?.content === - StorageDeviceAction.CREATE_LOGICAL_VOLUME && ( - - )} - {expanded?.content === StorageDeviceAction.CREATE_PARTITION && ( - - )} - {expanded?.content === StorageDeviceAction.DELETE_DISK && ( - { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.deleteDisk({ - blockId: disk.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: `Delete ${diskType}`, - category: "Machine storage", - label: `Remove ${diskType}`, - }} - statusKey="deletingDisk" - systemId={node.system_id} - /> - )} - {expanded?.content === StorageDeviceAction.DELETE_VOLUME_GROUP && ( - { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.deleteVolumeGroup({ - systemId: node.system_id, - volumeGroupId: disk.id, - }) - ); - }} - onSaveAnalytics={{ - action: "Delete volume group", - category: "Machine storage", - label: "Remove volume group", - }} - statusKey="deletingVolumeGroup" - systemId={node.system_id} - /> - )} - {expanded?.content === StorageDeviceAction.EDIT_DISK && ( - - )} - {expanded?.content === StorageDeviceAction.SET_BOOT_DISK && ( - { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.setBootDisk({ - blockId: disk.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Set boot disk", - category: "Machine storage", - label: "Set boot disk", - }} - statusKey="settingBootDisk" - submitAppearance="positive" - systemId={node.system_id} - /> - )} -
- ) : null, }); } @@ -516,79 +376,11 @@ const AvailableStorageTable = ({ isMachine, partition, actionsDisabled, - expanded, - setExpanded, selected, handleRowCheckbox, - setSidePanelContent + setSidePanelContent, + disk ), - expandedContent: isMachine ? ( -
- {expanded?.content === StorageDeviceAction.CREATE_BCACHE && ( - setExpanded(null)} - storageDevice={partition} - systemId={node.system_id} - /> - )} - {expanded?.content === StorageDeviceAction.CREATE_CACHE_SET && ( - { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.createCacheSet({ - partitionId: partition.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Create cache set from partition", - category: "Machine storage", - label: "Create cache set", - }} - statusKey="creatingCacheSet" - submitAppearance="positive" - systemId={node.system_id} - /> - )} - {expanded?.content === StorageDeviceAction.DELETE_PARTITION && ( - { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.deletePartition({ - partitionId: partition.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Delete partition", - category: "Machine storage", - label: "Remove partition", - }} - statusKey="deletingPartition" - systemId={node.system_id} - /> - )} - {expanded?.content === StorageDeviceAction.EDIT_PARTITION && ( - setExpanded(null)} - disk={disk} - partition={partition} - systemId={node.system_id} - /> - )} -
- ) : null, }); } }); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateBcache/CreateBcacheFields/CreateBcacheFields.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateBcache/CreateBcacheFields/CreateBcacheFields.tsx index 1089b24acd..0f9b22ad5a 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateBcache/CreateBcacheFields/CreateBcacheFields.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateBcache/CreateBcacheFields/CreateBcacheFields.tsx @@ -22,7 +22,7 @@ export const CreateBcacheFields = ({ }: Props): JSX.Element => { return ( - + - + diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.test.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.test.tsx index ee7deaebbb..97ba770183 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.test.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.test.tsx @@ -15,10 +15,11 @@ import { import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; const mockStore = configureStore(); +const partition = partitionFactory(); const disk = diskFactory({ id: 1, name: "floppy-disk", - partitions: [partitionFactory(), partitionFactory()], + partitions: [partition, partitionFactory()], }); const state = rootStateFactory({ @@ -58,3 +59,25 @@ it("should fire an action to create cache set", async () => { .some((action) => action.type === "machine/createCacheSet") ).toBe(true); }); + +it("should fire an action to create a cache set given a partition ID", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + await userEvent.click( + screen.getByRole("button", { name: "Create cache set" }) + ); + + expect( + store + .getActions() + .some((action) => action.type === "machine/createCacheSet") + ).toBe(true); +}); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.tsx index 00558e3fc7..1a7b0669d6 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet/CreateCacheSet.tsx @@ -3,16 +3,18 @@ import { useDispatch } from "react-redux"; import ModelActionForm from "@/app/base/components/ModelActionForm"; import { actions as machineActions } from "@/app/store/machine"; import type { Machine } from "@/app/store/machine/types"; -import type { Disk } from "@/app/store/types/node"; +import type { Disk, Partition } from "@/app/store/types/node"; type Props = { close: () => void; systemId: Machine["system_id"]; - diskId: Disk["id"]; + diskId?: Disk["id"]; + partitionId?: Partition["id"]; }; -const CreateCacheSet = ({ systemId, diskId, close }: Props) => { +const CreateCacheSet = ({ systemId, diskId, partitionId, close }: Props) => { const dispatch = useDispatch(); + const isDiskCacheSet = !!diskId; return ( { modelType="cache set" onCancel={close} onSaveAnalytics={{ - action: "Create cache set from disk", + action: `Create cache set from ${ + isDiskCacheSet ? "disk" : "partition" + }`, category: "Machine storage", label: "Create cache set", }} @@ -29,7 +33,7 @@ const CreateCacheSet = ({ systemId, diskId, close }: Props) => { dispatch(machineActions.cleanup()); dispatch( machineActions.createCacheSet({ - blockId: diskId, + blockId: isDiskCacheSet ? diskId : partitionId, systemId: systemId, }) ); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.test.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.test.tsx new file mode 100644 index 0000000000..b6c4f1739c --- /dev/null +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.test.tsx @@ -0,0 +1,69 @@ +import configureStore from "redux-mock-store"; + +import DeletePartition from "."; + +import type { RootState } from "@/app/store/root/types"; +import { + machineDetails as machineDetailsFactory, + machineState as machineStateFactory, + machineStatus as machineStatusFactory, + machineStatuses as machineStatusesFactory, + nodeDisk as diskFactory, + nodePartition as partitionFactory, + rootState as rootStateFactory, +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; + +const mockStore = configureStore(); +const partition = partitionFactory(); +const disk = diskFactory({ + id: 1, + name: "floppy-disk", + partitions: [partition, partitionFactory()], +}); + +const state = rootStateFactory({ + machine: machineStateFactory({ + items: [machineDetailsFactory({ disks: [disk], system_id: "abc123" })], + statuses: machineStatusesFactory({ + abc123: machineStatusFactory(), + }), + }), +}); + +it("should render the form", () => { + renderWithBrowserRouter( + , + { state } + ); + + expect( + screen.getByRole("form", { name: "Delete partition" }) + ).toBeInTheDocument(); +}); + +it("should fire an action to delete a partition", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + await userEvent.click( + screen.getByRole("button", { name: "Remove partition" }) + ); + + expect( + store + .getActions() + .some((action) => action.type === "machine/deletePartition") + ).toBe(true); +}); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.tsx new file mode 100644 index 0000000000..f99544fabe --- /dev/null +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/DeletePartition.tsx @@ -0,0 +1,44 @@ +import { useDispatch } from "react-redux"; + +import ModelActionForm from "@/app/base/components/ModelActionForm"; +import { actions as machineActions } from "@/app/store/machine"; +import type { Machine } from "@/app/store/machine/types"; +import type { Partition } from "@/app/store/types/node"; + +type Props = { + close: () => void; + systemId: Machine["system_id"]; + partitionId: Partition["id"]; +}; + +const DeletePartition = ({ systemId, partitionId, close }: Props) => { + const dispatch = useDispatch(); + return ( + Are you sure you want to remove this partition?} + modelType="partition" + onCancel={close} + onSaveAnalytics={{ + action: `Delete partition`, + category: "Machine storage", + label: `Remove partition`, + }} + onSubmit={() => { + dispatch(machineActions.cleanup()); + dispatch( + machineActions.deletePartition({ + partitionId, + systemId: systemId, + }) + ); + close(); + }} + submitAppearance="negative" + submitLabel="Remove partition" + /> + ); +}; + +export default DeletePartition; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/index.ts b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/index.ts new file mode 100644 index 0000000000..a1c7fdb73a --- /dev/null +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition/index.ts @@ -0,0 +1 @@ +export { default } from "./DeletePartition"; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.test.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.test.tsx new file mode 100644 index 0000000000..bd25cf3c10 --- /dev/null +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.test.tsx @@ -0,0 +1,60 @@ +import configureStore from "redux-mock-store"; + +import DeleteVolumeGroup from "."; + +import type { RootState } from "@/app/store/root/types"; +import { + machineDetails as machineDetailsFactory, + machineState as machineStateFactory, + machineStatus as machineStatusFactory, + machineStatuses as machineStatusesFactory, + nodeDisk as diskFactory, + nodePartition as partitionFactory, + rootState as rootStateFactory, +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; + +const mockStore = configureStore(); +const disk = diskFactory({ + id: 1, + name: "floppy-disk", + partitions: [partitionFactory(), partitionFactory()], +}); + +const state = rootStateFactory({ + machine: machineStateFactory({ + items: [machineDetailsFactory({ disks: [disk], system_id: "abc123" })], + statuses: machineStatusesFactory({ + abc123: machineStatusFactory(), + }), + }), +}); + +it("should render the form", () => { + renderWithBrowserRouter( + , + { state } + ); + + expect( + screen.getByRole("form", { name: "Delete volume group" }) + ).toBeInTheDocument(); +}); + +it("should fire an action to delete a volume group", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + await userEvent.click( + screen.getByRole("button", { name: "Remove volume group" }) + ); + + expect( + store + .getActions() + .some((action) => action.type === "machine/deleteVolumeGroup") + ).toBe(true); +}); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.tsx new file mode 100644 index 0000000000..48612f7533 --- /dev/null +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/DeleteVolumeGroup.tsx @@ -0,0 +1,44 @@ +import { useDispatch } from "react-redux"; + +import ModelActionForm from "@/app/base/components/ModelActionForm"; +import { actions as machineActions } from "@/app/store/machine"; +import type { Machine } from "@/app/store/machine/types"; +import type { Disk } from "@/app/store/types/node"; + +type Props = { + close: () => void; + systemId: Machine["system_id"]; + diskId: Disk["id"]; +}; + +const DeleteVolumeGroup = ({ systemId, diskId, close }: Props) => { + const dispatch = useDispatch(); + return ( + Are you sure you want to remove this volume group?} + modelType="volume group" + onCancel={close} + onSaveAnalytics={{ + action: "Delete volume group", + category: "Machine storage", + label: "Remove volume group", + }} + onSubmit={() => { + dispatch(machineActions.cleanup()); + dispatch( + machineActions.deleteVolumeGroup({ + volumeGroupId: diskId, + systemId: systemId, + }) + ); + close(); + }} + submitAppearance="negative" + submitLabel="Remove volume group" + /> + ); +}; + +export default DeleteVolumeGroup; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/index.ts b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/index.ts new file mode 100644 index 0000000000..2b4fa1a455 --- /dev/null +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteVolumeGroup"; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/EditPartition/EditPartitionFields/EditPartitionFields.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/EditPartition/EditPartitionFields/EditPartitionFields.tsx index 88b5f258b3..8dc0e07141 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/EditPartition/EditPartitionFields/EditPartitionFields.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/EditPartition/EditPartitionFields/EditPartitionFields.tsx @@ -17,7 +17,7 @@ export const EditPartitionFields = ({ }: Props): JSX.Element => { return ( - + - + diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/StorageDeviceActions/StorageDeviceActions.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/StorageDeviceActions/StorageDeviceActions.tsx index 6836f4bc09..077e569a8a 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/StorageDeviceActions/StorageDeviceActions.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/StorageDeviceActions/StorageDeviceActions.tsx @@ -51,6 +51,7 @@ const StorageDeviceActions = ({ label: "Add logical volume...", show: canCreateLogicalVolume(storageDevice), type: StorageDeviceAction.CREATE_LOGICAL_VOLUME, + view: MachineSidePanelViews.CREATE_LOGICAL_VOLUME, }, { label: "Add partition...", @@ -62,6 +63,7 @@ const StorageDeviceActions = ({ label: "Create bcache...", show: canCreateBcache(machine.disks, storageDevice), type: StorageDeviceAction.CREATE_BCACHE, + view: MachineSidePanelViews.CREATE_BCACHE, }, { label: "Create cache set...", @@ -85,6 +87,7 @@ const StorageDeviceActions = ({ label: "Remove volume group...", show: canBeDeleted(storageDevice) && isVolumeGroup(storageDevice), type: StorageDeviceAction.DELETE_VOLUME_GROUP, + view: MachineSidePanelViews.DELETE_VOLUME_GROUP, }, { label: `Remove ${formatType(storageDevice, true)}...`, @@ -99,20 +102,24 @@ const StorageDeviceActions = ({ { label: "Edit partition...", type: StorageDeviceAction.EDIT_PARTITION, + view: MachineSidePanelViews.EDIT_PARTITION, }, { label: "Create bcache...", show: canCreateBcache(machine.disks, storageDevice), type: StorageDeviceAction.CREATE_BCACHE, + view: MachineSidePanelViews.CREATE_BCACHE, }, { label: "Create cache set...", show: canCreateCacheSet(storageDevice), type: StorageDeviceAction.CREATE_CACHE_SET, + view: MachineSidePanelViews.CREATE_CACHE_SET, }, { label: "Remove partition...", type: StorageDeviceAction.DELETE_PARTITION, + view: MachineSidePanelViews.REMOVE_PARTITION, }, ]; } diff --git a/src/app/machines/components/MachineForms/MachineForms.tsx b/src/app/machines/components/MachineForms/MachineForms.tsx index 782e5bf8c3..c9da284f8c 100644 --- a/src/app/machines/components/MachineForms/MachineForms.tsx +++ b/src/app/machines/components/MachineForms/MachineForms.tsx @@ -10,14 +10,19 @@ import AddChassisForm from "./AddChassis/AddChassisForm"; import AddMachineForm from "./AddMachine/AddMachineForm"; import MachineActionFormWrapper from "./MachineActionFormWrapper"; +import AddLogicalVolume from "@/app/base/components/node/StorageTables/AvailableStorageTable/AddLogicalVolume"; import AddPartition from "@/app/base/components/node/StorageTables/AvailableStorageTable/AddPartition"; import CreateDatastore from "@/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore"; import CreateRaid from "@/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid"; import CreateVolumeGroup from "@/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup"; import UpdateDatastore from "@/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore"; +import CreateBcache from "@/app/base/components/node/StorageTables/AvailableStorageTable/CreateBcache"; import CreateCacheSet from "@/app/base/components/node/StorageTables/AvailableStorageTable/CreateCacheSet"; import DeleteDisk from "@/app/base/components/node/StorageTables/AvailableStorageTable/DeleteDisk"; +import DeletePartition from "@/app/base/components/node/StorageTables/AvailableStorageTable/DeletePartition"; +import DeleteVolumeGroup from "@/app/base/components/node/StorageTables/AvailableStorageTable/DeleteVolumeGroup"; import EditDisk from "@/app/base/components/node/StorageTables/AvailableStorageTable/EditDisk"; +import EditPartition from "@/app/base/components/node/StorageTables/AvailableStorageTable/EditPartition"; import SetBootDisk from "@/app/base/components/node/StorageTables/AvailableStorageTable/SetBootDisk"; import AddSpecialFilesystem from "@/app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem"; import type { SidePanelContentTypes } from "@/app/base/side-panel-context"; @@ -77,6 +82,8 @@ export const MachineForms = ({ const linkId = extras && "linkId" in extras ? extras.linkId : undefined; const nicId = extras && "nicId" in extras ? extras.nicId : undefined; const disk = extras && "disk" in extras ? extras.disk : undefined; + const partition = + extras && "partition" in extras ? extras.partition : undefined; switch (sidePanelContent.view) { case MachineSidePanelViews.ADD_CHASSIS: @@ -152,12 +159,23 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.CREATE_BCACHE: { + if (!systemId || (!disk && !partition)) return null; + return ( + + ); + } case MachineSidePanelViews.CREATE_CACHE_SET: { - if (!systemId || !disk) return null; + if (!systemId || (!disk && !partition)) return null; return ( ); @@ -172,6 +190,16 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.CREATE_LOGICAL_VOLUME: { + if (!systemId || !disk) return null; + return ( + + ); + } case MachineSidePanelViews.CREATE_PARTITION: { if (!systemId || !disk) return null; return ( @@ -212,6 +240,16 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.DELETE_VOLUME_GROUP: { + if (!disk || !systemId) return null; + return ( + + ); + } case MachineSidePanelViews.EDIT_DISK: { if (!disk || !systemId) return null; return ( @@ -222,6 +260,17 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.EDIT_PARTITION: { + if (!disk || !partition || !systemId) return null; + return ( + + ); + } case MachineSidePanelViews.EDIT_PHYSICAL: { if (!systemId || !selected || !setSelected) return null; return ( @@ -259,6 +308,16 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.REMOVE_PARTITION: { + if (!partition || !systemId) return null; + return ( + + ); + } case MachineSidePanelViews.REMOVE_PHYSICAL: { if (!systemId) return null; return ( diff --git a/src/app/machines/constants.ts b/src/app/machines/constants.ts index 02c211476c..57a9e686b1 100644 --- a/src/app/machines/constants.ts +++ b/src/app/machines/constants.ts @@ -39,16 +39,21 @@ export const MachineNonActionSidePanelViews = { ADD_SPECIAL_FILESYSTEM: ["machineNonActionForm", "addSpecialFilesystem"], ADD_VLAN: ["machineNonActionForm", "addVlan"], APPLY_STORAGE_LAYOUT: ["machineNonActionForm", "applyStorageLayout"], + CREATE_BCACHE: ["machineNonActionForm", "createBcache"], CREATE_CACHE_SET: ["machineNonActionForm", "createCacheSet"], CREATE_DATASTORE: ["machineNonActionForm", "createDatastore"], + CREATE_LOGICAL_VOLUME: ["machineNonActionForm", "createLogicalVolume"], CREATE_PARTITION: ["machineNonActionForm", "createPartition"], CREATE_RAID: ["machineNonActionForm", "createRaid"], CREATE_VOLUME_GROUP: ["machineNonActionForm", "createVolumeGroup"], DELETE_DISK: ["machineNonActionForm", "deleteDisk"], + DELETE_VOLUME_GROUP: ["machineNonActionForm", "deleteVolumeGroup"], EDIT_DISK: ["machineNonActionForm", "editDisk"], + EDIT_PARTITION: ["machineNonActionForm", "editPartition"], EDIT_PHYSICAL: ["machineNonActionForm", "editPhysical"], MARK_CONNECTED: ["machineNonActionForm", "markConnected"], MARK_DISCONNECTED: ["machineNonActionForm", "markDisconnected"], + REMOVE_PARTITION: ["machineNonActionForm", "removePartition"], REMOVE_PHYSICAL: ["machineNonActionForm", "removePhysical"], SET_BOOT_DISK: ["machineNonActionForm", "setBootDisk"], UPDATE_DATASTORE: ["machineNonActionForm", "updateDatastore"], diff --git a/src/app/machines/types.ts b/src/app/machines/types.ts index b60e7bdc9c..f5bba5e538 100644 --- a/src/app/machines/types.ts +++ b/src/app/machines/types.ts @@ -93,6 +93,7 @@ export type MachineSidePanelContent = { systemId?: Machine["system_id"]; disk?: Disk; + partition?: Partition; } >; diff --git a/src/app/store/utils/node/base.ts b/src/app/store/utils/node/base.ts index 8060d7af60..aea20753ec 100644 --- a/src/app/store/utils/node/base.ts +++ b/src/app/store/utils/node/base.ts @@ -222,10 +222,14 @@ export const getSidePanelTitle = ( return "Change storage layout"; case SidePanelViews.CLEAR_ALL_DISCOVERIES[1]: return "Clear all discoveries"; + case SidePanelViews.CREATE_BCACHE[1]: + return "Create bcache"; case SidePanelViews.CREATE_CACHE_SET[1]: return "Create cache set"; case SidePanelViews.CREATE_DATASTORE[1]: return "Create datastore"; + case SidePanelViews.CREATE_LOGICAL_VOLUME[1]: + return "Create logical volume"; case SidePanelViews.CREATE_PARTITION[1]: return "Create partition"; case SidePanelViews.CREATE_RAID[1]: @@ -238,12 +242,16 @@ export const getSidePanelTitle = ( return "Delete disk"; case SidePanelViews.DeleteTag[1]: return "Delete tag"; + case SidePanelViews.DELETE_VOLUME_GROUP[1]: + return "Delete volume group"; case SidePanelViews.EDIT_INTERFACE[1]: return "Edit interface"; case SidePanelViews.CREATE_ZONE[1]: return "Add AZ"; case SidePanelViews.EDIT_DISK[1]: return "Edit disk"; + case SidePanelViews.EDIT_PARTITION[1]: + return "Edit partition"; case SidePanelViews.EDIT_PHYSICAL[1]: return "Edit physical"; case SidePanelViews.DELETE_IMAGE[1]: @@ -258,6 +266,8 @@ export const getSidePanelTitle = ( return "Mark as disconnected"; case SidePanelViews.REMOVE_INTERFACE[1]: return "Remove interface"; + case SidePanelViews.REMOVE_PARTITION[1]: + return "Remove partition"; case SidePanelViews.REMOVE_PHYSICAL[1]: return "Remove physical"; case SidePanelViews.SET_BOOT_DISK[1]: