Skip to content

Commit

Permalink
WD-11665 - refactor: remove nested components in CharmActionsPanel (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimir-cucu authored Jul 4, 2024
1 parent b296b57 commit 194fa66
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe("ActionPayloadModal", () => {
} = renderComponent(
<ActionPayloadModal payload={null} onClose={vi.fn()} />,
);
expect(container.tagName).toBe("DIV");
expect(container.children.length).toBe(1);
expect(container.firstChild).toBeEmptyDOMElement();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { renderComponent } from "testing/utils";

import CharmActionsPanel from "./CharmActionsPanel";
import { Label } from "./types";
import { ConfirmationDialogLabel } from "./ConfirmationDialog";

vi.mock("juju/api-hooks/actions", () => {
return {
Expand Down Expand Up @@ -271,16 +271,20 @@ describe("CharmActionsPanel", () => {
await screen.findByRole("button", { name: "Run action" }),
);
await userEvent.click(
await screen.findByRole("button", { name: Label.CONFIRM_BUTTON }),
await screen.findByRole("button", {
name: ConfirmationDialogLabel.CONFIRM_BUTTON,
}),
);
const call = executeActionOnUnitsSpy.mock.calls[0];
expect(call[0]).toEqual(["ceph-0", "ceph-1"]);
expect(call[1]).toBe("pause");
expect(call[2]).toEqual({}); // no options
expect(await screen.findByText(Label.ACTION_SUCCESS)).toBeInTheDocument();
expect(
await screen.findByText(ConfirmationDialogLabel.ACTION_SUCCESS),
).toBeInTheDocument();
});

it("submits the action request to the api with options that are required", async () => {
it("should pass the selected action form values to the API call", async () => {
const executeActionOnUnitsSpy = vi
.fn()
.mockImplementation(() => Promise.resolve());
Expand Down Expand Up @@ -308,7 +312,9 @@ describe("CharmActionsPanel", () => {
await screen.findByRole("button", { name: "Run action" }),
);
await userEvent.click(
await screen.findByRole("button", { name: Label.CONFIRM_BUTTON }),
await screen.findByRole("button", {
name: ConfirmationDialogLabel.CONFIRM_BUTTON,
}),
);
const call = executeActionOnUnitsSpy.mock.calls[0];
expect(call[0]).toEqual(["ceph-0", "ceph-1"]);
Expand All @@ -319,40 +325,6 @@ describe("CharmActionsPanel", () => {
});
});

it("handles API errors", async () => {
const executeActionOnUnitsSpy = vi
.fn()
.mockImplementation(() => Promise.reject(new Error()));
vi.spyOn(actionsHooks, "useExecuteActionOnUnits").mockImplementation(
() => executeActionOnUnitsSpy,
);
renderComponent(
<CharmActionsPanel
charmURL={charmURL}
onRemovePanelQueryParams={vi.fn()}
/>,
{ path, url, state },
);
expect(
await screen.findByRole("button", { name: "Run action" }),
).toBeDisabled();
await userEvent.click(await screen.findByRole("radio", { name: "pause" }));
expect(
await screen.findByRole("button", { name: "Run action" }),
).not.toBeDisabled();
await userEvent.click(
await screen.findByRole("button", { name: "Run action" }),
);
await userEvent.click(
await screen.findByRole("button", { name: Label.CONFIRM_BUTTON }),
);
const call = executeActionOnUnitsSpy.mock.calls[0];
expect(call[0]).toEqual(["ceph-0", "ceph-1"]);
expect(call[1]).toBe("pause");
expect(call[2]).toEqual({}); // no options
expect(await screen.findByText(Label.ACTION_ERROR)).toBeInTheDocument();
});

it("should cancel the run selected action confirmation modal", async () => {
renderComponent(
<CharmActionsPanel
Expand All @@ -375,7 +347,9 @@ describe("CharmActionsPanel", () => {
screen.queryByRole("dialog", { name: "Run pause?" }),
).toBeInTheDocument();
await userEvent.click(
await screen.findByRole("button", { name: Label.CANCEL_BUTTON }),
await screen.findByRole("button", {
name: ConfirmationDialogLabel.CANCEL_BUTTON,
}),
);
expect(
screen.queryByRole("dialog", { name: "Run pause?" }),
Expand Down Expand Up @@ -409,9 +383,13 @@ describe("CharmActionsPanel", () => {
await screen.findByRole("button", { name: "Run action" }),
);
await userEvent.click(
screen.getByRole("button", { name: Label.CONFIRM_BUTTON }),
screen.getByRole("button", {
name: ConfirmationDialogLabel.CONFIRM_BUTTON,
}),
);
expect(executeActionOnUnitsSpy).toHaveBeenCalledTimes(1);
expect(screen.getByText(Label.ACTION_ERROR)).toBeInTheDocument();
expect(
screen.getByText(ConfirmationDialogLabel.ACTION_ERROR),
).toBeInTheDocument();
});
});
105 changes: 14 additions & 91 deletions src/panels/ActionsPanel/CharmActionsPanel/CharmActionsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Button, ConfirmationModal } from "@canonical/react-components";
import { Button } from "@canonical/react-components";
import { useCallback, useMemo, useRef, useState } from "react";
import reactHotToast from "react-hot-toast";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import usePortal from "react-useportal";

import LoadingHandler from "components/LoadingHandler";
import Panel from "components/Panel";
import RadioInputBox from "components/RadioInputBox";
import ToastCard from "components/ToastCard";
import useAnalytics from "hooks/useAnalytics";
import { useExecuteActionOnUnits } from "juju/api-hooks";
import ActionOptions from "panels/ActionsPanel/ActionOptions";
import type {
ActionOptionValue,
Expand All @@ -26,7 +20,7 @@ import {
getSelectedCharm,
} from "store/juju/selectors";

import { Label } from "./types";
import ConfirmationDialog from "./ConfirmationDialog";

const filterExist = <I,>(item: I | null): item is I => !!item;

Expand All @@ -39,17 +33,13 @@ export default function CharmActionsPanel({
charmURL,
onRemovePanelQueryParams,
}: Props): JSX.Element {
const sendAnalytics = useAnalytics();
const { userName, modelName } = useParams();
const { Portal } = usePortal();
const [disableSubmit, setDisableSubmit] = useState<boolean>(true);
const [confirmType, setConfirmType] = useState<ConfirmTypes>(null);
const [selectedAction, setSelectedAction] = useState<string>();
const actionOptionsValues = useRef<ActionOptionValues>({});

const selectedApplications = useSelector(getSelectedApplications(charmURL));
const selectedCharm = useSelector(getSelectedCharm(charmURL));
const executeActionOnUnits = useExecuteActionOnUnits(userName, modelName);
const actionData = useMemo(
() => selectedCharm?.actions?.specs || {},
[selectedCharm],
Expand All @@ -59,47 +49,6 @@ export default function CharmActionsPanel({
0,
);

const executeAction = () => {
sendAnalytics({
category: "ApplicationSearch",
action: "Run action (final step)",
});

if (!selectedAction) return;
executeActionOnUnits(
// transform applications to unit list for the API
selectedApplications
.map((a) =>
Array(a["unit-count"])
.fill("name" in a ? a.name : null)
.filter(Boolean)
.map((unit, i) => `${unit}-${i}`),
)
.flat(),
selectedAction,
actionOptionsValues.current[selectedAction],
)
.then((payload) => {
const error = payload?.actions?.find((e) => e.error);
if (error) {
throw error;
}
reactHotToast.custom((t) => (
<ToastCard toastInstance={t} type="positive">
{Label.ACTION_SUCCESS}
</ToastCard>
));
return;
})
.catch(() => {
reactHotToast.custom((t) => (
<ToastCard toastInstance={t} type="negative">
{Label.ACTION_ERROR}
</ToastCard>
));
});
};

const handleSubmit = () => {
setConfirmType(ConfirmType.SUBMIT);
};
Expand Down Expand Up @@ -136,43 +85,6 @@ export default function CharmActionsPanel({
[actionData],
);

const generateConfirmationModal = () => {
if (confirmType && selectedAction) {
// Allow for adding more confirmation types, like for cancel
// if inputs have been changed.
if (confirmType === "submit") {
const unitCount = selectedApplications.reduce(
(total, app) => total + (app["unit-count"] || 0),
0,
);
// Render the submit confirmation modal.
return (
<Portal>
<ConfirmationModal
title={`Run ${selectedAction}?`}
cancelButtonLabel={Label.CANCEL_BUTTON}
confirmButtonLabel={Label.CONFIRM_BUTTON}
confirmButtonAppearance="positive"
onConfirm={() => {
setConfirmType(null);
executeAction();
onRemovePanelQueryParams();
}}
close={() => setConfirmType(null)}
>
<h4 className="p-muted-heading u-no-margin--bottom">
APPLICATION COUNT (UNIT COUNT)
</h4>
<p data-testid="confirmation-modal-unit-count">
{selectedApplications.length} ({unitCount})
</p>
</ConfirmationModal>
</Portal>
);
}
}
};

return (
<Panel
drawer={
Expand Down Expand Up @@ -210,7 +122,18 @@ export default function CharmActionsPanel({
/>
</RadioInputBox>
))}
{generateConfirmationModal()}
{selectedAction ? (
<ConfirmationDialog
confirmType={confirmType}
selectedAction={selectedAction}
selectedApplications={selectedApplications}
setConfirmType={setConfirmType}
selectedActionOptionValue={
actionOptionsValues.current[selectedAction]
}
onRemovePanelQueryParams={onRemovePanelQueryParams}
/>
) : null}
</LoadingHandler>
</Panel>
);
Expand Down
Loading

0 comments on commit 194fa66

Please sign in to comment.