From 0d03dbc4e633c05ed5716455a447114d11026715 Mon Sep 17 00:00:00 2001 From: Jones Date: Thu, 21 Mar 2024 10:51:59 +0100 Subject: [PATCH] feat: move file system forms to side panel --- .../DeleteFilesystem.test.tsx | 60 +++++ .../DeleteFilesystem/DeleteFilesystem.tsx | 59 +++++ .../DeleteFilesystem/index.ts | 1 + .../DeleteSpecialFilesystem.test.tsx | 67 ++++++ .../DeleteSpecialFilesystem.tsx | 45 ++++ .../DeleteSpecialFilesystem/index.ts | 1 + .../FilesystemsTable.test.tsx | 97 +++----- .../FilesystemsTable/FilesystemsTable.tsx | 210 ++++-------------- .../UnmountFilesystem.test.tsx | 67 ++++++ .../UnmountFilesystem/UnmountFilesystem.tsx | 62 ++++++ .../UnmountFilesystem/index.ts | 1 + .../components/MachineForms/MachineForms.tsx | 37 +++ src/app/machines/constants.ts | 6 + src/app/machines/types.ts | 15 ++ src/app/store/utils/node/base.ts | 6 + 15 files changed, 500 insertions(+), 234 deletions(-) create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.test.tsx create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.tsx create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/index.ts create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.test.tsx create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.tsx create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/index.ts create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.test.tsx create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.tsx create mode 100644 src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/index.ts diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.test.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.test.tsx new file mode 100644 index 0000000000..9eb1779139 --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.test.tsx @@ -0,0 +1,60 @@ +import configureStore from "redux-mock-store"; + +import DeleteFilesystem from "./DeleteFilesystem"; + +import { actions as machineActions } from "@/app/store/machine"; +import type { RootState } from "@/app/store/root/types"; +import * as factory from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; + +const mockStore = configureStore(); +const filesystem = factory.nodeFilesystem({ mount_point: "/disk-fs/path" }); +const disk = factory.nodeDisk({ filesystem, partitions: [] }); +const machine = factory.machineDetails({ + disks: [disk], + system_id: "abc123", +}); +const state = factory.rootState({ + machine: factory.machineState({ + items: [machine], + statuses: factory.machineStatuses({ + abc123: factory.machineStatus(), + }), + }), +}); + +it("renders a delete confirmation form", () => { + renderWithBrowserRouter( + , + { state } + ); + + expect( + screen.getByRole("form", { name: "Delete filesystem" }) + ).toBeInTheDocument(); +}); + +it("can remove a disk's filesystem", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + expect( + screen.getByRole("form", { name: "Delete filesystem" }) + ).toBeInTheDocument(); + expect( + screen.getByText("Are you sure you want to remove this filesystem?") + ).toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Remove" })); + const expectedAction = machineActions.deleteFilesystem({ + blockDeviceId: disk.id, + filesystemId: filesystem.id, + systemId: machine.system_id, + }); + + expect( + store.getActions().find((action) => action.type === expectedAction.type) + ).toStrictEqual(expectedAction); +}); diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.tsx new file mode 100644 index 0000000000..4d4da8b193 --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/DeleteFilesystem.tsx @@ -0,0 +1,59 @@ +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, Partition } from "@/app/store/types/node"; +import { isDisk, isMounted } from "@/app/store/utils"; + +type Props = { + close: () => void; + systemId: Machine["system_id"]; + storageDevice: Disk | Partition; +}; + +const DeleteFilesystem = ({ close, systemId, storageDevice }: Props) => { + const dispatch = useDispatch(); + const deviceIsDisk = isDisk(storageDevice); + const storageFs = storageDevice.filesystem; + const isDiskFsDelete = deviceIsDisk && isMounted(storageFs); + + return ( + Are you sure you want to remove this filesystem?} + modelType="filesystem" + onCancel={close} + onSaveAnalytics={{ + action: `Delete ${isDiskFsDelete ? "disk" : "partition"} filesystem`, + category: "Machine storage", + label: "Remove", + }} + onSubmit={() => { + dispatch(machineActions.cleanup()); + if (isDiskFsDelete) { + dispatch( + machineActions.deleteFilesystem({ + blockDeviceId: storageDevice.id, + filesystemId: storageFs.id, + systemId, + }) + ); + } else { + dispatch( + machineActions.deletePartition({ + partitionId: storageDevice.id, + systemId, + }) + ); + } + close(); + }} + submitAppearance="negative" + submitLabel="Remove" + /> + ); +}; + +export default DeleteFilesystem; diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/index.ts b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/index.ts new file mode 100644 index 0000000000..37369252d6 --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteFilesystem"; diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.test.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.test.tsx new file mode 100644 index 0000000000..513e2cafcb --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.test.tsx @@ -0,0 +1,67 @@ +import configureStore from "redux-mock-store"; + +import DeleteSpecialFilesystem from "./DeleteSpecialFilesystem"; + +import { actions as machineActions } from "@/app/store/machine"; +import type { RootState } from "@/app/store/root/types"; +import * as factory from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; + +const mockStore = configureStore(); +const filesystem = factory.nodeFilesystem({ mount_point: "/disk-fs/path" }); +const disk = factory.nodeDisk({ filesystem, partitions: [] }); +const machine = factory.machineDetails({ + disks: [disk], + system_id: "abc123", +}); +const state = factory.rootState({ + machine: factory.machineState({ + items: [machine], + statuses: factory.machineStatuses({ + abc123: factory.machineStatus(), + }), + }), +}); + +it("renders a delete confirmation form", () => { + renderWithBrowserRouter( + , + { state } + ); + + expect( + screen.getByRole("form", { name: "Delete special filesystem" }) + ).toBeInTheDocument(); +}); + +it("can remove a special filesystem", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + expect( + screen.getByRole("form", { name: "Delete special filesystem" }) + ).toBeInTheDocument(); + expect( + screen.getByText("Are you sure you want to remove this special filesystem?") + ).toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Remove" })); + const expectedAction = machineActions.unmountSpecial({ + mountPoint: filesystem.mount_point, + systemId: machine.system_id, + }); + + expect( + store.getActions().find((action) => action.type === expectedAction.type) + ).toStrictEqual(expectedAction); +}); diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.tsx new file mode 100644 index 0000000000..aa92e358b5 --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/DeleteSpecialFilesystem.tsx @@ -0,0 +1,45 @@ +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 { Filesystem } from "@/app/store/types/node"; + +type Props = { + close: () => void; + mountPoint: Filesystem["mount_point"]; + systemId: Machine["system_id"]; +}; + +const DeleteSpecialFilesystem = ({ close, systemId, mountPoint }: Props) => { + const dispatch = useDispatch(); + + return ( + Are you sure you want to remove this special filesystem?} + modelType="special filesystem" + onCancel={close} + onSaveAnalytics={{ + action: "Unmount special filesystem", + category: "Machine storage", + label: "Remove", + }} + onSubmit={() => { + dispatch(machineActions.cleanup()); + dispatch( + machineActions.unmountSpecial({ + mountPoint, + systemId, + }) + ); + close(); + }} + submitAppearance="negative" + submitLabel="Remove" + /> + ); +}; + +export default DeleteSpecialFilesystem; diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/index.ts b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/index.ts new file mode 100644 index 0000000000..37064a3a7f --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteSpecialFilesystem"; diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.test.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.test.tsx index 23b0fa2d8f..bb0a0dd652 100644 --- a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.test.tsx +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.test.tsx @@ -5,11 +5,24 @@ import configureStore from "redux-mock-store"; import FilesystemsTable from "./FilesystemsTable"; -import { actions as machineActions } from "@/app/store/machine"; +import * as sidePanelHooks from "@/app/base/side-panel-context"; +import { MachineSidePanelViews } from "@/app/machines/constants"; import * as factory from "@/testing/factories"; import { userEvent, render, screen } from "@/testing/utils"; const mockStore = configureStore(); +const setSidePanelContent = vi.fn(); +beforeEach(() => { + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + setSidePanelContent, + sidePanelContent: null, + setSidePanelSize: vi.fn(), + sidePanelSize: "regular", + }); +}); +afterEach(() => { + vi.restoreAllMocks(); +}); it("can show an empty message", () => { const machine = factory.machineDetails({ @@ -263,19 +276,9 @@ it("can remove a disk's filesystem if node is a machine", async () => { await userEvent.click( screen.getByRole("button", { name: "Remove filesystem..." }) ); - await userEvent.click(screen.getByRole("button", { name: "Remove" })); - - const expectedAction = machineActions.deleteFilesystem({ - blockDeviceId: disk.id, - filesystemId: filesystem.id, - systemId: machine.system_id, - }); - expect( - screen.getByText("Are you sure you want to remove this filesystem?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.DELETE_FILESYSTEM }) + ); }); it("can remove a partition's filesystem if node is a machine", async () => { @@ -309,18 +312,9 @@ it("can remove a partition's filesystem if node is a machine", async () => { await userEvent.click( screen.getByRole("button", { name: "Remove filesystem..." }) ); - await userEvent.click(screen.getByRole("button", { name: "Remove" })); - - const expectedAction = machineActions.deletePartition({ - partitionId: partition.id, - systemId: machine.system_id, - }); - expect( - screen.getByText("Are you sure you want to remove this filesystem?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.DELETE_FILESYSTEM }) + ); }); it("can remove a special filesystem if node is a machine", async () => { @@ -356,18 +350,11 @@ it("can remove a special filesystem if node is a machine", async () => { await userEvent.click( screen.getByRole("button", { name: "Remove filesystem..." }) ); - await userEvent.click(screen.getByRole("button", { name: "Remove" })); - - const expectedAction = machineActions.unmountSpecial({ - mountPoint: filesystem.mount_point, - systemId: machine.system_id, - }); - expect( - screen.getByText("Are you sure you want to remove this special filesystem?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.DELETE_SPECIAL_FILESYSTEM, + }) + ); }); it("can unmount a disk's filesystem if node is a machine", async () => { @@ -400,20 +387,9 @@ it("can unmount a disk's filesystem if node is a machine", async () => { await userEvent.click( screen.getByRole("button", { name: "Unmount filesystem..." }) ); - await userEvent.click(screen.getByRole("button", { name: "Unmount" })); - - const expectedAction = machineActions.updateFilesystem({ - blockId: disk.id, - mountOptions: "", - mountPoint: "", - systemId: machine.system_id, - }); - expect( - screen.getByText("Are you sure you want to unmount this filesystem?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.UNMOUNT_FILESYSTEM }) + ); }); it("can unmount a partition's filesystem if node is a machine", async () => { @@ -447,18 +423,7 @@ it("can unmount a partition's filesystem if node is a machine", async () => { await userEvent.click( screen.getByRole("button", { name: "Unmount filesystem..." }) ); - await userEvent.click(screen.getByRole("button", { name: "Unmount" })); - - const expectedAction = machineActions.updateFilesystem({ - mountOptions: "", - mountPoint: "", - partitionId: partition.id, - systemId: machine.system_id, - }); - expect( - screen.getByText("Are you sure you want to unmount this filesystem?") - ).toBeInTheDocument(); - expect( - store.getActions().find((action) => action.type === expectedAction.type) - ).toStrictEqual(expectedAction); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ view: MachineSidePanelViews.UNMOUNT_FILESYSTEM }) + ); }); diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx index 7d5a475af4..6219282dc5 100644 --- a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx @@ -1,15 +1,11 @@ -import { useState } from "react"; - import { Button, MainTable, Tooltip } from "@canonical/react-components"; import type { MainTableRow } from "@canonical/react-components/dist/components/MainTable/MainTable"; -import { useDispatch } from "react-redux"; import TableActionsDropdown from "@/app/base/components/TableActionsDropdown"; -import ActionConfirm from "@/app/base/components/node/ActionConfirm"; import { useSidePanel } from "@/app/base/side-panel-context"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; import { MachineSidePanelViews } from "@/app/machines/constants"; import type { ControllerDetails } from "@/app/store/controller/types"; -import { actions as machineActions } from "@/app/store/machine"; import type { MachineDetails } from "@/app/store/machine/types"; import type { Filesystem, Disk, Partition } from "@/app/store/types/node"; import { @@ -24,11 +20,6 @@ export enum FilesystemAction { UNMOUNT = "unmountFilesystem", } -type Expanded = { - content: FilesystemAction; - id: string; -}; - type Props = { canEditStorage: boolean; node: ControllerDetails | MachineDetails; @@ -75,15 +66,12 @@ const normaliseRowData = ( rowId: string, fs: Filesystem, storageDevice: Disk | Partition | null, - expanded: Expanded | null, - setExpanded: (expanded: Expanded | null) => void, canEditStorage: Props["canEditStorage"], - isMachine: boolean + isMachine: boolean, + node: Props["node"], + setSidePanelContent: SetSidePanelContent ) => { - const isExpanded = expanded?.id === rowId && Boolean(expanded?.content); - return { - className: isExpanded ? "p-table__row is-active" : null, columns: [ { content: storageDevice?.name || "—" }, { content: storageDevice ? formatSize(storageDevice.size) : "—" }, @@ -101,23 +89,48 @@ const normaliseRowData = ( label: "Unmount filesystem...", show: usesStorage(fs.fstype), type: FilesystemAction.UNMOUNT, + view: MachineSidePanelViews.UNMOUNT_FILESYSTEM, }, { label: "Remove filesystem...", type: FilesystemAction.DELETE, + view: + node.special_filesystems && !storageDevice + ? MachineSidePanelViews.DELETE_SPECIAL_FILESYSTEM + : MachineSidePanelViews.DELETE_FILESYSTEM, }, ]} disabled={!canEditStorage} - onActionClick={(action: FilesystemAction) => - setExpanded({ content: action, id: rowId }) - } + onActionClick={(_, view) => { + if (view) { + if ( + node.special_filesystems && + view === MachineSidePanelViews.DELETE_SPECIAL_FILESYSTEM + ) { + setSidePanelContent({ + view, + extras: { + systemId: node.system_id, + mountPoint: fs.mount_point, + }, + }); + return; + } + setSidePanelContent({ + view, + extras: { + systemId: node.system_id, + storageDevice, + }, + }); + } + }} /> ), }, ] : []), ].map((column, i) => ({ ...column, "aria-label": headers[i].content })), - expanded: isExpanded, key: rowId, }; }; @@ -126,8 +139,6 @@ const FilesystemsTable = ({ canEditStorage, node, }: Props): JSX.Element | null => { - const dispatch = useDispatch(); - const [expanded, setExpanded] = useState(null); const isMachine = nodeIsMachine(node); const { setSidePanelContent } = useSidePanel(); @@ -142,66 +153,11 @@ const FilesystemsTable = ({ rowId, diskFs, disk, - expanded, - setExpanded, canEditStorage, - isMachine + isMachine, + node, + setSidePanelContent ), - expandedContent: isMachine ? ( -
- {expanded?.content === FilesystemAction.DELETE && ( - setExpanded(null)} - confirmLabel="Remove" - eventName="deleteFilesystem" - message="Are you sure you want to remove this filesystem?" - onConfirm={() => { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.deleteFilesystem({ - blockDeviceId: disk.id, - filesystemId: diskFs.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Delete disk filesystem", - category: "Machine storage", - label: "Remove", - }} - statusKey="deletingFilesystem" - systemId={node.system_id} - /> - )} - {expanded?.content === FilesystemAction.UNMOUNT && ( - setExpanded(null)} - confirmLabel="Unmount" - eventName="updateFilesystem" - message="Are you sure you want to unmount this filesystem?" - onConfirm={() => { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.updateFilesystem({ - blockId: disk.id, - mountOptions: "", - mountPoint: "", - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Unmount disk filesystem", - category: "Machine storage", - label: "Unmount", - }} - statusKey="updatingFilesystem" - systemId={node.system_id} - /> - )} -
- ) : null, }); } @@ -217,65 +173,11 @@ const FilesystemsTable = ({ rowId, partitionFs, partition, - expanded, - setExpanded, canEditStorage, - isMachine + isMachine, + node, + setSidePanelContent ), - expandedContent: isMachine ? ( -
- {expanded?.content === FilesystemAction.DELETE && ( - setExpanded(null)} - confirmLabel="Remove" - eventName="deletePartition" - message="Are you sure you want to remove this filesystem?" - onConfirm={() => { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.deletePartition({ - partitionId: partition.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Delete partition filesystem", - category: "Machine storage", - label: "Remove", - }} - statusKey="deletingPartition" - systemId={node.system_id} - /> - )} - {expanded?.content === FilesystemAction.UNMOUNT && ( - setExpanded(null)} - confirmLabel="Unmount" - eventName="updateFilesystem" - message="Are you sure you want to unmount this filesystem?" - onConfirm={() => { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.updateFilesystem({ - mountOptions: "", - mountPoint: "", - partitionId: partition.id, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Unmount partition filesystem", - category: "Machine storage", - label: "Unmount", - }} - statusKey="updatingFilesystem" - systemId={node.system_id} - /> - )} -
- ) : null, }); } }); @@ -292,39 +194,11 @@ const FilesystemsTable = ({ rowId, specialFs, null, - expanded, - setExpanded, canEditStorage, - isMachine + isMachine, + node, + setSidePanelContent ), - expandedContent: isMachine ? ( -
- {expanded?.content === FilesystemAction.DELETE && ( - setExpanded(null)} - confirmLabel="Remove" - eventName="unmountSpecial" - message="Are you sure you want to remove this special filesystem?" - onConfirm={() => { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.unmountSpecial({ - mountPoint: specialFs.mount_point, - systemId: node.system_id, - }) - ); - }} - onSaveAnalytics={{ - action: "Unmount special filesystem", - category: "Machine storage", - label: "Remove", - }} - statusKey="unmountingSpecial" - systemId={node.system_id} - /> - )} -
- ) : null, }); }); } diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.test.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.test.tsx new file mode 100644 index 0000000000..a88f781e02 --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.test.tsx @@ -0,0 +1,67 @@ +import configureStore from "redux-mock-store"; + +import UnmountFilesystem from "./UnmountFilesystem"; + +import { actions as machineActions } from "@/app/store/machine"; +import type { RootState } from "@/app/store/root/types"; +import * as factory from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; + +const mockStore = configureStore(); +const filesystem = factory.nodeFilesystem({ mount_point: "/disk-fs/path" }); +const disk = factory.nodeDisk({ filesystem, partitions: [] }); +const machine = factory.machineDetails({ + disks: [disk], + system_id: "abc123", +}); +const state = factory.rootState({ + machine: factory.machineState({ + items: [machine], + statuses: factory.machineStatuses({ + abc123: factory.machineStatus(), + }), + }), +}); + +it("renders a delete confirmation form", () => { + renderWithBrowserRouter( + , + + { state } + ); + + expect( + screen.getByRole("form", { name: "Unmount filesystem" }) + ).toBeInTheDocument(); + expect( + screen.getByText("Are you sure you want to unmount this filesystem?") + ).toBeInTheDocument(); +}); + +it("can remove a special filesystem", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + await userEvent.click(screen.getByRole("button", { name: "Remove" })); + const expectedAction = machineActions.updateFilesystem({ + blockId: disk.id, + mountOptions: "", + mountPoint: "", + systemId: "abc123", + }); + + expect( + store.getActions().find((action) => action.type === expectedAction.type) + ).toStrictEqual(expectedAction); +}); diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.tsx new file mode 100644 index 0000000000..15ad4b2c11 --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/UnmountFilesystem.tsx @@ -0,0 +1,62 @@ +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, Partition } from "@/app/store/types/node"; +import { isDisk, isMounted } from "@/app/store/utils"; + +type Props = { + close: () => void; + systemId: Machine["system_id"]; + storageDevice: Disk | Partition; +}; + +const UnmountFilesystem = ({ close, systemId, storageDevice }: Props) => { + const dispatch = useDispatch(); + const deviceIsDisk = isDisk(storageDevice); + const storageFs = storageDevice.filesystem; + const isDiskFsUnmount = deviceIsDisk && isMounted(storageFs); + + return ( + Are you sure you want to unmount this filesystem?} + modelType="filesystem" + onCancel={close} + onSaveAnalytics={{ + action: `Unmount ${isDiskFsUnmount ? "disk" : "partition"} filesystem`, + category: "Machine storage", + label: "Unmount", + }} + onSubmit={() => { + dispatch(machineActions.cleanup()); + if (isDiskFsUnmount) { + dispatch( + machineActions.updateFilesystem({ + blockId: storageDevice.id, + mountOptions: "", + mountPoint: "", + systemId, + }) + ); + } else { + dispatch( + machineActions.updateFilesystem({ + mountOptions: "", + mountPoint: "", + partitionId: storageDevice.id, + systemId, + }) + ); + } + close(); + }} + submitAppearance="negative" + submitLabel="Remove" + /> + ); +}; + +export default UnmountFilesystem; diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/index.ts b/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/index.ts new file mode 100644 index 0000000000..b253abc19b --- /dev/null +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem/index.ts @@ -0,0 +1 @@ +export { default } from "./UnmountFilesystem"; diff --git a/src/app/machines/components/MachineForms/MachineForms.tsx b/src/app/machines/components/MachineForms/MachineForms.tsx index c9da284f8c..8a95e64a35 100644 --- a/src/app/machines/components/MachineForms/MachineForms.tsx +++ b/src/app/machines/components/MachineForms/MachineForms.tsx @@ -25,6 +25,9 @@ import EditDisk from "@/app/base/components/node/StorageTables/AvailableStorageT 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 DeleteFilesystem from "@/app/base/components/node/StorageTables/FilesystemsTable/DeleteFilesystem"; +import DeleteSpecialFilesystem from "@/app/base/components/node/StorageTables/FilesystemsTable/DeleteSpecialFilesystem"; +import UnmountFilesystem from "@/app/base/components/node/StorageTables/FilesystemsTable/UnmountFilesystem"; import type { SidePanelContentTypes } from "@/app/base/side-panel-context"; import type { SetSearchFilter } from "@/app/base/types"; import type { MachineActionSidePanelViews } from "@/app/machines/constants"; @@ -84,6 +87,10 @@ export const MachineForms = ({ const disk = extras && "disk" in extras ? extras.disk : undefined; const partition = extras && "partition" in extras ? extras.partition : undefined; + const storageDevice = + extras && "storageDevice" in extras ? extras.storageDevice : undefined; + const mountPoint = + extras && "mountPoint" in extras ? extras.mountPoint : undefined; switch (sidePanelContent.view) { case MachineSidePanelViews.ADD_CHASSIS: @@ -240,6 +247,26 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.DELETE_FILESYSTEM: { + if (!storageDevice || !systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.DELETE_SPECIAL_FILESYSTEM: { + if (!mountPoint || !systemId) return null; + return ( + + ); + } case MachineSidePanelViews.DELETE_VOLUME_GROUP: { if (!disk || !systemId) return null; return ( @@ -339,6 +366,16 @@ export const MachineForms = ({ /> ); } + case MachineSidePanelViews.UNMOUNT_FILESYSTEM: { + if (!storageDevice || !systemId) return null; + return ( + + ); + } case MachineSidePanelViews.UPDATE_DATASTORE: { if (!bulkActionSelected || !systemId) return null; return ( diff --git a/src/app/machines/constants.ts b/src/app/machines/constants.ts index 57a9e686b1..8719fd00f8 100644 --- a/src/app/machines/constants.ts +++ b/src/app/machines/constants.ts @@ -47,6 +47,11 @@ export const MachineNonActionSidePanelViews = { CREATE_RAID: ["machineNonActionForm", "createRaid"], CREATE_VOLUME_GROUP: ["machineNonActionForm", "createVolumeGroup"], DELETE_DISK: ["machineNonActionForm", "deleteDisk"], + DELETE_FILESYSTEM: ["machineNonActionForm", "deleteFilesystem"], + DELETE_SPECIAL_FILESYSTEM: [ + "machineNonActionForm", + "deleteSpecialFilesystem", + ], DELETE_VOLUME_GROUP: ["machineNonActionForm", "deleteVolumeGroup"], EDIT_DISK: ["machineNonActionForm", "editDisk"], EDIT_PARTITION: ["machineNonActionForm", "editPartition"], @@ -56,6 +61,7 @@ export const MachineNonActionSidePanelViews = { REMOVE_PARTITION: ["machineNonActionForm", "removePartition"], REMOVE_PHYSICAL: ["machineNonActionForm", "removePhysical"], SET_BOOT_DISK: ["machineNonActionForm", "setBootDisk"], + UNMOUNT_FILESYSTEM: ["machineNonActionForm", "unmountFilesystem"], UPDATE_DATASTORE: ["machineNonActionForm", "updateDatastore"], } as const; diff --git a/src/app/machines/types.ts b/src/app/machines/types.ts index f5bba5e538..ad4121d6da 100644 --- a/src/app/machines/types.ts +++ b/src/app/machines/types.ts @@ -22,6 +22,7 @@ import type { import type { Script } from "@/app/store/script/types"; import type { Disk, + Filesystem, NetworkInterface, NetworkLink, Partition, @@ -95,6 +96,20 @@ export type MachineSidePanelContent = disk?: Disk; partition?: Partition; } + > + | SidePanelContent< + ValueOf, + { + systemId?: Machine["system_id"]; + storageDevice: Disk | Partition | null; + } + > + | SidePanelContent< + ValueOf, + { + systemId?: Machine["system_id"]; + mountPoint: Filesystem["mount_point"]; + } >; export type MachineSetSidePanelContent = diff --git a/src/app/store/utils/node/base.ts b/src/app/store/utils/node/base.ts index aea20753ec..95022553c8 100644 --- a/src/app/store/utils/node/base.ts +++ b/src/app/store/utils/node/base.ts @@ -240,6 +240,10 @@ export const getSidePanelTitle = ( return "Delete discovery"; case SidePanelViews.DELETE_DISK[1]: return "Delete disk"; + case SidePanelViews.DELETE_FILESYSTEM[1]: + return "Delete filesystem"; + case SidePanelViews.DELETE_SPECIAL_FILESYSTEM[1]: + return "Delete special filesystem"; case SidePanelViews.DeleteTag[1]: return "Delete tag"; case SidePanelViews.DELETE_VOLUME_GROUP[1]: @@ -274,6 +278,8 @@ export const getSidePanelTitle = ( return "Set boot disk"; case SidePanelViews.SET_DEFAULT[1]: return "Set default"; + case SidePanelViews.UNMOUNT_FILESYSTEM[1]: + return "Unmount filesystem"; case SidePanelViews.UPDATE_DATASTORE[1]: return "Update datastore"; case SidePanelViews.UpdateTag[1]: