From a4ee72a1bd03b4fae83413a2709de4e7febc6189 Mon Sep 17 00:00:00 2001 From: Jones Ogolo Date: Tue, 9 Jan 2024 10:27:32 +0100 Subject: [PATCH 1/9] feat: move forms to sidepanel --- .../MainContentSection/MainContentSection.tsx | 8 +- .../ModelDeleteForm/ModelDeleteForm.test.tsx | 49 +++ .../ModelDeleteForm/ModelDeleteForm.tsx | 43 +++ .../base/components/ModelDeleteForm/index.ts | 1 + .../NetworkActionRow.test.tsx | 38 --- .../NetworkActionRow/NetworkActionRow.tsx | 58 +++- .../base/components/SSHKeyForm/SSHKeyForm.tsx | 2 +- .../SSHKeyFormFields/SSHKeyFormFields.tsx | 21 +- .../components/TableActions/TableActions.tsx | 10 +- .../AvailableStorageTable.tsx | 1 - .../BulkActions/BulkActions.test.tsx | 165 +++++---- .../BulkActions/BulkActions.tsx | 92 ++--- .../CreateDatastore/CreateDatastore.tsx | 161 +++++---- .../BulkActions/CreateRaid/CreateRaid.tsx | 135 ++++---- .../CreateRaidFields/CreateRaidFields.tsx | 4 +- .../CreateVolumeGroup/CreateVolumeGroup.tsx | 167 +++++----- .../UpdateDatastore/UpdateDatastore.tsx | 91 +++-- .../UpdateDatastoreFields.tsx | 4 +- .../AvailableStorageTable/index.ts | 2 +- .../AddSpecialFilesystem.tsx | 143 ++++---- .../FilesystemsTable.test.tsx | 28 -- .../FilesystemsTable/FilesystemsTable.tsx | 32 +- .../DeviceNetworkForms.test.tsx | 43 +++ .../DeviceNetworkForms/DeviceNetworkForms.tsx | 66 ++++ .../components/DeviceNetworkForms/index.ts | 1 + src/app/devices/constants.ts | 3 + src/app/devices/types.ts | 11 +- .../views/DeviceDetails/DeviceDetails.tsx | 154 +++++---- .../AddInterface/AddInterface.tsx | 50 ++- .../DeviceNetwork/DeviceNetwork.tsx | 47 +-- .../DeviceNetworkTable.test.tsx | 116 +------ .../DeviceNetworkTable/DeviceNetworkTable.tsx | 111 +++--- .../RemoveInterface/RemoveInterface.test.tsx | 32 +- .../RemoveInterface/RemoveInterface.tsx | 73 ++-- .../EditInterface/EditInterface.tsx | 60 ++-- .../InterfaceFormFields.tsx | 6 +- .../components/MachineForms/MachineForms.tsx | 154 ++++++++- src/app/machines/constants.ts | 11 + src/app/machines/types.ts | 63 +++- .../AddBondForm/AddBondForm.test.tsx | 2 +- .../AddBondForm/AddBondForm.tsx | 132 ++++---- .../AddBridgeForm/AddBridgeForm.test.tsx | 10 +- .../AddBridgeForm/AddBridgeForm.tsx | 32 +- .../AddInterface/AddInterface.tsx | 129 ++++--- .../BondFormFields/BondFormFields.tsx | 4 +- .../BridgeFormFields/BridgeFormFields.tsx | 4 +- .../MachineNetwork/MachineNetwork.tsx | 2 + .../MachineNetworkActions.test.tsx | 64 +++- .../MachineNetworkActions.tsx | 6 +- .../ChangeStorageLayout.test.tsx | 62 ++-- .../ChangeStorageLayout.tsx | 204 +++++------- .../ChangeStorageLayoutMenu.test.tsx | 57 ++++ .../ChangeStorageLayoutMenu.tsx | 70 ++++ .../ChangeStorageLayoutMenu/index.ts | 1 + .../MachineStorage/MachineStorage.test.tsx | 2 +- .../MachineStorage/MachineStorage.tsx | 4 +- .../PoolDeleteForm/PoolDeleteForm.test.tsx | 11 + .../PoolDeleteForm/PoolDeleteForm.tsx | 30 ++ .../pools/components/PoolDeleteForm/index.ts | 1 + .../pools/components/PoolForm/PoolForm.tsx | 97 +++--- src/app/pools/urls.ts | 1 + .../views/PoolDelete/PoolDelete.test.tsx | 48 +++ src/app/pools/views/PoolDelete/PoolDelete.tsx | 14 + src/app/pools/views/PoolDelete/index.ts | 1 + .../pools/views/PoolList/PoolList.test.tsx | 78 +---- src/app/pools/views/PoolList/PoolList.tsx | 67 +--- src/app/pools/views/Pools.tsx | 110 ++++-- .../preferences/components/Routes/Routes.tsx | 100 ++++-- src/app/preferences/urls.ts | 1 + .../APIKeyDelete/APIKeyDelete.test.tsx | 38 +++ .../APIKeys/APIKeyDelete/APIKeyDelete.tsx | 13 + .../views/APIKeys/APIKeyDelete/index.ts | 1 + .../APIKeyDeleteForm/APIKeyDeleteForm.tsx | 33 ++ .../views/APIKeys/APIKeyDeleteForm/index.ts | 1 + .../views/APIKeys/APIKeyForm/APIKeyForm.tsx | 119 ++++--- .../views/APIKeys/APIKeyList/APIKeyList.tsx | 70 +--- .../preferences/views/Preferences.test.tsx | 12 - src/app/preferences/views/Preferences.tsx | 13 +- .../views/SSHKeys/AddSSHKey/AddSSHKey.tsx | 34 +- .../views/SSLKeys/AddSSLKey/AddSSLKey.tsx | 99 +++--- .../components/Routes/Routes.test.tsx | 46 --- src/app/settings/components/Routes/Routes.tsx | 315 ++++++++++++++---- src/app/settings/urls.ts | 1 + .../views/Dhcp/DhcpForm/DhcpForm.test.tsx | 2 +- .../settings/views/Dhcp/DhcpForm/DhcpForm.tsx | 34 +- .../LicenseKeyForm/LicenseKeyForm.tsx | 25 +- .../RepositoryForm/RepositoryForm.test.tsx | 12 +- .../RepositoryForm/RepositoryForm.tsx | 15 +- .../RepositoryFormFields.tsx | 4 +- .../Scripts/ScriptsUpload/ScriptsUpload.tsx | 19 +- src/app/settings/views/Settings.tsx | 6 +- .../views/Users/UserAdd/UserAdd.test.tsx | 4 +- .../Users/UserDelete/UserDelete.test.tsx | 43 +++ .../views/Users/UserDelete/UserDelete.tsx | 22 ++ .../settings/views/Users/UserDelete/index.ts | 1 + .../Users/UserDeleteForm/UserDeleteForm.tsx | 62 ++++ .../views/Users/UserDeleteForm/index.ts | 1 + .../views/Users/UserForm/UserForm.tsx | 101 +++--- .../views/Users/UsersList/UsersList.test.tsx | 89 +---- .../views/Users/UsersList/UsersList.tsx | 71 +--- src/app/store/machine/types/base.ts | 6 + src/app/store/machine/types/index.ts | 1 + src/app/store/utils/node/base.ts | 24 ++ .../ReservedRangeDeleteForm.tsx | 45 +++ .../ReservedRangeDeleteForm/index.ts | 1 + .../ReservedRangeForm.test.tsx | 33 +- .../ReservedRangeForm/ReservedRangeForm.tsx | 66 ++-- .../ReservedRanges/ReservedRanges.test.tsx | 106 +----- .../ReservedRanges/ReservedRanges.tsx | 179 ++++------ .../AddStaticRouteForm.test.tsx | 2 +- .../AddStaticRouteForm/AddStaticRouteForm.tsx | 50 +-- .../DeleteStaticRouteForm.test.tsx | 63 ++++ .../DeleteStaticRouteForm.tsx | 33 ++ .../DeleteStaticRouteform/index.ts | 1 + .../EditStaticRouteForm.test.tsx | 7 +- .../EditStaticRouteForm.tsx | 51 ++- .../StaticRoutes/StaticRoutes.test.tsx | 65 +--- .../StaticRoutes/StaticRoutes.tsx | 156 ++------- .../SubnetActionForms/SubnetActionForms.tsx | 13 +- .../SubnetDetailsHeader.test.tsx | 6 +- .../subnets/views/SubnetDetails/constants.ts | 27 +- 121 files changed, 3194 insertions(+), 2647 deletions(-) create mode 100644 src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx create mode 100644 src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx create mode 100644 src/app/base/components/ModelDeleteForm/index.ts create mode 100644 src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx create mode 100644 src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx create mode 100644 src/app/devices/components/DeviceNetworkForms/index.ts create mode 100644 src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx create mode 100644 src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx create mode 100644 src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/index.ts create mode 100644 src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx create mode 100644 src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx create mode 100644 src/app/pools/components/PoolDeleteForm/index.ts create mode 100644 src/app/pools/views/PoolDelete/PoolDelete.test.tsx create mode 100644 src/app/pools/views/PoolDelete/PoolDelete.tsx create mode 100644 src/app/pools/views/PoolDelete/index.ts create mode 100644 src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx create mode 100644 src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx create mode 100644 src/app/preferences/views/APIKeys/APIKeyDelete/index.ts create mode 100644 src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx create mode 100644 src/app/preferences/views/APIKeys/APIKeyDeleteForm/index.ts delete mode 100644 src/app/preferences/views/Preferences.test.tsx create mode 100644 src/app/settings/views/Users/UserDelete/UserDelete.test.tsx create mode 100644 src/app/settings/views/Users/UserDelete/UserDelete.tsx create mode 100644 src/app/settings/views/Users/UserDelete/index.ts create mode 100644 src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx create mode 100644 src/app/settings/views/Users/UserDeleteForm/index.ts create mode 100644 src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx create mode 100644 src/app/subnets/components/ReservedRangeDeleteForm/index.ts create mode 100644 src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx create mode 100644 src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx create mode 100644 src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/index.ts diff --git a/src/app/base/components/MainContentSection/MainContentSection.tsx b/src/app/base/components/MainContentSection/MainContentSection.tsx index 622ac9749d..c6882c31d8 100644 --- a/src/app/base/components/MainContentSection/MainContentSection.tsx +++ b/src/app/base/components/MainContentSection/MainContentSection.tsx @@ -2,6 +2,7 @@ import type { HTMLProps, ReactNode } from "react"; import { Col, Row } from "@canonical/react-components"; import type { ColSize } from "@canonical/react-components"; +import classNames from "classnames"; import NotificationList from "@/app/base/components/NotificationList"; import { COL_SIZES } from "@/app/base/constants"; @@ -35,7 +36,12 @@ const MainContentSection = ({ {sidebar} )} - + {!isNotificationListHidden && } {children} diff --git a/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx new file mode 100644 index 0000000000..242494b48e --- /dev/null +++ b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx @@ -0,0 +1,49 @@ +import ModelDeleteForm from "./ModelDeleteForm"; + +import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; + +it("renders", () => { + renderWithBrowserRouter( + + ); + expect( + screen.getByText("Are you sure you want to delete this machine?") + ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Delete" })).toBeInTheDocument(); +}); + +it("can confirm", async () => { + const onSubmit = jest.fn(); + renderWithBrowserRouter( + + ); + const submitBtn = screen.getByRole("button", { name: /delete/i }); + await userEvent.click(submitBtn); + expect(onSubmit).toHaveBeenCalled(); +}); + +it("can cancel", async () => { + const onCancel = jest.fn(); + renderWithBrowserRouter( + + ); + const cancelBtn = screen.getByRole("button", { name: /cancel/i }); + await userEvent.click(cancelBtn); + expect(onCancel).toHaveBeenCalled(); +}); diff --git a/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx new file mode 100644 index 0000000000..7c69f9e431 --- /dev/null +++ b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx @@ -0,0 +1,43 @@ +import { Col, Row } from "@canonical/react-components"; + +import type { Props as FormikFormProps } from "app/base/components/FormikForm/FormikForm"; +import FormikForm from "app/base/components/FormikForm/FormikForm"; +import type { EmptyObject } from "app/base/types"; + +type Props = { + modelType: string; + message?: string; +} & FormikFormProps; + +const ModelDeleteForm = ({ + modelType, + message, + submitAppearance = "negative", + submitLabel = "Delete", + initialValues = {}, + ...props +}: Props) => { + return ( + + + +

+ {message + ? message + : `Are you sure you want to delete this ${modelType}?`} +

+ + This action is permanent and can not be undone. + + +
+
+ ); +}; + +export default ModelDeleteForm; diff --git a/src/app/base/components/ModelDeleteForm/index.ts b/src/app/base/components/ModelDeleteForm/index.ts new file mode 100644 index 0000000000..1717de3f26 --- /dev/null +++ b/src/app/base/components/ModelDeleteForm/index.ts @@ -0,0 +1 @@ +export { default } from "./ModelDeleteForm"; diff --git a/src/app/base/components/NetworkActionRow/NetworkActionRow.test.tsx b/src/app/base/components/NetworkActionRow/NetworkActionRow.test.tsx index e19fb5f197..4cef8ce710 100644 --- a/src/app/base/components/NetworkActionRow/NetworkActionRow.test.tsx +++ b/src/app/base/components/NetworkActionRow/NetworkActionRow.test.tsx @@ -11,7 +11,6 @@ import { rootState as rootStateFactory, } from "@/testing/factories"; import { - userEvent, screen, renderWithBrowserRouter, expectTooltipOnHover, @@ -37,7 +36,6 @@ describe("NetworkActionRow", () => { const store = mockStore(state); renderWithBrowserRouter( { }); describe("add physical", () => { - it("sets the state to show the form when clicking the button", async () => { - const store = mockStore(state); - const setExpanded = vi.fn(); - renderWithBrowserRouter( - , - { route: "/machine/abc123", store } - ); - await userEvent.click( - screen.getByRole("button", { name: "Add interface" }) - ); - expect(setExpanded).toHaveBeenCalledWith({ - content: ExpandedState.ADD_PHYSICAL, - }); - }); - it("disables the button when networking is disabled", async () => { state.machine.items[0].status = NodeStatus.DEPLOYED; const store = mockStore(state); renderWithBrowserRouter( , @@ -93,21 +71,5 @@ describe("NetworkActionRow", () => { "Network can't be modified for this machine." ); }); - - it("disables the button when the form is expanded", () => { - state.machine.items[0].status = NodeStatus.DEPLOYED; - const store = mockStore(state); - renderWithBrowserRouter( - , - { route: "/machine/abc123", store } - ); - expect( - screen.getByRole("button", { name: "Add interface" }) - ).toBeDisabled(); - }); }); }); diff --git a/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx b/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx index 742c055295..476c4bbfbb 100644 --- a/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx +++ b/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx @@ -1,12 +1,20 @@ import type { ReactNode } from "react"; import { Button, Col, List, Row, Tooltip } from "@canonical/react-components"; +import { useLocation } from "react-router-dom"; -import type { Expanded, SetExpanded } from "../NodeNetworkTab/NodeNetworkTab"; +import type { SetExpanded } from "../NodeNetworkTab/NodeNetworkTab"; import { ExpandedState } from "../NodeNetworkTab/NodeNetworkTab"; -import { useIsAllNetworkingDisabled } from "@/app/base/hooks"; -import type { Node } from "@/app/store/types/node"; +import type { + Selected, + SetSelected, +} from "app/base/components/node/networking/types"; +import { useIsAllNetworkingDisabled } from "app/base/hooks"; +import { useSidePanel } from "app/base/side-panel-context"; +import { DeviceSidePanelViews } from "app/devices/constants"; +import { MachineSidePanelViews } from "app/machines/constants"; +import type { Node } from "app/store/types/node"; type Action = { disabled: [boolean, string?][]; @@ -15,38 +23,64 @@ type Action = { }; type Props = { - expanded: Expanded | null; extraActions?: Action[]; node: Node; rightContent?: ReactNode; setExpanded: SetExpanded; + selected?: Selected[]; + setSelected?: SetSelected; }; export const NETWORK_DISABLED_MESSAGE = "Network can't be modified for this machine."; const NetworkActionRow = ({ - expanded, extraActions, node, rightContent, setExpanded, + selected, + setSelected, }: Props): JSX.Element | null => { const isAllNetworkingDisabled = useIsAllNetworkingDisabled(node); + const { setSidePanelContent } = useSidePanel(); + const { pathname } = useLocation(); + const isMachinesPage = pathname.startsWith("/machine"); const actions: Action[] = [ { - disabled: [ - [isAllNetworkingDisabled, NETWORK_DISABLED_MESSAGE], - // Disable the button when the form is visible. - [expanded?.content === ExpandedState.ADD_PHYSICAL], - ], + disabled: [[isAllNetworkingDisabled, NETWORK_DISABLED_MESSAGE]], label: "Add interface", state: ExpandedState.ADD_PHYSICAL, }, ...(extraActions || []), ]; + const handleButtonClick = (state: ExpandedState) => { + if (state === ExpandedState.ADD_PHYSICAL) { + if (isMachinesPage) { + setSidePanelContent({ + view: MachineSidePanelViews.ADD_INTERFACE, + extras: { systemId: node.system_id }, + }); + } else { + setSidePanelContent({ view: DeviceSidePanelViews.ADD_INTERFACE }); + } + } else if (state === ExpandedState.ADD_BOND) { + setSidePanelContent({ + view: MachineSidePanelViews.ADD_BOND, + extras: { systemId: node.system_id, selected: selected, setSelected }, + }); + } else if (state === ExpandedState.ADD_BRIDGE) { + setSidePanelContent({ + view: MachineSidePanelViews.ADD_BRIDGE, + extras: { systemId: node.system_id, selected: selected, setSelected }, + }); + } else { + setExpanded({ content: state }); + } + }; + const buttons = actions.map((item) => { // Check if there is any reason to disable the button. const [disabled, tooltip] = @@ -55,9 +89,7 @@ const NetworkActionRow = ({ diff --git a/src/app/base/components/SSHKeyForm/SSHKeyForm.tsx b/src/app/base/components/SSHKeyForm/SSHKeyForm.tsx index 2e92ad6052..c1bfd406c9 100644 --- a/src/app/base/components/SSHKeyForm/SSHKeyForm.tsx +++ b/src/app/base/components/SSHKeyForm/SSHKeyForm.tsx @@ -61,7 +61,7 @@ export const SSHKeyForm = ({ cols, ...props }: Props): JSX.Element => { validationSchema={SSHKeySchema} {...props} > - + ); }; diff --git a/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx b/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx index cbc18ddbe6..c9ce398ead 100644 --- a/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx +++ b/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx @@ -1,31 +1,22 @@ import { ExternalLink } from "@canonical/maas-react-components"; import { Col, Row, Select, Textarea } from "@canonical/react-components"; -import type { ColSize } from "@canonical/react-components"; import { useFormikContext } from "formik"; import type { SSHKeyFormValues } from "../types"; -import FormikField from "@/app/base/components/FormikField"; -import TooltipButton from "@/app/base/components/TooltipButton"; -import { COL_SIZES } from "@/app/base/constants"; -import docsUrls from "@/app/base/docsUrls"; +import FormikField from "app/base/components/FormikField"; +import TooltipButton from "app/base/components/TooltipButton"; +import docsUrls from "app/base/docsUrls"; -type Props = { - cols?: number; -}; - -export const SSHKeyFormFields = ({ - cols = COL_SIZES.TOTAL, -}: Props): JSX.Element => { +export const SSHKeyFormFields = (): JSX.Element => { const { values } = useFormikContext(); const { protocol } = values; const uploadSelected = protocol === "upload"; - const colSize = cols / 2; return ( <> - + )} - +

Before you can deploy a machine you must import at least one public SSH key into MAAS, so the deployed machine can be accessed. diff --git a/src/app/base/components/TableActions/TableActions.tsx b/src/app/base/components/TableActions/TableActions.tsx index 0b3f578ed8..1168a861be 100644 --- a/src/app/base/components/TableActions/TableActions.tsx +++ b/src/app/base/components/TableActions/TableActions.tsx @@ -9,6 +9,7 @@ type Props = { copyValue?: string; deleteDisabled?: boolean; deleteTooltip?: string | null; + deletePath?: string; editDisabled?: boolean; editPath?: string; editTooltip?: string | null; @@ -22,6 +23,7 @@ const TableActions = ({ clearTooltip, copyValue, deleteDisabled, + deletePath, deleteTooltip, editDisabled, editPath, @@ -48,16 +50,18 @@ const TableActions = ({ )} - {onDelete && ( + {(onDelete || deletePath) && ( diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx index 26975bd6f3..fbfc26c929 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/AvailableStorageTable.tsx @@ -652,7 +652,6 @@ const AvailableStorageTable = ({ )} {isMachine && canEditStorage && ( { + const setSidePanelContent = vi.fn(); + + beforeAll(() => { + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + setSidePanelContent, + sidePanelContent: null, + setSidePanelSize: vi.fn(), + sidePanelSize: "regular", + }); + }); + it("disables create volume group button with tooltip if selected devices are not eligible", async () => { const selected = [ diskFactory({ @@ -42,7 +56,6 @@ describe("BulkActions", () => { }); renderWithBrowserRouter( { }); renderWithBrowserRouter( { }), }); renderWithBrowserRouter( - , - { state } + , + { + state, + } ); expect(screen.getByTestId("vmware-bulk-actions")).toBeInTheDocument(); @@ -136,7 +145,6 @@ describe("BulkActions", () => { }); renderWithBrowserRouter( { }); renderWithBrowserRouter( { ).not.toBeDisabled(); }); - it("can render the create datastore form", () => { + it("can trigger the create datastore sidepanel", async () => { + const datastore = diskFactory({ + filesystem: fsFactory({ fstype: "vmfs6" }), + }); + const selected = diskFactory({ filesystem: null, partitions: null }); const state = rootStateFactory({ machine: machineStateFactory({ items: [ machineDetailsFactory({ + detected_storage_layout: StorageLayout.VMFS6, + disks: [datastore, selected], system_id: "abc123", }), ], @@ -199,30 +212,40 @@ describe("BulkActions", () => { }); renderWithBrowserRouter( , { state } ); - // Ensure correct form inputs are shown - expect(screen.getByRole("textbox", { name: "Name" })).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Size" })).toBeInTheDocument(); - expect( - screen.getByRole("textbox", { name: "Filesystem" }) - ).toBeInTheDocument(); - expect( + await userEvent.click( screen.getByRole("button", { name: "Create datastore" }) - ).toBeInTheDocument(); + ); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.CREATE_DATASTORE, + }) + ); }); - it("can render the create RAID form", () => { + it("can trigger the create RAID sidepanel", async () => { + const selected = [ + diskFactory({ + filesystem: null, + type: DiskTypes.VIRTUAL, + }), + diskFactory({ + filesystem: null, + type: DiskTypes.VIRTUAL, + }), + ]; const state = rootStateFactory({ machine: machineStateFactory({ items: [ machineDetailsFactory({ + detected_storage_layout: StorageLayout.FLAT, + disks: selected, system_id: "abc123", }), ], @@ -233,40 +256,38 @@ describe("BulkActions", () => { }); renderWithBrowserRouter( , { state } ); - // Ensure the correct form inputs are shown - expect(screen.getByRole("textbox", { name: "Name" })).toBeInTheDocument(); - expect( - screen.getByRole("combobox", { name: "RAID level" }) - ).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Size" })).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Tags" })).toBeInTheDocument(); - expect( - screen.getByRole("combobox", { name: "Filesystem" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("textbox", { name: "Mount point" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("textbox", { name: "Mount options" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "Create RAID" }) - ).toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Create RAID" })); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.CREATE_RAID, + }) + ); }); - it("can render the create volume group form", () => { + it("can trigger the create volume group sidepanel", async () => { + const selected = [ + diskFactory({ + filesystem: null, + type: DiskTypes.VIRTUAL, + }), + diskFactory({ + filesystem: null, + type: DiskTypes.VIRTUAL, + }), + ]; const state = rootStateFactory({ machine: machineStateFactory({ items: [ machineDetailsFactory({ + detected_storage_layout: StorageLayout.FLAT, + disks: selected, system_id: "abc123", }), ], @@ -277,33 +298,35 @@ describe("BulkActions", () => { }); renderWithBrowserRouter( , { state } ); - // Ensure the correct form inputs are shown - expect( + await userEvent.click( screen.getByRole("button", { name: "Create volume group" }) - ).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Name" })).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Size" })).toBeInTheDocument(); - expect(screen.getByRole("textbox", { name: "Type" })).toBeInTheDocument(); + ); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.CREATE_VOLUME_GROUP, + }) + ); }); - it("can render the update datastore form", () => { + it("can trigger the update datastore sidepanel", async () => { const datastore = diskFactory({ filesystem: fsFactory({ fstype: "vmfs6" }), }); + const selected = diskFactory({ filesystem: null, partitions: null }); const state = rootStateFactory({ machine: machineStateFactory({ items: [ machineDetailsFactory({ + detected_storage_layout: StorageLayout.VMFS6, + disks: [datastore, selected], system_id: "abc123", - disks: [datastore], }), ], statuses: machineStatusesFactory({ @@ -311,25 +334,23 @@ describe("BulkActions", () => { }), }), }); + renderWithBrowserRouter( , { state } ); - // Ensure the correct form inputs are shown - expect( - screen.getByRole("combobox", { name: "Datastore" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("textbox", { name: "Mount point" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("textbox", { name: "Size to add" }) - ).toBeInTheDocument(); + await userEvent.click( + screen.getByRole("button", { name: "Add to existing datastore" }) + ); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.UPDATE_DATASTORE, + }) + ); }); }); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx index 1e2bfe0b62..cf27a501ba 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx @@ -3,16 +3,13 @@ import { useSelector } from "react-redux"; import { BulkAction } from "../AvailableStorageTable"; -import CreateDatastore from "./CreateDatastore"; -import CreateRaid from "./CreateRaid"; -import CreateVolumeGroup from "./CreateVolumeGroup"; -import UpdateDatastore from "./UpdateDatastore"; - -import machineSelectors from "@/app/store/machine/selectors"; -import type { Machine } from "@/app/store/machine/types"; -import { isMachineDetails } from "@/app/store/machine/utils"; -import type { RootState } from "@/app/store/root/types"; -import type { Disk, Partition } from "@/app/store/types/node"; +import { useSidePanel } from "app/base/side-panel-context"; +import { MachineSidePanelViews } from "app/machines/constants"; +import machineSelectors from "app/store/machine/selectors"; +import type { Machine } from "app/store/machine/types"; +import { isMachineDetails } from "app/store/machine/utils"; +import type { RootState } from "app/store/root/types"; +import type { Disk, Partition } from "app/store/types/node"; import { canCreateOrUpdateDatastore, canCreateRaid, @@ -22,14 +19,12 @@ import { } from "@/app/store/utils"; type Props = { - bulkAction: BulkAction | null; selected: (Disk | Partition)[]; setBulkAction: (bulkAction: BulkAction | null) => void; systemId: Machine["system_id"]; }; const BulkActions = ({ - bulkAction, selected, setBulkAction, systemId, @@ -37,51 +32,12 @@ const BulkActions = ({ const machine = useSelector((state: RootState) => machineSelectors.getById(state, systemId) ); + const { setSidePanelContent } = useSidePanel(); if (!isMachineDetails(machine)) { return null; } - if (bulkAction === BulkAction.CREATE_DATASTORE) { - return ( - setBulkAction(null)} - selected={selected} - systemId={systemId} - /> - ); - } - - if (bulkAction === BulkAction.CREATE_RAID) { - return ( - setBulkAction(null)} - selected={selected} - systemId={systemId} - /> - ); - } - - if (bulkAction === BulkAction.CREATE_VOLUME_GROUP) { - return ( - setBulkAction(null)} - selected={selected} - systemId={systemId} - /> - ); - } - - if (bulkAction === BulkAction.UPDATE_DATASTORE) { - return ( - setBulkAction(null)} - selected={selected} - systemId={systemId} - /> - ); - } - if (isVMWareLayout(machine.detected_storage_layout)) { const hasDatastores = machine.disks.some((disk) => isDatastore(disk.filesystem) @@ -115,7 +71,13 @@ const BulkActions = ({ @@ -128,7 +90,13 @@ const BulkActions = ({ @@ -158,7 +126,13 @@ const BulkActions = ({ @@ -175,7 +149,13 @@ const BulkActions = ({ diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx index 67a68beaed..18ed1ef0c3 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx @@ -10,17 +10,16 @@ import { import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormCard from "@/app/base/components/FormCard"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import machineSelectors from "@/app/store/machine/selectors"; -import type { Machine } from "@/app/store/machine/types"; -import type { MachineEventErrors } from "@/app/store/machine/types/base"; -import { isMachineDetails } from "@/app/store/machine/utils"; -import type { RootState } from "@/app/store/root/types"; -import type { Disk, Partition } from "@/app/store/types/node"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import machineSelectors from "app/store/machine/selectors"; +import type { Machine } from "app/store/machine/types"; +import type { MachineEventErrors } from "app/store/machine/types/base"; +import { isMachineDetails } from "app/store/machine/utils"; +import type { RootState } from "app/store/root/types"; +import type { Disk, Partition } from "app/store/types/node"; import { formatSize, formatType, @@ -78,78 +77,76 @@ export const CreateDatastore = ({ if (isMachineDetails(machine)) { return ( - - - allowUnchanged - cleanup={machineActions.cleanup} - errors={errors} - initialValues={{ - name: getInitialName(machine.disks), - }} - onCancel={closeForm} - onSaveAnalytics={{ - action: "Create datastore", - category: "Machine storage", - label: "Create datastore", - }} - onSubmit={(values: CreateDatastoreValues) => { - const [blockDeviceIds, partitionIds] = - splitDiskPartitionIds(selected); - const params = { - name: values.name, - systemId, - ...(blockDeviceIds.length > 0 && { blockDeviceIds }), - ...(partitionIds.length > 0 && { partitionIds }), - }; - dispatch(machineActions.createVmfsDatastore(params)); - }} - saved={saved} - saving={saving} - submitLabel="Create datastore" - validationSchema={CreateDatastoreSchema} - > - - - - - - Name - Size - Device type + + allowUnchanged + cleanup={machineActions.cleanup} + errors={errors} + initialValues={{ + name: getInitialName(machine.disks), + }} + onCancel={closeForm} + onSaveAnalytics={{ + action: "Create datastore", + category: "Machine storage", + label: "Create datastore", + }} + onSubmit={(values: CreateDatastoreValues) => { + const [blockDeviceIds, partitionIds] = + splitDiskPartitionIds(selected); + const params = { + name: values.name, + systemId, + ...(blockDeviceIds.length > 0 && { blockDeviceIds }), + ...(partitionIds.length > 0 && { partitionIds }), + }; + dispatch(machineActions.createVmfsDatastore(params)); + }} + saved={saved} + saving={saving} + submitLabel="Create datastore" + validationSchema={CreateDatastoreSchema} + > + + +
+ + + Name + Size + Device type + + + + {selected.map((device) => ( + + {device.name} + {formatSize(device.size)} + {formatType(device)} - - - {selected.map((device) => ( - - {device.name} - {formatSize(device.size)} - {formatType(device)} - - ))} - -
- - - - - - -
- -
+ ))} + + + + + + + + + + ); } return null; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx index 6ea67ecc95..065d0f34b5 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx @@ -3,18 +3,17 @@ import * as Yup from "yup"; import CreateRaidFields from "./CreateRaidFields"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import machineSelectors from "@/app/store/machine/selectors"; -import type { Machine } from "@/app/store/machine/types"; -import type { MachineEventErrors } from "@/app/store/machine/types/base"; -import { isMachineDetails } from "@/app/store/machine/utils"; -import type { RootState } from "@/app/store/root/types"; -import { DiskTypes } from "@/app/store/types/enum"; -import type { Disk, Partition } from "@/app/store/types/node"; -import { isRaid, splitDiskPartitionIds } from "@/app/store/utils"; +import FormikForm from "app/base/components/FormikForm"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import machineSelectors from "app/store/machine/selectors"; +import type { Machine } from "app/store/machine/types"; +import type { MachineEventErrors } from "app/store/machine/types/base"; +import { isMachineDetails } from "app/store/machine/utils"; +import type { RootState } from "app/store/root/types"; +import { DiskTypes } from "app/store/types/enum"; +import type { Disk, Partition } from "app/store/types/node"; +import { isRaid, splitDiskPartitionIds } from "app/store/utils"; export type CreateRaidValues = { blockDeviceIds: number[]; @@ -87,66 +86,64 @@ export const CreateRaid = ({ if (isMachineDetails(machine)) { return ( - - - allowUnchanged - cleanup={machineActions.cleanup} - errors={errors} - initialValues={{ - blockDeviceIds: initialBlockDevices, - fstype: "", - level: DiskTypes.RAID_0, - mountOptions: "", - mountPoint: "", - name: getInitialName(machine.disks), - partitionIds: initialPartitions, - spareBlockDeviceIds: [], - sparePartitionIds: [], - tags: [], - }} - onCancel={closeForm} - onSaveAnalytics={{ - action: "Create RAID", - category: "Machine storage", - label: "Create RAID", - }} - onSubmit={(values) => { - const { - blockDeviceIds, - fstype, + + allowUnchanged + cleanup={machineActions.cleanup} + errors={errors} + initialValues={{ + blockDeviceIds: initialBlockDevices, + fstype: "", + level: DiskTypes.RAID_0, + mountOptions: "", + mountPoint: "", + name: getInitialName(machine.disks), + partitionIds: initialPartitions, + spareBlockDeviceIds: [], + sparePartitionIds: [], + tags: [], + }} + onCancel={closeForm} + onSaveAnalytics={{ + action: "Create RAID", + category: "Machine storage", + label: "Create RAID", + }} + onSubmit={(values) => { + const { + blockDeviceIds, + fstype, + level, + mountOptions, + mountPoint, + name, + partitionIds, + spareBlockDeviceIds, + sparePartitionIds, + tags, + } = values; + dispatch( + machineActions.createRaid({ level, - mountOptions, - mountPoint, name, - partitionIds, - spareBlockDeviceIds, - sparePartitionIds, + systemId, tags, - } = values; - dispatch( - machineActions.createRaid({ - level, - name, - systemId, - tags, - ...(fstype && { fstype }), - ...(fstype && mountOptions && { mountOptions }), - ...(fstype && mountPoint && { mountPoint }), - ...(blockDeviceIds.length > 0 && { blockDeviceIds }), - ...(partitionIds.length > 0 && { partitionIds }), - ...(spareBlockDeviceIds.length > 0 && { spareBlockDeviceIds }), - ...(sparePartitionIds.length > 0 && { sparePartitionIds }), - }) - ); - }} - saved={saved} - saving={saving} - submitLabel="Create RAID" - validationSchema={CreateRaidSchema} - > - - - + ...(fstype && { fstype }), + ...(fstype && mountOptions && { mountOptions }), + ...(fstype && mountPoint && { mountPoint }), + ...(blockDeviceIds.length > 0 && { blockDeviceIds }), + ...(partitionIds.length > 0 && { partitionIds }), + ...(spareBlockDeviceIds.length > 0 && { spareBlockDeviceIds }), + ...(sparePartitionIds.length > 0 && { sparePartitionIds }), + }) + ); + }} + saved={saved} + saving={saving} + submitLabel="Create RAID" + validationSchema={CreateRaidSchema} + > + + ); } return null; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaidFields/CreateRaidFields.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaidFields/CreateRaidFields.tsx index 1507cb24bc..f96dd77117 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaidFields/CreateRaidFields.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaidFields/CreateRaidFields.tsx @@ -123,7 +123,7 @@ export const CreateRaidFields = ({ return ( <> - + component={Select} @@ -160,7 +160,7 @@ export const CreateRaidFields = ({ /> - + diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx index 06244954f5..95241b7b5e 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx @@ -10,23 +10,18 @@ import { import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormCard from "@/app/base/components/FormCard"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import machineSelectors from "@/app/store/machine/selectors"; -import type { Machine } from "@/app/store/machine/types"; -import type { MachineEventErrors } from "@/app/store/machine/types/base"; -import { isMachineDetails } from "@/app/store/machine/utils"; -import type { RootState } from "@/app/store/root/types"; -import { DiskTypes } from "@/app/store/types/enum"; -import type { Disk, Partition } from "@/app/store/types/node"; -import { - formatSize, - formatType, - splitDiskPartitionIds, -} from "@/app/store/utils"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import machineSelectors from "app/store/machine/selectors"; +import type { Machine } from "app/store/machine/types"; +import type { MachineEventErrors } from "app/store/machine/types/base"; +import { isMachineDetails } from "app/store/machine/utils"; +import type { RootState } from "app/store/root/types"; +import { DiskTypes } from "app/store/types/enum"; +import type { Disk, Partition } from "app/store/types/node"; +import { formatSize, formatType, splitDiskPartitionIds } from "app/store/utils"; type CreateVolumeGroupValues = { name: string; @@ -72,77 +67,75 @@ export const CreateVolumeGroup = ({ if (isMachineDetails(machine)) { return ( - - - allowUnchanged - cleanup={machineActions.cleanup} - errors={errors} - initialValues={{ - name: getInitialName(machine.disks), - }} - onCancel={closeForm} - onSaveAnalytics={{ - action: "Create volume group", - category: "Machine storage", - label: "Create volume group", - }} - onSubmit={(values: CreateVolumeGroupValues) => { - const [blockDeviceIds, partitionIds] = - splitDiskPartitionIds(selected); - const params = { - name: values.name, - systemId, - ...(blockDeviceIds.length > 0 && { blockDeviceIds }), - ...(partitionIds.length > 0 && { partitionIds }), - }; - dispatch(machineActions.createVolumeGroup(params)); - }} - saved={saved} - saving={saving} - submitLabel="Create volume group" - validationSchema={CreateVolumeGroupSchema} - > - - - - - - Name - Size - Device type + + allowUnchanged + cleanup={machineActions.cleanup} + errors={errors} + initialValues={{ + name: getInitialName(machine.disks), + }} + onCancel={closeForm} + onSaveAnalytics={{ + action: "Create volume group", + category: "Machine storage", + label: "Create volume group", + }} + onSubmit={(values: CreateVolumeGroupValues) => { + const [blockDeviceIds, partitionIds] = + splitDiskPartitionIds(selected); + const params = { + name: values.name, + systemId, + ...(blockDeviceIds.length > 0 && { blockDeviceIds }), + ...(partitionIds.length > 0 && { partitionIds }), + }; + dispatch(machineActions.createVolumeGroup(params)); + }} + saved={saved} + saving={saving} + submitLabel="Create volume group" + validationSchema={CreateVolumeGroupSchema} + > + + +
+ + + Name + Size + Device type + + + + {selected.map((device) => ( + + {device.name} + {formatSize(device.size)} + {formatType(device)} - - - {selected.map((device) => ( - - {device.name} - {formatSize(device.size)} - {formatType(device)} - - ))} - -
- - - - - - -
- -
+ ))} + + + + + + + + + + ); } return null; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx index c97406cc8b..9e3c4513c4 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx @@ -3,17 +3,16 @@ import * as Yup from "yup"; import UpdateDatastoreFields from "./UpdateDatastoreFields"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import machineSelectors from "@/app/store/machine/selectors"; -import type { Machine } from "@/app/store/machine/types"; -import type { MachineEventErrors } from "@/app/store/machine/types/base"; -import { isMachineDetails } from "@/app/store/machine/utils"; -import type { RootState } from "@/app/store/root/types"; -import type { Disk, Partition } from "@/app/store/types/node"; -import { isDatastore, splitDiskPartitionIds } from "@/app/store/utils"; +import FormikForm from "app/base/components/FormikForm"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import machineSelectors from "app/store/machine/selectors"; +import type { Machine } from "app/store/machine/types"; +import type { MachineEventErrors } from "app/store/machine/types/base"; +import { isMachineDetails } from "app/store/machine/utils"; +import type { RootState } from "app/store/root/types"; +import type { Disk, Partition } from "app/store/types/node"; +import { isDatastore, splitDiskPartitionIds } from "app/store/utils"; export type UpdateDatastoreValues = { datastore: number; @@ -58,42 +57,40 @@ export const UpdateDatastore = ({ } return ( - - - allowUnchanged - cleanup={machineActions.cleanup} - errors={errors} - initialValues={{ - datastore: datastores[0].id, - }} - onCancel={closeForm} - onSaveAnalytics={{ - action: "Update datastore", - category: "Machine storage", - label: "Add to datastore", - }} - onSubmit={(values: UpdateDatastoreValues) => { - const [blockDeviceIds, partitionIds] = - splitDiskPartitionIds(selected); - const params = { - systemId, - vmfsDatastoreId: values.datastore, - ...(blockDeviceIds.length > 0 && { blockDeviceIds }), - ...(partitionIds.length > 0 && { partitionIds }), - }; - dispatch(machineActions.updateVmfsDatastore(params)); - }} - saved={saved} - saving={saving} - submitLabel="Add to datastore" - validationSchema={UpdateDatastoreSchema} - > - - - + + allowUnchanged + cleanup={machineActions.cleanup} + errors={errors} + initialValues={{ + datastore: datastores[0].id, + }} + onCancel={closeForm} + onSaveAnalytics={{ + action: "Update datastore", + category: "Machine storage", + label: "Add to datastore", + }} + onSubmit={(values: UpdateDatastoreValues) => { + const [blockDeviceIds, partitionIds] = + splitDiskPartitionIds(selected); + const params = { + systemId, + vmfsDatastoreId: values.datastore, + ...(blockDeviceIds.length > 0 && { blockDeviceIds }), + ...(partitionIds.length > 0 && { partitionIds }), + }; + dispatch(machineActions.updateVmfsDatastore(params)); + }} + saved={saved} + saving={saving} + submitLabel="Add to datastore" + validationSchema={UpdateDatastoreSchema} + > + + ); } return null; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastoreFields/UpdateDatastoreFields.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastoreFields/UpdateDatastoreFields.tsx index 99bb0ce858..ec77051d83 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastoreFields/UpdateDatastoreFields.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastoreFields/UpdateDatastoreFields.tsx @@ -36,7 +36,7 @@ export const UpdateDatastoreFields = ({ return ( - + @@ -56,7 +56,7 @@ export const UpdateDatastoreFields = ({
- + - - aria-label="Add special filesystem" - cleanup={machineActions.cleanup} - errors={errors} - initialValues={{ - fstype: "", - mountOptions: "", - mountPoint: "", - }} - onCancel={closeForm} - onSaveAnalytics={{ - action: "Add special filesystem", - category: "Machine storage", - label: "Mount", - }} - onSubmit={(values) => { - dispatch(machineActions.cleanup()); - const params = { - fstype: values.fstype, - mountOptions: values.mountOptions, - mountPoint: values.mountPoint, - systemId: machine.system_id, - }; - dispatch(machineActions.mountSpecial(params)); - }} - saved={saved} - saving={saving} - submitLabel="Mount" - validationSchema={AddSpecialFilesystemSchema} - > - - - - - - - - - + + aria-label="Add special filesystem" + cleanup={machineActions.cleanup} + errors={errors} + initialValues={{ + fstype: "", + mountOptions: "", + mountPoint: "", + }} + onCancel={closeForm} + onSaveAnalytics={{ + action: "Add special filesystem", + category: "Machine storage", + label: "Mount", + }} + onSubmit={(values) => { + dispatch(machineActions.cleanup()); + const params = { + fstype: values.fstype, + mountOptions: values.mountOptions, + mountPoint: values.mountPoint, + systemId: machine.system_id, + }; + dispatch(machineActions.mountSpecial(params)); + }} + saved={saved} + saving={saving} + submitLabel="Mount" + validationSchema={AddSpecialFilesystemSchema} + > + + + + + + + + ); }; 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 56e16bfef8..057a1400fd 100644 --- a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.test.tsx +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.test.tsx @@ -240,34 +240,6 @@ it("disables the action menu if node is a machine and storage can't be edited", expect(screen.getByRole("button", { name: /Take action/ })).toBeDisabled(); }); -it("can show an add special filesystem form if node is a machine", async () => { - const machine = machineDetailsFactory({ system_id: "abc123" }); - const state = rootStateFactory({ - machine: machineStateFactory({ - items: [machine], - statuses: machineStatusesFactory({ - abc123: machineStatusFactory(), - }), - }), - }); - const store = mockStore(state); - render( - - - - - - - - ); - - await userEvent.click(screen.getByTestId("add-special-fs-button")); - - expect( - screen.getByRole("form", { name: "Add special filesystem" }) - ).toBeInTheDocument(); -}); - it("can remove a disk's filesystem if node is a machine", async () => { const filesystem = fsFactory({ mount_point: "/disk-fs/path" }); const disk = diskFactory({ filesystem, partitions: [] }); diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx index 988410c20f..a553ba102d 100644 --- a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx @@ -4,14 +4,14 @@ 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 AddSpecialFilesystem from "./AddSpecialFilesystem"; - -import TableActionsDropdown from "@/app/base/components/TableActionsDropdown"; -import ActionConfirm from "@/app/base/components/node/ActionConfirm"; -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 TableActionsDropdown from "app/base/components/TableActionsDropdown"; +import ActionConfirm from "app/base/components/node/ActionConfirm"; +import { useSidePanel } 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 { formatSize, isMounted, @@ -127,10 +127,9 @@ const FilesystemsTable = ({ node, }: Props): JSX.Element | null => { const dispatch = useDispatch(); - const [addSpecialFormOpen, setAddSpecialFormOpen] = useState(false); const [expanded, setExpanded] = useState(null); const isMachine = nodeIsMachine(node); - const closeAddSpecialForm = () => setAddSpecialFormOpen(false); + const { setSidePanelContent } = useSidePanel(); const rows = node.disks.reduce((rows, disk) => { const diskFs = disk.filesystem; @@ -346,19 +345,22 @@ const FilesystemsTable = ({ No filesystems defined.

)} - {canEditStorage && !addSpecialFormOpen && ( + {canEditStorage && ( )} - {isMachine && addSpecialFormOpen && ( - - )} ); }; diff --git a/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx new file mode 100644 index 0000000000..069aaf9a44 --- /dev/null +++ b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx @@ -0,0 +1,43 @@ +import configureStore from "redux-mock-store"; + +import DeviceNetworkForms from "./DeviceNetworkForms"; + +import { DeviceSidePanelViews } from "app/devices/constants"; +import type { DeviceSidePanelContent } from "app/devices/types"; +import type { RootState } from "app/store/root/types"; +import { + deviceDetails as deviceDetailsFactory, + deviceState as deviceStateFactory, + rootState as rootStateFactory, +} from "testing/factories"; +import { renderWithBrowserRouter, screen } from "testing/utils"; + +const mockStore = configureStore(); +let state: RootState; + +beforeEach(() => { + state = rootStateFactory({ + device: deviceStateFactory({ + items: [deviceDetailsFactory({ system_id: "abc123" })], + }), + }); +}); + +it("renders a form when appropriate sidepanel view is provided", () => { + const store = mockStore(state); + const sidePanelContent: DeviceSidePanelContent = { + view: DeviceSidePanelViews.ADD_INTERFACE, + }; + renderWithBrowserRouter( + , + { store } + ); + + expect( + screen.getByRole("form", { name: /add interface/i }) + ).toBeInTheDocument(); +}); diff --git a/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx new file mode 100644 index 0000000000..8e62ea43f2 --- /dev/null +++ b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx @@ -0,0 +1,66 @@ +import { useCallback } from "react"; + +import type { SidePanelContentTypes } from "app/base/side-panel-context"; +import { DeviceSidePanelViews } from "app/devices/constants"; +import AddInterface from "app/devices/views/DeviceDetails/DeviceNetwork/AddInterface"; +import RemoveInterface from "app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface"; +import EditInterface from "app/devices/views/DeviceDetails/DeviceNetwork/EditInterface"; +import type { Device } from "app/store/device/types"; + +type Props = SidePanelContentTypes & { + systemId: Device["system_id"]; +}; + +const DeviceNetworkForms = ({ + systemId, + sidePanelContent, + setSidePanelContent, +}: Props): JSX.Element | null => { + const clearSidePanelContent = useCallback( + () => setSidePanelContent(null), + [setSidePanelContent] + ); + + if (!sidePanelContent) { + return null; + } + + const linkId = + sidePanelContent.extras && "linkId" in sidePanelContent.extras + ? sidePanelContent.extras.linkId + : null; + + const nicId = + sidePanelContent.extras && "nicId" in sidePanelContent.extras + ? sidePanelContent.extras.nicId + : null; + + switch (sidePanelContent.view) { + case DeviceSidePanelViews.ADD_INTERFACE: + return ( + + ); + case DeviceSidePanelViews.EDIT_INTERFACE: + return ( + + ); + case DeviceSidePanelViews.REMOVE_INTERFACE: + return nicId ? ( + + ) : null; + + default: + return null; + } +}; + +export default DeviceNetworkForms; diff --git a/src/app/devices/components/DeviceNetworkForms/index.ts b/src/app/devices/components/DeviceNetworkForms/index.ts new file mode 100644 index 0000000000..027974ba28 --- /dev/null +++ b/src/app/devices/components/DeviceNetworkForms/index.ts @@ -0,0 +1 @@ +export { default } from "./DeviceNetworkForms"; diff --git a/src/app/devices/constants.ts b/src/app/devices/constants.ts index 65b9af379f..9f84a5587c 100644 --- a/src/app/devices/constants.ts +++ b/src/app/devices/constants.ts @@ -7,6 +7,9 @@ export const DeviceActionHeaderViews = { export const DeviceNonActionHeaderViews = { ADD_DEVICE: ["deviceNonActionForm", "addDevice"], + ADD_INTERFACE: ["deviceNonActionForm", "addInterface"], + EDIT_INTERFACE: ["deviceNonActionForm", "editInterface"], + REMOVE_INTERFACE: ["deviceNonActionForm", "removeInterface"], } as const; export const DeviceSidePanelViews = { diff --git a/src/app/devices/types.ts b/src/app/devices/types.ts index 06199f9cb1..c140279a1f 100644 --- a/src/app/devices/types.ts +++ b/src/app/devices/types.ts @@ -1,10 +1,15 @@ import type { ValueOf } from "@canonical/react-components"; -import type { SidePanelContent, SetSidePanelContent } from "@/app/base/types"; -import type { DeviceSidePanelViews } from "@/app/devices/constants"; +import type { SidePanelContent, SetSidePanelContent } from "app/base/types"; +import type { DeviceSidePanelViews } from "app/devices/constants"; +import type { NetworkInterface, NetworkLink } from "app/store/types/node"; export type DeviceSidePanelContent = SidePanelContent< - ValueOf + ValueOf, + { + linkId?: NetworkLink["id"]; + nicId?: NetworkInterface["id"]; + } >; export type DeviceSetSidePanelContent = diff --git a/src/app/devices/views/DeviceDetails/DeviceDetails.tsx b/src/app/devices/views/DeviceDetails/DeviceDetails.tsx index e3eaa2ef3d..e7f7ee448c 100644 --- a/src/app/devices/views/DeviceDetails/DeviceDetails.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceDetails.tsx @@ -9,19 +9,20 @@ import DeviceDetailsHeader from "./DeviceDetailsHeader"; import DeviceNetwork from "./DeviceNetwork"; import DeviceSummary from "./DeviceSummary"; -import ModelNotFound from "@/app/base/components/ModelNotFound"; -import PageContent from "@/app/base/components/PageContent"; -import { useGetURLId } from "@/app/base/hooks/urls"; -import { useSidePanel } from "@/app/base/side-panel-context"; -import urls from "@/app/base/urls"; -import DeviceHeaderForms from "@/app/devices/components/DeviceHeaderForms"; -import { actions as deviceActions } from "@/app/store/device"; -import deviceSelectors from "@/app/store/device/selectors"; -import { DeviceMeta } from "@/app/store/device/types"; -import type { RootState } from "@/app/store/root/types"; -import { actions as tagActions } from "@/app/store/tag"; -import { getSidePanelTitle } from "@/app/store/utils/node/base"; -import { isId, getRelativeRoute } from "@/app/utils"; +import ModelNotFound from "app/base/components/ModelNotFound"; +import PageContent from "app/base/components/PageContent"; +import { useGetURLId } from "app/base/hooks/urls"; +import { useSidePanel } from "app/base/side-panel-context"; +import urls from "app/base/urls"; +import DeviceHeaderForms from "app/devices/components/DeviceHeaderForms"; +import DeviceNetworkForms from "app/devices/components/DeviceNetworkForms"; +import { actions as deviceActions } from "app/store/device"; +import deviceSelectors from "app/store/device/selectors"; +import { DeviceMeta } from "app/store/device/types"; +import type { RootState } from "app/store/root/types"; +import { actions as tagActions } from "app/store/tag"; +import { getSidePanelTitle } from "app/store/utils/node/base"; +import { isId, getRelativeRoute } from "app/utils"; const DeviceDetails = (): JSX.Element => { const { sidePanelContent, setSidePanelContent } = useSidePanel(); @@ -54,51 +55,90 @@ const DeviceDetails = (): JSX.Element => { } const base = urls.devices.device.index(null); - return ( - - } - sidePanelContent={ - sidePanelContent && - device && ( - - ) - } - sidePanelTitle={getSidePanelTitle(device?.fqdn || "", sidePanelContent)} - > - {device && ( - - } - path={getRelativeRoute(urls.devices.device.summary(null), base)} - /> - } - path={getRelativeRoute(urls.devices.device.network(null), base)} - /> - } - path={getRelativeRoute( - urls.devices.device.configuration(null), - base + return device ? ( + + + } + sidePanelContent={ + sidePanelContent && + device && ( + + ) + } + sidePanelTitle={getSidePanelTitle( + device?.fqdn || "", + sidePanelContent )} - /> - } - path="/" - /> - - )} - + > + + + } + path={getRelativeRoute(urls.devices.device.summary(null), base)} + /> + + } + sidePanelContent={ + sidePanelContent && ( + + ) + } + sidePanelTitle={getSidePanelTitle( + device?.fqdn || "", + sidePanelContent + )} + > + + + } + path={getRelativeRoute(urls.devices.device.network(null), base)} + /> + + } + sidePanelContent={null} + sidePanelTitle={null} + > + + + } + path={getRelativeRoute(urls.devices.device.configuration(null), base)} + /> + } + path="/" + /> + + ) : ( + <> ); }; diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx index beb984188b..527432679d 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx @@ -3,10 +3,9 @@ import { useDispatch, useSelector } from "react-redux"; import InterfaceForm from "../InterfaceForm"; -import FormCard from "@/app/base/components/FormCard"; -import { useCycled, useScrollOnRender } from "@/app/base/hooks"; -import { actions as deviceActions } from "@/app/store/device"; -import deviceSelectors from "@/app/store/device/selectors"; +import { useCycled, useScrollOnRender } from "app/base/hooks"; +import { actions as deviceActions } from "app/store/device"; +import deviceSelectors from "app/store/device/selectors"; import type { CreateInterfaceParams, Device, @@ -44,28 +43,27 @@ const AddInterface = ({ closeForm, systemId }: Props): JSX.Element => { } return (
- - { - resetCreatedInterface(); - dispatch(deviceActions.cleanup()); - const payload = preparePayload({ - ...values, - system_id: device.system_id, - }) as CreateInterfaceParams; - dispatch(deviceActions.createInterface(payload)); - }} - saved={saved} - saving={creatingInterface} - systemId={systemId} - /> - + { + resetCreatedInterface(); + dispatch(deviceActions.cleanup()); + const payload = preparePayload({ + ...values, + system_id: device.system_id, + }) as CreateInterfaceParams; + dispatch(deviceActions.createInterface(payload)); + }} + saved={saved} + saving={creatingInterface} + systemId={systemId} + />
); }; diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx index 777eba71e7..5c8de07fac 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx @@ -3,17 +3,15 @@ import { useSelector } from "react-redux"; import AddInterface from "./AddInterface"; import DeviceNetworkTable from "./DeviceNetworkTable"; -import EditInterface from "./EditInterface"; -import DHCPTable from "@/app/base/components/DHCPTable"; -import NetworkActionRow from "@/app/base/components/NetworkActionRow"; -import NodeNetworkTab from "@/app/base/components/NodeNetworkTab"; -import { ExpandedState } from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; -import { useWindowTitle } from "@/app/base/hooks"; -import deviceSelectors from "@/app/store/device/selectors"; -import { DeviceMeta } from "@/app/store/device/types"; -import type { Device } from "@/app/store/device/types"; -import type { RootState } from "@/app/store/root/types"; +import DHCPTable from "app/base/components/DHCPTable"; +import NetworkActionRow from "app/base/components/NetworkActionRow"; +import NodeNetworkTab from "app/base/components/NodeNetworkTab"; +import { useWindowTitle } from "app/base/hooks"; +import deviceSelectors from "app/store/device/selectors"; +import { DeviceMeta } from "app/store/device/types"; +import type { Device } from "app/store/device/types"; +import type { RootState } from "app/store/root/types"; export enum Label { Title = "Device network", @@ -37,12 +35,8 @@ const DeviceNetwork = ({ systemId }: Props): JSX.Element => { return ( <> ( - + actions={(_, setExpanded) => ( + )} addInterface={(_, setExpanded) => ( { node={device} /> )} - expandedForm={(expanded, setExpanded) => { - if (expanded?.content === ExpandedState.EDIT) { - return ( - setExpanded(null)} - linkId={expanded?.linkId} - nicId={expanded?.nicId} - systemId={systemId} - /> - ); - } - return null; - }} - interfaceTable={(expanded, setExpanded) => ( - - )} + interfaceTable={() => } /> ); diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx index eeed74cb20..b9891dfc2f 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx @@ -2,9 +2,8 @@ import configureStore from "redux-mock-store"; import DeviceNetworkTable from "./DeviceNetworkTable"; -import { ExpandedState } from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; -import type { RootState } from "@/app/store/root/types"; -import { NetworkInterfaceTypes } from "@/app/store/types/enum"; +import type { RootState } from "app/store/root/types"; +import { NetworkInterfaceTypes } from "app/store/types/enum"; import { deviceDetails as deviceDetailsFactory, deviceInterface as deviceInterfaceFactory, @@ -49,27 +48,17 @@ describe("DeviceNetworkTable", () => { it("displays a spinner when loading", () => { state.device.items = []; const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); + renderWithBrowserRouter(, { + store, + }); expect(screen.getByText("Loading...")).toBeInTheDocument(); }); it("displays a table when loaded", () => { const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); + renderWithBrowserRouter(, { + store, + }); expect(screen.getByRole("grid")).toBeInTheDocument(); }); @@ -98,14 +87,9 @@ describe("DeviceNetworkTable", () => { }), ]; const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); + renderWithBrowserRouter(, { + store, + }); expect(screen.getByTestId("ip-mode")).toHaveTextContent("Unconfigured"); }); @@ -135,76 +119,15 @@ describe("DeviceNetworkTable", () => { }), ]; const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); + renderWithBrowserRouter(, { + store, + }); expect( screen.getByRole("link", { name: "subnet-cidr" }) ).toBeInTheDocument(); expect(screen.getByTestId("ip-address")).toHaveTextContent("1.2.3.99"); }); - it("expands a row when a matching link is found", () => { - state.device.items = [ - deviceDetailsFactory({ - interfaces: [ - deviceInterfaceFactory({ - discovered: null, - links: [networkLinkFactory(), networkLinkFactory({ id: 2 })], - name: "alias", - type: NetworkInterfaceTypes.ALIAS, - }), - ], - system_id: "abc123", - }), - ]; - const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); - const rows = screen.getAllByRole("row"); - expect(rows[1]).not.toHaveClass("is-active"); - expect(rows[2]).toHaveClass("is-active"); - }); - - it("expands a row when a matching nic is found", () => { - state.device.items = [ - deviceDetailsFactory({ - interfaces: [ - deviceInterfaceFactory({ - id: 2, - discovered: null, - links: [], - name: "eth0", - type: NetworkInterfaceTypes.PHYSICAL, - }), - ], - system_id: "abc123", - }), - ]; - const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); - const rows = screen.getAllByRole("row"); - expect(rows[1]).toHaveClass("is-active"); - }); - it("displays an empty table description", () => { state.device.items = [ deviceDetailsFactory({ @@ -213,14 +136,9 @@ describe("DeviceNetworkTable", () => { }), ]; const store = mockStore(state); - renderWithBrowserRouter( - , - { store } - ); + renderWithBrowserRouter(, { + store, + }); expect(screen.getByText("No interfaces available.")).toBeInTheDocument(); }); }); diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx index f6c1af443c..8fa17b05c8 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx @@ -1,37 +1,32 @@ import { MainTable, Spinner } from "@canonical/react-components"; import type { MainTableRow } from "@canonical/react-components/dist/components/MainTable/MainTable"; -import classNames from "classnames"; import { useSelector } from "react-redux"; -import RemoveInterface from "./RemoveInterface"; - -import MacAddressDisplay from "@/app/base/components/MacAddressDisplay"; -import type { - Expanded, - SetExpanded, -} from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; -import { ExpandedState } from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; -import TableHeader from "@/app/base/components/TableHeader"; -import TableMenu from "@/app/base/components/TableMenu"; -import SubnetColumn from "@/app/base/components/node/networking/SubnetColumn"; +import MacAddressDisplay from "app/base/components/MacAddressDisplay"; +import TableHeader from "app/base/components/TableHeader"; +import TableMenu from "app/base/components/TableMenu"; +import SubnetColumn from "app/base/components/node/networking/SubnetColumn"; import { useFetchActions, useIsAllNetworkingDisabled, useTableSort, -} from "@/app/base/hooks"; -import { SortDirection } from "@/app/base/types"; -import deviceSelectors from "@/app/store/device/selectors"; -import type { Device, DeviceMeta } from "@/app/store/device/types"; -import { isDeviceDetails } from "@/app/store/device/utils"; -import { actions as fabricActions } from "@/app/store/fabric"; -import fabricSelectors from "@/app/store/fabric/selectors"; -import type { Fabric } from "@/app/store/fabric/types"; -import type { RootState } from "@/app/store/root/types"; -import { actions as subnetActions } from "@/app/store/subnet"; -import subnetSelectors from "@/app/store/subnet/selectors"; -import type { Subnet } from "@/app/store/subnet/types"; -import { getSubnetDisplay } from "@/app/store/subnet/utils"; -import type { NetworkInterface, NetworkLink } from "@/app/store/types/node"; +} from "app/base/hooks"; +import type { SetSidePanelContent } from "app/base/side-panel-context"; +import { useSidePanel } from "app/base/side-panel-context"; +import { SortDirection } from "app/base/types"; +import { DeviceSidePanelViews } from "app/devices/constants"; +import deviceSelectors from "app/store/device/selectors"; +import type { Device, DeviceMeta } from "app/store/device/types"; +import { isDeviceDetails } from "app/store/device/utils"; +import { actions as fabricActions } from "app/store/fabric"; +import fabricSelectors from "app/store/fabric/selectors"; +import type { Fabric } from "app/store/fabric/types"; +import type { RootState } from "app/store/root/types"; +import { actions as subnetActions } from "app/store/subnet"; +import subnetSelectors from "app/store/subnet/selectors"; +import type { Subnet } from "app/store/subnet/types"; +import { getSubnetDisplay } from "app/store/subnet/utils"; +import type { NetworkInterface, NetworkLink } from "app/store/types/node"; import { getInterfaceIPAddress, getInterfaceName, @@ -59,8 +54,6 @@ type NetworkRow = Omit & { type SortKey = keyof NetworkRowSortData; type Props = { - expanded: Expanded | null; - setExpanded: SetExpanded; systemId: Device[DeviceMeta.PK]; }; @@ -70,15 +63,14 @@ const getSortValue = (sortKey: SortKey, row: NetworkRow) => { }; const generateRow = ( - expanded: Expanded | null, fabrics: Fabric[], isAllNetworkingDisabled: boolean, link: NetworkLink | null, device: Device, nic: NetworkInterface | null, - setExpanded: SetExpanded, subnets: Subnet[], - vlans: VLAN[] + vlans: VLAN[], + setSidePanelContent: SetSidePanelContent ): NetworkRow | null => { if (link && !nic) { [nic] = getLinkInterface(device, link); @@ -97,14 +89,9 @@ const generateRow = ( link ); const typeDisplay = getInterfaceTypeText(device, nic, link); - const isExpanded = - !!expanded && - ((link && expanded.linkId === link.id) || - (!link && expanded.nicId === nic?.id)); + return { - className: classNames("p-table__row", { - "is-active": isExpanded, - }), + className: "p-table__row", columns: [ { content: {nic.mac_address}, @@ -131,19 +118,17 @@ const generateRow = ( { children: `Edit ${typeDisplay}`, onClick: () => - setExpanded({ - content: ExpandedState.EDIT, - linkId: link?.id, - nicId: nic?.id, + setSidePanelContent({ + view: DeviceSidePanelViews.EDIT_INTERFACE, + extras: { linkId: link?.id, nicId: nic?.id }, }), }, { children: `Remove ${typeDisplay}`, onClick: () => - setExpanded({ - content: ExpandedState.REMOVE, - linkId: link?.id, - nicId: nic?.id, + setSidePanelContent({ + view: DeviceSidePanelViews.REMOVE_INTERFACE, + extras: { linkId: link?.id, nicId: nic?.id }, }), }, ]} @@ -153,18 +138,6 @@ const generateRow = ( ), }, ], - expanded: isExpanded, - expandedContent: ( -
- {expanded?.content === ExpandedState.REMOVE && ( - setExpanded(null)} - nicId={nic?.id} - systemId={device.system_id} - /> - )} -
- ), key: name, sortData: { ip_address: @@ -177,13 +150,12 @@ const generateRow = ( }; const generateRows = ( - expanded: Expanded | null, fabrics: Fabric[], isAllNetworkingDisabled: boolean, device: Device, - setExpanded: (expanded: Expanded | null) => void, subnets: Subnet[], - vlans: VLAN[] + vlans: VLAN[], + setSidePanelContent: SetSidePanelContent ): NetworkRow[] => { if (!isDeviceDetails(device)) { return []; @@ -196,15 +168,14 @@ const generateRows = ( nic: NetworkInterface | null ) => generateRow( - expanded, fabrics, isAllNetworkingDisabled, link, device, nic, - setExpanded, subnets, - vlans + vlans, + setSidePanelContent ); if (nic.links.length === 0) { const row = createRow(null, nic); @@ -223,17 +194,14 @@ const generateRows = ( return rows; }; -const DeviceNetworkTable = ({ - expanded, - setExpanded, - systemId, -}: Props): JSX.Element => { +const DeviceNetworkTable = ({ systemId }: Props): JSX.Element => { const device = useSelector((state: RootState) => deviceSelectors.getById(state, systemId) ); const fabrics = useSelector(fabricSelectors.all); const subnets = useSelector(subnetSelectors.all); const vlans = useSelector(vlanSelectors.all); + const { setSidePanelContent } = useSidePanel(); const isAllNetworkingDisabled = useIsAllNetworkingDisabled(device); const { currentSort, sortRows, updateSort } = useTableSort< NetworkRow, @@ -254,13 +222,12 @@ const DeviceNetworkTable = ({ } const rows = generateRows( - expanded, fabrics, isAllNetworkingDisabled, device, - setExpanded, subnets, - vlans + vlans, + setSidePanelContent ); const sortedRows = sortRows(rows); return ( diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx index a979214634..671c6b3cbd 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx @@ -13,8 +13,8 @@ import { deviceStatus as deviceStatusFactory, deviceStatuses as deviceStatusesFactory, rootState as rootStateFactory, -} from "@/testing/factories"; -import { userEvent, screen, renderWithMockStore } from "@/testing/utils"; +} from "testing/factories"; +import { userEvent, screen, renderWithBrowserRouter } from "testing/utils"; const mockStore = configureStore(); @@ -37,21 +37,17 @@ describe("RemoveInterface", () => { }); it("sends an analytics event and closes the form when saved", () => { - const closeExpanded = vi.fn(); + const closeForm = vi.fn(); const useSendMock = vi.spyOn(analyticsHooks, "useSendAnalyticsWhen"); // Mock interface successfully being deleted. vi.spyOn(baseHooks, "useCycled").mockReturnValue([true, () => null]); const store = mockStore(state); - renderWithMockStore( - , + renderWithBrowserRouter( + , { store } ); - expect(closeExpanded).toHaveBeenCalled(); + expect(closeForm).toHaveBeenCalled(); expect(useSendMock.mock.calls[0]).toEqual([ true, "Device network", @@ -84,24 +80,24 @@ describe("RemoveInterface", () => { }), ]; const store = mockStore(state); - renderWithMockStore( - , + renderWithBrowserRouter( + , { store } ); - expect(screen.getByTestId("error-message")).toHaveTextContent( - "Delete interface error for this device" - ); + expect( + screen.getByText("Delete interface error for this device") + ).toBeInTheDocument(); }); it("correctly dispatches an action to delete an interface", async () => { const store = mockStore(state); - renderWithMockStore( - , + renderWithBrowserRouter( + , { store } ); - await userEvent.click(screen.getByTestId("confirm-delete")); + await userEvent.click(screen.getByRole("button", { name: /remove/i })); const expectedAction = deviceActions.deleteInterface({ interface_id: 1, diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx index 9319709561..520bfe6536 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx @@ -1,17 +1,12 @@ import { useEffect } from "react"; -import { - ActionButton, - Button, - Col, - Notification, - Row, -} from "@canonical/react-components"; +import { Notification } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; -import { useCycled, useSendAnalyticsWhen } from "@/app/base/hooks"; -import { actions as deviceActions } from "@/app/store/device"; -import deviceSelectors from "@/app/store/device/selectors"; +import ModelDeleteForm from "app/base/components/ModelDeleteForm"; +import { useCycled, useSendAnalyticsWhen } from "app/base/hooks"; +import { actions as deviceActions } from "app/store/device"; +import deviceSelectors from "app/store/device/selectors"; import type { Device, DeviceMeta, @@ -21,13 +16,13 @@ import type { RootState } from "@/app/store/root/types"; import { formatErrors } from "@/app/utils"; type Props = { - closeExpanded: () => void; + closeForm: () => void; nicId: DeviceNetworkInterface["id"]; systemId: Device[DeviceMeta.PK]; }; const RemoveInterface = ({ - closeExpanded, + closeForm, nicId, systemId, }: Props): JSX.Element => { @@ -49,12 +44,12 @@ const RemoveInterface = ({ ); useEffect(() => { if (deletedInterface) { - closeExpanded(); + closeForm(); } - }, [closeExpanded, deletedInterface]); + }, [closeForm, deletedInterface]); return ( - + <> {deleteInterfaceError ? ( @@ -62,36 +57,24 @@ const RemoveInterface = ({ ) : null} - -

- Warning - Are you sure you want to remove this interface? -

- - - - { - dispatch(deviceActions.cleanup()); - dispatch( - deviceActions.deleteInterface({ - interface_id: nicId, - system_id: systemId, - }) - ); - }} - type="button" - > - Remove - - -
+ { + dispatch(deviceActions.cleanup()); + dispatch( + deviceActions.deleteInterface({ + interface_id: nicId, + system_id: systemId, + }) + ); + }} + saving={deletingInterface} + submitLabel="Remove" + /> + ); }; diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx index f006c8c751..e891ee0b58 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx @@ -3,12 +3,9 @@ import { useDispatch, useSelector } from "react-redux"; import InterfaceForm from "../InterfaceForm"; -import EditInterfaceTable from "./EditInterfaceTable"; - -import FormCard from "@/app/base/components/FormCard"; -import { useCycled } from "@/app/base/hooks"; -import { actions as deviceActions } from "@/app/store/device"; -import deviceSelectors from "@/app/store/device/selectors"; +import { useCycled } from "app/base/hooks"; +import { actions as deviceActions } from "app/store/device"; +import deviceSelectors from "app/store/device/selectors"; import type { Device, DeviceMeta, @@ -57,33 +54,30 @@ const EditInterface = ({ return ; } return ( - - - { - resetUpdatedInterface(); - dispatch(deviceActions.cleanup()); - const payload = preparePayload({ - ...values, - interface_id: nic.id, - system_id: device.system_id, - }) as UpdateInterfaceParams; - dispatch(deviceActions.updateInterface(payload)); - }} - saved={saved} - saving={updatingInterface} - showTitles - systemId={systemId} - /> - + { + resetUpdatedInterface(); + dispatch(deviceActions.cleanup()); + const payload = preparePayload({ + ...values, + interface_id: nic.id, + system_id: device.system_id, + }) as UpdateInterfaceParams; + dispatch(deviceActions.updateInterface(payload)); + }} + saved={saved} + saving={updatingInterface} + showTitles + systemId={systemId} + /> ); }; diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/InterfaceForm/InterfaceFormFields/InterfaceFormFields.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/InterfaceForm/InterfaceFormFields/InterfaceFormFields.tsx index 8dafa33d3b..35a7e480ff 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/InterfaceForm/InterfaceFormFields/InterfaceFormFields.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/InterfaceForm/InterfaceFormFields/InterfaceFormFields.tsx @@ -31,13 +31,13 @@ const InterfaceFormFields = ({ showTitles = false }: Props): JSX.Element => { {showTitles ? null : ( <> - {nameField} + {nameField}
)} - + {showTitles ? ( <>

{ - + {showTitles ? (

; case MachineSidePanelViews.ADD_MACHINE: return ; + case MachineSidePanelViews.CHANGE_STORAGE_LAYOUT: { + if (!systemId || !selectedLayout) { + return null; + } + return ( + + ); + } + case MachineSidePanelViews.ADD_INTERFACE: { + if (!systemId) return null; + return ; + } + case MachineSidePanelViews.ADD_BOND: { + if (!setSelected || !systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.ADD_BRIDGE: { + if (!setSelected || !systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.ADD_ALIAS: { + if (!systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.ADD_SPECIAL_FILESYSTEM: { + if (!node) return null; + return ( + + ); + } + case MachineSidePanelViews.ADD_VLAN: { + if (!systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.CREATE_DATASTORE: { + if (!bulkActionSelected || !systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.CREATE_RAID: { + if (!bulkActionSelected || !systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.CREATE_VOLUME_GROUP: { + if (!bulkActionSelected || !systemId) return null; + return ( + + ); + } + case MachineSidePanelViews.UPDATE_DATASTORE: { + if (!bulkActionSelected || !systemId) return null; + return ( + + ); + } + default: // We need to explicitly cast sidePanelContent.view here - TypeScript doesn't // seem to be able to infer remaining object tuple values as with string @@ -54,6 +188,12 @@ export const MachineForms = ({ extras: MachineSidePanelContent["extras"]; view: ValueOf; }; + const applyConfiguredNetworking = + extras && "applyConfiguredNetworking" in extras + ? extras.applyConfiguredNetworking + : undefined; + const hardwareType = + extras && "hardwareType" in extras ? extras.hardwareType : undefined; const [, action] = view; const conditionalProps = machines ? { machines } @@ -66,9 +206,9 @@ export const MachineForms = ({ return ( , - { - applyConfiguredNetworking?: Script["apply_configured_networking"]; - hardwareType?: HardwareType; - } ->; +export type MachineSidePanelContent = + | SidePanelContent< + ValueOf, + { + applyConfiguredNetworking?: Script["apply_configured_networking"]; + hardwareType?: HardwareType; + } + > + | SidePanelContent< + ValueOf, + { + systemId?: Machine["system_id"]; + selectedLayout?: StorageLayoutOption; + } + > + | SidePanelContent< + ValueOf, + { + systemId?: Machine["system_id"]; + selected: Selected[]; + setSelected: SetSelected; + } + > + | SidePanelContent< + ValueOf, + { + systemId?: Machine["system_id"]; + nic?: NetworkInterface; + } + > + | SidePanelContent< + ValueOf, + { + systemId?: Machine["system_id"]; + bulkActionSelected?: (Disk | Partition)[]; + } + > + | SidePanelContent< + ValueOf, + { + node: MachineDetails; + } + >; export type MachineSetSidePanelContent = SetSidePanelContent; diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.test.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.test.tsx index badfe6d4c6..742964ab44 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.test.tsx @@ -93,7 +93,7 @@ describe("AddBondForm", () => { { route, state } ); expect( - screen.getByRole("heading", { name: "Create bond" }) + screen.getByRole("form", { name: "Create bond" }) ).toBeInTheDocument(); expect(screen.getByRole("grid")).toBeInTheDocument(); }); diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx index 766639279e..29b16dcf93 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx @@ -19,8 +19,7 @@ import { networkFieldsInitialValues, } from "../NetworkFields/NetworkFields"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; +import FormikForm from "app/base/components/FormikForm"; import type { Selected, SetSelected, @@ -92,6 +91,10 @@ const AddBondForm = ({ const machine = useSelector((state: RootState) => machineSelectors.getById(state, systemId) ); + const handleClose = () => { + setSelected([]); + close(); + }; // Use the first selected interface as the canary for the fabric and VLAN. const firstSelected = machine ? getFirstSelected(machine, selected) : null; const firstNic = useSelector((state: RootState) => @@ -120,7 +123,7 @@ const AddBondForm = ({ systemId, "creatingBond", "createBond", - () => close() + () => handleClose() ); useFetchActions([ @@ -164,68 +167,67 @@ const AddBondForm = ({ : selected; const macAddress = firstNic?.mac_address || ""; return ( - - - allowUnchanged - cleanup={cleanup} - errors={errors} - initialValues={{ - ...networkFieldsInitialValues, - bond_downdelay: 0, - bond_lacp_rate: "", - bond_mode: BondMode.ACTIVE_BACKUP, - bond_miimon: 0, - bond_updelay: 0, - bond_xmit_hash_policy: "", - fabric: vlan ? vlan.fabric : "", - linkMonitoring: "", - mac_address: macAddress, - name: nextName || "", - macSource: MacSource.NIC, - macNic: macAddress, - subnet: subnet ? subnet.id : "", - tags: [], - vlan: bondVLAN || "", - }} - onCancel={close} - onSaveAnalytics={{ - action: "Create bond", - category: "Machine details networking", - label: "Create bond form", - }} - onSubmit={(values) => { - // Clear the errors from the previous submission. - dispatch(cleanup()); - const payload = prepareBondPayload( - values, - selected, - systemId - ) as CreateBondParams; - dispatch(machineActions.createBond(payload)); - }} - resetOnSave - saved={saved} - saving={saving} - submitDisabled={!hasEnoughNics} - submitLabel="Save interface" - validationSchema={InterfaceSchema} - > - - - - - + + allowUnchanged + aria-label="Create bond" + cleanup={cleanup} + errors={errors} + initialValues={{ + ...networkFieldsInitialValues, + bond_downdelay: 0, + bond_lacp_rate: "", + bond_mode: BondMode.ACTIVE_BACKUP, + bond_miimon: 0, + bond_updelay: 0, + bond_xmit_hash_policy: "", + fabric: vlan ? vlan.fabric : "", + linkMonitoring: "", + mac_address: macAddress, + name: nextName || "", + macSource: MacSource.NIC, + macNic: macAddress, + subnet: subnet ? subnet.id : "", + tags: [], + vlan: bondVLAN || "", + }} + onCancel={handleClose} + onSaveAnalytics={{ + action: "Create bond", + category: "Machine details networking", + label: "Create bond form", + }} + onSubmit={(values) => { + // Clear the errors from the previous submission. + dispatch(cleanup()); + const payload = prepareBondPayload( + values, + selected, + systemId + ) as CreateBondParams; + dispatch(machineActions.createBond(payload)); + }} + resetOnSave + saved={saved} + saving={saving} + submitDisabled={!hasEnoughNics} + submitLabel="Save interface" + validationSchema={InterfaceSchema} + > + + + + ); }; diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.test.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.test.tsx index 21e4f332eb..c4727109e4 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.test.tsx @@ -80,7 +80,12 @@ describe("AddBridgeForm", () => { ]; const selected = [{ nicId: nic.id }]; renderWithBrowserRouter( - , + , { route, state } ); const table = screen.getByRole("grid"); @@ -94,6 +99,7 @@ describe("AddBridgeForm", () => { , { route, store } @@ -108,6 +114,7 @@ describe("AddBridgeForm", () => { , { route, state } @@ -124,6 +131,7 @@ describe("AddBridgeForm", () => { , { route, store } diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx index e7e2793b26..a791ccc40e 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx @@ -13,14 +13,16 @@ import { import type { BridgeFormValues } from "./types"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; -import type { Selected } from "@/app/base/components/node/networking/types"; -import { useFetchActions } from "@/app/base/hooks"; -import { MAC_ADDRESS_REGEX } from "@/app/base/validation"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import machineSelectors from "@/app/store/machine/selectors"; +import FormikForm from "app/base/components/FormikForm"; +import type { + Selected, + SetSelected, +} from "app/base/components/node/networking/types"; +import { useFetchActions } from "app/base/hooks"; +import { MAC_ADDRESS_REGEX } from "app/base/validation"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import machineSelectors from "app/store/machine/selectors"; import type { CreateBridgeParams, MachineDetails, @@ -50,18 +52,24 @@ type Props = { close: () => void; selected: Selected[]; systemId: MachineDetails["system_id"]; + setSelected: SetSelected; }; const AddBridgeForm = ({ close, selected, systemId, + setSelected, }: Props): JSX.Element | null => { const dispatch = useDispatch(); const machine = useSelector((state: RootState) => machineSelectors.getById(state, systemId) ); const cleanup = useCallback(() => machineActions.cleanup(), []); + const handleClose = () => { + setSelected([]); + close(); + }; const nextName = getNextNicName(machine, NetworkInterfaceTypes.BRIDGE); const [{ linkId, nicId }] = selected; const nic = useSelector((state: RootState) => @@ -75,7 +83,7 @@ const AddBridgeForm = ({ systemId, "creatingBridge", "createBridge", - () => close() + () => handleClose() ); useFetchActions([vlanActions.fetch]); @@ -90,7 +98,7 @@ const AddBridgeForm = ({ } return ( - + <> allowUnchanged @@ -109,7 +117,7 @@ const AddBridgeForm = ({ // Prefill the vlan from the parent interface. vlan: nic.vlan_id, }} - onCancel={close} + onCancel={handleClose} onSaveAnalytics={{ action: "Create bridge", category: "Machine details networking", @@ -133,7 +141,7 @@ const AddBridgeForm = ({ > - + ); }; diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx index 9de99df74f..fb4579e517 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx @@ -11,16 +11,15 @@ import { } from "../NetworkFields/NetworkFields"; import type { NetworkValues } from "../NetworkFields/NetworkFields"; -import FormCard from "@/app/base/components/FormCard"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import MacAddressField from "@/app/base/components/MacAddressField"; -import TagNameField from "@/app/base/components/TagNameField"; -import { useScrollOnRender } from "@/app/base/hooks"; -import { MAC_ADDRESS_REGEX } from "@/app/base/validation"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import machineSelectors from "@/app/store/machine/selectors"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import MacAddressField from "app/base/components/MacAddressField"; +import TagNameField from "app/base/components/TagNameField"; +import { useScrollOnRender } from "app/base/hooks"; +import { MAC_ADDRESS_REGEX } from "app/base/validation"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import machineSelectors from "app/store/machine/selectors"; import type { CreatePhysicalParams, MachineDetails, @@ -73,62 +72,60 @@ const AddInterface = ({ close, systemId }: Props): JSX.Element | null => { } return (
- - - cleanup={cleanup} - errors={errors} - initialValues={{ - ...networkFieldsInitialValues, - mac_address: "", - name: nextName || "", - tags: [], - }} - onCancel={close} - onSaveAnalytics={{ - action: "Add interface", - category: "Machine details networking", - label: "Add interface form", - }} - onSubmit={(values) => { - // Clear the errors from the previous submission. - dispatch(cleanup()); - const payload = preparePayload({ - ...values, - system_id: systemId, - }) as CreatePhysicalParams; - dispatch(machineActions.createPhysical(payload)); - }} - resetOnSave - saved={saved} - saving={saving} - submitLabel="Save interface" - validateOnMount - validationSchema={InterfaceSchema} - > - - - - - -
- - - - - - - - - - - -
+ + cleanup={cleanup} + errors={errors} + initialValues={{ + ...networkFieldsInitialValues, + mac_address: "", + name: nextName || "", + tags: [], + }} + onCancel={close} + onSaveAnalytics={{ + action: "Add interface", + category: "Machine details networking", + label: "Add interface form", + }} + onSubmit={(values) => { + // Clear the errors from the previous submission. + dispatch(cleanup()); + const payload = preparePayload({ + ...values, + system_id: systemId, + }) as CreatePhysicalParams; + dispatch(machineActions.createPhysical(payload)); + }} + resetOnSave + saved={saved} + saving={saving} + submitLabel="Save interface" + validateOnMount + validationSchema={InterfaceSchema} + > + + + + + +
+ + + + + + + + + + +
); }; diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/BondForm/BondFormFields/BondFormFields.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/BondForm/BondFormFields/BondFormFields.tsx index ea81a44023..a950340406 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/BondForm/BondFormFields/BondFormFields.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/BondForm/BondFormFields/BondFormFields.tsx @@ -66,7 +66,7 @@ const BondFormFields = ({ selected, systemId }: Props): JSX.Element | null => { const showMonitoring = values.linkMonitoring === LinkMonitoring.MII; return ( - +

Bond details

{showHashPolicy && ( @@ -165,7 +165,7 @@ const BondFormFields = ({ selected, systemId }: Props): JSX.Element | null => { )} - +

Network

{ return ( - +

Bridge details

{ /> ) : null} - +

Network

diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetwork.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetwork.tsx index 5b426ab6ed..824eff5d78 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetwork.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetwork.tsx @@ -46,6 +46,7 @@ const MachineNetwork = ({ id, setSidePanelContent }: Props): JSX.Element => { expanded={expanded} selected={selected} setExpanded={setExpanded} + setSelected={setSelected} setSidePanelContent={setSidePanelContent} systemId={id} /> @@ -93,6 +94,7 @@ const MachineNetwork = ({ id, setSidePanelContent }: Props): JSX.Element => { setSelected([]); }} selected={selected} + setSelected={setSelected} systemId={id} /> ); diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx index e65f1b3f7e..a23e4e3fa3 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx @@ -1,10 +1,10 @@ import MachineNetworkActions from "./MachineNetworkActions"; -import { ExpandedState } from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; -import { MachineSidePanelViews } from "@/app/machines/constants"; -import type { RootState } from "@/app/store/root/types"; -import { NetworkInterfaceTypes } from "@/app/store/types/enum"; -import { NodeStatus } from "@/app/store/types/node"; +import * as sidePanelHooks from "app/base/side-panel-context"; +import { MachineSidePanelViews } from "app/machines/constants"; +import type { RootState } from "app/store/root/types"; +import { NetworkInterfaceTypes } from "app/store/types/enum"; +import { NodeStatus } from "app/store/types/node"; import { machineDetails as machineDetailsFactory, machineInterface as machineInterfaceFactory, @@ -53,6 +53,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -72,6 +73,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={setSidePanelContent} systemId="abc123" />, @@ -107,14 +109,21 @@ describe("MachineNetworkActions", () => { system_id: "abc123", }), ]; - const setExpanded = vi.fn(); + const setSidePanelContent = vi.fn(); + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + setSidePanelContent, + sidePanelContent: null, + setSidePanelSize: vi.fn(), + sidePanelSize: "regular", + }); renderWithBrowserRouter( , { state, route: "/machine/abc123" } @@ -124,9 +133,11 @@ describe("MachineNetworkActions", () => { screen.getByRole("button", { name: /Create bond/i }) ); - expect(setExpanded).toHaveBeenCalledWith({ - content: ExpandedState.ADD_BOND, - }); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.ADD_BOND, + }) + ); }); it("disables the button when networking is disabled", () => { @@ -137,6 +148,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -153,6 +165,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -186,6 +199,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[{ nicId: 1 }]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -226,6 +240,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[{ nicId: 1, linkId: 2 }, { nicId: 2 }]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -261,6 +276,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[{ nicId: 1, linkId: 2 }, { nicId: 2 }]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -287,14 +303,21 @@ describe("MachineNetworkActions", () => { system_id: "abc123", }), ]; - const setExpanded = vi.fn(); + const setSidePanelContent = vi.fn(); + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + setSidePanelContent, + sidePanelContent: null, + setSidePanelSize: vi.fn(), + sidePanelSize: "regular", + }); renderWithBrowserRouter( , { state, route: "/machine/abc123" } @@ -303,9 +326,11 @@ describe("MachineNetworkActions", () => { await userEvent.click( screen.getByRole("button", { name: /create bridge/i }) ); - expect(setExpanded).toHaveBeenCalledWith({ - content: ExpandedState.ADD_BRIDGE, - }); + expect(setSidePanelContent).toHaveBeenCalledWith( + expect.objectContaining({ + view: MachineSidePanelViews.ADD_BRIDGE, + }) + ); }); it("disables the button when networking is disabled", async () => { @@ -316,6 +341,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -334,6 +360,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -367,6 +394,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[{ nicId: 1 }, { nicId: 2 }]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -407,6 +435,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[{ nicId: 1, linkId: 2 }]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, @@ -441,6 +470,7 @@ describe("MachineNetworkActions", () => { expanded={null} selected={[{ nicId: 1 }, { nicId: 2 }]} setExpanded={vi.fn()} + setSelected={vi.fn()} setSidePanelContent={vi.fn()} systemId="abc123" />, diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx index fcd562f826..6cddcaedcf 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx @@ -34,6 +34,7 @@ type Props = { selected: Selected[]; setExpanded: SetExpanded; setSidePanelContent: MachineSetSidePanelContent; + setSelected: React.Dispatch>; systemId: Machine["system_id"]; }; @@ -79,11 +80,11 @@ const selectedDifferentVLANs = ( }; const MachineNetworkActions = ({ - expanded, setExpanded, selected, setSidePanelContent, systemId, + setSelected, }: Props): JSX.Element | null => { const machine = useSelector((state: RootState) => machineSelectors.getById(state, systemId) @@ -137,7 +138,6 @@ const MachineNetworkActions = ({ return ( } + selected={selected} setExpanded={setExpanded} + setSelected={setSelected} /> ); }; diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx index e7179ffb47..7604749d2c 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx @@ -1,5 +1,7 @@ import configureStore from "redux-mock-store"; +import { storageLayoutOptions } from "../ChangeStorageLayoutMenu/ChangeStorageLayoutMenu"; + import ChangeStorageLayout from "./ChangeStorageLayout"; import type { RootState } from "@/app/store/root/types"; @@ -21,7 +23,8 @@ import { const mockStore = configureStore(); describe("ChangeStorageLayout", () => { - it("shows a confirmation form if a storage layout is selected", async () => { + const sampleStoragelayout = storageLayoutOptions[0][0]; + it("shows a confirmation form if a storage layout is selected", () => { const state = rootStateFactory({ machine: machineStateFactory({ items: [machineDetailsFactory({ system_id: "abc123" })], @@ -30,16 +33,16 @@ describe("ChangeStorageLayout", () => { }), }), }); - renderWithBrowserRouter(, { - state, - }); - - // Open storage layout dropdown - await userEvent.click( - screen.getByRole("button", { name: "Change storage layout" }) + renderWithBrowserRouter( + , + { + state, + } ); - // Select flat storage layout - await userEvent.click(screen.getByRole("button", { name: "Flat" })); expect( getByTextContent( @@ -52,7 +55,7 @@ describe("ChangeStorageLayout", () => { ).toHaveAttribute("type", "submit"); }); - it("can show errors", async () => { + it("can show errors", () => { const state = rootStateFactory({ machine: machineStateFactory({ eventErrors: [ @@ -68,16 +71,16 @@ describe("ChangeStorageLayout", () => { }), }), }); - renderWithBrowserRouter(, { - state, - }); - - // Open storage layout dropdown - await userEvent.click( - screen.getByRole("button", { name: "Change storage layout" }) + renderWithBrowserRouter( + , + { + state, + } ); - // Select flat storage layout - await userEvent.click(screen.getByRole("button", { name: "Flat" })); expect(screen.getByText(/not possible/i)).toBeInTheDocument(); }); @@ -92,18 +95,17 @@ describe("ChangeStorageLayout", () => { }), }); const store = mockStore(state); - renderWithBrowserRouter(, { - store, - }); - - // Open storage layout dropdown - await userEvent.click( - screen.getByRole("button", { name: "Change storage layout" }) + renderWithBrowserRouter( + , + { + store, + } ); - // Select flat storage layout - await userEvent.click(screen.getByRole("button", { name: "Flat" })); - // Submit the form await userEvent.click( screen.getByRole("button", { name: "Change storage layout" }) diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx index 99cd140438..b7cd6265af 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx @@ -1,144 +1,92 @@ -import { useState } from "react"; - -import { ContextualMenu, Icon } from "@canonical/react-components"; +import { Icon } from "@canonical/react-components"; import { useDispatch } from "react-redux"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; -import type { EmptyObject } from "@/app/base/types"; -import { useMachineDetailsForm } from "@/app/machines/hooks"; -import { actions as machineActions } from "@/app/store/machine"; -import type { Machine } from "@/app/store/machine/types"; -import type { MachineEventErrors } from "@/app/store/machine/types/base"; -import { StorageLayout } from "@/app/store/types/enum"; -import { isVMWareLayout } from "@/app/store/utils"; +import FormikForm from "app/base/components/FormikForm"; +import type { ClearSidePanelContent, EmptyObject } from "app/base/types"; +import { useMachineDetailsForm } from "app/machines/hooks"; +import { actions as machineActions } from "app/store/machine"; +import type { Machine, StorageLayoutOption } from "app/store/machine/types"; +import type { MachineEventErrors } from "app/store/machine/types/base"; +import { StorageLayout } from "app/store/types/enum"; +import { isVMWareLayout } from "app/store/utils"; -type StorageLayoutOption = { - label: string; - sentenceLabel: string; - value: StorageLayout; +type Props = { + systemId: Machine["system_id"]; + clearSidePanelContent: ClearSidePanelContent; + selectedLayout: StorageLayoutOption; }; -type Props = { systemId: Machine["system_id"] }; - -// TODO: Once the API returns a list of allowed storage layouts for a given -// machine we should either filter this list, or add a boolean e.g. "allowable" -// to each layout. -// https://github.com/canonical/maas-ui/issues/3258 -export const storageLayoutOptions: StorageLayoutOption[][] = [ - [ - { label: "Flat", sentenceLabel: "flat", value: StorageLayout.FLAT }, - { label: "LVM", sentenceLabel: "LVM", value: StorageLayout.LVM }, - { label: "bcache", sentenceLabel: "bcache", value: StorageLayout.BCACHE }, - { label: "Custom", sentenceLabel: "custom", value: StorageLayout.CUSTOM }, - ], - [ - { - label: "VMFS6", - sentenceLabel: "VMFS6", - value: StorageLayout.VMFS6, - }, - { - label: "VMFS7", - sentenceLabel: "VMFS7", - value: StorageLayout.VMFS7, - }, - ], - [ - { - label: "No storage (blank) layout", - sentenceLabel: "blank", - value: StorageLayout.BLANK, - }, - ], -]; - -export const ChangeStorageLayout = ({ systemId }: Props): JSX.Element => { +export const ChangeStorageLayout = ({ + systemId, + clearSidePanelContent, + selectedLayout, +}: Props): JSX.Element => { const dispatch = useDispatch(); - const [selectedLayout, setSelectedLayout] = - useState(null); const { errors, saved, saving } = useMachineDetailsForm( systemId, "applyingStorageLayout", - "applyStorageLayout", - () => setSelectedLayout(null) + "applyStorageLayout" ); - return !selectedLayout ? ( -
- - group.map((option) => ({ - children: option.label, - onClick: () => setSelectedLayout(option), - })) - )} - position="right" - toggleLabel="Change storage layout" - /> -
- ) : ( - - - cleanup={machineActions.cleanup} - errors={errors} - initialValues={{}} - onCancel={() => setSelectedLayout(null)} - onSaveAnalytics={{ - action: `Change storage layout${ - selectedLayout ? ` to ${selectedLayout?.sentenceLabel}` : "" - }`, - category: "Machine storage", - label: "Change storage layout", - }} - onSubmit={() => { - dispatch(machineActions.cleanup()); - dispatch( - machineActions.applyStorageLayout({ - systemId, - storageLayout: selectedLayout.value, - }) - ); - }} - saved={saved} - saving={saving} - submitAppearance="negative" - submitLabel="Change storage layout" - > -
-

- + return ( + + cleanup={machineActions.cleanup} + errors={errors} + initialValues={{}} + onCancel={clearSidePanelContent} + onSaveAnalytics={{ + action: `Change storage layout${ + selectedLayout ? ` to ${selectedLayout?.sentenceLabel}` : "" + }`, + category: "Machine storage", + label: "Change storage layout", + }} + onSubmit={() => { + dispatch(machineActions.cleanup()); + dispatch( + machineActions.applyStorageLayout({ + systemId, + storageLayout: selectedLayout.value, + }) + ); + }} + saved={saved} + saving={saving} + submitAppearance="negative" + submitLabel="Change storage layout" + > +

+

+ +

+
+

+ + Are you sure you want to change the storage layout to{" "} + {selectedLayout.sentenceLabel}? + +
+ Any changes done already will be lost. +
+ {selectedLayout.value === StorageLayout.BLANK && ( + <> + Used disks will be returned to available, and any volume groups, + raid sets, caches, and filesystems removed. +
+ + )} + {isVMWareLayout(selectedLayout.value) && ( + <> + This layout allows only for the deployment of{" "} + VMware ESXi images. +
+ + )} + The storage layout will be applied to a node when it is deployed.

-
-

- - Are you sure you want to change the storage layout to{" "} - {selectedLayout.sentenceLabel}? - -
- Any changes done already will be lost. -
- {selectedLayout.value === StorageLayout.BLANK && ( - <> - Used disks will be returned to available, and any volume - groups, raid sets, caches, and filesystems removed. -
- - )} - {isVMWareLayout(selectedLayout.value) && ( - <> - This layout allows only for the deployment of{" "} - VMware ESXi images. -
- - )} - The storage layout will be applied to a node when it is deployed. -

-
- - +
+ ); }; diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx new file mode 100644 index 0000000000..f235ea9a35 --- /dev/null +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx @@ -0,0 +1,57 @@ +import configureStore from "redux-mock-store"; + +import ChangeStorageLayoutMenu, { + storageLayoutOptions, +} from "./ChangeStorageLayoutMenu"; + +import type { RootState } from "app/store/root/types"; +import { + machineDetails as machineDetailsFactory, + machineState as machineStateFactory, + machineStatus as machineStatusFactory, + machineStatuses as machineStatusesFactory, + rootState as rootStateFactory, +} from "testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; + +const mockStore = configureStore(); + +let state: RootState; + +beforeAll(() => { + state = rootStateFactory({ + machine: machineStateFactory({ + items: [machineDetailsFactory({ system_id: "abc123" })], + statuses: machineStatusesFactory({ + abc123: machineStatusFactory(), + }), + }), + }); +}); + +it("renders", () => { + const store = mockStore(state); + renderWithBrowserRouter(, { + store, + }); + + expect( + screen.getByRole("button", { name: "Change storage layout" }) + ).toBeInTheDocument(); +}); + +it("displays sub options when clicked", async () => { + const store = mockStore(state); + const testStorageOptions = storageLayoutOptions[0]; + renderWithBrowserRouter(, { + store, + }); + + const storageBtn = screen.getByRole("button", { + name: "Change storage layout", + }); + await userEvent.click(storageBtn); + testStorageOptions.forEach((option) => { + expect(screen.getByRole("button", { name: option.label })); + }); +}); diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx new file mode 100644 index 0000000000..6693621449 --- /dev/null +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx @@ -0,0 +1,70 @@ +import { ContextualMenu } from "@canonical/react-components"; + +import { useSidePanel } from "app/base/side-panel-context"; +import { MachineSidePanelViews } from "app/machines/constants"; +import type { Machine, StorageLayoutOption } from "app/store/machine/types"; +import { StorageLayout } from "app/store/types/enum"; + +// TODO: Once the API returns a list of allowed storage layouts for a given +// machine we should either filter this list, or add a boolean e.g. "allowable" +// to each layout. +// https://github.com/canonical/maas-ui/issues/3258 +export const storageLayoutOptions: StorageLayoutOption[][] = [ + [ + { label: "Flat", sentenceLabel: "flat", value: StorageLayout.FLAT }, + { label: "LVM", sentenceLabel: "LVM", value: StorageLayout.LVM }, + { label: "bcache", sentenceLabel: "bcache", value: StorageLayout.BCACHE }, + { label: "Custom", sentenceLabel: "custom", value: StorageLayout.CUSTOM }, + ], + [ + { + label: "VMFS6", + sentenceLabel: "VMFS6", + value: StorageLayout.VMFS6, + }, + { + label: "VMFS7", + sentenceLabel: "VMFS7", + value: StorageLayout.VMFS7, + }, + ], + [ + { + label: "No storage (blank) layout", + sentenceLabel: "blank", + value: StorageLayout.BLANK, + }, + ], +]; + +type Props = { + systemId: Machine["system_id"]; +}; + +const ChangeStorageLayoutMenu = ({ systemId }: Props) => { + const { setSidePanelContent } = useSidePanel(); + return ( +
+ + group.map((option) => ({ + children: option.label, + onClick: () => + setSidePanelContent({ + view: MachineSidePanelViews.CHANGE_STORAGE_LAYOUT, + extras: { + systemId, + selectedLayout: option, + }, + }), + })) + )} + position="right" + toggleLabel="Change storage layout" + /> +
+ ); +}; + +export default ChangeStorageLayoutMenu; diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/index.ts b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/index.ts new file mode 100644 index 0000000000..f132979b64 --- /dev/null +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/index.ts @@ -0,0 +1 @@ +export { default } from "./ChangeStorageLayoutMenu"; diff --git a/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.test.tsx b/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.test.tsx index aeec58ac11..1fe3f38504 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.test.tsx @@ -1,6 +1,6 @@ import { Route, Routes } from "react-router-dom-v5-compat"; -import { storageLayoutOptions } from "./ChangeStorageLayout/ChangeStorageLayout"; +import { storageLayoutOptions } from "./ChangeStorageLayoutMenu/ChangeStorageLayoutMenu"; import MachineStorage from "./MachineStorage"; import * as hooks from "@/app/base/hooks/analytics"; diff --git a/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.tsx b/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.tsx index 023c81c984..123bcda1fe 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/MachineStorage.tsx @@ -3,7 +3,7 @@ import { Spinner, Strip } from "@canonical/react-components"; import { useSelector } from "react-redux"; import { Link } from "react-router-dom-v5-compat"; -import ChangeStorageLayout from "./ChangeStorageLayout"; +import ChangeStorageLayoutMenu from "./ChangeStorageLayoutMenu"; import StorageTables from "@/app/base/components/node/StorageTables"; import docsUrls from "@/app/base/docsUrls"; @@ -29,7 +29,7 @@ const MachineStorage = (): JSX.Element => { if (isId(id) && isMachineDetails(machine)) { return ( <> - {canEditStorage && } + {canEditStorage && }

diff --git a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx new file mode 100644 index 0000000000..f42311861a --- /dev/null +++ b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx @@ -0,0 +1,11 @@ +import PoolDeleteForm from "./PoolDeleteForm"; + +import { renderWithBrowserRouter, screen } from "testing/utils"; + +it("renders", () => { + renderWithBrowserRouter(); + + expect( + screen.getByRole("form", { name: /Confirm pool deletion/i }) + ).toBeInTheDocument(); +}); diff --git a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx new file mode 100644 index 0000000000..53fc5fabc3 --- /dev/null +++ b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx @@ -0,0 +1,30 @@ +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom-v5-compat"; + +import ModelDeleteForm from "app/base/components/ModelDeleteForm"; +import urls from "app/base/urls"; +import { actions as resourcePoolActions } from "app/store/resourcepool"; +import resourcePoolSelectors from "app/store/resourcepool/selectors"; + +const PoolDeleteForm = ({ id }: { id: number }) => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const saved = useSelector(resourcePoolSelectors.saved); + const saving = useSelector(resourcePoolSelectors.saving); + + return ( + navigate({ pathname: urls.pools.index })} + onSubmit={() => { + dispatch(resourcePoolActions.delete(id)); + }} + saved={saved} + saving={saving} + /> + ); +}; + +export default PoolDeleteForm; diff --git a/src/app/pools/components/PoolDeleteForm/index.ts b/src/app/pools/components/PoolDeleteForm/index.ts new file mode 100644 index 0000000000..aa5a655eea --- /dev/null +++ b/src/app/pools/components/PoolDeleteForm/index.ts @@ -0,0 +1 @@ +export { default } from "./PoolDeleteForm"; diff --git a/src/app/pools/components/PoolForm/PoolForm.tsx b/src/app/pools/components/PoolForm/PoolForm.tsx index ee9fe01d50..f5c70be395 100644 --- a/src/app/pools/components/PoolForm/PoolForm.tsx +++ b/src/app/pools/components/PoolForm/PoolForm.tsx @@ -4,14 +4,13 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; -import FormCard from "@/app/base/components/FormCard"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useAddMessage, useWindowTitle } from "@/app/base/hooks"; -import urls from "@/app/base/urls"; -import { actions as poolActions } from "@/app/store/resourcepool"; -import poolSelectors from "@/app/store/resourcepool/selectors"; -import type { ResourcePool } from "@/app/store/resourcepool/types"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import { useAddMessage } from "app/base/hooks"; +import urls from "app/base/urls"; +import { actions as poolActions } from "app/store/resourcepool"; +import poolSelectors from "app/store/resourcepool/selectors"; +import type { ResourcePool } from "app/store/resourcepool/types"; type Props = { pool?: ResourcePool | null; @@ -66,50 +65,46 @@ export const PoolForm = ({ pool, ...props }: Props): JSX.Element => { }; } - useWindowTitle(title); - return ( - - navigate({ pathname: urls.pools.index })} - onSaveAnalytics={{ - action: "Saved", - category: "Resource pool", - label: "Add pool form", - }} - onSubmit={(values) => { - dispatch(poolActions.cleanup()); - if (pool) { - dispatch( - poolActions.update({ - ...values, - id: pool.id, - }) - ); - } else { - dispatch(poolActions.create(values)); - } - setSaving(values.name); - }} - saved={saved} - savedRedirect={urls.pools.index} - saving={saving} - submitLabel={Labels.SubmitLabel} - validationSchema={PoolSchema} - {...props} - > - - - - + navigate({ pathname: urls.pools.index })} + onSaveAnalytics={{ + action: "Saved", + category: "Resource pool", + label: "Add pool form", + }} + onSubmit={(values) => { + dispatch(poolActions.cleanup()); + if (pool) { + dispatch( + poolActions.update({ + ...values, + id: pool.id, + }) + ); + } else { + dispatch(poolActions.create(values)); + } + setSaving(values.name); + }} + saved={saved} + savedRedirect={urls.pools.index} + saving={saving} + submitLabel={Labels.SubmitLabel} + validationSchema={PoolSchema} + {...props} + > + + + ); }; diff --git a/src/app/pools/urls.ts b/src/app/pools/urls.ts index 61a22dd324..9b96238709 100644 --- a/src/app/pools/urls.ts +++ b/src/app/pools/urls.ts @@ -4,6 +4,7 @@ import { argPath } from "@/app/utils"; const urls = { add: "/pools/add", edit: argPath<{ id: ResourcePool["id"] }>("/pools/:id/edit"), + delete: argPath<{ id: ResourcePool["id"] }>("/pools/:id/delete"), index: "/pools", }; diff --git a/src/app/pools/views/PoolDelete/PoolDelete.test.tsx b/src/app/pools/views/PoolDelete/PoolDelete.test.tsx new file mode 100644 index 0000000000..e2f8dd4b92 --- /dev/null +++ b/src/app/pools/views/PoolDelete/PoolDelete.test.tsx @@ -0,0 +1,48 @@ +import configureStore from "redux-mock-store"; + +import PoolDelete from "./PoolDelete"; + +import { actions } from "app/store/resourcepool"; +import type { RootState } from "app/store/root/types"; +import { + resourcePool as resourcePoolFactory, + resourcePoolState as resourcePoolStateFactory, + rootState as rootStateFactory, +} from "testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; + +const mockStore = configureStore(); +let state: RootState; +beforeEach(() => { + state = rootStateFactory({ + resourcepool: resourcePoolStateFactory({ + loaded: true, + items: [ + resourcePoolFactory({ id: 1 }), + resourcePoolFactory({ name: "default", is_default: true }), + resourcePoolFactory({ name: "backup", is_default: false }), + ], + }), + }); +}); + +it("can delete a resource pool", async () => { + const store = mockStore(state); + + renderWithBrowserRouter(, { + store, + route: "/pools/1/delete", + routePattern: "/pools/:id/delete", + }); + + expect( + screen.getByRole("form", { name: /Confirm pool deletion/i }) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByRole("button", { name: /delete/i })); + + const action = store + .getActions() + .find((action) => action.type === "resourcepool/delete"); + expect(action).toEqual(actions.delete(1)); +}); diff --git a/src/app/pools/views/PoolDelete/PoolDelete.tsx b/src/app/pools/views/PoolDelete/PoolDelete.tsx new file mode 100644 index 0000000000..d6c20ae2f2 --- /dev/null +++ b/src/app/pools/views/PoolDelete/PoolDelete.tsx @@ -0,0 +1,14 @@ +import { useGetURLId } from "app/base/hooks"; +import PoolDeleteForm from "app/pools/components/PoolDeleteForm"; + +const PoolDelete = () => { + const id = useGetURLId("id"); + + if (!id) { + return

Resource pool not found

; + } + + return ; +}; + +export default PoolDelete; diff --git a/src/app/pools/views/PoolDelete/index.ts b/src/app/pools/views/PoolDelete/index.ts new file mode 100644 index 0000000000..a3fa8b57e4 --- /dev/null +++ b/src/app/pools/views/PoolDelete/index.ts @@ -0,0 +1 @@ +export { default } from "./PoolDelete"; diff --git a/src/app/pools/views/PoolList/PoolList.test.tsx b/src/app/pools/views/PoolList/PoolList.test.tsx index 2510be8263..9222392729 100644 --- a/src/app/pools/views/PoolList/PoolList.test.tsx +++ b/src/app/pools/views/PoolList/PoolList.test.tsx @@ -1,7 +1,5 @@ -import { Provider } from "react-redux"; import { MemoryRouter } from "react-router-dom"; import { CompatRouter } from "react-router-dom-v5-compat"; -import configureStore from "redux-mock-store"; import PoolList from "./PoolList"; @@ -12,16 +10,12 @@ import { rootState as rootStateFactory, } from "@/testing/factories"; import { - userEvent, screen, - render, within, renderWithMockStore, renderWithBrowserRouter, } from "@/testing/utils"; -const mockStore = configureStore(); - describe("PoolList", () => { let state: RootState; @@ -80,7 +74,7 @@ describe("PoolList", () => { ); }); - it("can show a delete confirmation", async () => { + it("displays a link to delete confirmation", async () => { state.resourcepool.items = [ resourcePoolFactory({ id: 0, @@ -104,69 +98,11 @@ describe("PoolList", () => { expect(row).not.toHaveClass("is-active"); - // Click on the delete button: - await userEvent.click(within(row).getByRole("button", { name: "Delete" })); - - expect(row).toHaveClass("is-active"); expect( - screen.getByText( - 'Are you sure you want to delete resourcepool "squambo"?' - ) + within(row).getByRole("link", { name: "Delete" }) ).toBeInTheDocument(); }); - it("can delete a pool", async () => { - state.resourcepool.items = [ - resourcePoolFactory({ - id: 2, - name: "squambo", - description: "a pool", - is_default: false, - machine_total_count: 0, - permissions: ["delete"], - }), - ]; - const store = mockStore(state); - - render( - - - - - - - - ); - const row = screen.getByRole("row", { name: "squambo" }); - - // Click on the delete button: - await userEvent.click(within(row).getByRole("button", { name: "Delete" })); - - // Click on the delete confirm button - await userEvent.click( - within( - within(row).getByRole("gridcell", { - name: 'Are you sure you want to delete resourcepool "squambo"? This action is permanent and can not be undone. Cancel Delete', - }) - ).getByRole("button", { name: "Delete" }) - ); - - expect( - store.getActions().find(({ type }) => type === "resourcepool/delete") - ).toStrictEqual({ - type: "resourcepool/delete", - payload: { - params: { - id: 2, - }, - }, - meta: { - model: "resourcepool", - method: "delete", - }, - }); - }); - it("disables the delete button for default pools", () => { state.resourcepool.items = [ resourcePoolFactory({ @@ -185,7 +121,10 @@ describe("PoolList", () => { , { state } ); - expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled(); + expect(screen.getByRole("link", { name: "Delete" })).toHaveAttribute( + "aria-disabled", + "true" + ); }); it("disables the delete button for pools that contain machines", () => { @@ -207,7 +146,10 @@ describe("PoolList", () => { , { state } ); - expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled(); + expect(screen.getByRole("link", { name: "Delete" })).toHaveAttribute( + "aria-disabled", + "true" + ); }); it("does not show a machine link for empty pools", () => { diff --git a/src/app/pools/views/PoolList/PoolList.tsx b/src/app/pools/views/PoolList/PoolList.tsx index 7eb46a2ae8..b919c02767 100644 --- a/src/app/pools/views/PoolList/PoolList.tsx +++ b/src/app/pools/views/PoolList/PoolList.tsx @@ -9,24 +9,15 @@ import { } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import { Link } from "react-router-dom-v5-compat"; -import type { Dispatch } from "redux"; -import TableActions from "@/app/base/components/TableActions"; -import TableDeleteConfirm from "@/app/base/components/TableDeleteConfirm"; -import { - useFetchActions, - useAddMessage, - useWindowTitle, -} from "@/app/base/hooks"; -import urls from "@/app/base/urls"; -import { FilterMachines } from "@/app/store/machine/utils"; -import { actions as resourcePoolActions } from "@/app/store/resourcepool"; -import resourcePoolSelectors from "@/app/store/resourcepool/selectors"; -import type { - ResourcePool, - ResourcePoolState, -} from "@/app/store/resourcepool/types"; -import { formatErrors } from "@/app/utils"; +import TableActions from "app/base/components/TableActions"; +import { useFetchActions, useAddMessage, useWindowTitle } from "app/base/hooks"; +import urls from "app/base/urls"; +import { FilterMachines } from "app/store/machine/utils"; +import { actions as resourcePoolActions } from "app/store/resourcepool"; +import resourcePoolSelectors from "app/store/resourcepool/selectors"; +import type { ResourcePool } from "app/store/resourcepool/types"; +import { formatErrors } from "app/utils"; export enum Label { Title = "Pool list", @@ -46,20 +37,10 @@ const getMachinesLabel = (row: ResourcePool) => { ); }; -const generateRows = ( - rows: ResourcePool[], - expandedId: ResourcePool["id"] | null, - setExpandedId: (expandedId: ResourcePool["id"] | null) => void, - dispatch: Dispatch, - setDeleting: (deleting: ResourcePool["name"] | null) => void, - saved: ResourcePoolState["saved"], - saving: ResourcePoolState["saving"] -) => +const generateRows = (rows: ResourcePool[]) => rows.map((row) => { - const expanded = expandedId === row.id; return { "aria-label": row.name, - className: expanded ? "p-table__row is-active" : null, columns: [ { content: row.name, @@ -78,6 +59,7 @@ const generateRows = ( row.is_default || row.machine_total_count > 0 } + deletePath={urls.pools.delete({ id: row.id })} deleteTooltip={ (row.is_default && "The default pool may not be deleted.") || (row.machine_total_count > 0 && @@ -86,28 +68,11 @@ const generateRows = ( } editDisabled={!row.permissions.includes("edit")} editPath={urls.pools.edit({ id: row.id })} - onDelete={() => setExpandedId(row.id)} /> ), className: "u-align--right", }, ], - expanded: expanded, - expandedContent: expanded && ( - setExpandedId(null)} - onConfirm={() => { - dispatch(resourcePoolActions.delete(row.id)); - setDeleting(row.name); - }} - sidebar={false} - /> - ), key: row.name, sortData: { name: row.name, @@ -121,7 +86,6 @@ const Pools = (): JSX.Element => { useWindowTitle("Pools"); const dispatch = useDispatch(); - const [expandedId, setExpandedId] = useState(null); const [deletingPool, setDeleting] = useState( null ); @@ -129,7 +93,6 @@ const Pools = (): JSX.Element => { const poolsLoaded = useSelector(resourcePoolSelectors.loaded); const poolsLoading = useSelector(resourcePoolSelectors.loading); const saved = useSelector(resourcePoolSelectors.saved); - const saving = useSelector(resourcePoolSelectors.saving); const errors = useSelector(resourcePoolSelectors.errors); const errorMessage = formatErrors(errors); @@ -191,15 +154,7 @@ const Pools = (): JSX.Element => { }, ]} paginate={50} - rows={generateRows( - resourcePools, - expandedId, - setExpandedId, - dispatch, - setDeleting, - saved, - saving - )} + rows={generateRows(resourcePools)} sortable /> )} diff --git a/src/app/pools/views/Pools.tsx b/src/app/pools/views/Pools.tsx index 6a15594712..1c92e45b7f 100644 --- a/src/app/pools/views/Pools.tsx +++ b/src/app/pools/views/Pools.tsx @@ -4,6 +4,7 @@ import pluralize from "pluralize"; import { useSelector } from "react-redux"; import { Link, Route, Routes } from "react-router-dom-v5-compat"; +import PoolDelete from "./PoolDelete"; import PoolList from "./PoolList"; import PageContent from "@/app/base/components/PageContent"; @@ -26,40 +27,83 @@ const Pools = (): JSX.Element => { const resourcePoolsCount = useSelector(resourcePoolSelectors.count); + const PoolsHeader = () => ( + + + {machineCount} machines + in {resourcePoolsCount} {pluralize("pool", resourcePoolsCount)} + + + + + + ); + return ( - - - {machineCount} machines - in {resourcePoolsCount} {pluralize("pool", resourcePoolsCount)} - - - - - - } - sidePanelContent={null} - sidePanelTitle={null} - > - - } - path={getRelativeRoute(urls.pools.index, base)} - /> - } - path={getRelativeRoute(urls.pools.add, base)} - /> - } - path={getRelativeRoute(urls.pools.edit(null), base)} - /> - } path="*" /> - - + + } + sidePanelContent={null} + sidePanelTitle={null} + > + + + } + path={getRelativeRoute(urls.pools.index, base)} + /> + } + sidePanelContent={} + sidePanelTitle="Add pool" + > + + + } + path={getRelativeRoute(urls.pools.add, base)} + /> + } + sidePanelContent={} + sidePanelTitle="Edit pool" + > + + + } + path={getRelativeRoute(urls.pools.edit(null), base)} + /> + } + sidePanelContent={} + sidePanelTitle="Delete pool" + > + + + } + path={getRelativeRoute(urls.pools.delete(null), base)} + /> + } + sidePanelContent={null} + sidePanelTitle={null} + > + + + } + path="*" + /> + ); }; diff --git a/src/app/preferences/components/Routes/Routes.tsx b/src/app/preferences/components/Routes/Routes.tsx index 543f831aba..4ca7dc1821 100644 --- a/src/app/preferences/components/Routes/Routes.tsx +++ b/src/app/preferences/components/Routes/Routes.tsx @@ -1,17 +1,20 @@ import { Redirect } from "react-router"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom-v5-compat"; -import urls from "@/app/base/urls"; -import NotFound from "@/app/base/views/NotFound"; -import APIKeyAdd from "@/app/preferences/views/APIKeys/APIKeyAdd"; -import APIKeyEdit from "@/app/preferences/views/APIKeys/APIKeyEdit"; -import APIKeyList from "@/app/preferences/views/APIKeys/APIKeyList"; -import Details from "@/app/preferences/views/Details"; -import AddSSHKey from "@/app/preferences/views/SSHKeys/AddSSHKey"; -import SSHKeyList from "@/app/preferences/views/SSHKeys/SSHKeyList"; -import AddSSLKey from "@/app/preferences/views/SSLKeys/AddSSLKey"; -import SSLKeyList from "@/app/preferences/views/SSLKeys/SSLKeyList"; -import { getRelativeRoute } from "@/app/utils"; +import PageContent from "app/base/components/PageContent"; +import urls from "app/base/urls"; +import NotFound from "app/base/views/NotFound"; +import APIKeyAdd from "app/preferences/views/APIKeys/APIKeyAdd"; +import APIKeyDelete from "app/preferences/views/APIKeys/APIKeyDelete"; +import APIKeyEdit from "app/preferences/views/APIKeys/APIKeyEdit"; +import APIKeyList from "app/preferences/views/APIKeys/APIKeyList"; +import Details from "app/preferences/views/Details"; +import { Labels as PreferenceLabels } from "app/preferences/views/Preferences"; +import AddSSHKey from "app/preferences/views/SSHKeys/AddSSHKey"; +import SSHKeyList from "app/preferences/views/SSHKeys/SSHKeyList"; +import AddSSLKey from "app/preferences/views/SSLKeys/AddSSLKey"; +import SSLKeyList from "app/preferences/views/SSLKeys/SSLKeyList"; +import { getRelativeRoute } from "app/utils"; const Routes = (): JSX.Element => { const base = urls.preferences.index; @@ -19,35 +22,94 @@ const Routes = (): JSX.Element => { } path="/" /> } + element={ + +
+ + } path={getRelativeRoute(urls.preferences.details, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.preferences.apiKeys.index, base)} /> } + element={ + } + sidePanelTitle="Generate MAAS API key" + > + + + } path={getRelativeRoute(urls.preferences.apiKeys.add, base)} /> } + element={ + } + sidePanelTitle="Edit MAAS API key" + > + + + } path={getRelativeRoute(urls.preferences.apiKeys.edit(null), base)} /> } + element={ + } + sidePanelTitle="Delete MAAS API key" + > + + + } + path={getRelativeRoute(urls.preferences.apiKeys.delete(null), base)} + /> + + + + } path={getRelativeRoute(urls.preferences.sshKeys.index, base)} /> } + element={ + } + sidePanelTitle="Add SSH key" + > + + + } path={getRelativeRoute(urls.preferences.sshKeys.add, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.preferences.sslKeys.index, base)} /> } + element={ + } + sidePanelTitle="Add SSL key" + > + + + } path={getRelativeRoute(urls.preferences.sslKeys.add, base)} /> } path="*" /> diff --git a/src/app/preferences/urls.ts b/src/app/preferences/urls.ts index 633caf843d..ca8d9d629f 100644 --- a/src/app/preferences/urls.ts +++ b/src/app/preferences/urls.ts @@ -5,6 +5,7 @@ const urls = { apiKeys: { add: "/account/prefs/api-keys/add", edit: argPath<{ id: Token["id"] }>("/account/prefs/api-keys/:id/edit"), + delete: argPath<{ id: Token["id"] }>("/account/prefs/api-keys/:id/delete"), index: "/account/prefs/api-keys", }, details: "/account/prefs/details", diff --git a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx new file mode 100644 index 0000000000..410afdecb4 --- /dev/null +++ b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx @@ -0,0 +1,38 @@ +import APIKeyDelete from "./APIKeyDelete"; + +import type { RootState } from "app/store/root/types"; +import { + token as tokenFactory, + tokenState as tokenStateFactory, + rootState as rootStateFactory, +} from "testing/factories"; +import { renderWithBrowserRouter, screen } from "testing/utils"; + +let state: RootState; +rootStateFactory({ + token: tokenStateFactory({ + items: [ + tokenFactory({ + id: 1, + key: "ssh-rsa aabb", + consumer: { key: "abc", name: "Name" }, + }), + tokenFactory({ + id: 2, + key: "ssh-rsa ccdd", + consumer: { key: "abc", name: "Name" }, + }), + ], + }), +}); + +it("renders", () => { + renderWithBrowserRouter(, { + state, + route: "/account/prefs/api-keys/1/delete", + routePattern: "/account/prefs/api-keys/:id/delete", + }); + expect( + screen.getByRole("form", { name: "Delete API Key" }) + ).toBeInTheDocument(); +}); diff --git a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx new file mode 100644 index 0000000000..a4b0b55b45 --- /dev/null +++ b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx @@ -0,0 +1,13 @@ +import { useGetURLId } from "app/base/hooks"; +import APIKeyDeleteForm from "app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm"; + +const APIKeyDelete = () => { + const id = useGetURLId("id"); + if (!id) { + return

API Key not found

; + } + + return ; +}; + +export default APIKeyDelete; diff --git a/src/app/preferences/views/APIKeys/APIKeyDelete/index.ts b/src/app/preferences/views/APIKeys/APIKeyDelete/index.ts new file mode 100644 index 0000000000..4b3fdab73e --- /dev/null +++ b/src/app/preferences/views/APIKeys/APIKeyDelete/index.ts @@ -0,0 +1 @@ +export { default } from "./APIKeyDelete"; diff --git a/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx b/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx new file mode 100644 index 0000000000..8e034bee93 --- /dev/null +++ b/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx @@ -0,0 +1,33 @@ +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom-v5-compat"; + +import ModelDeleteForm from "app/base/components/ModelDeleteForm"; +import urls from "app/base/urls"; +import { actions as tokenActions } from "app/store/token"; +import tokenSelectors from "app/store/token/selectors"; + +const APIKeyDeleteForm = ({ id }: { id: number }) => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const saved = useSelector(tokenSelectors.saved); + const saving = useSelector(tokenSelectors.saving); + + return ( + navigate({ pathname: urls.preferences.apiKeys.index })} + onSubmit={() => { + dispatch(tokenActions.delete(id)); + }} + saved={saved} + savedRedirect={urls.preferences.apiKeys.index} + saving={saving} + submitAppearance="negative" + submitLabel="Delete" + /> + ); +}; + +export default APIKeyDeleteForm; diff --git a/src/app/preferences/views/APIKeys/APIKeyDeleteForm/index.ts b/src/app/preferences/views/APIKeys/APIKeyDeleteForm/index.ts new file mode 100644 index 0000000000..fa3274b35f --- /dev/null +++ b/src/app/preferences/views/APIKeys/APIKeyDeleteForm/index.ts @@ -0,0 +1 @@ +export { default } from "./APIKeyDeleteForm"; diff --git a/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx b/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx index b1c6263425..022c14b8e2 100644 --- a/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx @@ -3,14 +3,13 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; -import FormCard from "@/app/base/components/FormCard"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useAddMessage, useWindowTitle } from "@/app/base/hooks"; -import urls from "@/app/base/urls"; -import { actions as tokenActions } from "@/app/store/token"; -import tokenSelectors from "@/app/store/token/selectors"; -import type { Token } from "@/app/store/token/types"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import { useAddMessage } from "app/base/hooks"; +import urls from "app/base/urls"; +import { actions as tokenActions } from "app/store/token"; +import tokenSelectors from "app/store/token/selectors"; +import type { Token } from "app/store/token/types"; export enum Label { AddTitle = "Generate MAAS API key", @@ -42,9 +41,7 @@ export const APIKeyForm = ({ token }: Props): JSX.Element => { const errors = useSelector(tokenSelectors.errors); const saved = useSelector(tokenSelectors.saved); const saving = useSelector(tokenSelectors.saving); - const title = editing ? Label.EditTitle : Label.AddTitle; - useWindowTitle(title); useAddMessage( saved, tokenActions.cleanup, @@ -52,59 +49,57 @@ export const APIKeyForm = ({ token }: Props): JSX.Element => { ); return ( - - navigate({ pathname: urls.preferences.apiKeys.index })} - onSaveAnalytics={{ - action: "Saved", - category: "API keys preferences", - label: "Generate API key form", - }} - onSubmit={(values) => { - if (editing) { - if (token) { - dispatch( - tokenActions.update({ - id: token.id, - name: values.name, - }) - ); - } - } else { - dispatch(tokenActions.create(values)); + navigate({ pathname: urls.preferences.apiKeys.index })} + onSaveAnalytics={{ + action: "Saved", + category: "API keys preferences", + label: "Generate API key form", + }} + onSubmit={(values) => { + if (editing) { + if (token) { + dispatch( + tokenActions.update({ + id: token.id, + name: values.name, + }) + ); } - }} - saved={saved} - savedRedirect={urls.preferences.apiKeys.index} - saving={saving} - submitLabel={editing ? Label.EditSubmit : Label.AddSubmit} - validationSchema={editing ? APIKeyEditSchema : APIKeyAddSchema} - > - - - - - -

- The API key is used to log in to the API from the MAAS CLI and by - other services connecting to MAAS, such as Juju. -

- -
-
-
+ } else { + dispatch(tokenActions.create(values)); + } + }} + saved={saved} + savedRedirect={urls.preferences.apiKeys.index} + saving={saving} + submitLabel={editing ? Label.EditSubmit : Label.AddSubmit} + validationSchema={editing ? APIKeyEditSchema : APIKeyAddSchema} + > + + + + + +

+ The API key is used to log in to the API from the MAAS CLI and by + other services connecting to MAAS, such as Juju. +

+ +
+ ); }; diff --git a/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx b/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx index 65f4c6aa42..82c5a58338 100644 --- a/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx @@ -1,42 +1,24 @@ -import { useState } from "react"; - import { Notification } from "@canonical/react-components"; -import { useDispatch, useSelector } from "react-redux"; -import type { Dispatch } from "redux"; +import { useSelector } from "react-redux"; -import TableActions from "@/app/base/components/TableActions"; -import TableDeleteConfirm from "@/app/base/components/TableDeleteConfirm"; -import { - useFetchActions, - useAddMessage, - useWindowTitle, -} from "@/app/base/hooks"; -import urls from "@/app/base/urls"; -import SettingsTable from "@/app/settings/components/SettingsTable"; -import { actions as tokenActions } from "@/app/store/token"; -import tokenSelectors from "@/app/store/token/selectors"; -import type { Token, TokenMeta, TokenState } from "@/app/store/token/types"; +import TableActions from "app/base/components/TableActions"; +import { useFetchActions, useAddMessage, useWindowTitle } from "app/base/hooks"; +import urls from "app/base/urls"; +import SettingsTable from "app/settings/components/SettingsTable"; +import { actions as tokenActions } from "app/store/token"; +import tokenSelectors from "app/store/token/selectors"; +import type { Token } from "app/store/token/types"; export enum Label { Title = "API keys", EmptyList = "No API keys available.", } -const generateRows = ( - tokens: Token[], - expandedId: Token[TokenMeta.PK] | null, - setExpandedId: (id: Token[TokenMeta.PK] | null) => void, - hideExpanded: () => void, - dispatch: Dispatch, - saved: TokenState["saved"], - saving: TokenState["saving"] -) => +const generateRows = (tokens: Token[]) => tokens.map(({ consumer, id, key, secret }) => { const { name } = consumer; - const expanded = expandedId === id; const token = `${consumer.key}:${key}:${secret}`; return { - className: expanded ? "p-table__row is-active" : null, columns: [ { content: name, @@ -49,26 +31,13 @@ const generateRows = ( content: ( setExpandedId(id)} /> ), className: "u-align--right", }, ], - expanded: expanded, - expandedContent: expanded && ( - { - dispatch(tokenActions.delete(id)); - }} - /> - ), key: id, sortData: { name: name, @@ -77,20 +46,11 @@ const generateRows = ( }); const APIKeyList = (): JSX.Element => { - const [expandedId, setExpandedId] = useState( - null - ); const errors = useSelector(tokenSelectors.errors); const loading = useSelector(tokenSelectors.loading); const loaded = useSelector(tokenSelectors.loaded); const tokens = useSelector(tokenSelectors.all); const saved = useSelector(tokenSelectors.saved); - const saving = useSelector(tokenSelectors.saving); - const dispatch = useDispatch(); - - const hideExpanded = () => { - setExpandedId(null); - }; useAddMessage(saved, tokenActions.cleanup, "API key deleted successfully."); @@ -129,15 +89,7 @@ const APIKeyList = (): JSX.Element => { ]} loaded={loaded} loading={loading} - rows={generateRows( - tokens, - expandedId, - setExpandedId, - hideExpanded, - dispatch, - saved, - saving - )} + rows={generateRows(tokens)} tableClassName="apikey-list" /> diff --git a/src/app/preferences/views/Preferences.test.tsx b/src/app/preferences/views/Preferences.test.tsx deleted file mode 100644 index aafe1d0295..0000000000 --- a/src/app/preferences/views/Preferences.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Preferences, { Labels as PreferencesLabels } from "./Preferences"; - -import { screen, renderWithBrowserRouter, getTestState } from "@/testing/utils"; - -describe("Preferences", () => { - it("renders", () => { - const state = getTestState(); - renderWithBrowserRouter(, { route: "/preferences", state }); - - expect(screen.getByLabelText(PreferencesLabels.Title)).toBeInTheDocument(); - }); -}); diff --git a/src/app/preferences/views/Preferences.tsx b/src/app/preferences/views/Preferences.tsx index 19318af1bf..89b7d84ea4 100644 --- a/src/app/preferences/views/Preferences.tsx +++ b/src/app/preferences/views/Preferences.tsx @@ -1,18 +1,9 @@ -import PageContent from "@/app/base/components/PageContent/PageContent"; -import Routes from "@/app/preferences/components/Routes"; +import Routes from "app/preferences/components/Routes"; export enum Labels { Title = "My preferences", } -const Preferences = (): JSX.Element => ( - - - -); +const Preferences = (): JSX.Element => ; export default Preferences; diff --git a/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx b/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx index 617ac0f7c7..e804887f3b 100644 --- a/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx +++ b/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx @@ -1,36 +1,28 @@ import { useNavigate } from "react-router-dom-v5-compat"; -import FormCard from "@/app/base/components/FormCard"; -import SSHKeyForm from "@/app/base/components/SSHKeyForm"; -import { COL_SIZES } from "@/app/base/constants"; -import { useWindowTitle } from "@/app/base/hooks"; -import urls from "@/app/base/urls"; +import SSHKeyForm from "app/base/components/SSHKeyForm"; +import urls from "app/base/urls"; export enum Label { Title = "Add SSH key", FormLabel = "Add SSH key form", } -const { CARD_TITLE, SIDEBAR, TOTAL } = COL_SIZES; - export const AddSSHKey = (): JSX.Element => { const navigate = useNavigate(); - useWindowTitle(Label.Title); return ( - - navigate({ pathname: urls.preferences.sshKeys.index })} - onSaveAnalytics={{ - action: "Saved", - category: "SSH keys preferences", - label: "Import SSH key form", - }} - savedRedirect={urls.preferences.sshKeys.index} - /> - + navigate({ pathname: urls.preferences.sshKeys.index })} + onSaveAnalytics={{ + action: "Saved", + category: "SSH keys preferences", + label: "Import SSH key form", + }} + savedRedirect={urls.preferences.sshKeys.index} + /> ); }; diff --git a/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx b/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx index 9874b6a2fa..7e6192dced 100644 --- a/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx +++ b/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx @@ -4,13 +4,12 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; -import FormCard from "@/app/base/components/FormCard"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useAddMessage, useWindowTitle } from "@/app/base/hooks"; -import urls from "@/app/base/urls"; -import { actions as sslkeyActions } from "@/app/store/sslkey"; -import sslkeySelectors from "@/app/store/sslkey/selectors"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import { useAddMessage } from "app/base/hooks"; +import urls from "app/base/urls"; +import { actions as sslkeyActions } from "app/store/sslkey"; +import sslkeySelectors from "app/store/sslkey/selectors"; export enum Label { Title = "Add SSL key", @@ -36,54 +35,50 @@ export const AddSSLKey = (): JSX.Element => { const saved = useSelector(sslkeySelectors.saved); const errors = useSelector(sslkeySelectors.errors); - useWindowTitle(Label.Title); - useAddMessage(saved, sslkeyActions.cleanup, "SSL key successfully added."); return ( - - navigate({ pathname: urls.preferences.sslKeys.index })} - onSaveAnalytics={{ - action: "Saved", - category: "SSL keys preferences", - label: "Add SSL key form", - }} - onSubmit={(values) => { - dispatch(sslkeyActions.create(values)); - }} - saved={saved} - savedRedirect={urls.preferences.sslKeys.index} - saving={saving} - submitLabel={Label.SubmitLabel} - validationSchema={SSLKeySchema} - > - - - - - -

- You will be able to access Windows winrm service with a registered - key. -

- -
-
-
+ navigate({ pathname: urls.preferences.sslKeys.index })} + onSaveAnalytics={{ + action: "Saved", + category: "SSL keys preferences", + label: "Add SSL key form", + }} + onSubmit={(values) => { + dispatch(sslkeyActions.create(values)); + }} + saved={saved} + savedRedirect={urls.preferences.sslKeys.index} + saving={saving} + submitLabel={Label.SubmitLabel} + validationSchema={SSLKeySchema} + > + + + + + +

+ You will be able to access Windows winrm service with a registered + key. +

+ +
+
); }; diff --git a/src/app/settings/components/Routes/Routes.test.tsx b/src/app/settings/components/Routes/Routes.test.tsx index 8d373109ed..7015605571 100644 --- a/src/app/settings/components/Routes/Routes.test.tsx +++ b/src/app/settings/components/Routes/Routes.test.tsx @@ -89,29 +89,10 @@ const routes = [ title: "Users", path: urls.settings.users.index, }, - { - title: "Add user", - path: urls.settings.users.add, - }, - { - title: `Editing \`${user.username}\``, - path: urls.settings.users.edit({ id: user.id }), - }, { title: "License keys", path: urls.settings.licenseKeys.index, }, - { - title: "Add license key", - path: urls.settings.licenseKeys.add, - }, - { - title: "Update license key", - path: urls.settings.licenseKeys.edit({ - osystem: licensekey.osystem, - distro_series: licensekey.distro_series, - }), - }, { title: "Storage", path: urls.settings.storage, @@ -140,45 +121,18 @@ const routes = [ title: "Commissioning scripts", path: urls.settings.scripts.commissioning.index, }, - { - title: "Upload commissioning script", - path: urls.settings.scripts.commissioning.upload, - }, { title: "Testing scripts", path: urls.settings.scripts.testing.index, }, - { - title: "Upload testing script", - path: urls.settings.scripts.testing.upload, - }, { title: "DHCP snippets", path: urls.settings.dhcp.index, }, - { - title: "Add DHCP snippet", - path: urls.settings.dhcp.add, - }, - { - title: `Editing \`${dhcpSnippet.name}\``, - path: urls.settings.dhcp.edit({ id: dhcpSnippet.id }), - }, { title: "Package repos", path: urls.settings.repositories.index, }, - { - title: "Add PPA", - path: urls.settings.repositories.add({ type: "ppa" }), - }, - { - title: "Edit PPA", - path: urls.settings.repositories.edit({ - id: packageRepository.id, - type: "ppa", - }), - }, { title: "Windows", path: urls.settings.images.windows, diff --git a/src/app/settings/components/Routes/Routes.tsx b/src/app/settings/components/Routes/Routes.tsx index b5b36541a5..9ebb62921e 100644 --- a/src/app/settings/components/Routes/Routes.tsx +++ b/src/app/settings/components/Routes/Routes.tsx @@ -1,62 +1,80 @@ import { Redirect } from "react-router-dom"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom-v5-compat"; -import urls from "@/app/base/urls"; -import NotFound from "@/app/base/views/NotFound"; -import Commissioning from "@/app/settings/views/Configuration/Commissioning"; -import Deploy from "@/app/settings/views/Configuration/Deploy"; -import General from "@/app/settings/views/Configuration/General"; -import KernelParameters from "@/app/settings/views/Configuration/KernelParameters"; -import DhcpAdd from "@/app/settings/views/Dhcp/DhcpAdd"; -import DhcpEdit from "@/app/settings/views/Dhcp/DhcpEdit"; -import DhcpList from "@/app/settings/views/Dhcp/DhcpList"; -import ThirdPartyDrivers from "@/app/settings/views/Images/ThirdPartyDrivers"; -import VMWare from "@/app/settings/views/Images/VMWare"; -import Windows from "@/app/settings/views/Images/Windows"; -import LicenseKeyAdd from "@/app/settings/views/LicenseKeys/LicenseKeyAdd"; -import LicenseKeyEdit from "@/app/settings/views/LicenseKeys/LicenseKeyEdit"; -import LicenseKeyList from "@/app/settings/views/LicenseKeys/LicenseKeyList"; -import DnsForm from "@/app/settings/views/Network/DnsForm"; -import NetworkDiscoveryForm from "@/app/settings/views/Network/NetworkDiscoveryForm"; -import NtpForm from "@/app/settings/views/Network/NtpForm"; -import ProxyForm from "@/app/settings/views/Network/ProxyForm"; -import SyslogForm from "@/app/settings/views/Network/SyslogForm"; -import RepositoriesList from "@/app/settings/views/Repositories/RepositoriesList"; -import RepositoryAdd from "@/app/settings/views/Repositories/RepositoryAdd"; -import RepositoryEdit from "@/app/settings/views/Repositories/RepositoryEdit"; -import ScriptsList from "@/app/settings/views/Scripts/ScriptsList"; -import ScriptsUpload from "@/app/settings/views/Scripts/ScriptsUpload"; -import IpmiSettings from "@/app/settings/views/Security/IpmiSettings"; -import SecretStorage from "@/app/settings/views/Security/SecretStorage"; -import SecurityProtocols from "@/app/settings/views/Security/SecurityProtocols"; -import SessionTimeout from "@/app/settings/views/Security/SessionTimeout"; -import StorageForm from "@/app/settings/views/Storage/StorageForm"; -import UserAdd from "@/app/settings/views/Users/UserAdd"; -import UserEdit from "@/app/settings/views/Users/UserEdit"; -import UsersList from "@/app/settings/views/Users/UsersList"; -import { getRelativeRoute } from "@/app/utils"; +import PageContent from "app/base/components/PageContent"; +import urls from "app/base/urls"; +import NotFound from "app/base/views/NotFound"; +import Commissioning from "app/settings/views/Configuration/Commissioning"; +import Deploy from "app/settings/views/Configuration/Deploy"; +import General from "app/settings/views/Configuration/General"; +import KernelParameters from "app/settings/views/Configuration/KernelParameters"; +import DhcpAdd from "app/settings/views/Dhcp/DhcpAdd"; +import DhcpEdit from "app/settings/views/Dhcp/DhcpEdit"; +import DhcpList from "app/settings/views/Dhcp/DhcpList"; +import ThirdPartyDrivers from "app/settings/views/Images/ThirdPartyDrivers"; +import VMWare from "app/settings/views/Images/VMWare"; +import Windows from "app/settings/views/Images/Windows"; +import LicenseKeyAdd from "app/settings/views/LicenseKeys/LicenseKeyAdd"; +import LicenseKeyEdit from "app/settings/views/LicenseKeys/LicenseKeyEdit"; +import LicenseKeyList from "app/settings/views/LicenseKeys/LicenseKeyList"; +import DnsForm from "app/settings/views/Network/DnsForm"; +import NetworkDiscoveryForm from "app/settings/views/Network/NetworkDiscoveryForm"; +import NtpForm from "app/settings/views/Network/NtpForm"; +import ProxyForm from "app/settings/views/Network/ProxyForm"; +import SyslogForm from "app/settings/views/Network/SyslogForm"; +import RepositoriesList from "app/settings/views/Repositories/RepositoriesList"; +import RepositoryAdd from "app/settings/views/Repositories/RepositoryAdd"; +import RepositoryEdit from "app/settings/views/Repositories/RepositoryEdit"; +import ScriptsList from "app/settings/views/Scripts/ScriptsList"; +import ScriptsUpload from "app/settings/views/Scripts/ScriptsUpload"; +import IpmiSettings from "app/settings/views/Security/IpmiSettings"; +import SecretStorage from "app/settings/views/Security/SecretStorage"; +import SecurityProtocols from "app/settings/views/Security/SecurityProtocols"; +import SessionTimeout from "app/settings/views/Security/SessionTimeout"; +import StorageForm from "app/settings/views/Storage/StorageForm"; +import UserAdd from "app/settings/views/Users/UserAdd"; +import UserDelete from "app/settings/views/Users/UserDelete"; +import UserEdit from "app/settings/views/Users/UserEdit"; +import UsersList from "app/settings/views/Users/UsersList"; +import { getRelativeRoute } from "app/utils"; const Routes = (): JSX.Element => { const base = urls.settings.index; return ( } + element={ + + + + } path={getRelativeRoute(urls.settings.configuration.general, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.configuration.commissioning, base)} /> } + element={ + + + + } path={getRelativeRoute( urls.settings.configuration.kernelParameters, base )} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.configuration.deploy, base)} /> { path={getRelativeRoute(urls.settings.configuration.index, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.security.securityProtocols, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.security.secretStorage, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.security.sessionTimeout, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.security.ipmiSettings, base)} /> { path={getRelativeRoute(urls.settings.security.index, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.users.index, base)} /> } + element={ + } sidePanelTitle="Add User"> + + + } path={getRelativeRoute(urls.settings.users.add, base)} /> } + element={ + } + sidePanelTitle="Edit User" + > + + + } path={getRelativeRoute(urls.settings.users.edit(null), base)} /> } + element={ + } + sidePanelTitle="Delete User" + > + + + } + path={getRelativeRoute(urls.settings.users.delete(null), base)} + /> + + + + } path={getRelativeRoute(urls.settings.licenseKeys.index, base)} /> } + element={ + } + sidePanelTitle="Add license key" + > + + + } path={getRelativeRoute(urls.settings.licenseKeys.add, base)} /> } + element={ + } + sidePanelTitle="Update license key" + > + + + } path={getRelativeRoute(urls.settings.licenseKeys.edit(null), base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.storage, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.network.proxy, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.network.dns, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.network.ntp, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.network.syslog, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.network.networkDiscovery, base)} /> { path={getRelativeRoute(urls.settings.network.index, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.scripts.commissioning.index, base)} /> } + element={ + } + sidePanelTitle="Upload commissioning script" + > + + + } path={getRelativeRoute( urls.settings.scripts.commissioning.upload, base )} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.scripts.testing.index, base)} /> } + element={ + } + sidePanelTitle="Upload testing script" + > + + + } path={getRelativeRoute(urls.settings.scripts.testing.upload, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.dhcp.index, base)} /> } + element={ + } + sidePanelTitle="Add DHCP snippet" + > + + + } path={getRelativeRoute(urls.settings.dhcp.add, base)} /> } + element={ + } + sidePanelTitle="Edit DHCP snippet" + > + + + } path={getRelativeRoute(urls.settings.dhcp.edit(null), base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.repositories.index, base)} /> } + element={ + } + sidePanelTitle="Add Repository" + > + + + } path={getRelativeRoute(urls.settings.repositories.add(null), base)} /> } + element={ + } + sidePanelTitle={"Edit Repository"} + > + + + } path={getRelativeRoute(urls.settings.repositories.edit(null), base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.images.windows, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.images.vmware, base)} /> } + element={ + + + + } path={getRelativeRoute(urls.settings.images.ubuntu, base)} /> - } path="*" /> + + + + } + path="*" + /> ); }; diff --git a/src/app/settings/urls.ts b/src/app/settings/urls.ts index 6d4253fb76..8f58ebe505 100644 --- a/src/app/settings/urls.ts +++ b/src/app/settings/urls.ts @@ -69,6 +69,7 @@ const urls = { users: { add: "/settings/users/add", edit: argPath<{ id: User["id"] }>("/settings/users/:id/edit"), + delete: argPath<{ id: User["id"] }>("/settings/users/:id/delete"), index: "/settings/users", }, } as const; diff --git a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx index 4af3de4bfd..0975132717 100644 --- a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx +++ b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.test.tsx @@ -80,7 +80,7 @@ describe("DhcpForm", () => { ); expect( - screen.getByRole("heading", { name: "Editing `lease`" }) + screen.getByRole("form", { name: "Editing `lease`" }) ).toBeInTheDocument(); }); }); diff --git a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx index a28e158b00..17f60bab99 100644 --- a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx +++ b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx @@ -2,12 +2,10 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom-v5-compat"; -import BaseDhcpForm from "@/app/base/components/DhcpForm"; -import type { DHCPFormValues } from "@/app/base/components/DhcpForm/types"; -import FormCard from "@/app/base/components/FormCard"; -import { useWindowTitle } from "@/app/base/hooks"; -import settingsURLs from "@/app/settings/urls"; -import type { DHCPSnippet } from "@/app/store/dhcpsnippet/types"; +import BaseDhcpForm from "app/base/components/DhcpForm"; +import type { DHCPFormValues } from "app/base/components/DhcpForm/types"; +import settingsURLs from "app/settings/urls"; +import type { DHCPSnippet } from "app/store/dhcpsnippet/types"; type Props = { dhcpSnippet?: DHCPSnippet; @@ -19,21 +17,17 @@ export const DhcpForm = ({ dhcpSnippet }: Props): JSX.Element => { const editing = !!dhcpSnippet; const title = editing ? `Editing \`${name}\`` : "Add DHCP snippet"; - useWindowTitle(title); - return ( - - navigate({ pathname: settingsURLs.dhcp.index })} - onValuesChanged={(values) => { - setName(values.name); - }} - savedRedirect={settingsURLs.dhcp.index} - /> - + navigate({ pathname: settingsURLs.dhcp.index })} + onValuesChanged={(values) => { + setName(values.name); + }} + savedRedirect={settingsURLs.dhcp.index} + /> ); }; diff --git a/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx b/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx index fd2a8028e9..e2c9b38e22 100644 --- a/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx +++ b/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx @@ -9,16 +9,15 @@ import LicenseKeyFormFields from "../LicenseKeyFormFields"; import type { LicenseKeyFormValues } from "./types"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useAddMessage, useWindowTitle } from "@/app/base/hooks"; -import settingsURLs from "@/app/settings/urls"; -import { actions as generalActions } from "@/app/store/general"; -import { osInfo as osInfoSelectors } from "@/app/store/general/selectors"; -import { actions as licenseKeysActions } from "@/app/store/licensekeys"; -import licenseKeysSelectors from "@/app/store/licensekeys/selectors"; -import type { LicenseKeys } from "@/app/store/licensekeys/types"; -import { LicenseKeysMeta } from "@/app/store/licensekeys/types"; +import FormikForm from "app/base/components/FormikForm"; +import { useAddMessage } from "app/base/hooks"; +import settingsURLs from "app/settings/urls"; +import { actions as generalActions } from "app/store/general"; +import { osInfo as osInfoSelectors } from "app/store/general/selectors"; +import { actions as licenseKeysActions } from "app/store/licensekeys"; +import licenseKeysSelectors from "app/store/licensekeys/selectors"; +import type { LicenseKeys } from "app/store/licensekeys/types"; +import { LicenseKeysMeta } from "app/store/licensekeys/types"; type Props = { licenseKey?: LicenseKeys; @@ -50,8 +49,6 @@ export const LicenseKeyForm = ({ licenseKey }: Props): JSX.Element => { const title = licenseKey ? "Update license key" : "Add license key"; - useWindowTitle(title); - const editing = !!licenseKey; useAddMessage( @@ -71,7 +68,7 @@ export const LicenseKeyForm = ({ licenseKey }: Props): JSX.Element => { }, [dispatch, osInfoLoaded, licenseKeysLoaded]); return ( - + <> {!isLoaded ? ( ) : osystems.length > 0 ? ( @@ -125,7 +122,7 @@ export const LicenseKeyForm = ({ licenseKey }: Props): JSX.Element => { ) : ( No available licensed operating systems. )} - + ); }; diff --git a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx index 069e995bac..c0988b4287 100644 --- a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx +++ b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.test.tsx @@ -120,7 +120,9 @@ describe("RepositoryForm", () => { ); - expect(screen.getByText("Add repository")).toBeInTheDocument(); + expect( + screen.getByRole("form", { name: "Add repository" }) + ).toBeInTheDocument(); rerender( @@ -133,7 +135,7 @@ describe("RepositoryForm", () => { ); - expect(screen.getByText("Add PPA")).toBeInTheDocument(); + expect(screen.getByRole("form", { name: "Add PPA" })).toBeInTheDocument(); rerender( @@ -149,7 +151,9 @@ describe("RepositoryForm", () => { ); - expect(screen.getByText("Edit repository")).toBeInTheDocument(); + expect( + screen.getByRole("form", { name: "Edit repository" }) + ).toBeInTheDocument(); rerender( @@ -165,7 +169,7 @@ describe("RepositoryForm", () => { ); - expect(screen.getByText("Edit PPA")).toBeInTheDocument(); + expect(screen.getByRole("form", { name: "Edit PPA" })).toBeInTheDocument(); }); it("cleans up when unmounting", async () => { diff --git a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx index c6ae6a8b20..fc7f6aac96 100644 --- a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx +++ b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx @@ -9,11 +9,10 @@ import RepositoryFormFields from "../RepositoryFormFields"; import type { RepositoryFormValues } from "./types"; -import FormCard from "@/app/base/components/FormCard"; -import FormikForm from "@/app/base/components/FormikForm"; -import { useAddMessage, useWindowTitle } from "@/app/base/hooks"; -import settingsURLs from "@/app/settings/urls"; -import { actions as generalActions } from "@/app/store/general"; +import FormikForm from "app/base/components/FormikForm"; +import { useAddMessage } from "app/base/hooks"; +import settingsURLs from "app/settings/urls"; +import { actions as generalActions } from "app/store/general"; import { componentsToDisable as componentsToDisableSelectors, knownArchitectures as knownArchitecturesSelectors, @@ -119,14 +118,12 @@ export const RepositoryForm = ({ type, repository }: Props): JSX.Element => { }; } - useWindowTitle(title); - return ( <> {!allLoaded ? ( ) : ( - + <> aria-label={title} cleanup={repositoryActions.cleanup} @@ -183,7 +180,7 @@ export const RepositoryForm = ({ type, repository }: Props): JSX.Element => { > - + )} ); diff --git a/src/app/settings/views/Repositories/RepositoryFormFields/RepositoryFormFields.tsx b/src/app/settings/views/Repositories/RepositoryFormFields/RepositoryFormFields.tsx index ecf3743078..560c7975da 100644 --- a/src/app/settings/views/Repositories/RepositoryFormFields/RepositoryFormFields.tsx +++ b/src/app/settings/views/Repositories/RepositoryFormFields/RepositoryFormFields.tsx @@ -75,7 +75,7 @@ const RepositoryFormFields = ({ type }: Props): JSX.Element => { return ( - + { )} - + { const [script, setScript] = useState(null); const dispatch = useDispatch(); const navigate = useNavigate(); - const title = `Upload ${type} script`; const listLocation = `/settings/scripts/${type}`; - useWindowTitle(title); - useEffect(() => { if (hasErrors && errors && typeof errors === "object") { Object.values(errors).forEach((error) => { @@ -127,7 +122,7 @@ const ScriptsUpload = ({ type }: Props): JSX.Element => { const uploadedFile: FileWithPath = acceptedFiles[0]; return ( - +
{ ) : null} - +
); }; diff --git a/src/app/settings/views/Settings.tsx b/src/app/settings/views/Settings.tsx index 99ef84e755..d8df0d5fb1 100644 --- a/src/app/settings/views/Settings.tsx +++ b/src/app/settings/views/Settings.tsx @@ -24,11 +24,7 @@ const Settings = (): JSX.Element => { ); } - return ( - - - - ); + return ; }; export default Settings; diff --git a/src/app/settings/views/Users/UserAdd/UserAdd.test.tsx b/src/app/settings/views/Users/UserAdd/UserAdd.test.tsx index ef51fc8ae7..d4563786ff 100644 --- a/src/app/settings/views/Users/UserAdd/UserAdd.test.tsx +++ b/src/app/settings/views/Users/UserAdd/UserAdd.test.tsx @@ -32,8 +32,6 @@ describe("UserAdd", () => { , { state } ); - expect( - screen.getByRole("heading", { name: "Add user" }) - ).toBeInTheDocument(); + expect(screen.getByRole("form", { name: "Add user" })).toBeInTheDocument(); }); }); diff --git a/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx b/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx new file mode 100644 index 0000000000..cce98dddcb --- /dev/null +++ b/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx @@ -0,0 +1,43 @@ +import UserDelete from "./UserDelete"; + +import type { RootState } from "app/store/root/types"; +import { + rootState as rootStateFactory, + statusState as statusStateFactory, + user as userFactory, + userState as userStateFactory, +} from "testing/factories"; +import { renderWithBrowserRouter, screen } from "testing/utils"; + +let state: RootState; + +beforeEach(() => { + state = rootStateFactory({ + status: statusStateFactory({ + externalAuthURL: null, + }), + user: userStateFactory({ + loaded: true, + items: [ + userFactory({ + email: "admin@example.com", + global_permissions: ["machine_create"], + id: 1, + is_superuser: true, + last_name: "", + sshkeys_count: 0, + username: "admin", + }), + ], + }), + }); +}); + +it("renders", () => { + renderWithBrowserRouter(, { + state, + route: "/settings/users/1/edit", + routePattern: "/settings/users/:id/edit", + }); + expect(screen.getByRole("form", { name: "Delete user" })); +}); diff --git a/src/app/settings/views/Users/UserDelete/UserDelete.tsx b/src/app/settings/views/Users/UserDelete/UserDelete.tsx new file mode 100644 index 0000000000..1e7734f229 --- /dev/null +++ b/src/app/settings/views/Users/UserDelete/UserDelete.tsx @@ -0,0 +1,22 @@ +import { useSelector } from "react-redux"; + +import { useGetURLId } from "app/base/hooks"; +import UserDeleteForm from "app/settings/views/Users/UserDeleteForm"; +import type { RootState } from "app/store/root/types"; +import userSelectors from "app/store/user/selectors"; +import { UserMeta } from "app/store/user/types"; + +const UserDelete = () => { + const id = useGetURLId(UserMeta.PK); + const user = useSelector((state: RootState) => + userSelectors.getById(state, id) + ); + + if (!user) { + return

User not found

; + } + + return ; +}; + +export default UserDelete; diff --git a/src/app/settings/views/Users/UserDelete/index.ts b/src/app/settings/views/Users/UserDelete/index.ts new file mode 100644 index 0000000000..5ec02b0eb6 --- /dev/null +++ b/src/app/settings/views/Users/UserDelete/index.ts @@ -0,0 +1 @@ +export { default } from "./UserDelete"; diff --git a/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx b/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx new file mode 100644 index 0000000000..faa9ad89c7 --- /dev/null +++ b/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; + +import { Col, Row } from "@canonical/react-components"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom-v5-compat"; + +import FormikForm from "app/base/components/FormikForm"; +import { useAddMessage } from "app/base/hooks"; +import type { EmptyObject } from "app/base/types"; +import settingsURLs from "app/settings/urls"; +import { actions as userActions } from "app/store/user"; +import userSelectors from "app/store/user/selectors"; +import type { User } from "app/store/user/types"; + +type UserDeleteProps = { + user: User; +}; + +const UserDeleteForm = ({ user }: UserDeleteProps) => { + const [deletedUser, setDeletedUser] = useState(null); + const navigate = useNavigate(); + const saved = useSelector(userSelectors.saved); + const saving = useSelector(userSelectors.saving); + const errors = useSelector(userSelectors.errors); + const dispatch = useDispatch(); + + useAddMessage( + saved && !errors, + userActions.cleanup, + `Deleted ${deletedUser} from list` + ); + + return ( + + aria-label="Delete user" + initialValues={{}} + onCancel={() => navigate({ pathname: settingsURLs.users.index })} + onSubmit={() => { + dispatch(userActions.delete(user.id)); + setDeletedUser(user.username); + }} + saved={saved} + savedRedirect={settingsURLs.users.index} + saving={saving} + submitAppearance="negative" + submitLabel="Delete" + > + + +

+ {`Are you sure you want to delete \`${user.username}\`?`} +

+ + This action is permanent and can not be undone. + + +
+ + ); +}; + +export default UserDeleteForm; diff --git a/src/app/settings/views/Users/UserDeleteForm/index.ts b/src/app/settings/views/Users/UserDeleteForm/index.ts new file mode 100644 index 0000000000..39e0db39a2 --- /dev/null +++ b/src/app/settings/views/Users/UserDeleteForm/index.ts @@ -0,0 +1 @@ +export { default } from "./UserDeleteForm"; diff --git a/src/app/settings/views/Users/UserForm/UserForm.tsx b/src/app/settings/views/Users/UserForm/UserForm.tsx index 2fff1f62e4..887229fb4e 100644 --- a/src/app/settings/views/Users/UserForm/UserForm.tsx +++ b/src/app/settings/views/Users/UserForm/UserForm.tsx @@ -3,15 +3,14 @@ import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; -import FormCard from "@/app/base/components/FormCard"; -import BaseUserForm from "@/app/base/components/UserForm"; -import type { Props as UserFormProps } from "@/app/base/components/UserForm/UserForm"; -import { useAddMessage, useWindowTitle } from "@/app/base/hooks"; -import settingsURLs from "@/app/settings/urls"; -import { actions as authActions } from "@/app/store/auth"; -import { actions as userActions } from "@/app/store/user"; -import userSelectors from "@/app/store/user/selectors"; -import type { User } from "@/app/store/user/types"; +import BaseUserForm from "app/base/components/UserForm"; +import type { Props as UserFormProps } from "app/base/components/UserForm/UserForm"; +import { useAddMessage } from "app/base/hooks"; +import settingsURLs from "app/settings/urls"; +import { actions as authActions } from "app/store/auth"; +import { actions as userActions } from "app/store/user"; +import userSelectors from "app/store/user/selectors"; +import type { User } from "app/store/user/types"; export enum Labels { Save = "Save user", @@ -30,8 +29,6 @@ export const UserForm = ({ user }: PropTypes): JSX.Element => { const editing = !!user; const title = editing ? `Editing \`${name}\`` : "Add user"; - useWindowTitle(title); - useAddMessage( saved, userActions.cleanup, @@ -40,55 +37,53 @@ export const UserForm = ({ user }: PropTypes): JSX.Element => { ); return ( - - navigate(-1)} - onSave={(values) => { - const params = { - email: values.email, - is_superuser: values.isSuperuser, - last_name: values.fullName, - username: values.username, - }; - if (editing && user) { - dispatch(userActions.update({ ...params, id: user.id })); - if (values.password && values.passwordConfirm) { - dispatch( - authActions.adminChangePassword({ - ...params, - id: user.id, - password1: values.password, - password2: values.passwordConfirm, - }) - ); - } - } else if (!editing && values.password && values.passwordConfirm) { + navigate(-1)} + onSave={(values) => { + const params = { + email: values.email, + is_superuser: values.isSuperuser, + last_name: values.fullName, + username: values.username, + }; + if (editing && user) { + dispatch(userActions.update({ ...params, id: user.id })); + if (values.password && values.passwordConfirm) { dispatch( - userActions.create({ + authActions.adminChangePassword({ ...params, + id: user.id, password1: values.password, password2: values.passwordConfirm, }) ); } - setSaving(values.username); - }} - onSaveAnalytics={{ - action: "Saved", - category: "Users settings", - label: `${editing ? "Edit" : "Add"} user form`, - }} - onUpdateFields={(values) => { - setName(values.username); - }} - savedRedirect={settingsURLs.users.index} - submitLabel="Save user" - user={user} - /> - + } else if (!editing && values.password && values.passwordConfirm) { + dispatch( + userActions.create({ + ...params, + password1: values.password, + password2: values.passwordConfirm, + }) + ); + } + setSaving(values.username); + }} + onSaveAnalytics={{ + action: "Saved", + category: "Users settings", + label: `${editing ? "Edit" : "Add"} user form`, + }} + onUpdateFields={(values) => { + setName(values.username); + }} + savedRedirect={settingsURLs.users.index} + submitLabel="Save user" + user={user} + /> ); }; diff --git a/src/app/settings/views/Users/UsersList/UsersList.test.tsx b/src/app/settings/views/Users/UsersList/UsersList.test.tsx index 7dc22c00ae..b30842e856 100644 --- a/src/app/settings/views/Users/UsersList/UsersList.test.tsx +++ b/src/app/settings/views/Users/UsersList/UsersList.test.tsx @@ -62,90 +62,6 @@ describe("UsersList", () => { }); }); - it("can show a delete confirmation", async () => { - const store = mockStore(state); - const { rerender } = render( - - - - - - - - ); - let row = screen.getAllByTestId("user-row")[1]; - expect(row).not.toHaveClass("is-active"); - - // Click on the delete button: - await userEvent.click(within(row).getByTestId("table-actions-delete")); - - rerender( - - - - - - - - ); - - row = screen.getAllByTestId("user-row")[1]; - expect(row).toHaveClass("is-active"); - }); - - it("can delete a user", async () => { - const store = mockStore(state); - const { rerender } = render( - - - - - - - - ); - let row = screen.getAllByTestId("user-row")[1]; - - // Click on the delete button: - await userEvent.click(within(row).getByTestId("table-actions-delete")); - - rerender( - - - - - - - - ); - - row = screen.getAllByTestId("user-row")[1]; - - // Click on the delete confirm button - await userEvent.click(within(row).getByTestId("action-confirm")); - - expect(store.getActions()[1]).toEqual({ - type: "user/delete", - payload: { - params: { - id: 2, - }, - }, - meta: { - model: "user", - method: "delete", - }, - }); - }); - it("disables delete for the current user", () => { renderWithMockStore( { { state } ); let row = screen.getAllByTestId("user-row")[0]; - expect(within(row).getByTestId("table-actions-delete")).toBeDisabled(); + expect(within(row).getByRole("link", { name: /delete/i })).toHaveAttribute( + "aria-disabled", + "true" + ); }); it("links to preferences for the current user", () => { diff --git a/src/app/settings/views/Users/UsersList/UsersList.tsx b/src/app/settings/views/Users/UsersList/UsersList.tsx index 40143650c5..1b3c971786 100644 --- a/src/app/settings/views/Users/UsersList/UsersList.tsx +++ b/src/app/settings/views/Users/UsersList/UsersList.tsx @@ -4,44 +4,35 @@ import { ContentSection } from "@canonical/maas-react-components"; import { Notification } from "@canonical/react-components"; import { format, parse } from "date-fns"; import { useDispatch, useSelector } from "react-redux"; -import type { Dispatch } from "redux"; -import TableActions from "@/app/base/components/TableActions"; -import TableDeleteConfirm from "@/app/base/components/TableDeleteConfirm"; -import TableHeader from "@/app/base/components/TableHeader"; +import TableActions from "app/base/components/TableActions"; +import TableHeader from "app/base/components/TableHeader"; import { useFetchActions, useAddMessage, useTableSort, useWindowTitle, -} from "@/app/base/hooks"; -import { SortDirection } from "@/app/base/types"; -import urls from "@/app/base/urls"; -import SettingsTable from "@/app/settings/components/SettingsTable"; -import settingsURLs from "@/app/settings/urls"; -import authSelectors from "@/app/store/auth/selectors"; -import type { RootState } from "@/app/store/root/types"; -import statusSelectors from "@/app/store/status/selectors"; -import { actions as userActions } from "@/app/store/user"; -import userSelectors from "@/app/store/user/selectors"; -import type { User, UserMeta, UserState } from "@/app/store/user/types"; -import { isComparable } from "@/app/utils"; +} from "app/base/hooks"; +import { SortDirection } from "app/base/types"; +import urls from "app/base/urls"; +import SettingsTable from "app/settings/components/SettingsTable"; +import settingsURLs from "app/settings/urls"; +import authSelectors from "app/store/auth/selectors"; +import type { RootState } from "app/store/root/types"; +import statusSelectors from "app/store/status/selectors"; +import { actions as userActions } from "app/store/user"; +import userSelectors from "app/store/user/selectors"; +import type { User } from "app/store/user/types"; +import { isComparable } from "app/utils"; type SortKey = keyof User; const generateUserRows = ( users: User[], authUser: User | null, - expandedId: User[UserMeta.PK] | null, - setExpandedId: (expandedId: User[UserMeta.PK] | null) => void, - dispatch: Dispatch, - displayUsername: boolean, - setDeleting: (deletingUser: User["username"] | null) => void, - saved: UserState["saved"], - saving: UserState["saving"] + displayUsername: boolean ) => users.map((user) => { - const expanded = expandedId === user.id; const isAuthUser = user.id === authUser?.id; // Dates are in the format: Thu, 15 Aug. 2019 06:21:39. const last_login = user.last_login @@ -52,7 +43,7 @@ const generateUserRows = ( : "Never"; const fullName = user.last_name; return { - className: expanded ? "p-table__row is-active" : "p-table__row", + className: "p-table__row", columns: [ { content: displayUsername ? user.username : fullName || <>—, @@ -73,6 +64,7 @@ const generateUserRows = ( content: ( setExpandedId(user.id)} /> ), className: "u-align--right", }, ], "data-testid": "user-row", - expanded: expanded, - expandedContent: expanded && ( - setExpandedId(null)} - onConfirm={() => { - dispatch(userActions.delete(user.id)); - setDeleting(user.username); - }} - /> - ), key: user.username, sortData: { username: user.username, @@ -122,7 +99,6 @@ const getSortValue = (sortKey: SortKey, user: User) => { }; const UsersList = (): JSX.Element => { - const [expandedId, setExpandedId] = useState(null); const [searchText, setSearchText] = useState(""); const [displayUsername, setDisplayUsername] = useState(true); const [deletingUser, setDeleting] = useState(null); @@ -133,7 +109,6 @@ const UsersList = (): JSX.Element => { const loaded = useSelector(userSelectors.loaded); const authUser = useSelector(authSelectors.get); const saved = useSelector(userSelectors.saved); - const saving = useSelector(userSelectors.saving); const externalAuthURL = useSelector(statusSelectors.externalAuthURL); const dispatch = useDispatch(); @@ -254,17 +229,7 @@ const UsersList = (): JSX.Element => { ]} loaded={loaded} loading={loading} - rows={generateUserRows( - sortedUsers, - authUser, - expandedId, - setExpandedId, - dispatch, - displayUsername, - setDeleting, - saved, - saving - )} + rows={generateUserRows(sortedUsers, authUser, displayUsername)} searchOnChange={setSearchText} searchPlaceholder="Search users" searchText={searchText} diff --git a/src/app/store/machine/types/base.ts b/src/app/store/machine/types/base.ts index 73ab35de76..23f0c91de1 100644 --- a/src/app/store/machine/types/base.ts +++ b/src/app/store/machine/types/base.ts @@ -371,3 +371,9 @@ export type MachineState = { selected: SelectedMachines | null; statuses: MachineStatuses; } & GenericState; + +export type StorageLayoutOption = { + label: string; + sentenceLabel: string; + value: StorageLayout; +}; diff --git a/src/app/store/machine/types/index.ts b/src/app/store/machine/types/index.ts index 8ba79ba7eb..42a082c018 100644 --- a/src/app/store/machine/types/index.ts +++ b/src/app/store/machine/types/index.ts @@ -67,6 +67,7 @@ export type { MachineStatus, MachineStatuses, SelectedMachines, + StorageLayoutOption, } from "./base"; export { FilterGroupKey, FilterGroupType } from "./base"; diff --git a/src/app/store/utils/node/base.ts b/src/app/store/utils/node/base.ts index 2a6088145a..9b8686107f 100644 --- a/src/app/store/utils/node/base.ts +++ b/src/app/store/utils/node/base.ts @@ -190,24 +190,48 @@ export const getSidePanelTitle = ( if (sidePanelContent) { const [, name] = sidePanelContent.view; switch (name) { + case SidePanelViews.ADD_ALIAS[1]: + return "Add alias"; + case SidePanelViews.ADD_BOND[1]: + return "Create bond"; + case SidePanelViews.ADD_BRIDGE[1]: + return "Create bridge"; case SidePanelViews.ADD_CONTROLLER[1]: return "Add controller"; case SidePanelViews.ADD_CHASSIS[1]: return "Add chassis"; + case SidePanelViews.ADD_INTERFACE[1]: + return "Add interface"; case SidePanelViews.ADD_MACHINE[1]: return "Add machine"; case SidePanelViews.ADD_DEVICE[1]: return "Add device"; + case SidePanelViews.ADD_SPECIAL_FILESYSTEM[1]: + return "Add special filesystem"; case SidePanelViews.AddTag[1]: return "Create new tag"; + case SidePanelViews.ADD_VLAN[1]: + return "Add VLAN"; + case SidePanelViews.CREATE_DATASTORE[1]: + return "Create datastore"; + case SidePanelViews.CREATE_RAID[1]: + return "Create raid"; + case SidePanelViews.CREATE_VOLUME_GROUP[1]: + return "Create volume group"; case SidePanelViews.DeleteTag[1]: return "Delete tag"; + case SidePanelViews.EDIT_INTERFACE[1]: + return "Edit interface"; case SidePanelViews.CREATE_ZONE[1]: return "Add AZ"; case SidePanelViews.DELETE_SPACE[1]: return "Delete space"; case SidePanelViews.DELETE_FABRIC[1]: return "Delete fabric"; + case SidePanelViews.REMOVE_INTERFACE[1]: + return "Remove interface"; + case SidePanelViews.UPDATE_DATASTORE[1]: + return "Update datastore"; default: return name ? getNodeActionTitle(name as NodeActions) : defaultTitle; } diff --git a/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx b/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx new file mode 100644 index 0000000000..3b2588be62 --- /dev/null +++ b/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx @@ -0,0 +1,45 @@ +import { useDispatch, useSelector } from "react-redux"; + +import ModelDeleteForm from "app/base/components/ModelDeleteForm"; +import { + useSidePanel, + type SetSidePanelContent, +} from "app/base/side-panel-context"; +import { actions as ipRangeActions } from "app/store/iprange"; +import ipRangeSelectors from "app/store/iprange/selectors"; + +type Props = { + setActiveForm: SetSidePanelContent; +}; + +const ReservedRangeDeleteForm = ({ setActiveForm }: Props) => { + const dispatch = useDispatch(); + const { sidePanelContent } = useSidePanel(); + const saved = useSelector(ipRangeSelectors.saved); + const saving = useSelector(ipRangeSelectors.saving); + const ipRangeId = + sidePanelContent?.extras && "ipRangeId" in sidePanelContent.extras + ? sidePanelContent?.extras?.ipRangeId + : null; + + if (!ipRangeId && ipRangeId !== 0) { + return

IP range not provided

; + } + + return ( + setActiveForm(null)} + onSubmit={() => { + dispatch(ipRangeActions.delete(ipRangeId)); + }} + saved={saved} + saving={saving} + /> + ); +}; + +export default ReservedRangeDeleteForm; diff --git a/src/app/subnets/components/ReservedRangeDeleteForm/index.ts b/src/app/subnets/components/ReservedRangeDeleteForm/index.ts new file mode 100644 index 0000000000..98e3b4dd47 --- /dev/null +++ b/src/app/subnets/components/ReservedRangeDeleteForm/index.ts @@ -0,0 +1 @@ +export { default } from "./ReservedRangeDeleteForm"; diff --git a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx index c68ae65908..ad2c92112e 100644 --- a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx +++ b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx @@ -45,7 +45,10 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - +
@@ -61,7 +64,7 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -77,7 +80,10 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -104,7 +110,10 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -127,8 +136,8 @@ describe("ReservedRangeForm", () => { @@ -169,7 +178,10 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -201,7 +213,10 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -236,7 +251,7 @@ describe("ReservedRangeForm", () => { diff --git a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx index 01e01f8166..a4d23f1175 100644 --- a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx +++ b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx @@ -4,21 +4,25 @@ import { Col, Row, Spinner } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import { actions as ipRangeActions } from "@/app/store/iprange"; -import ipRangeSelectors from "@/app/store/iprange/selectors"; -import type { IPRange } from "@/app/store/iprange/types"; -import { IPRangeType, IPRangeMeta } from "@/app/store/iprange/types"; -import type { RootState } from "@/app/store/root/types"; -import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; -import { isId } from "@/app/utils"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import { + useSidePanel, + type SetSidePanelContent, +} from "app/base/side-panel-context"; +import { actions as ipRangeActions } from "app/store/iprange"; +import ipRangeSelectors from "app/store/iprange/selectors"; +import type { IPRange } from "app/store/iprange/types"; +import { IPRangeType, IPRangeMeta } from "app/store/iprange/types"; +import type { RootState } from "app/store/root/types"; +import type { Subnet, SubnetMeta } from "app/store/subnet/types"; +import { isId } from "app/utils"; type Props = { createType?: IPRangeType; - id?: IPRange[IPRangeMeta.PK] | null; - onClose: () => void; - subnetId?: Subnet[SubnetMeta.PK] | null; + ipRangeId?: IPRange[IPRangeMeta.PK] | null; + setActiveForm: SetSidePanelContent; + id?: Subnet[SubnetMeta.PK] | null; }; export type FormValues = { @@ -43,20 +47,36 @@ const Schema = Yup.object().shape({ const ReservedRangeForm = ({ createType, + ipRangeId, + setActiveForm, id, - onClose, - subnetId, ...props }: Props): JSX.Element | null => { const dispatch = useDispatch(); + const { sidePanelContent } = useSidePanel(); + let computedIpRangeId = ipRangeId; + if (!ipRangeId) { + computedIpRangeId = + sidePanelContent?.extras && "ipRangeId" in sidePanelContent.extras + ? sidePanelContent?.extras?.ipRangeId + : undefined; + } const ipRange = useSelector((state: RootState) => - ipRangeSelectors.getById(state, id) + ipRangeSelectors.getById(state, computedIpRangeId) ); const saved = useSelector(ipRangeSelectors.saved); const saving = useSelector(ipRangeSelectors.saving); const errors = useSelector(ipRangeSelectors.errors); const cleanup = useCallback(() => ipRangeActions.cleanup(), []); - const isEditing = isId(id); + const isEditing = isId(computedIpRangeId); + const onClose = () => setActiveForm(null); + let computedCreateType = createType; + if (!createType) { + computedCreateType = + sidePanelContent?.extras && "createType" in sidePanelContent.extras + ? sidePanelContent?.extras?.createType + : undefined; + } if (isEditing && !ipRange) { return ( @@ -92,11 +112,11 @@ const ReservedRangeForm = ({ onSubmit={(values) => { // Clear the errors from the previous submission. dispatch(cleanup()); - if (!isEditing && createType) { + if (!isEditing && computedCreateType) { dispatch( ipRangeActions.create({ - subnet: subnetId, - type: createType, + subnet: id, + type: computedCreateType, ...values, }) ); @@ -120,7 +140,7 @@ const ReservedRangeForm = ({ {...props} > - + - + - {isEditing || createType === IPRangeType.Reserved ? ( - + {isEditing || computedCreateType === IPRangeType.Reserved ? ( + { ).toHaveTextContent("what a beaut"); }); -it("displays an edit form", async () => { - const store = mockStore(state); - render( - - - - - - - - ); - await userEvent.click(screen.getByRole("button", { name: "Edit" })); - - await waitFor(() => - expect( - screen.getByRole("form", { name: ReservedRangeFormLabels.EditRange }) - ).toBeInTheDocument() - ); -}); - -it("displays confirm delete message", async () => { - const store = mockStore(state); - render( - - - - - - - - ); - await userEvent.click(screen.getByRole("button", { name: "Delete" })); - - await waitFor(() => { - expect( - screen.getByText( - new RegExp("Are you sure you want to remove this IP range?") - ) - ).toBeInTheDocument(); - }); -}); - -it("dispatches an action to delete a reserved range", async () => { - const store = mockStore(state); - render( - - - - - - - - ); - await userEvent.click(screen.getByTestId("table-actions-delete")); - await userEvent.click(screen.getByTestId("action-confirm")); - - const expectedAction = ipRangeActions.delete(ipRange.id); - - await waitFor(() => { - const actualAction = store - .getActions() - .find((action) => action.type === expectedAction.type); - expect(actualAction).toStrictEqual(expectedAction); - }); -}); - it("displays an add button when it is reserved", () => { ipRange.type = IPRangeType.Reserved; state.iprange.items = [ipRange]; @@ -375,33 +306,6 @@ it("disables the add button if there are no subnets in a VLAN", () => { ).toHaveAttribute("disabled"); }); -it("can display an add form", async () => { - const store = mockStore(state); - render( - - - - - - - - ); - await userEvent.click( - screen.queryAllByRole("button", { - name: Labels.ReserveRange, - })[0] - ); - await userEvent.click(screen.getByTestId("reserve-range-menu-item")); - - await waitFor(() => { - expect( - screen.getByRole("form", { - name: ReservedRangeFormLabels.CreateRange, - }) - ).toBeInTheDocument(); - }); -}); - it("displays the subnet column when the table is for a VLAN", () => { state.iprange.items = [ ipRangeFactory({ start_ip: "11.1.1.1", vlan: vlan.id }), diff --git a/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx b/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx index 486faba939..5f49555e4f 100644 --- a/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx +++ b/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx @@ -9,31 +9,32 @@ import { } from "@canonical/react-components"; import type { MainTableCell } from "@canonical/react-components/dist/components/MainTable/MainTable"; import classNames from "classnames"; -import { useDispatch, useSelector } from "react-redux"; -import type { Dispatch } from "redux"; +import { useSelector } from "react-redux"; -import ReservedRangeForm from "../ReservedRangeForm"; - -import FormCard from "@/app/base/components/FormCard"; -import SubnetLink from "@/app/base/components/SubnetLink"; -import TableActions from "@/app/base/components/TableActions"; -import TableDeleteConfirm from "@/app/base/components/TableDeleteConfirm"; -import TitledSection from "@/app/base/components/TitledSection"; -import docsUrls from "@/app/base/docsUrls"; -import { useFetchActions } from "@/app/base/hooks"; -import { actions as ipRangeActions } from "@/app/store/iprange"; -import ipRangeSelectors from "@/app/store/iprange/selectors"; -import type { IPRange, IPRangeMeta } from "@/app/store/iprange/types"; -import { IPRangeType } from "@/app/store/iprange/types"; +import SubnetLink from "app/base/components/SubnetLink"; +import TableActions from "app/base/components/TableActions"; +import TitledSection from "app/base/components/TitledSection"; +import docsUrls from "app/base/docsUrls"; +import { useFetchActions } from "app/base/hooks"; +import type { SetSidePanelContent } from "app/base/side-panel-context"; +import { useSidePanel } from "app/base/side-panel-context"; +import { actions as ipRangeActions } from "app/store/iprange"; +import ipRangeSelectors from "app/store/iprange/selectors"; +import type { IPRange } from "app/store/iprange/types"; +import { IPRangeType } from "app/store/iprange/types"; import { getCommentDisplay, getOwnerDisplay, getTypeDisplay, -} from "@/app/store/iprange/utils"; -import type { RootState } from "@/app/store/root/types"; -import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; -import type { VLAN, VLANMeta } from "@/app/store/vlan/types"; -import { generateEmptyStateMsg, getTableStatus, isId } from "@/app/utils"; +} from "app/store/iprange/utils"; +import type { RootState } from "app/store/root/types"; +import type { Subnet, SubnetMeta } from "app/store/subnet/types"; +import type { VLAN, VLANMeta } from "app/store/vlan/types"; +import { + SubnetActionTypes, + SubnetDetailsSidePanelViews, +} from "app/subnets/views/SubnetDetails/constants"; +import { generateEmptyStateMsg, getTableStatus, isId } from "app/utils"; export type SubnetProps = { subnetId: Subnet[SubnetMeta.PK] | null; @@ -68,58 +69,17 @@ export enum ExpandedType { Update, } -type Expanded = { - id?: IPRange[IPRangeMeta.PK]; - type: ExpandedType; -}; - -const toggleExpanded = ( - id: IPRange[IPRangeMeta.PK], - expanded: Expanded | null, - expandedType: ExpandedType, - setExpanded: (expanded: Expanded | null) => void -) => - setExpanded( - expanded?.id === id && expanded.type === expandedType - ? null - : { - id, - type: expandedType, - } - ); - const generateRows = ( - dispatch: Dispatch, ipRanges: IPRange[], - expanded: Expanded | null, - setExpanded: (expanded: Expanded | null) => void, - saved: boolean, - saving: boolean, - showSubnetColumn: boolean + showSubnetColumn: boolean, + setSidePanelContent: SetSidePanelContent ) => ipRanges.map((ipRange: IPRange) => { - const isExpanded = expanded?.id === ipRange.id; const comment = getCommentDisplay(ipRange); const owner = getOwnerDisplay(ipRange); const type = getTypeDisplay(ipRange); let expandedContent: ReactNode | null = null; - const onClose = () => setExpanded(null); - if (expanded?.type === ExpandedType.Delete) { - expandedContent = ( - { - dispatch(ipRangeActions.delete(ipRange.id)); - }} - sidebar={false} - /> - ); - } else if (expanded?.type === ExpandedType.Update) { - expandedContent = ; - } + const columns: MainTableCell[] = [ { "aria-label": Labels.StartIP, @@ -151,22 +111,26 @@ const generateRows = ( className: "actions-col u-align--right", content: ( { - toggleExpanded( - ipRange.id, - expanded, - ExpandedType.Delete, - setExpanded - ); - }} - onEdit={() => { - toggleExpanded( - ipRange.id, - expanded, - ExpandedType.Update, - setExpanded - ); - }} + onDelete={() => + setSidePanelContent({ + view: SubnetDetailsSidePanelViews[ + SubnetActionTypes.DeleteReservedRange + ], + extras: { + ipRangeId: ipRange.id, + }, + }) + } + onEdit={() => + setSidePanelContent({ + view: SubnetDetailsSidePanelViews[ + SubnetActionTypes.ReserveRange + ], + extras: { + ipRangeId: ipRange.id, + }, + }) + } /> ), }, @@ -179,9 +143,7 @@ const generateRows = ( }); } return { - className: isExpanded ? "p-table__row is-active" : null, columns, - expanded: isExpanded, expandedContent: expandedContent, key: ipRange.id, sortData: { @@ -199,19 +161,15 @@ const ReservedRanges = ({ subnetId, vlanId, }: Props): JSX.Element | null => { - const dispatch = useDispatch(); - const [expanded, setExpanded] = useState(null); + const [isAddingDynamic, setIsAddingDynamic] = useState(false); + const { setSidePanelContent } = useSidePanel(); const isSubnet = isId(subnetId); const ipRangeLoading = useSelector(ipRangeSelectors.loading); - const saved = useSelector(ipRangeSelectors.saved); - const saving = useSelector(ipRangeSelectors.saving); const ipRanges = useSelector((state: RootState) => isSubnet ? ipRangeSelectors.getBySubnet(state, subnetId) : ipRangeSelectors.getByVLAN(state, vlanId) ); - const isAddingDynamic = expanded?.type === ExpandedType.CreateDynamic; - const isAdding = expanded?.type === ExpandedType.Create || isAddingDynamic; const isDisabled = isId(vlanId) && !hasVLANSubnets; const showSubnetColumn = isId(vlanId); @@ -268,12 +226,32 @@ const ReservedRanges = ({ { children: Labels.ReserveRange, "data-testid": "reserve-range-menu-item", - onClick: () => setExpanded({ type: ExpandedType.Create }), + onClick: () => { + setSidePanelContent({ + view: SubnetDetailsSidePanelViews[ + SubnetActionTypes.ReserveRange + ], + extras: { + createType: IPRangeType.Reserved, + }, + }); + setIsAddingDynamic(false); + }, }, { children: Labels.ReserveDynamicRange, "data-testid": "reserve-dynamic-range-menu-item", - onClick: () => setExpanded({ type: ExpandedType.CreateDynamic }), + onClick: () => { + setSidePanelContent({ + view: SubnetDetailsSidePanelViews[ + SubnetActionTypes.ReserveRange + ], + extras: { + createType: IPRangeType.Dynamic, + }, + }); + setIsAddingDynamic(true); + }, }, ]} position="right" @@ -309,28 +287,9 @@ const ReservedRanges = ({ expanding headers={headers} responsive - rows={generateRows( - dispatch, - ipRanges, - expanded, - setExpanded, - saved, - saving, - showSubnetColumn - )} + rows={generateRows(ipRanges, showSubnetColumn, setSidePanelContent)} sortable /> - {isAdding ? ( - - setExpanded(null)} - subnetId={subnetId} - /> - - ) : null} About IP ranges ); diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.test.tsx index 9645367527..1261bb75e0 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.test.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.test.tsx @@ -46,7 +46,7 @@ it("dispatches a correct action on add static route form submit", async () => { - + diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx index e2d9fd34c4..22cdd89be4 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx @@ -4,18 +4,19 @@ import * as Yup from "yup"; import { Labels } from "../StaticRoutes"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import SubnetSelect from "@/app/base/components/SubnetSelect"; -import { useFetchActions } from "@/app/base/hooks"; -import type { RootState } from "@/app/store/root/types"; -import { actions as staticRouteActions } from "@/app/store/staticroute"; -import staticRouteSelectors from "@/app/store/staticroute/selectors"; -import { actions as subnetActions } from "@/app/store/subnet"; -import subnetSelectors from "@/app/store/subnet/selectors"; -import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; -import { getIsDestinationForSource } from "@/app/store/subnet/utils"; -import { toFormikNumber } from "@/app/utils"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import SubnetSelect from "app/base/components/SubnetSelect"; +import { useFetchActions } from "app/base/hooks"; +import type { SetSidePanelContent } from "app/base/side-panel-context"; +import type { RootState } from "app/store/root/types"; +import { actions as staticRouteActions } from "app/store/staticroute"; +import staticRouteSelectors from "app/store/staticroute/selectors"; +import { actions as subnetActions } from "app/store/subnet"; +import subnetSelectors from "app/store/subnet/selectors"; +import type { Subnet, SubnetMeta } from "app/store/subnet/types"; +import { getIsDestinationForSource } from "app/store/subnet/utils"; +import { toFormikNumber } from "app/utils"; export type AddStaticRouteValues = { source: Subnet[SubnetMeta.PK]; @@ -37,22 +38,23 @@ const addStaticRouteSchema = Yup.object().shape({ }); export type Props = { - subnetId: Subnet[SubnetMeta.PK]; - handleDismiss: () => void; + id: Subnet[SubnetMeta.PK]; + setActiveForm: SetSidePanelContent; }; const AddStaticRouteForm = ({ - subnetId, - handleDismiss, + id, + setActiveForm, }: Props): JSX.Element | null => { const staticRouteErrors = useSelector(staticRouteSelectors.errors); const saving = useSelector(staticRouteSelectors.saving); const saved = useSelector(staticRouteSelectors.saved); const dispatch = useDispatch(); + const handleClose = () => setActiveForm(null); const staticRoutesLoading = useSelector(staticRouteSelectors.loading); const subnetsLoading = useSelector(subnetSelectors.loading); const loading = staticRoutesLoading || subnetsLoading; const source = useSelector((state: RootState) => - subnetSelectors.getById(state, subnetId) + subnetSelectors.getById(state, id) ); useFetchActions([subnetActions.fetch]); @@ -67,12 +69,12 @@ const AddStaticRouteForm = ({ cleanup={staticRouteActions.cleanup} errors={staticRouteErrors} initialValues={{ - source: subnetId, + source: id, gateway_ip: "", destination: "", metric: "0", }} - onCancel={handleDismiss} + onCancel={handleClose} onSaveAnalytics={{ action: AddStaticRouteFormLabels.Save, category: "Subnet", @@ -82,7 +84,7 @@ const AddStaticRouteForm = ({ dispatch(staticRouteActions.cleanup()); dispatch( staticRouteActions.create({ - source: subnetId, + source: id, gateway_ip, destination: toFormikNumber(destination) as number, metric: toFormikNumber(metric), @@ -90,7 +92,7 @@ const AddStaticRouteForm = ({ ); }} onSuccess={() => { - handleDismiss(); + handleClose(); }} resetOnSave saved={saved} @@ -99,10 +101,10 @@ const AddStaticRouteForm = ({ validationSchema={addStaticRouteSchema} > - + - + - + diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx new file mode 100644 index 0000000000..7256a57e98 --- /dev/null +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx @@ -0,0 +1,63 @@ +import configureStore from "redux-mock-store"; + +import DeleteStaticRouteForm from "./DeleteStaticRouteForm"; + +import type { RootState } from "app/store/root/types"; +import { actions as staticRouteActions } from "app/store/staticroute"; +import { + rootState as rootStateFactory, + staticRouteState as staticRouteStateFactory, + subnet as subnetFactory, + subnetState as subnetStateFactory, + authState as authStateFactory, + user as userFactory, + userState as userStateFactory, +} from "testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; + +let state: RootState; +const mockStore = configureStore(); + +const subnet = subnetFactory({ id: 1, cidr: "172.16.1.0/24" }); +const destinationSubnet = subnetFactory({ id: 2, cidr: "223.16.1.0/24" }); +state = rootStateFactory({ + user: userStateFactory({ + auth: authStateFactory({ + user: userFactory(), + }), + items: [userFactory(), userFactory(), userFactory()], + }), + staticroute: staticRouteStateFactory({ + loaded: true, + items: [], + }), + subnet: subnetStateFactory({ + loaded: true, + items: [subnet, destinationSubnet], + }), +}); + +it("renders", () => { + renderWithBrowserRouter( + , + { state } + ); + + expect(screen.getByRole("form", { name: "Confirm static route deletion" })); +}); + +it("dispatches the correct action to delete a static route", async () => { + const store = mockStore(state); + renderWithBrowserRouter( + , + { store } + ); + + await userEvent.click(screen.getByRole("button", { name: /delete/i })); + + const action = store + .getActions() + .find((action) => action.type === staticRouteActions.delete.type); + + expect(action).toStrictEqual(staticRouteActions.delete(subnet.id)); +}); diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx new file mode 100644 index 0000000000..686cc6f3e1 --- /dev/null +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx @@ -0,0 +1,33 @@ +import { useDispatch, useSelector } from "react-redux"; + +import ModelDeleteForm from "app/base/components/ModelDeleteForm"; +import type { SetSidePanelContent } from "app/base/side-panel-context"; +import { actions as staticRouteActions } from "app/store/staticroute"; +import staticRouteSelectors from "app/store/staticroute/selectors"; +import type { Subnet, SubnetMeta } from "app/store/subnet/types"; + +type Props = { + id: Subnet[SubnetMeta.PK]; + setActiveForm: SetSidePanelContent; +}; + +const DeleteStaticRouteForm = ({ id, setActiveForm }: Props) => { + const dispatch = useDispatch(); + const saved = useSelector(staticRouteSelectors.saved); + const saving = useSelector(staticRouteSelectors.saving); + return ( + setActiveForm(null)} + onSubmit={() => { + dispatch(staticRouteActions.delete(id)); + }} + saved={saved} + saving={saving} + /> + ); +}; + +export default DeleteStaticRouteForm; diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/index.ts b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/index.ts new file mode 100644 index 0000000000..2c376627a6 --- /dev/null +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteStaticRouteForm"; diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx index ceb8b9dea5..ec24e6e79c 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx @@ -36,7 +36,7 @@ it("displays loading text on load", async () => { - + @@ -79,10 +79,7 @@ it("dispatches a correct action on edit static route form submit", async () => { - + diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx index 80ebc82eeb..e8e333e3d5 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx @@ -4,21 +4,19 @@ import * as Yup from "yup"; import { Labels } from "../StaticRoutes"; -import FormikField from "@/app/base/components/FormikField"; -import FormikForm from "@/app/base/components/FormikForm"; -import SubnetSelect from "@/app/base/components/SubnetSelect"; -import { useFetchActions } from "@/app/base/hooks"; -import type { RootState } from "@/app/store/root/types"; -import { actions as staticRouteActions } from "@/app/store/staticroute"; -import staticRouteSelectors from "@/app/store/staticroute/selectors"; -import type { - StaticRoute, - StaticRouteMeta, -} from "@/app/store/staticroute/types"; -import { actions as subnetActions } from "@/app/store/subnet"; -import subnetSelectors from "@/app/store/subnet/selectors"; -import { getIsDestinationForSource } from "@/app/store/subnet/utils"; -import { toFormikNumber } from "@/app/utils"; +import FormikField from "app/base/components/FormikField"; +import FormikForm from "app/base/components/FormikForm"; +import SubnetSelect from "app/base/components/SubnetSelect"; +import { useFetchActions } from "app/base/hooks"; +import type { SetSidePanelContent } from "app/base/side-panel-context"; +import type { RootState } from "app/store/root/types"; +import { actions as staticRouteActions } from "app/store/staticroute"; +import staticRouteSelectors from "app/store/staticroute/selectors"; +import type { StaticRoute, StaticRouteMeta } from "app/store/staticroute/types"; +import { actions as subnetActions } from "app/store/subnet"; +import subnetSelectors from "app/store/subnet/selectors"; +import { getIsDestinationForSource } from "app/store/subnet/utils"; +import { toFormikNumber } from "app/utils"; export type EditStaticRouteValues = Pick< StaticRoute, @@ -38,22 +36,23 @@ const editStaticRouteSchema = Yup.object().shape({ }); export type Props = { - staticRouteId: StaticRoute[StaticRouteMeta.PK]; - handleDismiss: () => void; + id: StaticRoute[StaticRouteMeta.PK]; + setActiveForm: SetSidePanelContent; }; const EditStaticRouteForm = ({ - staticRouteId, - handleDismiss, + id, + setActiveForm, }: Props): JSX.Element | null => { const staticRouteErrors = useSelector(staticRouteSelectors.errors); const saving = useSelector(staticRouteSelectors.saving); const saved = useSelector(staticRouteSelectors.saved); const dispatch = useDispatch(); + const handleClose = () => setActiveForm(null); const staticRoutesLoading = useSelector(staticRouteSelectors.loading); const subnetsLoading = useSelector(subnetSelectors.loading); const loading = staticRoutesLoading || subnetsLoading; const staticRoute = useSelector((state: RootState) => - staticRouteSelectors.getById(state, staticRouteId) + staticRouteSelectors.getById(state, id) ); const source = useSelector((state: RootState) => subnetSelectors.getById(state, staticRoute?.source) @@ -78,7 +77,7 @@ const EditStaticRouteForm = ({ destination: staticRoute.destination, metric: staticRoute.metric, }} - onCancel={handleDismiss} + onCancel={handleClose} onSaveAnalytics={{ action: EditStaticRouteFormLabels.Save, category: "Subnet", @@ -88,7 +87,7 @@ const EditStaticRouteForm = ({ dispatch(staticRouteActions.cleanup()); dispatch( staticRouteActions.update({ - id: staticRouteId, + id: id, source: staticRoute.source, gateway_ip, destination: toFormikNumber(destination) as number, @@ -97,7 +96,7 @@ const EditStaticRouteForm = ({ ); }} onSuccess={() => { - handleDismiss(); + handleClose(); }} resetOnSave saved={saved} @@ -106,10 +105,10 @@ const EditStaticRouteForm = ({ validationSchema={editStaticRouteSchema} > - + - + - + diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx index 68b63d44f1..fe88614f62 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx @@ -4,7 +4,6 @@ import { CompatRouter } from "react-router-dom-v5-compat"; import configureStore from "redux-mock-store"; import { AddStaticRouteFormLabels } from "./AddStaticRouteForm/AddStaticRouteForm"; -import { EditStaticRouteFormLabels } from "./EditStaticRouteForm/EditStaticRouteForm"; import StaticRoutes, { Labels } from "./StaticRoutes"; import { @@ -16,8 +15,8 @@ import { authState as authStateFactory, user as userFactory, userState as userStateFactory, -} from "@/testing/factories"; -import { userEvent, render, screen, waitFor, within } from "@/testing/utils"; +} from "testing/factories"; +import { render, screen, waitFor } from "testing/utils"; const mockStore = configureStore(); @@ -72,7 +71,7 @@ it("renders for a subnet", () => { ).toBeInTheDocument(); }); -it("can open and close the add static route form", async () => { +it("has a button to open the static route form", async () => { const subnet = subnetFactory({ id: 1 }); const state = rootStateFactory({ user: userStateFactory({ @@ -106,37 +105,9 @@ it("can open and close the add static route form", async () => { }) ).toBeInTheDocument() ); - await userEvent.click( - screen.getByRole("button", { - name: AddStaticRouteFormLabels.AddStaticRoute, - }) - ); - await waitFor(() => - expect( - screen.getByRole("form", { - name: AddStaticRouteFormLabels.AddStaticRoute, - }) - ) - ); - - await userEvent.click( - within( - screen.getByRole("form", { - name: AddStaticRouteFormLabels.AddStaticRoute, - }) - ).getByRole("button", { name: "Cancel" }) - ); - - await waitFor(() => - expect( - screen.queryByRole("form", { - name: AddStaticRouteFormLabels.AddStaticRoute, - }) - ).not.toBeInTheDocument() - ); }); -it("can open and close the edit static route form", async () => { +it("has a button to open the edit static route form", async () => { const subnet = subnetFactory({ id: 1 }); const state = rootStateFactory({ staticroute: staticRouteStateFactory({ @@ -163,33 +134,9 @@ it("can open and close the edit static route form", async () => { ); - await userEvent.click( + expect( screen.getByRole("button", { name: "Edit", }) - ); - - await waitFor(() => - expect( - screen.getByRole("form", { - name: EditStaticRouteFormLabels.EditStaticRoute, - }) - ).toBeInTheDocument() - ); - - await userEvent.click( - within( - screen.getByRole("form", { - name: EditStaticRouteFormLabels.EditStaticRoute, - }) - ).getByRole("button", { name: "Cancel" }) - ); - - await waitFor(() => - expect( - screen.queryByRole("form", { - name: EditStaticRouteFormLabels.EditStaticRoute, - }) - ).not.toBeInTheDocument() - ); + ).toBeInTheDocument(); }); diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx index 3cb4d308c3..9ff1ac46a1 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx @@ -1,30 +1,22 @@ -import type { ReactNode } from "react"; -import { useState } from "react"; - import { Button, MainTable, Spinner } from "@canonical/react-components"; -import { useDispatch, useSelector } from "react-redux"; -import type { Dispatch } from "redux"; +import { useSelector } from "react-redux"; -import AddStaticRouteForm from "./AddStaticRouteForm"; -import EditStaticRouteForm from "./EditStaticRouteForm"; +import { SubnetActionTypes, SubnetDetailsSidePanelViews } from "../constants"; -import FormCard from "@/app/base/components/FormCard"; -import SubnetLink from "@/app/base/components/SubnetLink"; -import TableActions from "@/app/base/components/TableActions"; -import TableDeleteConfirm from "@/app/base/components/TableDeleteConfirm"; -import TitledSection from "@/app/base/components/TitledSection"; -import { useFetchActions } from "@/app/base/hooks"; -import authSelectors from "@/app/store/auth/selectors"; -import { actions as staticRouteActions } from "@/app/store/staticroute"; -import staticRouteSelectors from "@/app/store/staticroute/selectors"; -import type { - StaticRoute, - StaticRouteMeta, -} from "@/app/store/staticroute/types"; -import { actions as subnetActions } from "@/app/store/subnet"; -import subnetSelectors from "@/app/store/subnet/selectors"; -import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; -import { getSubnetDisplay } from "@/app/store/subnet/utils"; +import SubnetLink from "app/base/components/SubnetLink"; +import TableActions from "app/base/components/TableActions"; +import TitledSection from "app/base/components/TitledSection"; +import { useFetchActions } from "app/base/hooks"; +import type { SetSidePanelContent } from "app/base/side-panel-context"; +import { useSidePanel } from "app/base/side-panel-context"; +import authSelectors from "app/store/auth/selectors"; +import { actions as staticRouteActions } from "app/store/staticroute"; +import staticRouteSelectors from "app/store/staticroute/selectors"; +import type { StaticRoute } from "app/store/staticroute/types"; +import { actions as subnetActions } from "app/store/subnet"; +import subnetSelectors from "app/store/subnet/selectors"; +import type { Subnet, SubnetMeta } from "app/store/subnet/types"; +import { getSubnetDisplay } from "app/store/subnet/utils"; export type Props = { subnetId: Subnet[SubnetMeta.PK]; @@ -43,72 +35,16 @@ export enum ExpandedType { Update, } -type Expanded = { - id?: StaticRoute[StaticRouteMeta.PK]; - type: ExpandedType; -}; - -const toggleExpanded = ( - id: StaticRoute[StaticRouteMeta.PK], - expanded: Expanded | null, - expandedType: ExpandedType, - setExpanded: (expanded: Expanded | null) => void -) => - setExpanded( - expanded?.id === id && expanded.type === expandedType - ? null - : { - id, - type: expandedType, - } - ); - const generateRows = ( - dispatch: Dispatch, staticRoutes: StaticRoute[], subnets: Subnet[], - expanded: Expanded | null, - setExpanded: (expanded: Expanded | null) => void, - saved: boolean, - saving: boolean + setSidePanelContent: SetSidePanelContent ) => staticRoutes.map((staticRoute: StaticRoute) => { const subnet = subnets.find( (subnet) => subnet.id === staticRoute.destination ); - const isExpanded = expanded?.id === staticRoute.id; - let expandedContent: ReactNode | null = null; - const onClose = () => setExpanded(null); - if (expanded?.type === ExpandedType.Delete) { - expandedContent = ( - { - dispatch(staticRouteActions.delete(staticRoute.id)); - }} - sidebar={false} - /> - ); - } else if (expanded?.type === ExpandedType.Update) { - expandedContent = ( - - toggleExpanded( - staticRoute.id, - expanded, - ExpandedType.Update, - setExpanded - ) - } - staticRouteId={staticRoute.id} - /> - ); - } return { - className: isExpanded ? "p-table__row is-active" : null, columns: [ { "aria-label": Labels.GatewayIp, @@ -127,28 +63,24 @@ const generateRows = ( content: ( { - toggleExpanded( - staticRoute.id, - expanded, - ExpandedType.Delete, - setExpanded - ); + setSidePanelContent({ + view: SubnetDetailsSidePanelViews[ + SubnetActionTypes.DeleteStaticRoute + ], + }); }} onEdit={() => { - toggleExpanded( - staticRoute.id, - expanded, - ExpandedType.Update, - setExpanded - ); + setSidePanelContent({ + view: SubnetDetailsSidePanelViews[ + SubnetActionTypes.EditStaticRoute + ], + }); }} /> ), className: "u-align--right", }, ], - expanded: isExpanded, - expandedContent: expandedContent, key: staticRoute.id, sortData: { destination: getSubnetDisplay(subnet), @@ -159,11 +91,8 @@ const generateRows = ( }); const StaticRoutes = ({ subnetId }: Props): JSX.Element | null => { - const dispatch = useDispatch(); - const [expanded, setExpanded] = useState(null); + const { sidePanelContent, setSidePanelContent } = useSidePanel(); const staticRoutesLoading = useSelector(staticRouteSelectors.loading); - const saved = useSelector(staticRouteSelectors.saved); - const saving = useSelector(staticRouteSelectors.saving); const staticRoutes = useSelector(staticRouteSelectors.all).filter( (staticRoute) => staticRoute.source === subnetId ); @@ -171,7 +100,9 @@ const StaticRoutes = ({ subnetId }: Props): JSX.Element | null => { const subnetsLoading = useSelector(subnetSelectors.loading); const isAdmin = useSelector(authSelectors.isAdmin); const loading = staticRoutesLoading || subnetsLoading; - const isAddStaticRouteOpen = expanded?.type === ExpandedType.Create; + const isAddStaticRouteOpen = + sidePanelContent?.view === + SubnetDetailsSidePanelViews[SubnetActionTypes.AddStaticRoute]; useFetchActions([staticRouteActions.fetch, subnetActions.fetch]); @@ -182,7 +113,11 @@ const StaticRoutes = ({ subnetId }: Props): JSX.Element | null => { -
-

- Select images to be imported and kept in sync daily. Images will - be available for deploying to machines managed by MAAS. -

- - - - - )} + <> +
+

+ {Labels.SyncedFrom} {getImageSyncText(sources)} +

+ +
+

+ Select images to be imported and kept in sync daily. Images will + be available for deploying to machines managed by MAAS. +

+ + + +
diff --git a/src/app/intro/views/ImagesIntro/ImagesIntro.tsx b/src/app/intro/views/ImagesIntro/ImagesIntro.tsx index 7859f42536..60919d6553 100644 --- a/src/app/intro/views/ImagesIntro/ImagesIntro.tsx +++ b/src/app/intro/views/ImagesIntro/ImagesIntro.tsx @@ -37,7 +37,7 @@ const ImagesIntro = (): JSX.Element => { return ( - +
} selected={selected} - setExpanded={setExpanded} setSelected={setSelected} /> ); From b3654c90c6cd9a9c3a162a556993d3e7d8c8867d Mon Sep 17 00:00:00 2001 From: Jones Ogolo Date: Mon, 15 Jan 2024 10:37:14 +0100 Subject: [PATCH 7/9] test: update bulk actions missing tests driveby: add missing props --- .../BulkActions/BulkActions.test.tsx | 10 ++++------ src/app/tags/views/TagList/TagList.test.tsx | 1 + .../views/TagList/TagTable/TagTable.test.tsx | 19 +++++++++++++++++++ .../tags/views/TagUpdate/TagUpdate.test.tsx | 12 ++++++------ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx index 2dcab7a80e..6b4090bda3 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx @@ -2,8 +2,9 @@ import { vi } from "vitest"; import BulkActions from "./BulkActions"; -import { MachineSidePanelViews } from "@/app/machines/constants"; -import { DiskTypes, StorageLayout } from "@/app/store/types/enum"; +import * as sidePanelHooks from "app/base/side-panel-context"; +import { MachineSidePanelViews } from "app/machines/constants"; +import { DiskTypes, StorageLayout } from "app/store/types/enum"; import { machineDetails as machineDetailsFactory, machineState as machineStateFactory, @@ -19,12 +20,10 @@ import { renderWithBrowserRouter, screen, userEvent, -} from "@/testing/utils"; -import * as sidePanelHooks from "app/base/side-panel-context"; +} from "testing/utils"; describe("BulkActions", () => { const setSidePanelContent = vi.fn(); - beforeAll(() => { vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ setSidePanelContent, @@ -33,7 +32,6 @@ describe("BulkActions", () => { sidePanelSize: "regular", }); }); - it("disables create volume group button with tooltip if selected devices are not eligible", async () => { const selected = [ diskFactory({ diff --git a/src/app/tags/views/TagList/TagList.test.tsx b/src/app/tags/views/TagList/TagList.test.tsx index 6d71c5fff6..8aafa66226 100644 --- a/src/app/tags/views/TagList/TagList.test.tsx +++ b/src/app/tags/views/TagList/TagList.test.tsx @@ -42,6 +42,7 @@ it("renders", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tableId="test-table" diff --git a/src/app/tags/views/TagList/TagTable/TagTable.test.tsx b/src/app/tags/views/TagList/TagTable/TagTable.test.tsx index b3305edf00..b01d8646e0 100644 --- a/src/app/tags/views/TagList/TagTable/TagTable.test.tsx +++ b/src/app/tags/views/TagList/TagTable/TagTable.test.tsx @@ -60,6 +60,7 @@ it("displays tags", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -86,6 +87,7 @@ it("displays the tags in order", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -111,6 +113,7 @@ it("can change the sort order", async () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -159,6 +162,7 @@ it("displays the tags for the current page", () => { currentPage={2} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -186,6 +190,7 @@ it("shows an icon for automatic tags", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -212,6 +217,7 @@ it("does not show an icon for manual tags", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -238,6 +244,7 @@ it("shows an icon for kernel options", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -264,6 +271,7 @@ it("does not show an icon for tags without kernel options", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -298,6 +306,7 @@ it("can link to nodes", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -346,6 +355,7 @@ it("does not display a message if there are tags", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={[]} @@ -367,6 +377,7 @@ it("displays a message if there are no automatic tags", () => { currentPage={1} filter={TagSearchFilter.Auto} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={[]} @@ -390,6 +401,7 @@ it("displays a message if there are no manual tags", () => { currentPage={1} filter={TagSearchFilter.Manual} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={[]} @@ -413,6 +425,7 @@ it("displays a message if none match the search terms", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="nothing" setCurrentPage={vi.fn()} tags={[]} @@ -436,6 +449,7 @@ it("displays a message if none match the filter and search terms", () => { currentPage={1} filter={TagSearchFilter.Auto} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="nothing" setCurrentPage={vi.fn()} tags={[]} @@ -460,6 +474,7 @@ it("returns to the first page if the search changes", () => { currentPage={1} filter={TagSearchFilter.Auto} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={setCurrentPage} tags={[]} @@ -476,6 +491,7 @@ it("returns to the first page if the search changes", () => { currentPage={1} filter={TagSearchFilter.Auto} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="new" setCurrentPage={setCurrentPage} tags={[]} @@ -498,6 +514,7 @@ it("returns to the first page if the filter changes", () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={setCurrentPage} tags={[]} @@ -514,6 +531,7 @@ it("returns to the first page if the filter changes", () => { currentPage={1} filter={TagSearchFilter.Manual} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={setCurrentPage} tags={[]} @@ -541,6 +559,7 @@ it("can go to the tag edit page", async () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} + onUpdate={vi.fn()} searchText="" setCurrentPage={vi.fn()} tags={tags} diff --git a/src/app/tags/views/TagUpdate/TagUpdate.test.tsx b/src/app/tags/views/TagUpdate/TagUpdate.test.tsx index 7ea233f9d0..def85fd936 100644 --- a/src/app/tags/views/TagUpdate/TagUpdate.test.tsx +++ b/src/app/tags/views/TagUpdate/TagUpdate.test.tsx @@ -45,7 +45,7 @@ it("dispatches actions to fetch necessary data", () => { > } + component={() => } exact path={urls.tags.tag.index(null)} /> @@ -80,7 +80,7 @@ it("shows a spinner if the tag has not loaded yet", () => { > } + component={() => } exact path={urls.tags.tag.index(null)} /> @@ -98,7 +98,7 @@ it("can update the tag", async () => { - + @@ -143,7 +143,7 @@ it("can return to the previous page on save", async () => { } + component={() => } exact path={urls.tags.tag.update(null)} /> @@ -175,7 +175,7 @@ it("goes to the tag details page if it can't go back", async () => { } + component={() => } exact path={urls.tags.tag.update(null)} /> @@ -203,7 +203,7 @@ it("shows a confirmation when a tag's definition is updated", async () => { - + From dee6a8e6b3ddf074a14d0e5e9b16ba155fd9bb8c Mon Sep 17 00:00:00 2001 From: Jones Ogolo Date: Mon, 15 Jan 2024 14:10:12 +0100 Subject: [PATCH 8/9] fix: close sidepanel forms on escape --- .../PoolDeleteForm/PoolDeleteForm.tsx | 5 ++- .../pools/components/PoolForm/PoolForm.tsx | 7 ++-- src/app/pools/views/PoolAdd/PoolAdd.tsx | 11 +++++- src/app/pools/views/PoolDelete/PoolDelete.tsx | 7 ++++ src/app/pools/views/PoolEdit/PoolEdit.tsx | 23 ++++++----- .../APIKeys/APIKeyDelete/APIKeyDelete.tsx | 8 ++++ .../views/APIKeys/APIKeyForm/APIKeyForm.tsx | 6 ++- .../views/SSHKeys/AddSSHKey/AddSSHKey.tsx | 5 ++- .../views/SSLKeys/AddSSLKey/AddSSLKey.tsx | 11 +++++- .../tags/views/TagDetails/TagDetails.test.tsx | 12 ------ .../tags/views/TagUpdate/TagUpdate.test.tsx | 38 ++----------------- src/app/tags/views/Tags.test.tsx | 5 --- 12 files changed, 65 insertions(+), 73 deletions(-) diff --git a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx index 7522edd52a..1373f3ff01 100644 --- a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx +++ b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx @@ -1,3 +1,4 @@ +import { useOnEscapePressed } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; @@ -16,6 +17,8 @@ const PoolDeleteForm = ({ id }: { id: number }) => { ); const saved = useSelector(resourcePoolSelectors.saved); const saving = useSelector(resourcePoolSelectors.saving); + const onCancel = () => navigate({ pathname: urls.pools.index }); + useOnEscapePressed(() => onCancel()); useAddMessage( saved, resourcePoolActions.cleanup, @@ -27,7 +30,7 @@ const PoolDeleteForm = ({ id }: { id: number }) => { aria-label="Confirm pool deletion" initialValues={{}} modelType="resource pool" - onCancel={() => navigate({ pathname: urls.pools.index })} + onCancel={onCancel} onSubmit={() => { dispatch(resourcePoolActions.delete(id)); }} diff --git a/src/app/pools/components/PoolForm/PoolForm.tsx b/src/app/pools/components/PoolForm/PoolForm.tsx index f5c70be395..148bff2373 100644 --- a/src/app/pools/components/PoolForm/PoolForm.tsx +++ b/src/app/pools/components/PoolForm/PoolForm.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; import FormikField from "app/base/components/FormikField"; @@ -14,6 +13,7 @@ import type { ResourcePool } from "app/store/resourcepool/types"; type Props = { pool?: ResourcePool | null; + onClose?: () => void; }; type PoolFormValues = { @@ -34,9 +34,8 @@ const PoolSchema = Yup.object().shape({ description: Yup.string(), }); -export const PoolForm = ({ pool, ...props }: Props): JSX.Element => { +export const PoolForm = ({ pool, onClose, ...props }: Props): JSX.Element => { const dispatch = useDispatch(); - const navigate = useNavigate(); const saved = useSelector(poolSelectors.saved); const saving = useSelector(poolSelectors.saving); const errors = useSelector(poolSelectors.errors); @@ -71,7 +70,7 @@ export const PoolForm = ({ pool, ...props }: Props): JSX.Element => { cleanup={poolActions.cleanup} errors={errors} initialValues={initialValues} - onCancel={() => navigate({ pathname: urls.pools.index })} + onCancel={onClose} onSaveAnalytics={{ action: "Saved", category: "Resource pool", diff --git a/src/app/pools/views/PoolAdd/PoolAdd.tsx b/src/app/pools/views/PoolAdd/PoolAdd.tsx index 963f7e5e77..6ca2e5fd29 100644 --- a/src/app/pools/views/PoolAdd/PoolAdd.tsx +++ b/src/app/pools/views/PoolAdd/PoolAdd.tsx @@ -1,11 +1,18 @@ -import PoolForm from "@/app/pools/components/PoolForm"; +import { useOnEscapePressed } from "@canonical/react-components"; +import { useNavigate } from "react-router-dom-v5-compat"; + +import urls from "app/base/urls"; +import PoolForm from "app/pools/components/PoolForm"; export enum Label { Title = "Add pool form", } export const PoolAdd = (): JSX.Element => { - return ; + const navigate = useNavigate(); + const onCancel = () => navigate({ pathname: urls.pools.index }); + useOnEscapePressed(() => onCancel()); + return ; }; export default PoolAdd; diff --git a/src/app/pools/views/PoolDelete/PoolDelete.tsx b/src/app/pools/views/PoolDelete/PoolDelete.tsx index d6c20ae2f2..89fef75118 100644 --- a/src/app/pools/views/PoolDelete/PoolDelete.tsx +++ b/src/app/pools/views/PoolDelete/PoolDelete.tsx @@ -1,8 +1,15 @@ +import { useOnEscapePressed } from "@canonical/react-components"; +import { useNavigate } from "react-router-dom-v5-compat"; + import { useGetURLId } from "app/base/hooks"; +import urls from "app/base/urls"; import PoolDeleteForm from "app/pools/components/PoolDeleteForm"; const PoolDelete = () => { const id = useGetURLId("id"); + const navigate = useNavigate(); + const onCancel = () => navigate({ pathname: urls.pools.index }); + useOnEscapePressed(() => onCancel()); if (!id) { return

Resource pool not found

; diff --git a/src/app/pools/views/PoolEdit/PoolEdit.tsx b/src/app/pools/views/PoolEdit/PoolEdit.tsx index da697c868e..df21eb5f46 100644 --- a/src/app/pools/views/PoolEdit/PoolEdit.tsx +++ b/src/app/pools/views/PoolEdit/PoolEdit.tsx @@ -1,13 +1,15 @@ -import { Spinner } from "@canonical/react-components"; +import { Spinner, useOnEscapePressed } from "@canonical/react-components"; import { useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom-v5-compat"; -import ModelNotFound from "@/app/base/components/ModelNotFound"; -import { useGetURLId } from "@/app/base/hooks/urls"; -import PoolForm from "@/app/pools/components/PoolForm"; -import poolURLs from "@/app/pools/urls"; -import poolSelectors from "@/app/store/resourcepool/selectors"; -import { ResourcePoolMeta } from "@/app/store/resourcepool/types"; -import type { RootState } from "@/app/store/root/types"; +import ModelNotFound from "app/base/components/ModelNotFound"; +import { useGetURLId } from "app/base/hooks/urls"; +import urls from "app/base/urls"; +import PoolForm from "app/pools/components/PoolForm"; +import poolURLs from "app/pools/urls"; +import poolSelectors from "app/store/resourcepool/selectors"; +import { ResourcePoolMeta } from "app/store/resourcepool/types"; +import type { RootState } from "app/store/root/types"; export enum Label { Title = "Edit pool form", @@ -16,6 +18,9 @@ export enum Label { export const PoolEdit = (): JSX.Element => { const id = useGetURLId(ResourcePoolMeta.PK); const loading = useSelector(poolSelectors.loading); + const navigate = useNavigate(); + const onCancel = () => navigate({ pathname: urls.pools.index }); + useOnEscapePressed(() => onCancel()); const pool = useSelector((state: RootState) => poolSelectors.getById(state, id) ); @@ -32,7 +37,7 @@ export const PoolEdit = (): JSX.Element => { /> ); } - return ; + return ; }; export default PoolEdit; diff --git a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx index a4b0b55b45..3cefd487b7 100644 --- a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx @@ -1,8 +1,16 @@ +import { useOnEscapePressed } from "@canonical/react-components"; +import { useNavigate } from "react-router-dom-v5-compat"; + import { useGetURLId } from "app/base/hooks"; +import urls from "app/base/urls"; import APIKeyDeleteForm from "app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm"; const APIKeyDelete = () => { const id = useGetURLId("id"); + const navigate = useNavigate(); + const onCancel = () => navigate({ pathname: urls.preferences.apiKeys.index }); + useOnEscapePressed(() => onCancel()); + if (!id) { return

API Key not found

; } diff --git a/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx b/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx index 022c14b8e2..94415a572d 100644 --- a/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx @@ -1,4 +1,4 @@ -import { Col, Row } from "@canonical/react-components"; +import { Col, Row, useOnEscapePressed } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; @@ -41,6 +41,8 @@ export const APIKeyForm = ({ token }: Props): JSX.Element => { const errors = useSelector(tokenSelectors.errors); const saved = useSelector(tokenSelectors.saved); const saving = useSelector(tokenSelectors.saving); + const onCancel = () => navigate({ pathname: urls.preferences.apiKeys.index }); + useOnEscapePressed(() => onCancel()); useAddMessage( saved, @@ -57,7 +59,7 @@ export const APIKeyForm = ({ token }: Props): JSX.Element => { initialValues={{ name: token ? token.consumer.name : "", }} - onCancel={() => navigate({ pathname: urls.preferences.apiKeys.index })} + onCancel={onCancel} onSaveAnalytics={{ action: "Saved", category: "API keys preferences", diff --git a/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx b/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx index e804887f3b..a301420dd0 100644 --- a/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx +++ b/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx @@ -1,3 +1,4 @@ +import { useOnEscapePressed } from "@canonical/react-components"; import { useNavigate } from "react-router-dom-v5-compat"; import SSHKeyForm from "app/base/components/SSHKeyForm"; @@ -10,12 +11,14 @@ export enum Label { export const AddSSHKey = (): JSX.Element => { const navigate = useNavigate(); + const onCancel = () => navigate({ pathname: urls.preferences.sshKeys.index }); + useOnEscapePressed(() => onCancel()); return ( navigate({ pathname: urls.preferences.sshKeys.index })} + onCancel={onCancel} onSaveAnalytics={{ action: "Saved", category: "SSH keys preferences", diff --git a/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx b/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx index 7e6192dced..c60313b2e8 100644 --- a/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx +++ b/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx @@ -1,4 +1,9 @@ -import { Col, Row, Textarea } from "@canonical/react-components"; +import { + Col, + Row, + Textarea, + useOnEscapePressed, +} from "@canonical/react-components"; import type { TextareaProps } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; @@ -34,6 +39,8 @@ export const AddSSLKey = (): JSX.Element => { const saving = useSelector(sslkeySelectors.saving); const saved = useSelector(sslkeySelectors.saved); const errors = useSelector(sslkeySelectors.errors); + const onCancel = () => navigate({ pathname: urls.preferences.sslKeys.index }); + useOnEscapePressed(() => onCancel()); useAddMessage(saved, sslkeyActions.cleanup, "SSL key successfully added."); @@ -43,7 +50,7 @@ export const AddSSLKey = (): JSX.Element => { cleanup={sslkeyActions.cleanup} errors={errors} initialValues={{ key: "" }} - onCancel={() => navigate({ pathname: urls.preferences.sslKeys.index })} + onCancel={onCancel} onSaveAnalytics={{ action: "Saved", category: "SSL keys preferences", diff --git a/src/app/tags/views/TagDetails/TagDetails.test.tsx b/src/app/tags/views/TagDetails/TagDetails.test.tsx index 4d9b7a43d6..d7ae50566f 100644 --- a/src/app/tags/views/TagDetails/TagDetails.test.tsx +++ b/src/app/tags/views/TagDetails/TagDetails.test.tsx @@ -1,8 +1,6 @@ import { Route, Routes } from "react-router-dom-v5-compat"; import configureStore from "redux-mock-store"; -import { Label } from "../TagUpdate/TagUpdate"; - import TagDetails from "./TagDetails"; import urls from "app/base/urls"; @@ -88,13 +86,3 @@ it("shows a spinner if the tag has not loaded yet", () => { expect(screen.getByTestId("Spinner")).toBeInTheDocument(); }); - -it("can display the edit form", () => { - renderWithBrowserRouter( - - } path={urls.tags.tag.update(null)} /> - , - { route: urls.tags.tag.update({ id: 1 }), state } - ); - expect(screen.getByRole("form", { name: Label.Form })).toBeInTheDocument(); -}); diff --git a/src/app/tags/views/TagUpdate/TagUpdate.test.tsx b/src/app/tags/views/TagUpdate/TagUpdate.test.tsx index def85fd936..8c21598521 100644 --- a/src/app/tags/views/TagUpdate/TagUpdate.test.tsx +++ b/src/app/tags/views/TagUpdate/TagUpdate.test.tsx @@ -129,43 +129,11 @@ it("can update the tag", async () => { ); }); -it("can return to the previous page on save", async () => { - const history = createMemoryHistory({ - initialEntries: [{ pathname: urls.tags.index }], - }); - history.push({ - pathname: urls.tags.tag.update({ id: 1 }), - state: { canGoBack: true }, - }); - const store = mockStore(state); - render( - - - - } - exact - path={urls.tags.tag.update(null)} - /> - - - - ); - expect(history.location.pathname).toBe(urls.tags.tag.update({ id: 1 })); - await userEvent.type( - screen.getByRole("textbox", { name: Label.Name }), - "tag1" - ); - mockFormikFormSaved(); - await userEvent.click(screen.getByRole("button", { name: "Save changes" })); - await waitFor(() => expect(history.location.pathname).toBe(urls.tags.index)); -}); - it("goes to the tag details page if it can't go back", async () => { const history = createMemoryHistory({ initialEntries: [ { - pathname: urls.tags.tag.update({ id: 1 }), + pathname: urls.tags.tag.index({ id: 1 }), }, ], }); @@ -177,13 +145,13 @@ it("goes to the tag details page if it can't go back", async () => { } exact - path={urls.tags.tag.update(null)} + path={urls.tags.tag.index(null)} />
); - expect(history.location.pathname).toBe(urls.tags.tag.update({ id: 1 })); + expect(history.location.pathname).toBe(urls.tags.tag.index({ id: 1 })); await userEvent.type( screen.getByRole("textbox", { name: Label.Name }), "tag1" diff --git a/src/app/tags/views/Tags.test.tsx b/src/app/tags/views/Tags.test.tsx index c65506531f..d972aee70d 100644 --- a/src/app/tags/views/Tags.test.tsx +++ b/src/app/tags/views/Tags.test.tsx @@ -4,7 +4,6 @@ import { Label as TagsHeaderLabel } from "../components/TagsHeader/TagsHeader"; import { Label as TagDetailsLabel } from "./TagDetails/TagDetails"; import { Label as TagListLabel } from "./TagList/TagList"; -import { Label as TagUpdateLabel } from "./TagUpdate/TagUpdate"; import Tags from "./Tags"; import urls from "@/app/base/urls"; @@ -42,10 +41,6 @@ describe("Tags", () => { label: TagDetailsLabel.Title, path: urls.tags.tag.index({ id: 1 }), }, - { - label: TagUpdateLabel.Form, - path: urls.tags.tag.update({ id: 1 }), - }, { label: TagListLabel.Title, path: urls.tags.index, From c7c81de8ffa6036cf37ba20760001c6dde176f26 Mon Sep 17 00:00:00 2001 From: Jones Ogolo Date: Mon, 15 Jan 2024 17:05:09 +0100 Subject: [PATCH 9/9] fix: merge conflicts from main --- .../ModelDeleteForm/ModelDeleteForm.test.tsx | 10 +-- .../ModelDeleteForm/ModelDeleteForm.tsx | 6 +- .../NetworkActionRow/NetworkActionRow.tsx | 12 ++-- .../SSHKeyFormFields/SSHKeyFormFields.tsx | 6 +- .../BulkActions/BulkActions.test.tsx | 16 ++--- .../BulkActions/BulkActions.tsx | 14 ++-- .../CreateDatastore/CreateDatastore.tsx | 20 +++--- .../BulkActions/CreateRaid/CreateRaid.tsx | 22 +++--- .../CreateVolumeGroup/CreateVolumeGroup.tsx | 28 ++++---- .../UpdateDatastore/UpdateDatastore.tsx | 20 +++--- .../AddSpecialFilesystem.tsx | 14 ++-- .../FilesystemsTable/FilesystemsTable.tsx | 16 ++--- src/app/base/side-panel-context.tsx | 14 ++-- .../DeviceNetworkForms.test.tsx | 12 ++-- .../DeviceNetworkForms/DeviceNetworkForms.tsx | 12 ++-- src/app/devices/types.ts | 6 +- .../views/DeviceDetails/DeviceDetails.tsx | 28 ++++---- .../AddInterface/AddInterface.tsx | 6 +- .../DeviceNetwork/DeviceNetwork.tsx | 16 ++--- .../DeviceNetworkTable.test.tsx | 4 +- .../DeviceNetworkTable/DeviceNetworkTable.tsx | 42 +++++------ .../RemoveInterface/RemoveInterface.test.tsx | 4 +- .../RemoveInterface/RemoveInterface.tsx | 8 +-- .../EditInterface/EditInterface.tsx | 6 +- .../ImagesForms/ImagesForms.test.tsx | 14 ++-- .../components/ImagesForms/ImagesForms.tsx | 6 +- .../DeleteImageConfirm.test.tsx | 12 ++-- .../DeleteImageConfirm/DeleteImageConfirm.tsx | 10 +-- .../ImagesTable/ImagesTable.test.tsx | 14 ++-- .../components/ImagesTable/ImagesTable.tsx | 22 +++--- src/app/images/types.ts | 2 +- src/app/images/views/ImageList/ImageList.tsx | 18 ++--- .../SyncedImages/SyncedImages.test.tsx | 12 ++-- .../ImageList/SyncedImages/SyncedImages.tsx | 10 +-- .../components/MachineForms/MachineForms.tsx | 32 ++++----- src/app/machines/types.ts | 10 +-- .../AddBondForm/AddBondForm.tsx | 2 +- .../AddBridgeForm/AddBridgeForm.tsx | 14 ++-- .../AddInterface/AddInterface.tsx | 18 ++--- .../MachineNetworkActions.test.tsx | 10 +-- .../MachineNetworkActions.tsx | 26 +++---- .../ChangeStorageLayout.test.tsx | 6 +- .../ChangeStorageLayout.tsx | 16 ++--- .../ChangeStorageLayoutMenu.test.tsx | 6 +- .../ChangeStorageLayoutMenu.tsx | 8 +-- .../PoolDeleteForm/PoolDeleteForm.test.tsx | 2 +- .../PoolDeleteForm/PoolDeleteForm.tsx | 12 ++-- .../pools/components/PoolForm/PoolForm.tsx | 14 ++-- src/app/pools/views/PoolAdd/PoolAdd.tsx | 4 +- .../views/PoolDelete/PoolDelete.test.tsx | 8 +-- src/app/pools/views/PoolDelete/PoolDelete.tsx | 6 +- src/app/pools/views/PoolEdit/PoolEdit.tsx | 16 ++--- src/app/pools/views/PoolList/PoolList.tsx | 16 ++--- .../preferences/components/Routes/Routes.tsx | 28 ++++---- .../APIKeyDelete/APIKeyDelete.test.tsx | 6 +- .../APIKeys/APIKeyDelete/APIKeyDelete.tsx | 6 +- .../APIKeyDeleteForm/APIKeyDeleteForm.tsx | 8 +-- .../views/APIKeys/APIKeyForm/APIKeyForm.tsx | 14 ++-- .../views/APIKeys/APIKeyList/APIKeyList.tsx | 18 +++-- src/app/preferences/views/Preferences.tsx | 2 +- .../views/SSHKeys/AddSSHKey/AddSSHKey.tsx | 4 +- .../views/SSLKeys/AddSSLKey/AddSSLKey.tsx | 12 ++-- src/app/settings/components/Routes/Routes.tsx | 72 +++++++++---------- .../settings/views/Dhcp/DhcpForm/DhcpForm.tsx | 8 +-- .../LicenseKeyForm/LicenseKeyForm.tsx | 18 ++--- .../RepositoryForm/RepositoryForm.tsx | 8 +-- .../Scripts/ScriptsUpload/ScriptsUpload.tsx | 10 +-- .../Users/UserDelete/UserDelete.test.tsx | 6 +- .../views/Users/UserDelete/UserDelete.tsx | 10 +-- .../Users/UserDeleteForm/UserDeleteForm.tsx | 14 ++-- .../views/Users/UserForm/UserForm.tsx | 16 ++--- .../views/Users/UsersList/UsersList.tsx | 28 ++++---- .../ReservedRangeDeleteForm.tsx | 8 +-- .../ReservedRangeForm.test.tsx | 25 ++----- .../ReservedRangeForm/ReservedRangeForm.tsx | 20 +++--- .../ReservedRanges/ReservedRanges.test.tsx | 10 +-- .../ReservedRanges/ReservedRanges.tsx | 34 ++++----- .../AddStaticRouteForm/AddStaticRouteForm.tsx | 26 +++---- .../DeleteStaticRouteForm.test.tsx | 12 ++-- .../DeleteStaticRouteForm.tsx | 10 +-- .../EditStaticRouteForm.test.tsx | 4 +- .../EditStaticRouteForm.tsx | 29 ++++---- .../StaticRoutes/StaticRoutes.test.tsx | 4 +- .../StaticRoutes/StaticRoutes.tsx | 28 ++++---- .../SubnetActionForms/SubnetActionForms.tsx | 6 +- .../subnets/views/SubnetDetails/constants.ts | 4 +- .../TagsHeader/TagForms/TagForms.test.tsx | 2 +- .../TagsHeader/TagForms/TagForms.tsx | 6 +- .../tags/views/TagDetails/TagDetails.test.tsx | 6 +- src/app/tags/views/TagDetails/TagDetails.tsx | 20 +++--- .../views/TagList/TagTable/TagTable.test.tsx | 8 +-- .../tags/views/TagUpdate/TagUpdate.test.tsx | 10 +-- src/app/tags/views/TagUpdate/TagUpdate.tsx | 16 ++--- 93 files changed, 630 insertions(+), 634 deletions(-) diff --git a/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx index 242494b48e..b5f45b4cfb 100644 --- a/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx +++ b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.test.tsx @@ -1,13 +1,13 @@ import ModelDeleteForm from "./ModelDeleteForm"; -import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; it("renders", () => { renderWithBrowserRouter( ); @@ -18,7 +18,7 @@ it("renders", () => { }); it("can confirm", async () => { - const onSubmit = jest.fn(); + const onSubmit = vi.fn(); renderWithBrowserRouter( { }); it("can cancel", async () => { - const onCancel = jest.fn(); + const onCancel = vi.fn(); renderWithBrowserRouter( ); const cancelBtn = screen.getByRole("button", { name: /cancel/i }); diff --git a/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx index 7c69f9e431..95f5ee2948 100644 --- a/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx +++ b/src/app/base/components/ModelDeleteForm/ModelDeleteForm.tsx @@ -1,8 +1,8 @@ import { Col, Row } from "@canonical/react-components"; -import type { Props as FormikFormProps } from "app/base/components/FormikForm/FormikForm"; -import FormikForm from "app/base/components/FormikForm/FormikForm"; -import type { EmptyObject } from "app/base/types"; +import type { Props as FormikFormProps } from "@/app/base/components/FormikForm/FormikForm"; +import FormikForm from "@/app/base/components/FormikForm/FormikForm"; +import type { EmptyObject } from "@/app/base/types"; type Props = { modelType: string; diff --git a/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx b/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx index 718626b8e8..40400ff92f 100644 --- a/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx +++ b/src/app/base/components/NetworkActionRow/NetworkActionRow.tsx @@ -8,12 +8,12 @@ import { ExpandedState } from "../NodeNetworkTab/NodeNetworkTab"; import type { Selected, SetSelected, -} from "app/base/components/node/networking/types"; -import { useIsAllNetworkingDisabled } from "app/base/hooks"; -import { useSidePanel } from "app/base/side-panel-context"; -import { DeviceSidePanelViews } from "app/devices/constants"; -import { MachineSidePanelViews } from "app/machines/constants"; -import type { Node } from "app/store/types/node"; +} from "@/app/base/components/node/networking/types"; +import { useIsAllNetworkingDisabled } from "@/app/base/hooks"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { DeviceSidePanelViews } from "@/app/devices/constants"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import type { Node } from "@/app/store/types/node"; type Action = { disabled: [boolean, string?][]; diff --git a/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx b/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx index c9ce398ead..37947b9dce 100644 --- a/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx +++ b/src/app/base/components/SSHKeyForm/SSHKeyFormFields/SSHKeyFormFields.tsx @@ -4,9 +4,9 @@ import { useFormikContext } from "formik"; import type { SSHKeyFormValues } from "../types"; -import FormikField from "app/base/components/FormikField"; -import TooltipButton from "app/base/components/TooltipButton"; -import docsUrls from "app/base/docsUrls"; +import FormikField from "@/app/base/components/FormikField"; +import TooltipButton from "@/app/base/components/TooltipButton"; +import docsUrls from "@/app/base/docsUrls"; export const SSHKeyFormFields = (): JSX.Element => { const { values } = useFormikContext(); diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx index 6b4090bda3..d631ae011b 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.test.tsx @@ -2,9 +2,9 @@ import { vi } from "vitest"; import BulkActions from "./BulkActions"; -import * as sidePanelHooks from "app/base/side-panel-context"; -import { MachineSidePanelViews } from "app/machines/constants"; -import { DiskTypes, StorageLayout } from "app/store/types/enum"; +import * as sidePanelHooks from "@/app/base/side-panel-context"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import { DiskTypes, StorageLayout } from "@/app/store/types/enum"; import { machineDetails as machineDetailsFactory, machineState as machineStateFactory, @@ -20,7 +20,7 @@ import { renderWithBrowserRouter, screen, userEvent, -} from "testing/utils"; +} from "@/testing/utils"; describe("BulkActions", () => { const setSidePanelContent = vi.fn(); @@ -211,7 +211,7 @@ describe("BulkActions", () => { renderWithBrowserRouter( , { state } @@ -255,7 +255,7 @@ describe("BulkActions", () => { renderWithBrowserRouter( , { state } @@ -297,7 +297,7 @@ describe("BulkActions", () => { renderWithBrowserRouter( , { state } @@ -336,7 +336,7 @@ describe("BulkActions", () => { renderWithBrowserRouter( , { state } diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx index cf27a501ba..d239a3e694 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/BulkActions.tsx @@ -3,13 +3,13 @@ import { useSelector } from "react-redux"; import { BulkAction } from "../AvailableStorageTable"; -import { useSidePanel } from "app/base/side-panel-context"; -import { MachineSidePanelViews } from "app/machines/constants"; -import machineSelectors from "app/store/machine/selectors"; -import type { Machine } from "app/store/machine/types"; -import { isMachineDetails } from "app/store/machine/utils"; -import type { RootState } from "app/store/root/types"; -import type { Disk, Partition } from "app/store/types/node"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import machineSelectors from "@/app/store/machine/selectors"; +import type { Machine } from "@/app/store/machine/types"; +import { isMachineDetails } from "@/app/store/machine/utils"; +import type { RootState } from "@/app/store/root/types"; +import type { Disk, Partition } from "@/app/store/types/node"; import { canCreateOrUpdateDatastore, canCreateRaid, diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx index 18ed1ef0c3..12f8ec3956 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateDatastore/CreateDatastore.tsx @@ -10,16 +10,16 @@ import { import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import machineSelectors from "app/store/machine/selectors"; -import type { Machine } from "app/store/machine/types"; -import type { MachineEventErrors } from "app/store/machine/types/base"; -import { isMachineDetails } from "app/store/machine/utils"; -import type { RootState } from "app/store/root/types"; -import type { Disk, Partition } from "app/store/types/node"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import machineSelectors from "@/app/store/machine/selectors"; +import type { Machine } from "@/app/store/machine/types"; +import type { MachineEventErrors } from "@/app/store/machine/types/base"; +import { isMachineDetails } from "@/app/store/machine/utils"; +import type { RootState } from "@/app/store/root/types"; +import type { Disk, Partition } from "@/app/store/types/node"; import { formatSize, formatType, diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx index 065d0f34b5..0dc79039c3 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateRaid/CreateRaid.tsx @@ -3,17 +3,17 @@ import * as Yup from "yup"; import CreateRaidFields from "./CreateRaidFields"; -import FormikForm from "app/base/components/FormikForm"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import machineSelectors from "app/store/machine/selectors"; -import type { Machine } from "app/store/machine/types"; -import type { MachineEventErrors } from "app/store/machine/types/base"; -import { isMachineDetails } from "app/store/machine/utils"; -import type { RootState } from "app/store/root/types"; -import { DiskTypes } from "app/store/types/enum"; -import type { Disk, Partition } from "app/store/types/node"; -import { isRaid, splitDiskPartitionIds } from "app/store/utils"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import machineSelectors from "@/app/store/machine/selectors"; +import type { Machine } from "@/app/store/machine/types"; +import type { MachineEventErrors } from "@/app/store/machine/types/base"; +import { isMachineDetails } from "@/app/store/machine/utils"; +import type { RootState } from "@/app/store/root/types"; +import { DiskTypes } from "@/app/store/types/enum"; +import type { Disk, Partition } from "@/app/store/types/node"; +import { isRaid, splitDiskPartitionIds } from "@/app/store/utils"; export type CreateRaidValues = { blockDeviceIds: number[]; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx index 95241b7b5e..48e22c53c2 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/CreateVolumeGroup/CreateVolumeGroup.tsx @@ -10,18 +10,22 @@ import { import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import machineSelectors from "app/store/machine/selectors"; -import type { Machine } from "app/store/machine/types"; -import type { MachineEventErrors } from "app/store/machine/types/base"; -import { isMachineDetails } from "app/store/machine/utils"; -import type { RootState } from "app/store/root/types"; -import { DiskTypes } from "app/store/types/enum"; -import type { Disk, Partition } from "app/store/types/node"; -import { formatSize, formatType, splitDiskPartitionIds } from "app/store/utils"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import machineSelectors from "@/app/store/machine/selectors"; +import type { Machine } from "@/app/store/machine/types"; +import type { MachineEventErrors } from "@/app/store/machine/types/base"; +import { isMachineDetails } from "@/app/store/machine/utils"; +import type { RootState } from "@/app/store/root/types"; +import { DiskTypes } from "@/app/store/types/enum"; +import type { Disk, Partition } from "@/app/store/types/node"; +import { + formatSize, + formatType, + splitDiskPartitionIds, +} from "@/app/store/utils"; type CreateVolumeGroupValues = { name: string; diff --git a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx index 9e3c4513c4..22da89bc82 100644 --- a/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx +++ b/src/app/base/components/node/StorageTables/AvailableStorageTable/BulkActions/UpdateDatastore/UpdateDatastore.tsx @@ -3,16 +3,16 @@ import * as Yup from "yup"; import UpdateDatastoreFields from "./UpdateDatastoreFields"; -import FormikForm from "app/base/components/FormikForm"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import machineSelectors from "app/store/machine/selectors"; -import type { Machine } from "app/store/machine/types"; -import type { MachineEventErrors } from "app/store/machine/types/base"; -import { isMachineDetails } from "app/store/machine/utils"; -import type { RootState } from "app/store/root/types"; -import type { Disk, Partition } from "app/store/types/node"; -import { isDatastore, splitDiskPartitionIds } from "app/store/utils"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import machineSelectors from "@/app/store/machine/selectors"; +import type { Machine } from "@/app/store/machine/types"; +import type { MachineEventErrors } from "@/app/store/machine/types/base"; +import { isMachineDetails } from "@/app/store/machine/utils"; +import type { RootState } from "@/app/store/root/types"; +import type { Disk, Partition } from "@/app/store/types/node"; +import { isDatastore, splitDiskPartitionIds } from "@/app/store/utils"; export type UpdateDatastoreValues = { datastore: number; diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem/AddSpecialFilesystem.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem/AddSpecialFilesystem.tsx index c7ac431af6..9a72ad0452 100644 --- a/src/app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem/AddSpecialFilesystem.tsx +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem/AddSpecialFilesystem.tsx @@ -2,13 +2,13 @@ import { Col, Row, Select } from "@canonical/react-components"; import { useDispatch } from "react-redux"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import type { MachineDetails } from "app/store/machine/types"; -import type { MachineEventErrors } from "app/store/machine/types/base"; -import { usesStorage } from "app/store/utils"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import type { MachineDetails } from "@/app/store/machine/types"; +import type { MachineEventErrors } from "@/app/store/machine/types/base"; +import { usesStorage } from "@/app/store/utils"; const AddSpecialFilesystemSchema = Yup.object().shape({ fstype: Yup.string().required(), diff --git a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx index a553ba102d..7d5a475af4 100644 --- a/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx +++ b/src/app/base/components/node/StorageTables/FilesystemsTable/FilesystemsTable.tsx @@ -4,14 +4,14 @@ 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 { 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 TableActionsDropdown from "@/app/base/components/TableActionsDropdown"; +import ActionConfirm from "@/app/base/components/node/ActionConfirm"; +import { useSidePanel } 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 { formatSize, isMounted, diff --git a/src/app/base/side-panel-context.tsx b/src/app/base/side-panel-context.tsx index 0d1507c6c6..801a41e240 100644 --- a/src/app/base/side-panel-context.tsx +++ b/src/app/base/side-panel-context.tsx @@ -12,13 +12,13 @@ import { import { DomainListSidePanelViews, type DomainListSidePanelContent, -} from "app/domains/views/DomainsList/constants"; -import { ImageSidePanelViews } from "app/images/constants"; -import type { ImageSidePanelContent } from "app/images/types"; -import { KVMSidePanelViews } from "app/kvm/constants"; -import type { KVMSidePanelContent } from "app/kvm/types"; -import { MachineSidePanelViews } from "app/machines/constants"; -import type { MachineSidePanelContent } from "app/machines/types"; +} from "@/app/domains/views/DomainsList/constants"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import type { ImageSidePanelContent } from "@/app/images/types"; +import { KVMSidePanelViews } from "@/app/kvm/constants"; +import type { KVMSidePanelContent } from "@/app/kvm/types"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import type { MachineSidePanelContent } from "@/app/machines/types"; import { NetworkDiscoverySidePanelViews, type NetworkDiscoverySidePanelContent, diff --git a/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx index 069aaf9a44..6ce3424c08 100644 --- a/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx +++ b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.test.tsx @@ -2,15 +2,15 @@ import configureStore from "redux-mock-store"; import DeviceNetworkForms from "./DeviceNetworkForms"; -import { DeviceSidePanelViews } from "app/devices/constants"; -import type { DeviceSidePanelContent } from "app/devices/types"; -import type { RootState } from "app/store/root/types"; +import { DeviceSidePanelViews } from "@/app/devices/constants"; +import type { DeviceSidePanelContent } from "@/app/devices/types"; +import type { RootState } from "@/app/store/root/types"; import { deviceDetails as deviceDetailsFactory, deviceState as deviceStateFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen } from "@/testing/utils"; const mockStore = configureStore(); let state: RootState; @@ -30,7 +30,7 @@ it("renders a form when appropriate sidepanel view is provided", () => { }; renderWithBrowserRouter( , diff --git a/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx index 8e62ea43f2..ed329fae2c 100644 --- a/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx +++ b/src/app/devices/components/DeviceNetworkForms/DeviceNetworkForms.tsx @@ -1,11 +1,11 @@ import { useCallback } from "react"; -import type { SidePanelContentTypes } from "app/base/side-panel-context"; -import { DeviceSidePanelViews } from "app/devices/constants"; -import AddInterface from "app/devices/views/DeviceDetails/DeviceNetwork/AddInterface"; -import RemoveInterface from "app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface"; -import EditInterface from "app/devices/views/DeviceDetails/DeviceNetwork/EditInterface"; -import type { Device } from "app/store/device/types"; +import type { SidePanelContentTypes } from "@/app/base/side-panel-context"; +import { DeviceSidePanelViews } from "@/app/devices/constants"; +import AddInterface from "@/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface"; +import RemoveInterface from "@/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface"; +import EditInterface from "@/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface"; +import type { Device } from "@/app/store/device/types"; type Props = SidePanelContentTypes & { systemId: Device["system_id"]; diff --git a/src/app/devices/types.ts b/src/app/devices/types.ts index c140279a1f..ef17d02281 100644 --- a/src/app/devices/types.ts +++ b/src/app/devices/types.ts @@ -1,8 +1,8 @@ import type { ValueOf } from "@canonical/react-components"; -import type { SidePanelContent, SetSidePanelContent } from "app/base/types"; -import type { DeviceSidePanelViews } from "app/devices/constants"; -import type { NetworkInterface, NetworkLink } from "app/store/types/node"; +import type { SidePanelContent, SetSidePanelContent } from "@/app/base/types"; +import type { DeviceSidePanelViews } from "@/app/devices/constants"; +import type { NetworkInterface, NetworkLink } from "@/app/store/types/node"; export type DeviceSidePanelContent = SidePanelContent< ValueOf, diff --git a/src/app/devices/views/DeviceDetails/DeviceDetails.tsx b/src/app/devices/views/DeviceDetails/DeviceDetails.tsx index e7f7ee448c..f1bee7f986 100644 --- a/src/app/devices/views/DeviceDetails/DeviceDetails.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceDetails.tsx @@ -9,20 +9,20 @@ import DeviceDetailsHeader from "./DeviceDetailsHeader"; import DeviceNetwork from "./DeviceNetwork"; import DeviceSummary from "./DeviceSummary"; -import ModelNotFound from "app/base/components/ModelNotFound"; -import PageContent from "app/base/components/PageContent"; -import { useGetURLId } from "app/base/hooks/urls"; -import { useSidePanel } from "app/base/side-panel-context"; -import urls from "app/base/urls"; -import DeviceHeaderForms from "app/devices/components/DeviceHeaderForms"; -import DeviceNetworkForms from "app/devices/components/DeviceNetworkForms"; -import { actions as deviceActions } from "app/store/device"; -import deviceSelectors from "app/store/device/selectors"; -import { DeviceMeta } from "app/store/device/types"; -import type { RootState } from "app/store/root/types"; -import { actions as tagActions } from "app/store/tag"; -import { getSidePanelTitle } from "app/store/utils/node/base"; -import { isId, getRelativeRoute } from "app/utils"; +import ModelNotFound from "@/app/base/components/ModelNotFound"; +import PageContent from "@/app/base/components/PageContent"; +import { useGetURLId } from "@/app/base/hooks/urls"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import urls from "@/app/base/urls"; +import DeviceHeaderForms from "@/app/devices/components/DeviceHeaderForms"; +import DeviceNetworkForms from "@/app/devices/components/DeviceNetworkForms"; +import { actions as deviceActions } from "@/app/store/device"; +import deviceSelectors from "@/app/store/device/selectors"; +import { DeviceMeta } from "@/app/store/device/types"; +import type { RootState } from "@/app/store/root/types"; +import { actions as tagActions } from "@/app/store/tag"; +import { getSidePanelTitle } from "@/app/store/utils/node/base"; +import { isId, getRelativeRoute } from "@/app/utils"; const DeviceDetails = (): JSX.Element => { const { sidePanelContent, setSidePanelContent } = useSidePanel(); diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx index 527432679d..8f74eaaf76 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/AddInterface/AddInterface.tsx @@ -3,9 +3,9 @@ import { useDispatch, useSelector } from "react-redux"; import InterfaceForm from "../InterfaceForm"; -import { useCycled, useScrollOnRender } from "app/base/hooks"; -import { actions as deviceActions } from "app/store/device"; -import deviceSelectors from "app/store/device/selectors"; +import { useCycled, useScrollOnRender } from "@/app/base/hooks"; +import { actions as deviceActions } from "@/app/store/device"; +import deviceSelectors from "@/app/store/device/selectors"; import type { CreateInterfaceParams, Device, diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx index d4061537a2..df056bbdf4 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetwork.tsx @@ -3,14 +3,14 @@ import { useSelector } from "react-redux"; import DeviceNetworkTable from "./DeviceNetworkTable"; -import DHCPTable from "app/base/components/DHCPTable"; -import NetworkActionRow from "app/base/components/NetworkActionRow"; -import NodeNetworkTab from "app/base/components/NodeNetworkTab"; -import { useWindowTitle } from "app/base/hooks"; -import deviceSelectors from "app/store/device/selectors"; -import { DeviceMeta } from "app/store/device/types"; -import type { Device } from "app/store/device/types"; -import type { RootState } from "app/store/root/types"; +import DHCPTable from "@/app/base/components/DHCPTable"; +import NetworkActionRow from "@/app/base/components/NetworkActionRow"; +import NodeNetworkTab from "@/app/base/components/NodeNetworkTab"; +import { useWindowTitle } from "@/app/base/hooks"; +import deviceSelectors from "@/app/store/device/selectors"; +import { DeviceMeta } from "@/app/store/device/types"; +import type { Device } from "@/app/store/device/types"; +import type { RootState } from "@/app/store/root/types"; export enum Label { Title = "Device network", diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx index b9891dfc2f..f0b0c82d5d 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.test.tsx @@ -2,8 +2,8 @@ import configureStore from "redux-mock-store"; import DeviceNetworkTable from "./DeviceNetworkTable"; -import type { RootState } from "app/store/root/types"; -import { NetworkInterfaceTypes } from "app/store/types/enum"; +import type { RootState } from "@/app/store/root/types"; +import { NetworkInterfaceTypes } from "@/app/store/types/enum"; import { deviceDetails as deviceDetailsFactory, deviceInterface as deviceInterfaceFactory, diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx index 8fa17b05c8..5b71b15b7d 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/DeviceNetworkTable.tsx @@ -2,31 +2,31 @@ import { MainTable, Spinner } from "@canonical/react-components"; import type { MainTableRow } from "@canonical/react-components/dist/components/MainTable/MainTable"; import { useSelector } from "react-redux"; -import MacAddressDisplay from "app/base/components/MacAddressDisplay"; -import TableHeader from "app/base/components/TableHeader"; -import TableMenu from "app/base/components/TableMenu"; -import SubnetColumn from "app/base/components/node/networking/SubnetColumn"; +import MacAddressDisplay from "@/app/base/components/MacAddressDisplay"; +import TableHeader from "@/app/base/components/TableHeader"; +import TableMenu from "@/app/base/components/TableMenu"; +import SubnetColumn from "@/app/base/components/node/networking/SubnetColumn"; import { useFetchActions, useIsAllNetworkingDisabled, useTableSort, -} from "app/base/hooks"; -import type { SetSidePanelContent } from "app/base/side-panel-context"; -import { useSidePanel } from "app/base/side-panel-context"; -import { SortDirection } from "app/base/types"; -import { DeviceSidePanelViews } from "app/devices/constants"; -import deviceSelectors from "app/store/device/selectors"; -import type { Device, DeviceMeta } from "app/store/device/types"; -import { isDeviceDetails } from "app/store/device/utils"; -import { actions as fabricActions } from "app/store/fabric"; -import fabricSelectors from "app/store/fabric/selectors"; -import type { Fabric } from "app/store/fabric/types"; -import type { RootState } from "app/store/root/types"; -import { actions as subnetActions } from "app/store/subnet"; -import subnetSelectors from "app/store/subnet/selectors"; -import type { Subnet } from "app/store/subnet/types"; -import { getSubnetDisplay } from "app/store/subnet/utils"; -import type { NetworkInterface, NetworkLink } from "app/store/types/node"; +} from "@/app/base/hooks"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { SortDirection } from "@/app/base/types"; +import { DeviceSidePanelViews } from "@/app/devices/constants"; +import deviceSelectors from "@/app/store/device/selectors"; +import type { Device, DeviceMeta } from "@/app/store/device/types"; +import { isDeviceDetails } from "@/app/store/device/utils"; +import { actions as fabricActions } from "@/app/store/fabric"; +import fabricSelectors from "@/app/store/fabric/selectors"; +import type { Fabric } from "@/app/store/fabric/types"; +import type { RootState } from "@/app/store/root/types"; +import { actions as subnetActions } from "@/app/store/subnet"; +import subnetSelectors from "@/app/store/subnet/selectors"; +import type { Subnet } from "@/app/store/subnet/types"; +import { getSubnetDisplay } from "@/app/store/subnet/utils"; +import type { NetworkInterface, NetworkLink } from "@/app/store/types/node"; import { getInterfaceIPAddress, getInterfaceName, diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx index 671c6b3cbd..d6fd9ba306 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.test.tsx @@ -13,8 +13,8 @@ import { deviceStatus as deviceStatusFactory, deviceStatuses as deviceStatusesFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { userEvent, screen, renderWithBrowserRouter } from "testing/utils"; +} from "@/testing/factories"; +import { userEvent, screen, renderWithBrowserRouter } from "@/testing/utils"; const mockStore = configureStore(); diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx index 520bfe6536..f79e36e8d1 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/DeviceNetworkTable/RemoveInterface/RemoveInterface.tsx @@ -3,10 +3,10 @@ import { useEffect } from "react"; import { Notification } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; -import ModelDeleteForm from "app/base/components/ModelDeleteForm"; -import { useCycled, useSendAnalyticsWhen } from "app/base/hooks"; -import { actions as deviceActions } from "app/store/device"; -import deviceSelectors from "app/store/device/selectors"; +import ModelDeleteForm from "@/app/base/components/ModelDeleteForm"; +import { useCycled, useSendAnalyticsWhen } from "@/app/base/hooks"; +import { actions as deviceActions } from "@/app/store/device"; +import deviceSelectors from "@/app/store/device/selectors"; import type { Device, DeviceMeta, diff --git a/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx b/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx index e891ee0b58..806a5840de 100644 --- a/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx +++ b/src/app/devices/views/DeviceDetails/DeviceNetwork/EditInterface/EditInterface.tsx @@ -3,9 +3,9 @@ import { useDispatch, useSelector } from "react-redux"; import InterfaceForm from "../InterfaceForm"; -import { useCycled } from "app/base/hooks"; -import { actions as deviceActions } from "app/store/device"; -import deviceSelectors from "app/store/device/selectors"; +import { useCycled } from "@/app/base/hooks"; +import { actions as deviceActions } from "@/app/store/device"; +import deviceSelectors from "@/app/store/device/selectors"; import type { Device, DeviceMeta, diff --git a/src/app/images/components/ImagesForms/ImagesForms.test.tsx b/src/app/images/components/ImagesForms/ImagesForms.test.tsx index 0a896937ba..a70eead574 100644 --- a/src/app/images/components/ImagesForms/ImagesForms.test.tsx +++ b/src/app/images/components/ImagesForms/ImagesForms.test.tsx @@ -2,18 +2,18 @@ import configureStore from "redux-mock-store"; import ImagesForms from "./ImagesForms"; -import { ImageSidePanelViews } from "app/images/constants"; -import type { ImageSidePanelContent } from "app/images/types"; -import { ConfigNames } from "app/store/config/types"; -import type { RootState } from "app/store/root/types"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import type { ImageSidePanelContent } from "@/app/images/types"; +import { ConfigNames } from "@/app/store/config/types"; +import type { RootState } from "@/app/store/root/types"; import { bootResourceState as bootResourceStateFactory, bootResource as resourceFactory, config as configFactory, configState as configStateFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen } from "@/testing/utils"; const mockStore = configureStore(); let state: RootState; @@ -49,7 +49,7 @@ it("renders a form when appropriate sidepanel view is provided", () => { }; renderWithBrowserRouter( , { store } diff --git a/src/app/images/components/ImagesForms/ImagesForms.tsx b/src/app/images/components/ImagesForms/ImagesForms.tsx index 70d4284430..740375914f 100644 --- a/src/app/images/components/ImagesForms/ImagesForms.tsx +++ b/src/app/images/components/ImagesForms/ImagesForms.tsx @@ -2,9 +2,9 @@ import { useCallback } from "react"; import DeleteImageConfirm from "../ImagesTable/DeleteImageConfirm"; -import type { SidePanelContentTypes } from "app/base/side-panel-context"; -import { ImageSidePanelViews } from "app/images/constants"; -import ChangeSource from "app/images/views/ImageList/SyncedImages/ChangeSource"; +import type { SidePanelContentTypes } from "@/app/base/side-panel-context"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import ChangeSource from "@/app/images/views/ImageList/SyncedImages/ChangeSource"; type Props = SidePanelContentTypes & {}; diff --git a/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.test.tsx b/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.test.tsx index 1b8a2ade18..6f6e51e601 100644 --- a/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.test.tsx +++ b/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.test.tsx @@ -10,8 +10,8 @@ import { bootResource as bootResourceFactory, bootResourceState as bootResourceStateFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { userEvent, screen, renderWithBrowserRouter } from "testing/utils"; +} from "@/testing/factories"; +import { userEvent, screen, renderWithBrowserRouter } from "@/testing/utils"; const mockStore = configureStore(); @@ -44,8 +44,8 @@ describe("DeleteImageConfirm", () => { }); const store = mockStore(state); const { unmount } = renderWithBrowserRouter( - - + + , { store } ); @@ -67,8 +67,8 @@ describe("DeleteImageConfirm", () => { }); const store = mockStore(state); renderWithBrowserRouter( - - + + , { store } ); diff --git a/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.tsx b/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.tsx index ae0e15f70e..58844414fd 100644 --- a/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.tsx +++ b/src/app/images/components/ImagesTable/DeleteImageConfirm/DeleteImageConfirm.tsx @@ -3,11 +3,11 @@ import { useEffect } from "react"; import { usePrevious } from "@canonical/react-components/dist/hooks"; import { useDispatch, useSelector } from "react-redux"; -import ModelDeleteForm from "app/base/components/ModelDeleteForm"; -import { actions as bootResourceActions } from "app/store/bootresource"; -import bootResourceSelectors from "app/store/bootresource/selectors"; -import type { BootResource } from "app/store/bootresource/types"; -import { BootResourceAction } from "app/store/bootresource/types"; +import ModelDeleteForm from "@/app/base/components/ModelDeleteForm"; +import { actions as bootResourceActions } from "@/app/store/bootresource"; +import bootResourceSelectors from "@/app/store/bootresource/selectors"; +import type { BootResource } from "@/app/store/bootresource/types"; +import { BootResourceAction } from "@/app/store/bootresource/types"; type Props = { closeForm: () => void; diff --git a/src/app/images/components/ImagesTable/ImagesTable.test.tsx b/src/app/images/components/ImagesTable/ImagesTable.test.tsx index 9fdd622dd8..372e86f430 100644 --- a/src/app/images/components/ImagesTable/ImagesTable.test.tsx +++ b/src/app/images/components/ImagesTable/ImagesTable.test.tsx @@ -3,10 +3,10 @@ import timezoneMock from "timezone-mock"; import ImagesTable, { Labels as ImagesTableLabels } from "./ImagesTable"; -import * as sidePanelHooks from "app/base/side-panel-context"; -import { ImageSidePanelViews } from "app/images/constants"; -import { ConfigNames } from "app/store/config/types"; -import type { RootState } from "app/store/root/types"; +import * as sidePanelHooks from "@/app/base/side-panel-context"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import { ConfigNames } from "@/app/store/config/types"; +import type { RootState } from "@/app/store/root/types"; import { bootResource as resourceFactory, bootResourceState as bootResourceStateFactory, @@ -184,11 +184,11 @@ describe("ImagesTable", () => { it(`can open the delete image confirmation if the image does not use the default commissioning release`, async () => { - const setSidePanelContent = jest.fn(); - jest.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + const setSidePanelContent = vi.fn(); + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ setSidePanelContent, sidePanelContent: null, - setSidePanelSize: jest.fn(), + setSidePanelSize: vi.fn(), sidePanelSize: "regular", }); const resources = [ diff --git a/src/app/images/components/ImagesTable/ImagesTable.tsx b/src/app/images/components/ImagesTable/ImagesTable.tsx index 743e07c94c..ab1d39e699 100644 --- a/src/app/images/components/ImagesTable/ImagesTable.tsx +++ b/src/app/images/components/ImagesTable/ImagesTable.tsx @@ -1,17 +1,17 @@ import { Icon, MainTable, Spinner } from "@canonical/react-components"; import { useSelector } from "react-redux"; -import DoubleRow from "app/base/components/DoubleRow"; -import TableActions from "app/base/components/TableActions"; -import TooltipButton from "app/base/components/TooltipButton/TooltipButton"; -import { useSidePanel } from "app/base/side-panel-context"; -import { ImageSidePanelViews } from "app/images/constants"; -import type { ImageSetSidePanelContent, ImageValue } from "app/images/types"; -import type { BootResource } from "app/store/bootresource/types"; -import { splitResourceName } from "app/store/bootresource/utils"; -import configSelectors from "app/store/config/selectors"; -import { sizeStringToNumber } from "app/utils/formatBytes"; -import { getTimeDistanceString, parseUtcDatetime } from "app/utils/time"; +import DoubleRow from "@/app/base/components/DoubleRow"; +import TableActions from "@/app/base/components/TableActions"; +import TooltipButton from "@/app/base/components/TooltipButton/TooltipButton"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import type { ImageSetSidePanelContent, ImageValue } from "@/app/images/types"; +import type { BootResource } from "@/app/store/bootresource/types"; +import { splitResourceName } from "@/app/store/bootresource/utils"; +import configSelectors from "@/app/store/config/selectors"; +import { sizeStringToNumber } from "@/app/utils/formatBytes"; +import { getTimeDistanceString, parseUtcDatetime } from "@/app/utils/time"; type Props = { handleClear?: (image: ImageValue) => void; diff --git a/src/app/images/types.ts b/src/app/images/types.ts index b71e9e05c6..74d3f02151 100644 --- a/src/app/images/types.ts +++ b/src/app/images/types.ts @@ -2,7 +2,7 @@ import type { ValueOf } from "@canonical/react-components"; import type { ImageSidePanelViews } from "./constants"; -import type { SetSidePanelContent, SidePanelContent } from "app/base/types"; +import type { SetSidePanelContent, SidePanelContent } from "@/app/base/types"; import type { BootResource, BootResourceMeta, diff --git a/src/app/images/views/ImageList/ImageList.tsx b/src/app/images/views/ImageList/ImageList.tsx index ac9aee0c49..27702af11c 100644 --- a/src/app/images/views/ImageList/ImageList.tsx +++ b/src/app/images/views/ImageList/ImageList.tsx @@ -8,15 +8,15 @@ import GeneratedImages from "./GeneratedImages"; import ImageListHeader from "./ImageListHeader"; import SyncedImages from "./SyncedImages"; -import PageContent from "app/base/components/PageContent"; -import { useWindowTitle } from "app/base/hooks"; -import { useSidePanel } from "app/base/side-panel-context"; -import ImagesForms from "app/images/components/ImagesForms"; -import { actions as bootResourceActions } from "app/store/bootresource"; -import bootResourceSelectors from "app/store/bootresource/selectors"; -import { actions as configActions } from "app/store/config"; -import configSelectors from "app/store/config/selectors"; -import { getSidePanelTitle } from "app/store/utils/node/base"; +import PageContent from "@/app/base/components/PageContent"; +import { useWindowTitle } from "@/app/base/hooks"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import ImagesForms from "@/app/images/components/ImagesForms"; +import { actions as bootResourceActions } from "@/app/store/bootresource"; +import bootResourceSelectors from "@/app/store/bootresource/selectors"; +import { actions as configActions } from "@/app/store/config"; +import configSelectors from "@/app/store/config/selectors"; +import { getSidePanelTitle } from "@/app/store/utils/node/base"; export enum Labels { SyncDisabled = "Automatic image updates are disabled. This may mean that images won't be automatically updated and receive the latest package versions and security fixes.", diff --git a/src/app/images/views/ImageList/SyncedImages/SyncedImages.test.tsx b/src/app/images/views/ImageList/SyncedImages/SyncedImages.test.tsx index 903b914c6f..da90171aef 100644 --- a/src/app/images/views/ImageList/SyncedImages/SyncedImages.test.tsx +++ b/src/app/images/views/ImageList/SyncedImages/SyncedImages.test.tsx @@ -1,8 +1,8 @@ import SyncedImages, { Labels as SyncedImagesLabels } from "./SyncedImages"; -import * as sidePanelHooks from "app/base/side-panel-context"; -import { ImageSidePanelViews } from "app/images/constants"; -import { BootResourceSourceType } from "app/store/bootresource/types"; +import * as sidePanelHooks from "@/app/base/side-panel-context"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import { BootResourceSourceType } from "@/app/store/bootresource/types"; import { bootResource as bootResourceFactory, bootResourceState as bootResourceStateFactory, @@ -18,13 +18,13 @@ import { } from "@/testing/utils"; describe("SyncedImages", () => { - const setSidePanelContent = jest.fn(); + const setSidePanelContent = vi.fn(); beforeEach(() => { - jest.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ + vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({ setSidePanelContent, sidePanelContent: null, - setSidePanelSize: jest.fn(), + setSidePanelSize: vi.fn(), sidePanelSize: "regular", }); }); diff --git a/src/app/images/views/ImageList/SyncedImages/SyncedImages.tsx b/src/app/images/views/ImageList/SyncedImages/SyncedImages.tsx index ed28106313..3c8e63e343 100644 --- a/src/app/images/views/ImageList/SyncedImages/SyncedImages.tsx +++ b/src/app/images/views/ImageList/SyncedImages/SyncedImages.tsx @@ -15,11 +15,11 @@ import OtherImages from "./OtherImages"; import UbuntuCoreImages from "./UbuntuCoreImages"; import UbuntuImages from "./UbuntuImages"; -import { useSidePanel } from "app/base/side-panel-context"; -import { ImageSidePanelViews } from "app/images/constants"; -import bootResourceSelectors from "app/store/bootresource/selectors"; -import type { BootResourceUbuntuSource } from "app/store/bootresource/types"; -import { BootResourceSourceType } from "app/store/bootresource/types"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { ImageSidePanelViews } from "@/app/images/constants"; +import bootResourceSelectors from "@/app/store/bootresource/selectors"; +import type { BootResourceUbuntuSource } from "@/app/store/bootresource/types"; +import { BootResourceSourceType } from "@/app/store/bootresource/types"; const getImageSyncText = (sources: BootResourceUbuntuSource[]) => { if (sources.length === 1) { diff --git a/src/app/machines/components/MachineForms/MachineForms.tsx b/src/app/machines/components/MachineForms/MachineForms.tsx index a7eb9f743c..2b518b00ed 100644 --- a/src/app/machines/components/MachineForms/MachineForms.tsx +++ b/src/app/machines/components/MachineForms/MachineForms.tsx @@ -6,25 +6,25 @@ import AddChassisForm from "./AddChassis/AddChassisForm"; import AddMachineForm from "./AddMachine/AddMachineForm"; import MachineActionFormWrapper from "./MachineActionFormWrapper"; -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 AddSpecialFilesystem from "app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem"; -import type { SidePanelContentTypes } from "app/base/side-panel-context"; -import type { SetSearchFilter } from "app/base/types"; -import { MachineSidePanelViews } from "app/machines/constants"; -import type { MachineActionSidePanelViews } from "app/machines/constants"; +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 AddSpecialFilesystem from "@/app/base/components/node/StorageTables/FilesystemsTable/AddSpecialFilesystem"; +import type { SidePanelContentTypes } from "@/app/base/side-panel-context"; +import type { SetSearchFilter } from "@/app/base/types"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import type { MachineActionSidePanelViews } from "@/app/machines/constants"; import type { MachineActionVariableProps, MachineSidePanelContent, -} from "app/machines/types"; -import AddAliasOrVlan from "app/machines/views/MachineDetails/MachineNetwork/AddAliasOrVlan"; -import AddBondForm from "app/machines/views/MachineDetails/MachineNetwork/AddBondForm"; -import AddBridgeForm from "app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm"; -import AddInterface from "app/machines/views/MachineDetails/MachineNetwork/AddInterface"; -import ChangeStorageLayout from "app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout"; -import { NetworkInterfaceTypes } from "app/store/types/enum"; +} from "@/app/machines/types"; +import AddAliasOrVlan from "@/app/machines/views/MachineDetails/MachineNetwork/AddAliasOrVlan"; +import AddBondForm from "@/app/machines/views/MachineDetails/MachineNetwork/AddBondForm"; +import AddBridgeForm from "@/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm"; +import AddInterface from "@/app/machines/views/MachineDetails/MachineNetwork/AddInterface"; +import ChangeStorageLayout from "@/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout"; +import { NetworkInterfaceTypes } from "@/app/store/types/enum"; type Props = SidePanelContentTypes & { setSearchFilter?: SetSearchFilter; diff --git a/src/app/machines/types.ts b/src/app/machines/types.ts index 1fffe7eff9..ea48af2ff0 100644 --- a/src/app/machines/types.ts +++ b/src/app/machines/types.ts @@ -5,8 +5,8 @@ import type { MachineSidePanelViews } from "./constants"; import type { Selected, SetSelected, -} from "app/base/components/node/networking/types"; -import type { HardwareType } from "app/base/enum"; +} from "@/app/base/components/node/networking/types"; +import type { HardwareType } from "@/app/base/enum"; import type { CommonActionFormProps, SidePanelContent, @@ -18,9 +18,9 @@ import type { MachineEventErrors, SelectedMachines, StorageLayoutOption, -} from "app/store/machine/types"; -import type { Script } from "app/store/script/types"; -import type { Disk, NetworkInterface, Partition } from "app/store/types/node"; +} from "@/app/store/machine/types"; +import type { Script } from "@/app/store/script/types"; +import type { Disk, NetworkInterface, Partition } from "@/app/store/types/node"; export type MachineSidePanelContent = | SidePanelContent< diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx index 29b16dcf93..88703e3148 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddBondForm/AddBondForm.tsx @@ -19,7 +19,7 @@ import { networkFieldsInitialValues, } from "../NetworkFields/NetworkFields"; -import FormikForm from "app/base/components/FormikForm"; +import FormikForm from "@/app/base/components/FormikForm"; import type { Selected, SetSelected, diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx index a791ccc40e..ab8629d3b9 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddBridgeForm/AddBridgeForm.tsx @@ -13,16 +13,16 @@ import { import type { BridgeFormValues } from "./types"; -import FormikForm from "app/base/components/FormikForm"; +import FormikForm from "@/app/base/components/FormikForm"; import type { Selected, SetSelected, -} from "app/base/components/node/networking/types"; -import { useFetchActions } from "app/base/hooks"; -import { MAC_ADDRESS_REGEX } from "app/base/validation"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import machineSelectors from "app/store/machine/selectors"; +} from "@/app/base/components/node/networking/types"; +import { useFetchActions } from "@/app/base/hooks"; +import { MAC_ADDRESS_REGEX } from "@/app/base/validation"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import machineSelectors from "@/app/store/machine/selectors"; import type { CreateBridgeParams, MachineDetails, diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx index fb4579e517..861ef795e0 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/AddInterface/AddInterface.tsx @@ -11,15 +11,15 @@ import { } from "../NetworkFields/NetworkFields"; import type { NetworkValues } from "../NetworkFields/NetworkFields"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import MacAddressField from "app/base/components/MacAddressField"; -import TagNameField from "app/base/components/TagNameField"; -import { useScrollOnRender } from "app/base/hooks"; -import { MAC_ADDRESS_REGEX } from "app/base/validation"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import machineSelectors from "app/store/machine/selectors"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import MacAddressField from "@/app/base/components/MacAddressField"; +import TagNameField from "@/app/base/components/TagNameField"; +import { useScrollOnRender } from "@/app/base/hooks"; +import { MAC_ADDRESS_REGEX } from "@/app/base/validation"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import machineSelectors from "@/app/store/machine/selectors"; import type { CreatePhysicalParams, MachineDetails, diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx index 5f1511fe75..04d0a0d5f1 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.test.tsx @@ -1,10 +1,10 @@ import MachineNetworkActions from "./MachineNetworkActions"; -import * as sidePanelHooks from "app/base/side-panel-context"; -import { MachineSidePanelViews } from "app/machines/constants"; -import type { RootState } from "app/store/root/types"; -import { NetworkInterfaceTypes } from "app/store/types/enum"; -import { NodeStatus } from "app/store/types/node"; +import * as sidePanelHooks from "@/app/base/side-panel-context"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import type { RootState } from "@/app/store/root/types"; +import { NetworkInterfaceTypes } from "@/app/store/types/enum"; +import { NodeStatus } from "@/app/store/types/node"; import { machineDetails as machineDetailsFactory, machineInterface as machineInterfaceFactory, diff --git a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx index ed040eafdd..242f360e6c 100644 --- a/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx +++ b/src/app/machines/views/MachineDetails/MachineNetwork/MachineNetworkActions/MachineNetworkActions.tsx @@ -1,19 +1,19 @@ import { Button } from "@canonical/react-components"; import { useSelector } from "react-redux"; -import NetworkActionRow from "app/base/components/NetworkActionRow"; -import { NETWORK_DISABLED_MESSAGE } from "app/base/components/NetworkActionRow/NetworkActionRow"; -import type { Expanded } from "app/base/components/NodeNetworkTab/NodeNetworkTab"; -import { ExpandedState } from "app/base/components/NodeNetworkTab/NodeNetworkTab"; -import type { Selected } from "app/base/components/node/networking/types"; -import { useIsAllNetworkingDisabled, useSendAnalytics } from "app/base/hooks"; -import { MachineSidePanelViews } from "app/machines/constants"; -import type { MachineSetSidePanelContent } from "app/machines/types"; -import machineSelectors from "app/store/machine/selectors"; -import type { Machine, MachineDetails } from "app/store/machine/types"; -import { isMachineDetails } from "app/store/machine/utils"; -import type { RootState } from "app/store/root/types"; -import { NetworkInterfaceTypes } from "app/store/types/enum"; +import NetworkActionRow from "@/app/base/components/NetworkActionRow"; +import { NETWORK_DISABLED_MESSAGE } from "@/app/base/components/NetworkActionRow/NetworkActionRow"; +import type { Expanded } from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; +import { ExpandedState } from "@/app/base/components/NodeNetworkTab/NodeNetworkTab"; +import type { Selected } from "@/app/base/components/node/networking/types"; +import { useIsAllNetworkingDisabled, useSendAnalytics } from "@/app/base/hooks"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import type { MachineSetSidePanelContent } from "@/app/machines/types"; +import machineSelectors from "@/app/store/machine/selectors"; +import type { Machine, MachineDetails } from "@/app/store/machine/types"; +import { isMachineDetails } from "@/app/store/machine/utils"; +import type { RootState } from "@/app/store/root/types"; +import { NetworkInterfaceTypes } from "@/app/store/types/enum"; import { getInterfaceType, getInterfaceById, diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx index 7604749d2c..c4ee6252bd 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.test.tsx @@ -35,7 +35,7 @@ describe("ChangeStorageLayout", () => { }); renderWithBrowserRouter( , @@ -73,7 +73,7 @@ describe("ChangeStorageLayout", () => { }); renderWithBrowserRouter( , @@ -97,7 +97,7 @@ describe("ChangeStorageLayout", () => { const store = mockStore(state); renderWithBrowserRouter( , diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx index b7cd6265af..753d31f16f 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayout/ChangeStorageLayout.tsx @@ -1,14 +1,14 @@ import { Icon } from "@canonical/react-components"; import { useDispatch } from "react-redux"; -import FormikForm from "app/base/components/FormikForm"; -import type { ClearSidePanelContent, EmptyObject } from "app/base/types"; -import { useMachineDetailsForm } from "app/machines/hooks"; -import { actions as machineActions } from "app/store/machine"; -import type { Machine, StorageLayoutOption } from "app/store/machine/types"; -import type { MachineEventErrors } from "app/store/machine/types/base"; -import { StorageLayout } from "app/store/types/enum"; -import { isVMWareLayout } from "app/store/utils"; +import FormikForm from "@/app/base/components/FormikForm"; +import type { ClearSidePanelContent, EmptyObject } from "@/app/base/types"; +import { useMachineDetailsForm } from "@/app/machines/hooks"; +import { actions as machineActions } from "@/app/store/machine"; +import type { Machine, StorageLayoutOption } from "@/app/store/machine/types"; +import type { MachineEventErrors } from "@/app/store/machine/types/base"; +import { StorageLayout } from "@/app/store/types/enum"; +import { isVMWareLayout } from "@/app/store/utils"; type Props = { systemId: Machine["system_id"]; diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx index f235ea9a35..41863ff371 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.test.tsx @@ -4,15 +4,15 @@ import ChangeStorageLayoutMenu, { storageLayoutOptions, } from "./ChangeStorageLayoutMenu"; -import type { RootState } from "app/store/root/types"; +import type { RootState } from "@/app/store/root/types"; import { machineDetails as machineDetailsFactory, machineState as machineStateFactory, machineStatus as machineStatusFactory, machineStatuses as machineStatusesFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; const mockStore = configureStore(); diff --git a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx index 6693621449..0efe769025 100644 --- a/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx +++ b/src/app/machines/views/MachineDetails/MachineStorage/ChangeStorageLayoutMenu/ChangeStorageLayoutMenu.tsx @@ -1,9 +1,9 @@ import { ContextualMenu } from "@canonical/react-components"; -import { useSidePanel } from "app/base/side-panel-context"; -import { MachineSidePanelViews } from "app/machines/constants"; -import type { Machine, StorageLayoutOption } from "app/store/machine/types"; -import { StorageLayout } from "app/store/types/enum"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { MachineSidePanelViews } from "@/app/machines/constants"; +import type { Machine, StorageLayoutOption } from "@/app/store/machine/types"; +import { StorageLayout } from "@/app/store/types/enum"; // TODO: Once the API returns a list of allowed storage layouts for a given // machine we should either filter this list, or add a boolean e.g. "allowable" diff --git a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx index f42311861a..dc2a239f52 100644 --- a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx +++ b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.test.tsx @@ -1,6 +1,6 @@ import PoolDeleteForm from "./PoolDeleteForm"; -import { renderWithBrowserRouter, screen } from "testing/utils"; +import { renderWithBrowserRouter, screen } from "@/testing/utils"; it("renders", () => { renderWithBrowserRouter(); diff --git a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx index 1373f3ff01..22fd25f8a5 100644 --- a/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx +++ b/src/app/pools/components/PoolDeleteForm/PoolDeleteForm.tsx @@ -2,12 +2,12 @@ import { useOnEscapePressed } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; -import ModelDeleteForm from "app/base/components/ModelDeleteForm"; -import { useAddMessage } from "app/base/hooks"; -import urls from "app/base/urls"; -import { actions as resourcePoolActions } from "app/store/resourcepool"; -import resourcePoolSelectors from "app/store/resourcepool/selectors"; -import type { RootState } from "app/store/root/types"; +import ModelDeleteForm from "@/app/base/components/ModelDeleteForm"; +import { useAddMessage } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import { actions as resourcePoolActions } from "@/app/store/resourcepool"; +import resourcePoolSelectors from "@/app/store/resourcepool/selectors"; +import type { RootState } from "@/app/store/root/types"; const PoolDeleteForm = ({ id }: { id: number }) => { const dispatch = useDispatch(); diff --git a/src/app/pools/components/PoolForm/PoolForm.tsx b/src/app/pools/components/PoolForm/PoolForm.tsx index 148bff2373..45fdc4456b 100644 --- a/src/app/pools/components/PoolForm/PoolForm.tsx +++ b/src/app/pools/components/PoolForm/PoolForm.tsx @@ -3,13 +3,13 @@ import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import { useAddMessage } from "app/base/hooks"; -import urls from "app/base/urls"; -import { actions as poolActions } from "app/store/resourcepool"; -import poolSelectors from "app/store/resourcepool/selectors"; -import type { ResourcePool } from "app/store/resourcepool/types"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useAddMessage } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import { actions as poolActions } from "@/app/store/resourcepool"; +import poolSelectors from "@/app/store/resourcepool/selectors"; +import type { ResourcePool } from "@/app/store/resourcepool/types"; type Props = { pool?: ResourcePool | null; diff --git a/src/app/pools/views/PoolAdd/PoolAdd.tsx b/src/app/pools/views/PoolAdd/PoolAdd.tsx index 6ca2e5fd29..e3d891c075 100644 --- a/src/app/pools/views/PoolAdd/PoolAdd.tsx +++ b/src/app/pools/views/PoolAdd/PoolAdd.tsx @@ -1,8 +1,8 @@ import { useOnEscapePressed } from "@canonical/react-components"; import { useNavigate } from "react-router-dom-v5-compat"; -import urls from "app/base/urls"; -import PoolForm from "app/pools/components/PoolForm"; +import urls from "@/app/base/urls"; +import PoolForm from "@/app/pools/components/PoolForm"; export enum Label { Title = "Add pool form", diff --git a/src/app/pools/views/PoolDelete/PoolDelete.test.tsx b/src/app/pools/views/PoolDelete/PoolDelete.test.tsx index e2f8dd4b92..09ebe88850 100644 --- a/src/app/pools/views/PoolDelete/PoolDelete.test.tsx +++ b/src/app/pools/views/PoolDelete/PoolDelete.test.tsx @@ -2,14 +2,14 @@ import configureStore from "redux-mock-store"; import PoolDelete from "./PoolDelete"; -import { actions } from "app/store/resourcepool"; -import type { RootState } from "app/store/root/types"; +import { actions } from "@/app/store/resourcepool"; +import type { RootState } from "@/app/store/root/types"; import { resourcePool as resourcePoolFactory, resourcePoolState as resourcePoolStateFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; const mockStore = configureStore(); let state: RootState; diff --git a/src/app/pools/views/PoolDelete/PoolDelete.tsx b/src/app/pools/views/PoolDelete/PoolDelete.tsx index 89fef75118..cf17859fd2 100644 --- a/src/app/pools/views/PoolDelete/PoolDelete.tsx +++ b/src/app/pools/views/PoolDelete/PoolDelete.tsx @@ -1,9 +1,9 @@ import { useOnEscapePressed } from "@canonical/react-components"; import { useNavigate } from "react-router-dom-v5-compat"; -import { useGetURLId } from "app/base/hooks"; -import urls from "app/base/urls"; -import PoolDeleteForm from "app/pools/components/PoolDeleteForm"; +import { useGetURLId } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import PoolDeleteForm from "@/app/pools/components/PoolDeleteForm"; const PoolDelete = () => { const id = useGetURLId("id"); diff --git a/src/app/pools/views/PoolEdit/PoolEdit.tsx b/src/app/pools/views/PoolEdit/PoolEdit.tsx index df21eb5f46..ad4bfc229d 100644 --- a/src/app/pools/views/PoolEdit/PoolEdit.tsx +++ b/src/app/pools/views/PoolEdit/PoolEdit.tsx @@ -2,14 +2,14 @@ import { Spinner, useOnEscapePressed } from "@canonical/react-components"; import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; -import ModelNotFound from "app/base/components/ModelNotFound"; -import { useGetURLId } from "app/base/hooks/urls"; -import urls from "app/base/urls"; -import PoolForm from "app/pools/components/PoolForm"; -import poolURLs from "app/pools/urls"; -import poolSelectors from "app/store/resourcepool/selectors"; -import { ResourcePoolMeta } from "app/store/resourcepool/types"; -import type { RootState } from "app/store/root/types"; +import ModelNotFound from "@/app/base/components/ModelNotFound"; +import { useGetURLId } from "@/app/base/hooks/urls"; +import urls from "@/app/base/urls"; +import PoolForm from "@/app/pools/components/PoolForm"; +import poolURLs from "@/app/pools/urls"; +import poolSelectors from "@/app/store/resourcepool/selectors"; +import { ResourcePoolMeta } from "@/app/store/resourcepool/types"; +import type { RootState } from "@/app/store/root/types"; export enum Label { Title = "Edit pool form", diff --git a/src/app/pools/views/PoolList/PoolList.tsx b/src/app/pools/views/PoolList/PoolList.tsx index 489a3d78cf..fe7c71c913 100644 --- a/src/app/pools/views/PoolList/PoolList.tsx +++ b/src/app/pools/views/PoolList/PoolList.tsx @@ -8,14 +8,14 @@ import { import { useDispatch, useSelector } from "react-redux"; import { Link } from "react-router-dom-v5-compat"; -import TableActions from "app/base/components/TableActions"; -import { useFetchActions, useWindowTitle } from "app/base/hooks"; -import urls from "app/base/urls"; -import { FilterMachines } from "app/store/machine/utils"; -import { actions as resourcePoolActions } from "app/store/resourcepool"; -import resourcePoolSelectors from "app/store/resourcepool/selectors"; -import type { ResourcePool } from "app/store/resourcepool/types"; -import { formatErrors } from "app/utils"; +import TableActions from "@/app/base/components/TableActions"; +import { useFetchActions, useWindowTitle } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import { FilterMachines } from "@/app/store/machine/utils"; +import { actions as resourcePoolActions } from "@/app/store/resourcepool"; +import resourcePoolSelectors from "@/app/store/resourcepool/selectors"; +import type { ResourcePool } from "@/app/store/resourcepool/types"; +import { formatErrors } from "@/app/utils"; export enum Label { Title = "Pool list", diff --git a/src/app/preferences/components/Routes/Routes.tsx b/src/app/preferences/components/Routes/Routes.tsx index 4ca7dc1821..5aa4523990 100644 --- a/src/app/preferences/components/Routes/Routes.tsx +++ b/src/app/preferences/components/Routes/Routes.tsx @@ -1,20 +1,20 @@ import { Redirect } from "react-router"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom-v5-compat"; -import PageContent from "app/base/components/PageContent"; -import urls from "app/base/urls"; -import NotFound from "app/base/views/NotFound"; -import APIKeyAdd from "app/preferences/views/APIKeys/APIKeyAdd"; -import APIKeyDelete from "app/preferences/views/APIKeys/APIKeyDelete"; -import APIKeyEdit from "app/preferences/views/APIKeys/APIKeyEdit"; -import APIKeyList from "app/preferences/views/APIKeys/APIKeyList"; -import Details from "app/preferences/views/Details"; -import { Labels as PreferenceLabels } from "app/preferences/views/Preferences"; -import AddSSHKey from "app/preferences/views/SSHKeys/AddSSHKey"; -import SSHKeyList from "app/preferences/views/SSHKeys/SSHKeyList"; -import AddSSLKey from "app/preferences/views/SSLKeys/AddSSLKey"; -import SSLKeyList from "app/preferences/views/SSLKeys/SSLKeyList"; -import { getRelativeRoute } from "app/utils"; +import PageContent from "@/app/base/components/PageContent"; +import urls from "@/app/base/urls"; +import NotFound from "@/app/base/views/NotFound"; +import APIKeyAdd from "@/app/preferences/views/APIKeys/APIKeyAdd"; +import APIKeyDelete from "@/app/preferences/views/APIKeys/APIKeyDelete"; +import APIKeyEdit from "@/app/preferences/views/APIKeys/APIKeyEdit"; +import APIKeyList from "@/app/preferences/views/APIKeys/APIKeyList"; +import Details from "@/app/preferences/views/Details"; +import { Labels as PreferenceLabels } from "@/app/preferences/views/Preferences"; +import AddSSHKey from "@/app/preferences/views/SSHKeys/AddSSHKey"; +import SSHKeyList from "@/app/preferences/views/SSHKeys/SSHKeyList"; +import AddSSLKey from "@/app/preferences/views/SSLKeys/AddSSLKey"; +import SSLKeyList from "@/app/preferences/views/SSLKeys/SSLKeyList"; +import { getRelativeRoute } from "@/app/utils"; const Routes = (): JSX.Element => { const base = urls.preferences.index; diff --git a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx index 410afdecb4..c9b4642a0f 100644 --- a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.test.tsx @@ -1,12 +1,12 @@ import APIKeyDelete from "./APIKeyDelete"; -import type { RootState } from "app/store/root/types"; +import type { RootState } from "@/app/store/root/types"; import { token as tokenFactory, tokenState as tokenStateFactory, rootState as rootStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen } from "@/testing/utils"; let state: RootState; rootStateFactory({ diff --git a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx index 3cefd487b7..3d2c0f7615 100644 --- a/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyDelete/APIKeyDelete.tsx @@ -1,9 +1,9 @@ import { useOnEscapePressed } from "@canonical/react-components"; import { useNavigate } from "react-router-dom-v5-compat"; -import { useGetURLId } from "app/base/hooks"; -import urls from "app/base/urls"; -import APIKeyDeleteForm from "app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm"; +import { useGetURLId } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import APIKeyDeleteForm from "@/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm"; const APIKeyDelete = () => { const id = useGetURLId("id"); diff --git a/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx b/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx index 8e034bee93..305168877c 100644 --- a/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyDeleteForm/APIKeyDeleteForm.tsx @@ -1,10 +1,10 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; -import ModelDeleteForm from "app/base/components/ModelDeleteForm"; -import urls from "app/base/urls"; -import { actions as tokenActions } from "app/store/token"; -import tokenSelectors from "app/store/token/selectors"; +import ModelDeleteForm from "@/app/base/components/ModelDeleteForm"; +import urls from "@/app/base/urls"; +import { actions as tokenActions } from "@/app/store/token"; +import tokenSelectors from "@/app/store/token/selectors"; const APIKeyDeleteForm = ({ id }: { id: number }) => { const dispatch = useDispatch(); diff --git a/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx b/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx index 94415a572d..31e4487999 100644 --- a/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyForm/APIKeyForm.tsx @@ -3,13 +3,13 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import { useAddMessage } from "app/base/hooks"; -import urls from "app/base/urls"; -import { actions as tokenActions } from "app/store/token"; -import tokenSelectors from "app/store/token/selectors"; -import type { Token } from "app/store/token/types"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useAddMessage } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import { actions as tokenActions } from "@/app/store/token"; +import tokenSelectors from "@/app/store/token/selectors"; +import type { Token } from "@/app/store/token/types"; export enum Label { AddTitle = "Generate MAAS API key", diff --git a/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx b/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx index 82c5a58338..59d2606b87 100644 --- a/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx +++ b/src/app/preferences/views/APIKeys/APIKeyList/APIKeyList.tsx @@ -1,13 +1,17 @@ import { Notification } from "@canonical/react-components"; import { useSelector } from "react-redux"; -import TableActions from "app/base/components/TableActions"; -import { useFetchActions, useAddMessage, useWindowTitle } from "app/base/hooks"; -import urls from "app/base/urls"; -import SettingsTable from "app/settings/components/SettingsTable"; -import { actions as tokenActions } from "app/store/token"; -import tokenSelectors from "app/store/token/selectors"; -import type { Token } from "app/store/token/types"; +import TableActions from "@/app/base/components/TableActions"; +import { + useFetchActions, + useAddMessage, + useWindowTitle, +} from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import SettingsTable from "@/app/settings/components/SettingsTable"; +import { actions as tokenActions } from "@/app/store/token"; +import tokenSelectors from "@/app/store/token/selectors"; +import type { Token } from "@/app/store/token/types"; export enum Label { Title = "API keys", diff --git a/src/app/preferences/views/Preferences.tsx b/src/app/preferences/views/Preferences.tsx index 89b7d84ea4..764ccc606b 100644 --- a/src/app/preferences/views/Preferences.tsx +++ b/src/app/preferences/views/Preferences.tsx @@ -1,4 +1,4 @@ -import Routes from "app/preferences/components/Routes"; +import Routes from "@/app/preferences/components/Routes"; export enum Labels { Title = "My preferences", diff --git a/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx b/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx index a301420dd0..b5b489b3e5 100644 --- a/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx +++ b/src/app/preferences/views/SSHKeys/AddSSHKey/AddSSHKey.tsx @@ -1,8 +1,8 @@ import { useOnEscapePressed } from "@canonical/react-components"; import { useNavigate } from "react-router-dom-v5-compat"; -import SSHKeyForm from "app/base/components/SSHKeyForm"; -import urls from "app/base/urls"; +import SSHKeyForm from "@/app/base/components/SSHKeyForm"; +import urls from "@/app/base/urls"; export enum Label { Title = "Add SSH key", diff --git a/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx b/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx index c60313b2e8..40adc27d8a 100644 --- a/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx +++ b/src/app/preferences/views/SSLKeys/AddSSLKey/AddSSLKey.tsx @@ -9,12 +9,12 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import { useAddMessage } from "app/base/hooks"; -import urls from "app/base/urls"; -import { actions as sslkeyActions } from "app/store/sslkey"; -import sslkeySelectors from "app/store/sslkey/selectors"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useAddMessage } from "@/app/base/hooks"; +import urls from "@/app/base/urls"; +import { actions as sslkeyActions } from "@/app/store/sslkey"; +import sslkeySelectors from "@/app/store/sslkey/selectors"; export enum Label { Title = "Add SSL key", diff --git a/src/app/settings/components/Routes/Routes.tsx b/src/app/settings/components/Routes/Routes.tsx index 9ebb62921e..19fa409e5a 100644 --- a/src/app/settings/components/Routes/Routes.tsx +++ b/src/app/settings/components/Routes/Routes.tsx @@ -1,42 +1,42 @@ import { Redirect } from "react-router-dom"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom-v5-compat"; -import PageContent from "app/base/components/PageContent"; -import urls from "app/base/urls"; -import NotFound from "app/base/views/NotFound"; -import Commissioning from "app/settings/views/Configuration/Commissioning"; -import Deploy from "app/settings/views/Configuration/Deploy"; -import General from "app/settings/views/Configuration/General"; -import KernelParameters from "app/settings/views/Configuration/KernelParameters"; -import DhcpAdd from "app/settings/views/Dhcp/DhcpAdd"; -import DhcpEdit from "app/settings/views/Dhcp/DhcpEdit"; -import DhcpList from "app/settings/views/Dhcp/DhcpList"; -import ThirdPartyDrivers from "app/settings/views/Images/ThirdPartyDrivers"; -import VMWare from "app/settings/views/Images/VMWare"; -import Windows from "app/settings/views/Images/Windows"; -import LicenseKeyAdd from "app/settings/views/LicenseKeys/LicenseKeyAdd"; -import LicenseKeyEdit from "app/settings/views/LicenseKeys/LicenseKeyEdit"; -import LicenseKeyList from "app/settings/views/LicenseKeys/LicenseKeyList"; -import DnsForm from "app/settings/views/Network/DnsForm"; -import NetworkDiscoveryForm from "app/settings/views/Network/NetworkDiscoveryForm"; -import NtpForm from "app/settings/views/Network/NtpForm"; -import ProxyForm from "app/settings/views/Network/ProxyForm"; -import SyslogForm from "app/settings/views/Network/SyslogForm"; -import RepositoriesList from "app/settings/views/Repositories/RepositoriesList"; -import RepositoryAdd from "app/settings/views/Repositories/RepositoryAdd"; -import RepositoryEdit from "app/settings/views/Repositories/RepositoryEdit"; -import ScriptsList from "app/settings/views/Scripts/ScriptsList"; -import ScriptsUpload from "app/settings/views/Scripts/ScriptsUpload"; -import IpmiSettings from "app/settings/views/Security/IpmiSettings"; -import SecretStorage from "app/settings/views/Security/SecretStorage"; -import SecurityProtocols from "app/settings/views/Security/SecurityProtocols"; -import SessionTimeout from "app/settings/views/Security/SessionTimeout"; -import StorageForm from "app/settings/views/Storage/StorageForm"; -import UserAdd from "app/settings/views/Users/UserAdd"; -import UserDelete from "app/settings/views/Users/UserDelete"; -import UserEdit from "app/settings/views/Users/UserEdit"; -import UsersList from "app/settings/views/Users/UsersList"; -import { getRelativeRoute } from "app/utils"; +import PageContent from "@/app/base/components/PageContent"; +import urls from "@/app/base/urls"; +import NotFound from "@/app/base/views/NotFound"; +import Commissioning from "@/app/settings/views/Configuration/Commissioning"; +import Deploy from "@/app/settings/views/Configuration/Deploy"; +import General from "@/app/settings/views/Configuration/General"; +import KernelParameters from "@/app/settings/views/Configuration/KernelParameters"; +import DhcpAdd from "@/app/settings/views/Dhcp/DhcpAdd"; +import DhcpEdit from "@/app/settings/views/Dhcp/DhcpEdit"; +import DhcpList from "@/app/settings/views/Dhcp/DhcpList"; +import ThirdPartyDrivers from "@/app/settings/views/Images/ThirdPartyDrivers"; +import VMWare from "@/app/settings/views/Images/VMWare"; +import Windows from "@/app/settings/views/Images/Windows"; +import LicenseKeyAdd from "@/app/settings/views/LicenseKeys/LicenseKeyAdd"; +import LicenseKeyEdit from "@/app/settings/views/LicenseKeys/LicenseKeyEdit"; +import LicenseKeyList from "@/app/settings/views/LicenseKeys/LicenseKeyList"; +import DnsForm from "@/app/settings/views/Network/DnsForm"; +import NetworkDiscoveryForm from "@/app/settings/views/Network/NetworkDiscoveryForm"; +import NtpForm from "@/app/settings/views/Network/NtpForm"; +import ProxyForm from "@/app/settings/views/Network/ProxyForm"; +import SyslogForm from "@/app/settings/views/Network/SyslogForm"; +import RepositoriesList from "@/app/settings/views/Repositories/RepositoriesList"; +import RepositoryAdd from "@/app/settings/views/Repositories/RepositoryAdd"; +import RepositoryEdit from "@/app/settings/views/Repositories/RepositoryEdit"; +import ScriptsList from "@/app/settings/views/Scripts/ScriptsList"; +import ScriptsUpload from "@/app/settings/views/Scripts/ScriptsUpload"; +import IpmiSettings from "@/app/settings/views/Security/IpmiSettings"; +import SecretStorage from "@/app/settings/views/Security/SecretStorage"; +import SecurityProtocols from "@/app/settings/views/Security/SecurityProtocols"; +import SessionTimeout from "@/app/settings/views/Security/SessionTimeout"; +import StorageForm from "@/app/settings/views/Storage/StorageForm"; +import UserAdd from "@/app/settings/views/Users/UserAdd"; +import UserDelete from "@/app/settings/views/Users/UserDelete"; +import UserEdit from "@/app/settings/views/Users/UserEdit"; +import UsersList from "@/app/settings/views/Users/UsersList"; +import { getRelativeRoute } from "@/app/utils"; const Routes = (): JSX.Element => { const base = urls.settings.index; diff --git a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx index 17f60bab99..f2f95e602d 100644 --- a/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx +++ b/src/app/settings/views/Dhcp/DhcpForm/DhcpForm.tsx @@ -2,10 +2,10 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom-v5-compat"; -import BaseDhcpForm from "app/base/components/DhcpForm"; -import type { DHCPFormValues } from "app/base/components/DhcpForm/types"; -import settingsURLs from "app/settings/urls"; -import type { DHCPSnippet } from "app/store/dhcpsnippet/types"; +import BaseDhcpForm from "@/app/base/components/DhcpForm"; +import type { DHCPFormValues } from "@/app/base/components/DhcpForm/types"; +import settingsURLs from "@/app/settings/urls"; +import type { DHCPSnippet } from "@/app/store/dhcpsnippet/types"; type Props = { dhcpSnippet?: DHCPSnippet; diff --git a/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx b/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx index e2c9b38e22..ea0ce46cb7 100644 --- a/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx +++ b/src/app/settings/views/LicenseKeys/LicenseKeyForm/LicenseKeyForm.tsx @@ -9,15 +9,15 @@ import LicenseKeyFormFields from "../LicenseKeyFormFields"; import type { LicenseKeyFormValues } from "./types"; -import FormikForm from "app/base/components/FormikForm"; -import { useAddMessage } from "app/base/hooks"; -import settingsURLs from "app/settings/urls"; -import { actions as generalActions } from "app/store/general"; -import { osInfo as osInfoSelectors } from "app/store/general/selectors"; -import { actions as licenseKeysActions } from "app/store/licensekeys"; -import licenseKeysSelectors from "app/store/licensekeys/selectors"; -import type { LicenseKeys } from "app/store/licensekeys/types"; -import { LicenseKeysMeta } from "app/store/licensekeys/types"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useAddMessage } from "@/app/base/hooks"; +import settingsURLs from "@/app/settings/urls"; +import { actions as generalActions } from "@/app/store/general"; +import { osInfo as osInfoSelectors } from "@/app/store/general/selectors"; +import { actions as licenseKeysActions } from "@/app/store/licensekeys"; +import licenseKeysSelectors from "@/app/store/licensekeys/selectors"; +import type { LicenseKeys } from "@/app/store/licensekeys/types"; +import { LicenseKeysMeta } from "@/app/store/licensekeys/types"; type Props = { licenseKey?: LicenseKeys; diff --git a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx index fc7f6aac96..c88dc2913e 100644 --- a/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx +++ b/src/app/settings/views/Repositories/RepositoryForm/RepositoryForm.tsx @@ -9,10 +9,10 @@ import RepositoryFormFields from "../RepositoryFormFields"; import type { RepositoryFormValues } from "./types"; -import FormikForm from "app/base/components/FormikForm"; -import { useAddMessage } from "app/base/hooks"; -import settingsURLs from "app/settings/urls"; -import { actions as generalActions } from "app/store/general"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useAddMessage } from "@/app/base/hooks"; +import settingsURLs from "@/app/settings/urls"; +import { actions as generalActions } from "@/app/store/general"; import { componentsToDisable as componentsToDisableSelectors, knownArchitectures as knownArchitecturesSelectors, diff --git a/src/app/settings/views/Scripts/ScriptsUpload/ScriptsUpload.tsx b/src/app/settings/views/Scripts/ScriptsUpload/ScriptsUpload.tsx index 32c92ae394..0195320d0f 100644 --- a/src/app/settings/views/Scripts/ScriptsUpload/ScriptsUpload.tsx +++ b/src/app/settings/views/Scripts/ScriptsUpload/ScriptsUpload.tsx @@ -10,11 +10,11 @@ import { useNavigate } from "react-router-dom-v5-compat"; import type { ReadScriptResponse } from "./readScript"; import readScript from "./readScript"; -import FormikForm from "app/base/components/FormikForm"; -import { actions as messageActions } from "app/store/message"; -import { actions as scriptActions } from "app/store/script"; -import scriptSelectors from "app/store/script/selectors"; -import { ScriptType } from "app/store/script/types"; +import FormikForm from "@/app/base/components/FormikForm"; +import { actions as messageActions } from "@/app/store/message"; +import { actions as scriptActions } from "@/app/store/script"; +import scriptSelectors from "@/app/store/script/selectors"; +import { ScriptType } from "@/app/store/script/types"; type Props = { type: "commissioning" | "testing"; diff --git a/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx b/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx index cce98dddcb..be1b48b9ad 100644 --- a/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx +++ b/src/app/settings/views/Users/UserDelete/UserDelete.test.tsx @@ -1,13 +1,13 @@ import UserDelete from "./UserDelete"; -import type { RootState } from "app/store/root/types"; +import type { RootState } from "@/app/store/root/types"; import { rootState as rootStateFactory, statusState as statusStateFactory, user as userFactory, userState as userStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen } from "@/testing/utils"; let state: RootState; diff --git a/src/app/settings/views/Users/UserDelete/UserDelete.tsx b/src/app/settings/views/Users/UserDelete/UserDelete.tsx index 1e7734f229..e7471bc73f 100644 --- a/src/app/settings/views/Users/UserDelete/UserDelete.tsx +++ b/src/app/settings/views/Users/UserDelete/UserDelete.tsx @@ -1,10 +1,10 @@ import { useSelector } from "react-redux"; -import { useGetURLId } from "app/base/hooks"; -import UserDeleteForm from "app/settings/views/Users/UserDeleteForm"; -import type { RootState } from "app/store/root/types"; -import userSelectors from "app/store/user/selectors"; -import { UserMeta } from "app/store/user/types"; +import { useGetURLId } from "@/app/base/hooks"; +import UserDeleteForm from "@/app/settings/views/Users/UserDeleteForm"; +import type { RootState } from "@/app/store/root/types"; +import userSelectors from "@/app/store/user/selectors"; +import { UserMeta } from "@/app/store/user/types"; const UserDelete = () => { const id = useGetURLId(UserMeta.PK); diff --git a/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx b/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx index faa9ad89c7..59dda011a8 100644 --- a/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx +++ b/src/app/settings/views/Users/UserDeleteForm/UserDeleteForm.tsx @@ -4,13 +4,13 @@ import { Col, Row } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; -import FormikForm from "app/base/components/FormikForm"; -import { useAddMessage } from "app/base/hooks"; -import type { EmptyObject } from "app/base/types"; -import settingsURLs from "app/settings/urls"; -import { actions as userActions } from "app/store/user"; -import userSelectors from "app/store/user/selectors"; -import type { User } from "app/store/user/types"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useAddMessage } from "@/app/base/hooks"; +import type { EmptyObject } from "@/app/base/types"; +import settingsURLs from "@/app/settings/urls"; +import { actions as userActions } from "@/app/store/user"; +import userSelectors from "@/app/store/user/selectors"; +import type { User } from "@/app/store/user/types"; type UserDeleteProps = { user: User; diff --git a/src/app/settings/views/Users/UserForm/UserForm.tsx b/src/app/settings/views/Users/UserForm/UserForm.tsx index 887229fb4e..b667dcc6d2 100644 --- a/src/app/settings/views/Users/UserForm/UserForm.tsx +++ b/src/app/settings/views/Users/UserForm/UserForm.tsx @@ -3,14 +3,14 @@ import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom-v5-compat"; -import BaseUserForm from "app/base/components/UserForm"; -import type { Props as UserFormProps } from "app/base/components/UserForm/UserForm"; -import { useAddMessage } from "app/base/hooks"; -import settingsURLs from "app/settings/urls"; -import { actions as authActions } from "app/store/auth"; -import { actions as userActions } from "app/store/user"; -import userSelectors from "app/store/user/selectors"; -import type { User } from "app/store/user/types"; +import BaseUserForm from "@/app/base/components/UserForm"; +import type { Props as UserFormProps } from "@/app/base/components/UserForm/UserForm"; +import { useAddMessage } from "@/app/base/hooks"; +import settingsURLs from "@/app/settings/urls"; +import { actions as authActions } from "@/app/store/auth"; +import { actions as userActions } from "@/app/store/user"; +import userSelectors from "@/app/store/user/selectors"; +import type { User } from "@/app/store/user/types"; export enum Labels { Save = "Save user", diff --git a/src/app/settings/views/Users/UsersList/UsersList.tsx b/src/app/settings/views/Users/UsersList/UsersList.tsx index 1b3c971786..2b2b00aa37 100644 --- a/src/app/settings/views/Users/UsersList/UsersList.tsx +++ b/src/app/settings/views/Users/UsersList/UsersList.tsx @@ -5,25 +5,25 @@ import { Notification } from "@canonical/react-components"; import { format, parse } from "date-fns"; import { useDispatch, useSelector } from "react-redux"; -import TableActions from "app/base/components/TableActions"; -import TableHeader from "app/base/components/TableHeader"; +import TableActions from "@/app/base/components/TableActions"; +import TableHeader from "@/app/base/components/TableHeader"; import { useFetchActions, useAddMessage, useTableSort, useWindowTitle, -} from "app/base/hooks"; -import { SortDirection } from "app/base/types"; -import urls from "app/base/urls"; -import SettingsTable from "app/settings/components/SettingsTable"; -import settingsURLs from "app/settings/urls"; -import authSelectors from "app/store/auth/selectors"; -import type { RootState } from "app/store/root/types"; -import statusSelectors from "app/store/status/selectors"; -import { actions as userActions } from "app/store/user"; -import userSelectors from "app/store/user/selectors"; -import type { User } from "app/store/user/types"; -import { isComparable } from "app/utils"; +} from "@/app/base/hooks"; +import { SortDirection } from "@/app/base/types"; +import urls from "@/app/base/urls"; +import SettingsTable from "@/app/settings/components/SettingsTable"; +import settingsURLs from "@/app/settings/urls"; +import authSelectors from "@/app/store/auth/selectors"; +import type { RootState } from "@/app/store/root/types"; +import statusSelectors from "@/app/store/status/selectors"; +import { actions as userActions } from "@/app/store/user"; +import userSelectors from "@/app/store/user/selectors"; +import type { User } from "@/app/store/user/types"; +import { isComparable } from "@/app/utils"; type SortKey = keyof User; diff --git a/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx b/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx index 3b2588be62..ed6340dd1e 100644 --- a/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx +++ b/src/app/subnets/components/ReservedRangeDeleteForm/ReservedRangeDeleteForm.tsx @@ -1,12 +1,12 @@ import { useDispatch, useSelector } from "react-redux"; -import ModelDeleteForm from "app/base/components/ModelDeleteForm"; +import ModelDeleteForm from "@/app/base/components/ModelDeleteForm"; import { useSidePanel, type SetSidePanelContent, -} from "app/base/side-panel-context"; -import { actions as ipRangeActions } from "app/store/iprange"; -import ipRangeSelectors from "app/store/iprange/selectors"; +} from "@/app/base/side-panel-context"; +import { actions as ipRangeActions } from "@/app/store/iprange"; +import ipRangeSelectors from "@/app/store/iprange/selectors"; type Props = { setActiveForm: SetSidePanelContent; diff --git a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx index ad2c92112e..269098a942 100644 --- a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx +++ b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.test.tsx @@ -45,10 +45,7 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -80,10 +77,7 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -110,10 +104,7 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -178,10 +169,7 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + @@ -213,10 +201,7 @@ describe("ReservedRangeForm", () => { initialEntries={[{ pathname: "/machines", key: "testKey" }]} > - + diff --git a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx index a4d23f1175..e2a054a188 100644 --- a/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx +++ b/src/app/subnets/components/ReservedRangeForm/ReservedRangeForm.tsx @@ -4,19 +4,19 @@ import { Col, Row, Spinner } from "@canonical/react-components"; import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; import { useSidePanel, type SetSidePanelContent, -} from "app/base/side-panel-context"; -import { actions as ipRangeActions } from "app/store/iprange"; -import ipRangeSelectors from "app/store/iprange/selectors"; -import type { IPRange } from "app/store/iprange/types"; -import { IPRangeType, IPRangeMeta } from "app/store/iprange/types"; -import type { RootState } from "app/store/root/types"; -import type { Subnet, SubnetMeta } from "app/store/subnet/types"; -import { isId } from "app/utils"; +} from "@/app/base/side-panel-context"; +import { actions as ipRangeActions } from "@/app/store/iprange"; +import ipRangeSelectors from "@/app/store/iprange/selectors"; +import type { IPRange } from "@/app/store/iprange/types"; +import { IPRangeType, IPRangeMeta } from "@/app/store/iprange/types"; +import type { RootState } from "@/app/store/root/types"; +import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; +import { isId } from "@/app/utils"; type Props = { createType?: IPRangeType; diff --git a/src/app/subnets/components/ReservedRanges/ReservedRanges.test.tsx b/src/app/subnets/components/ReservedRanges/ReservedRanges.test.tsx index aec4444799..bcb90149fa 100644 --- a/src/app/subnets/components/ReservedRanges/ReservedRanges.test.tsx +++ b/src/app/subnets/components/ReservedRanges/ReservedRanges.test.tsx @@ -5,11 +5,11 @@ import configureStore from "redux-mock-store"; import ReservedRanges, { Labels } from "./ReservedRanges"; -import type { IPRange } from "app/store/iprange/types"; -import { IPRangeType } from "app/store/iprange/types"; -import type { RootState } from "app/store/root/types"; -import type { Subnet } from "app/store/subnet/types"; -import type { VLAN } from "app/store/vlan/types"; +import type { IPRange } from "@/app/store/iprange/types"; +import { IPRangeType } from "@/app/store/iprange/types"; +import type { RootState } from "@/app/store/root/types"; +import type { Subnet } from "@/app/store/subnet/types"; +import type { VLAN } from "@/app/store/vlan/types"; import { rootState as rootStateFactory, ipRange as ipRangeFactory, diff --git a/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx b/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx index 5f49555e4f..dd08441425 100644 --- a/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx +++ b/src/app/subnets/components/ReservedRanges/ReservedRanges.tsx @@ -11,30 +11,30 @@ import type { MainTableCell } from "@canonical/react-components/dist/components/ import classNames from "classnames"; import { useSelector } from "react-redux"; -import SubnetLink from "app/base/components/SubnetLink"; -import TableActions from "app/base/components/TableActions"; -import TitledSection from "app/base/components/TitledSection"; -import docsUrls from "app/base/docsUrls"; -import { useFetchActions } from "app/base/hooks"; -import type { SetSidePanelContent } from "app/base/side-panel-context"; -import { useSidePanel } from "app/base/side-panel-context"; -import { actions as ipRangeActions } from "app/store/iprange"; -import ipRangeSelectors from "app/store/iprange/selectors"; -import type { IPRange } from "app/store/iprange/types"; -import { IPRangeType } from "app/store/iprange/types"; +import SubnetLink from "@/app/base/components/SubnetLink"; +import TableActions from "@/app/base/components/TableActions"; +import TitledSection from "@/app/base/components/TitledSection"; +import docsUrls from "@/app/base/docsUrls"; +import { useFetchActions } from "@/app/base/hooks"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import { actions as ipRangeActions } from "@/app/store/iprange"; +import ipRangeSelectors from "@/app/store/iprange/selectors"; +import type { IPRange } from "@/app/store/iprange/types"; +import { IPRangeType } from "@/app/store/iprange/types"; import { getCommentDisplay, getOwnerDisplay, getTypeDisplay, -} from "app/store/iprange/utils"; -import type { RootState } from "app/store/root/types"; -import type { Subnet, SubnetMeta } from "app/store/subnet/types"; -import type { VLAN, VLANMeta } from "app/store/vlan/types"; +} from "@/app/store/iprange/utils"; +import type { RootState } from "@/app/store/root/types"; +import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; +import type { VLAN, VLANMeta } from "@/app/store/vlan/types"; import { SubnetActionTypes, SubnetDetailsSidePanelViews, -} from "app/subnets/views/SubnetDetails/constants"; -import { generateEmptyStateMsg, getTableStatus, isId } from "app/utils"; +} from "@/app/subnets/views/SubnetDetails/constants"; +import { generateEmptyStateMsg, getTableStatus, isId } from "@/app/utils"; export type SubnetProps = { subnetId: Subnet[SubnetMeta.PK] | null; diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx index 22cdd89be4..492225472f 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/AddStaticRouteForm/AddStaticRouteForm.tsx @@ -4,19 +4,19 @@ import * as Yup from "yup"; import { Labels } from "../StaticRoutes"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import SubnetSelect from "app/base/components/SubnetSelect"; -import { useFetchActions } from "app/base/hooks"; -import type { SetSidePanelContent } from "app/base/side-panel-context"; -import type { RootState } from "app/store/root/types"; -import { actions as staticRouteActions } from "app/store/staticroute"; -import staticRouteSelectors from "app/store/staticroute/selectors"; -import { actions as subnetActions } from "app/store/subnet"; -import subnetSelectors from "app/store/subnet/selectors"; -import type { Subnet, SubnetMeta } from "app/store/subnet/types"; -import { getIsDestinationForSource } from "app/store/subnet/utils"; -import { toFormikNumber } from "app/utils"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import SubnetSelect from "@/app/base/components/SubnetSelect"; +import { useFetchActions } from "@/app/base/hooks"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; +import type { RootState } from "@/app/store/root/types"; +import { actions as staticRouteActions } from "@/app/store/staticroute"; +import staticRouteSelectors from "@/app/store/staticroute/selectors"; +import { actions as subnetActions } from "@/app/store/subnet"; +import subnetSelectors from "@/app/store/subnet/selectors"; +import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; +import { getIsDestinationForSource } from "@/app/store/subnet/utils"; +import { toFormikNumber } from "@/app/utils"; export type AddStaticRouteValues = { source: Subnet[SubnetMeta.PK]; diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx index 7256a57e98..0d7508f76e 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.test.tsx @@ -2,8 +2,8 @@ import configureStore from "redux-mock-store"; import DeleteStaticRouteForm from "./DeleteStaticRouteForm"; -import type { RootState } from "app/store/root/types"; -import { actions as staticRouteActions } from "app/store/staticroute"; +import type { RootState } from "@/app/store/root/types"; +import { actions as staticRouteActions } from "@/app/store/staticroute"; import { rootState as rootStateFactory, staticRouteState as staticRouteStateFactory, @@ -12,8 +12,8 @@ import { authState as authStateFactory, user as userFactory, userState as userStateFactory, -} from "testing/factories"; -import { renderWithBrowserRouter, screen, userEvent } from "testing/utils"; +} from "@/testing/factories"; +import { renderWithBrowserRouter, screen, userEvent } from "@/testing/utils"; let state: RootState; const mockStore = configureStore(); @@ -39,7 +39,7 @@ state = rootStateFactory({ it("renders", () => { renderWithBrowserRouter( - , + , { state } ); @@ -49,7 +49,7 @@ it("renders", () => { it("dispatches the correct action to delete a static route", async () => { const store = mockStore(state); renderWithBrowserRouter( - , + , { store } ); diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx index 686cc6f3e1..a61d59f6f2 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/DeleteStaticRouteform/DeleteStaticRouteForm.tsx @@ -1,10 +1,10 @@ import { useDispatch, useSelector } from "react-redux"; -import ModelDeleteForm from "app/base/components/ModelDeleteForm"; -import type { SetSidePanelContent } from "app/base/side-panel-context"; -import { actions as staticRouteActions } from "app/store/staticroute"; -import staticRouteSelectors from "app/store/staticroute/selectors"; -import type { Subnet, SubnetMeta } from "app/store/subnet/types"; +import ModelDeleteForm from "@/app/base/components/ModelDeleteForm"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; +import { actions as staticRouteActions } from "@/app/store/staticroute"; +import staticRouteSelectors from "@/app/store/staticroute/selectors"; +import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; type Props = { id: Subnet[SubnetMeta.PK]; diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx index ec24e6e79c..b6454c129e 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.test.tsx @@ -36,7 +36,7 @@ it("displays loading text on load", async () => { - + @@ -79,7 +79,7 @@ it("dispatches a correct action on edit static route form submit", async () => { - + diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx index e8e333e3d5..51a3cf6b55 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/EditStaticRouteForm/EditStaticRouteForm.tsx @@ -4,19 +4,22 @@ import * as Yup from "yup"; import { Labels } from "../StaticRoutes"; -import FormikField from "app/base/components/FormikField"; -import FormikForm from "app/base/components/FormikForm"; -import SubnetSelect from "app/base/components/SubnetSelect"; -import { useFetchActions } from "app/base/hooks"; -import type { SetSidePanelContent } from "app/base/side-panel-context"; -import type { RootState } from "app/store/root/types"; -import { actions as staticRouteActions } from "app/store/staticroute"; -import staticRouteSelectors from "app/store/staticroute/selectors"; -import type { StaticRoute, StaticRouteMeta } from "app/store/staticroute/types"; -import { actions as subnetActions } from "app/store/subnet"; -import subnetSelectors from "app/store/subnet/selectors"; -import { getIsDestinationForSource } from "app/store/subnet/utils"; -import { toFormikNumber } from "app/utils"; +import FormikField from "@/app/base/components/FormikField"; +import FormikForm from "@/app/base/components/FormikForm"; +import SubnetSelect from "@/app/base/components/SubnetSelect"; +import { useFetchActions } from "@/app/base/hooks"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; +import type { RootState } from "@/app/store/root/types"; +import { actions as staticRouteActions } from "@/app/store/staticroute"; +import staticRouteSelectors from "@/app/store/staticroute/selectors"; +import type { + StaticRoute, + StaticRouteMeta, +} from "@/app/store/staticroute/types"; +import { actions as subnetActions } from "@/app/store/subnet"; +import subnetSelectors from "@/app/store/subnet/selectors"; +import { getIsDestinationForSource } from "@/app/store/subnet/utils"; +import { toFormikNumber } from "@/app/utils"; export type EditStaticRouteValues = Pick< StaticRoute, diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx index fe88614f62..3e0e3539d0 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.test.tsx @@ -15,8 +15,8 @@ import { authState as authStateFactory, user as userFactory, userState as userStateFactory, -} from "testing/factories"; -import { render, screen, waitFor } from "testing/utils"; +} from "@/testing/factories"; +import { render, screen, waitFor } from "@/testing/utils"; const mockStore = configureStore(); diff --git a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx index 9ff1ac46a1..06c1d51b92 100644 --- a/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx +++ b/src/app/subnets/views/SubnetDetails/StaticRoutes/StaticRoutes.tsx @@ -3,20 +3,20 @@ import { useSelector } from "react-redux"; import { SubnetActionTypes, SubnetDetailsSidePanelViews } from "../constants"; -import SubnetLink from "app/base/components/SubnetLink"; -import TableActions from "app/base/components/TableActions"; -import TitledSection from "app/base/components/TitledSection"; -import { useFetchActions } from "app/base/hooks"; -import type { SetSidePanelContent } from "app/base/side-panel-context"; -import { useSidePanel } from "app/base/side-panel-context"; -import authSelectors from "app/store/auth/selectors"; -import { actions as staticRouteActions } from "app/store/staticroute"; -import staticRouteSelectors from "app/store/staticroute/selectors"; -import type { StaticRoute } from "app/store/staticroute/types"; -import { actions as subnetActions } from "app/store/subnet"; -import subnetSelectors from "app/store/subnet/selectors"; -import type { Subnet, SubnetMeta } from "app/store/subnet/types"; -import { getSubnetDisplay } from "app/store/subnet/utils"; +import SubnetLink from "@/app/base/components/SubnetLink"; +import TableActions from "@/app/base/components/TableActions"; +import TitledSection from "@/app/base/components/TitledSection"; +import { useFetchActions } from "@/app/base/hooks"; +import type { SetSidePanelContent } from "@/app/base/side-panel-context"; +import { useSidePanel } from "@/app/base/side-panel-context"; +import authSelectors from "@/app/store/auth/selectors"; +import { actions as staticRouteActions } from "@/app/store/staticroute"; +import staticRouteSelectors from "@/app/store/staticroute/selectors"; +import type { StaticRoute } from "@/app/store/staticroute/types"; +import { actions as subnetActions } from "@/app/store/subnet"; +import subnetSelectors from "@/app/store/subnet/selectors"; +import type { Subnet, SubnetMeta } from "@/app/store/subnet/types"; +import { getSubnetDisplay } from "@/app/store/subnet/utils"; export type Props = { subnetId: Subnet[SubnetMeta.PK]; diff --git a/src/app/subnets/views/SubnetDetails/SubnetDetailsHeader/SubnetActionForms/SubnetActionForms.tsx b/src/app/subnets/views/SubnetDetails/SubnetDetailsHeader/SubnetActionForms/SubnetActionForms.tsx index 3605e9d32e..d4788b7c19 100644 --- a/src/app/subnets/views/SubnetDetails/SubnetDetailsHeader/SubnetActionForms/SubnetActionForms.tsx +++ b/src/app/subnets/views/SubnetDetails/SubnetDetailsHeader/SubnetActionForms/SubnetActionForms.tsx @@ -6,9 +6,9 @@ import DeleteSubnet from "./components/DeleteSubnet"; import EditBootArchitectures from "./components/EditBootArchitectures"; import MapSubnet from "./components/MapSubnet"; -import ReservedRangeDeleteForm from "app/subnets/components/ReservedRangeDeleteForm"; -import ReservedRangeForm from "app/subnets/components/ReservedRangeForm"; -import { SubnetActionTypes } from "app/subnets/views/SubnetDetails/constants"; +import ReservedRangeDeleteForm from "@/app/subnets/components/ReservedRangeDeleteForm"; +import ReservedRangeForm from "@/app/subnets/components/ReservedRangeForm"; +import { SubnetActionTypes } from "@/app/subnets/views/SubnetDetails/constants"; import type { SubnetAction, SubnetActionProps, diff --git a/src/app/subnets/views/SubnetDetails/constants.ts b/src/app/subnets/views/SubnetDetails/constants.ts index 776b424eec..8511d6270f 100644 --- a/src/app/subnets/views/SubnetDetails/constants.ts +++ b/src/app/subnets/views/SubnetDetails/constants.ts @@ -1,7 +1,7 @@ import type { ValueOf } from "@canonical/react-components"; -import type { SidePanelContent } from "app/base/types"; -import type { IPRangeType } from "app/store/iprange/types"; +import type { SidePanelContent } from "@/app/base/types"; +import type { IPRangeType } from "@/app/store/iprange/types"; export const SubnetActionTypes = { MapSubnet: "MapSubnet", diff --git a/src/app/tags/components/TagsHeader/TagForms/TagForms.test.tsx b/src/app/tags/components/TagsHeader/TagForms/TagForms.test.tsx index 505716c30c..7bce41c2da 100644 --- a/src/app/tags/components/TagsHeader/TagForms/TagForms.test.tsx +++ b/src/app/tags/components/TagsHeader/TagForms/TagForms.test.tsx @@ -68,7 +68,7 @@ it("can display the update tag form", () => { }); renderWithBrowserRouter( { expect(setCurrentPage).toHaveBeenCalledWith(1); }); -it("can go to the tag edit page", async () => { +it("can trigger the tag edit sidepanel", async () => { const path = urls.tags.tag.machines({ id: 1 }); const history = createMemoryHistory({ initialEntries: [{ pathname: path }], }); const store = mockStore(state); + const onUpdate = vi.fn(); render( @@ -559,7 +560,7 @@ it("can go to the tag edit page", async () => { currentPage={1} filter={TagSearchFilter.All} onDelete={vi.fn()} - onUpdate={vi.fn()} + onUpdate={onUpdate} searchText="" setCurrentPage={vi.fn()} tags={tags} @@ -573,6 +574,5 @@ it("can go to the tag edit page", async () => { ); await userEvent.click(screen.getAllByRole("button", { name: "Edit" })[0]); - expect(history.location.pathname).toBe(urls.tags.tag.update({ id: 2 })); - expect(history.location.state).toStrictEqual({ canGoBack: true }); + expect(onUpdate).toHaveBeenCalled(); }); diff --git a/src/app/tags/views/TagUpdate/TagUpdate.test.tsx b/src/app/tags/views/TagUpdate/TagUpdate.test.tsx index 8c21598521..062a2d502f 100644 --- a/src/app/tags/views/TagUpdate/TagUpdate.test.tsx +++ b/src/app/tags/views/TagUpdate/TagUpdate.test.tsx @@ -45,7 +45,7 @@ it("dispatches actions to fetch necessary data", () => { > } + component={() => } exact path={urls.tags.tag.index(null)} /> @@ -80,7 +80,7 @@ it("shows a spinner if the tag has not loaded yet", () => { > } + component={() => } exact path={urls.tags.tag.index(null)} /> @@ -98,7 +98,7 @@ it("can update the tag", async () => { - + @@ -143,7 +143,7 @@ it("goes to the tag details page if it can't go back", async () => { } + component={() => } exact path={urls.tags.tag.index(null)} /> @@ -171,7 +171,7 @@ it("shows a confirmation when a tag's definition is updated", async () => { - + diff --git a/src/app/tags/views/TagUpdate/TagUpdate.tsx b/src/app/tags/views/TagUpdate/TagUpdate.tsx index fea958397d..b6996c00ee 100644 --- a/src/app/tags/views/TagUpdate/TagUpdate.tsx +++ b/src/app/tags/views/TagUpdate/TagUpdate.tsx @@ -4,14 +4,14 @@ import * as Yup from "yup"; import TagUpdateFormFields from "./TagUpdateFormFields"; -import FormikForm from "app/base/components/FormikForm"; -import { useFetchActions } from "app/base/hooks"; -import { actions as messageActions } from "app/store/message"; -import type { RootState } from "app/store/root/types"; -import { actions as tagActions } from "app/store/tag"; -import tagSelectors from "app/store/tag/selectors"; -import type { Tag, UpdateParams, TagMeta } from "app/store/tag/types"; -import { NewDefinitionMessage } from "app/tags/constants"; +import FormikForm from "@/app/base/components/FormikForm"; +import { useFetchActions } from "@/app/base/hooks"; +import { actions as messageActions } from "@/app/store/message"; +import type { RootState } from "@/app/store/root/types"; +import { actions as tagActions } from "@/app/store/tag"; +import tagSelectors from "@/app/store/tag/selectors"; +import type { Tag, UpdateParams, TagMeta } from "@/app/store/tag/types"; +import { NewDefinitionMessage } from "@/app/tags/constants"; type Props = { id: Tag[TagMeta.PK];