Skip to content

Commit

Permalink
feat(zones): Add delete zones as a table action MAASENG-4308 (#5590)
Browse files Browse the repository at this point in the history
  • Loading branch information
abuyukyi101198 authored Jan 24, 2025
1 parent b12b50b commit 1480b3d
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 227 deletions.
1 change: 1 addition & 0 deletions src/app/store/utils/node/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ const sidePanelTitleMap: Record<string, string> = {
[SidePanelViews.DOWNLOAD_IMAGE[1]]: "Download image",
[SidePanelViews.EDIT_INTERFACE[1]]: "Edit interface",
[SidePanelViews.CREATE_ZONE[1]]: "Add AZ",
[SidePanelViews.DELETE_ZONE[1]]: "Delete AZ",
[SidePanelViews.EDIT_DISK[1]]: "Edit disk",
[SidePanelViews.EDIT_PARTITION[1]]: "Edit partition",
[SidePanelViews.EDIT_PHYSICAL[1]]: "Edit physical",
Expand Down
4 changes: 3 additions & 1 deletion src/app/zones/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import type { SidePanelContent } from "@/app/base/types";

export const ZoneActionSidePanelViews = {
CREATE_ZONE: ["zoneForm", "createZone"],
DELETE_ZONE: ["zoneForm", "deleteZone"],
} as const;

export type ZoneSidePanelContent = SidePanelContent<
ValueOf<typeof ZoneActionSidePanelViews>
ValueOf<typeof ZoneActionSidePanelViews>,
{ zoneId: number }
>;

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -57,44 +57,4 @@ describe("ZoneDetailsHeader", () => {
);
expect(await findByText("Availability zone not found")).toBeInTheDocument();
});

it("shows delete az button when zone id isn't 1", async () => {
renderWithBrowserRouter(<ZoneDetailsHeader id={2} />, {
state,
queryData,
route: "/zone/2",
});

expect(
await screen.findByRole("button", { name: "Delete AZ" })
).toBeInTheDocument();
});

it("hides delete button when zone id is 1 (as this is the default)", () => {
renderWithBrowserRouter(<ZoneDetailsHeader id={1} />, {
state,
queryData,
route: "/zone/1",
});

expect(screen.queryByTestId("delete-zone")).not.toBeInTheDocument();
});

it("hides delete button for all zones when user isn't admin", () => {
const nonAdminState = factory.rootState({
user: factory.userState({
auth: factory.authState({
user: factory.user({ is_superuser: false }),
}),
}),
});

renderWithBrowserRouter(<ZoneDetailsHeader id={2} />, {
state: nonAdminState,
queryData,
route: "/zone/2",
});

expect(screen.queryByTestId("delete-zone")).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import { useEffect, useState } from "react";

import { Button } from "@canonical/react-components";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";

import DeleteConfirm from "./DeleteConfirm";
import React from "react";

import { useZoneById } from "@/app/api/query/zones";
import SectionHeader from "@/app/base/components/SectionHeader";
import urls from "@/app/base/urls";
import authSelectors from "@/app/store/auth/selectors";
import type { RootState } from "@/app/store/root/types";
import { zoneActions } from "@/app/store/zone";
import { ZONE_ACTIONS } from "@/app/store/zone/constants";
import zoneSelectors from "@/app/store/zone/selectors";

type Props = {
id: number;
};

const ZoneDetailsHeader = ({ id }: Props): JSX.Element => {
const [showConfirm, setShowConfirm] = useState(false);
const deleteStatus = useSelector((state: RootState) =>
zoneSelectors.getModelActionStatus(state, ZONE_ACTIONS.delete, id)
);
const ZoneDetailsHeader: React.FC<Props> = ({ id }) => {
const zone = useZoneById(id);
const dispatch = useDispatch();
const navigate = useNavigate();

let title = "";

if (!zone.isPending) {
Expand All @@ -35,73 +18,13 @@ const ZoneDetailsHeader = ({ id }: Props): JSX.Element => {
: "Availability zone not found";
}

useEffect(() => {
if (deleteStatus === "success") {
dispatch(zoneActions.cleanup([ZONE_ACTIONS.delete]));
navigate({ pathname: urls.zones.index });
}
}, [dispatch, deleteStatus, navigate]);

const isAdmin = useSelector(authSelectors.isAdmin);
const isDefaultZone = id === 1;

const deleteZone = () => {
if (isAdmin && !isDefaultZone) {
dispatch(zoneActions.delete({ id }));
}
};

const closeExpanded = () => setShowConfirm(false);

let buttons: JSX.Element[] | null = [
<Button
data-testid="delete-zone"
key="delete-zone"
onClick={() => setShowConfirm(true)}
>
Delete AZ
</Button>,
];

if (showConfirm || isDefaultZone || !isAdmin) {
buttons = null;
}

let confirmDelete = null;

if (showConfirm && isAdmin && !isDefaultZone) {
confirmDelete = (
<>
<hr />
<DeleteConfirm
closeExpanded={closeExpanded}
confirmLabel="Delete AZ"
deleting={deleteStatus === "loading"}
message="Are you sure you want to delete this AZ?"
onConfirm={deleteZone}
/>
</>
);
}

if (!zone.isPending && zone.data) {
title = `Availability zone: ${zone.data.name}`;
} else if (zone.isFetched) {
title = "Availability zone not found";
buttons = null;
}

return (
<>
<SectionHeader
buttons={buttons}
loading={!zone.isFetched}
title={title}
/>

{confirmDelete}
</>
);
return <SectionHeader loading={!zone.isFetched} title={title} />;
};

export default ZoneDetailsHeader;
24 changes: 22 additions & 2 deletions src/app/zones/views/ZonesList/ZonesList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from "react";

import ZonesListForm from "./ZonesListForm";
import ZonesListHeader from "./ZonesListHeader";
import ZonesListTable from "./ZonesListTable";
Expand All @@ -6,9 +8,11 @@ import { useZoneCount } from "@/app/api/query/zones";
import PageContent from "@/app/base/components/PageContent";
import { useWindowTitle } from "@/app/base/hooks";
import { useSidePanel } from "@/app/base/side-panel-context";
import { getSidePanelTitle } from "@/app/store/utils/node/base";
import { ZoneActionSidePanelViews } from "@/app/zones/constants";
import DeleteZone from "@/app/zones/views/ZonesList/ZonesListTable/DeleteZone";

const ZonesList = (): JSX.Element => {
const ZonesList: React.FC = () => {
const zonesCount = useZoneCount();
const { sidePanelContent, setSidePanelContent } = useSidePanel();

Expand All @@ -28,13 +32,29 @@ const ZonesList = (): JSX.Element => {
key="add-zone-form"
/>
);
} else if (
sidePanelContent &&
sidePanelContent.view === ZoneActionSidePanelViews.DELETE_ZONE
) {
const zoneId =
sidePanelContent.extras && "zoneId" in sidePanelContent.extras
? sidePanelContent.extras.zoneId
: null;
content = zoneId ? (
<DeleteZone
closeForm={() => {
setSidePanelContent(null);
}}
id={zoneId as number}
/>
) : null;
}

return (
<PageContent
header={<ZonesListHeader setSidePanelContent={setSidePanelContent} />}
sidePanelContent={content}
sidePanelTitle="Add AZ"
sidePanelTitle={getSidePanelTitle("Zones", sidePanelContent)}
>
{zonesCount?.data && zonesCount.data > 0 && <ZonesListTable />}
</PageContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Formik } from "formik";

import DeleteZone from "./DeleteZone";

import { userEvent, screen, renderWithBrowserRouter } from "@/testing/utils";

describe("DeleteZone", () => {
it("calls closeForm on cancel click", async () => {
const closeForm = vi.fn();
renderWithBrowserRouter(
<Formik initialValues={{ images: [] }} onSubmit={vi.fn()}>
<DeleteZone closeForm={closeForm} id={2} />
</Formik>
);

await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(closeForm).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";

import { useDispatch, useSelector } from "react-redux";

import ModelActionForm from "@/app/base/components/ModelActionForm";
import type { RootState } from "@/app/store/root/types";
import { zoneActions } from "@/app/store/zone";
import { ZONE_ACTIONS } from "@/app/store/zone/constants";
import zoneSelectors from "@/app/store/zone/selectors";

type DeleteZoneProps = {
closeForm: () => void;
id: number;
};

const DeleteZone: React.FC<DeleteZoneProps> = ({ closeForm, id }) => {
const dispatch = useDispatch();
const deleteStatus = useSelector((state: RootState) =>
zoneSelectors.getModelActionStatus(state, ZONE_ACTIONS.delete, id)
);

return (
<ModelActionForm
aria-label="Confirm AZ deletion"
initialValues={{}}
message="Are you sure you want to delete this AZ?"
modelType="zone"
onCancel={closeForm}
onSubmit={() => {
dispatch(zoneActions.delete({ id }));
}}
onSuccess={() => {
dispatch(zoneActions.cleanup([ZONE_ACTIONS.delete]));
closeForm();
}}
saved={deleteStatus === "success"}
saving={deleteStatus === "loading"}
/>
);
};

export default DeleteZone;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./DeleteZone";
Loading

0 comments on commit 1480b3d

Please sign in to comment.