From b296b57d19318545199e71b43d9be20537498399 Mon Sep 17 00:00:00 2001
From: Vladimir Cucu <108150922+vladimir-cucu@users.noreply.github.com>
Date: Thu, 4 Jul 2024 11:00:22 +0300
Subject: [PATCH] WD-11666 - refactor: remove nested components in ConfigPanel
(#1769)
---
src/panels/ConfigPanel/ConfigPanel.test.tsx | 301 ++++------
src/panels/ConfigPanel/ConfigPanel.tsx | 214 +-------
.../ConfirmationDialog.test.tsx | 514 ++++++++++++++++++
.../ConfirmationDialog/ConfirmationDialog.tsx | 201 +++++++
.../ConfigPanel/ConfirmationDialog/index.ts | 1 +
.../ConfigPanel/ConfirmationDialog/types.ts | 18 +
src/panels/ConfigPanel/types.ts | 30 +-
7 files changed, 867 insertions(+), 412 deletions(-)
create mode 100644 src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx
create mode 100644 src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx
create mode 100644 src/panels/ConfigPanel/ConfirmationDialog/index.ts
create mode 100644 src/panels/ConfigPanel/ConfirmationDialog/types.ts
diff --git a/src/panels/ConfigPanel/ConfigPanel.test.tsx b/src/panels/ConfigPanel/ConfigPanel.test.tsx
index 9d0ffb0e2..46df39fc9 100644
--- a/src/panels/ConfigPanel/ConfigPanel.test.tsx
+++ b/src/panels/ConfigPanel/ConfigPanel.test.tsx
@@ -34,7 +34,8 @@ import { rootStateFactory } from "testing/factories/root";
import { renderComponent } from "testing/utils";
import ConfigPanel from "./ConfigPanel";
-import { Label } from "./types";
+import { Label as ConfirmationDialogLabel } from "./ConfirmationDialog/types";
+import { Label as ConfigPanelLabel } from "./types";
vi.mock("juju/api-hooks/application", () => ({
useGetApplicationConfig: vi.fn(),
@@ -154,7 +155,7 @@ describe("ConfigPanel", () => {
);
renderComponent(, { state, path, url });
// Use findBy to wait for the async events to finish
- await screen.findByText(Label.NONE);
+ await screen.findByText(ConfigPanelLabel.NONE);
expect(document.querySelector(".config-panel__message")).toMatchSnapshot();
});
@@ -205,7 +206,7 @@ describe("ConfigPanel", () => {
expect(email).toHaveTextContent("eggman@example.com");
expect(name).toHaveTextContent("not eggman");
await userEvent.click(
- screen.getByRole("button", { name: Label.RESET_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.RESET_BUTTON }),
);
expect(email).toHaveTextContent("");
expect(name).toHaveTextContent("eggman");
@@ -221,10 +222,10 @@ describe("ConfigPanel", () => {
expect(
within(
screen.getByRole("dialog", {
- name: Label.CANCEL_CONFIRM,
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM,
}),
).getByRole("heading", {
- name: Label.CANCEL_CONFIRM,
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM,
}),
).toBeInTheDocument();
expect(router.state.location.search).toBe(`?${params.toString()}`);
@@ -235,7 +236,7 @@ describe("ConfigPanel", () => {
await userEvent.click(document.body);
expect(
within(screen.getByRole("dialog", { name: "" })).queryByRole("heading", {
- name: Label.CANCEL_CONFIRM,
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM,
}),
).not.toBeInTheDocument();
expect(router.state.location.search).toBeFalsy();
@@ -248,15 +249,15 @@ describe("ConfigPanel", () => {
"eggman@example.com",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.CANCEL_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.CANCEL_BUTTON }),
);
expect(
within(
screen.getByRole("dialog", {
- name: Label.CANCEL_CONFIRM,
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM,
}),
).getByRole("heading", {
- name: Label.CANCEL_CONFIRM,
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM,
}),
).toBeInTheDocument();
expect(router.state.location.search).toBe(`?${params.toString()}`);
@@ -269,57 +270,27 @@ describe("ConfigPanel", () => {
"eggman@example.com",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.CANCEL_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.CANCEL_BUTTON }),
);
- expect(
- within(
- screen.getByRole("dialog", {
- name: Label.CANCEL_CONFIRM,
- }),
- ).getByRole("heading", {
- name: Label.CANCEL_CONFIRM,
- }),
- ).toBeInTheDocument();
expect(router.state.location.search).toBe(`?${params.toString()}`);
await userEvent.click(
- screen.getByRole("button", { name: Label.CANCEL_CONFIRM_CONFIRM_BUTTON }),
- );
- expect(router.state.location.search).toBeFalsy();
- });
-
- it("can cancel the cancel confirmation", async () => {
- const { router } = renderComponent(, { state, path, url });
- await userEvent.type(
- within(await screen.findByTestId("email")).getByRole("textbox"),
- "eggman@example.com",
- );
- await userEvent.click(
- screen.getByRole("button", { name: Label.CANCEL_BUTTON }),
- );
- expect(
- within(
- screen.getByRole("dialog", {
- name: Label.CANCEL_CONFIRM,
- }),
- ).getByRole("heading", {
- name: Label.CANCEL_CONFIRM,
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM_CONFIRM_BUTTON,
}),
- ).toBeInTheDocument();
- expect(router.state.location.search).toBe(`?${params.toString()}`);
- await userEvent.click(
- screen.getByRole("button", { name: Label.CANCEL_CONFIRM_CANCEL_BUTTON }),
);
- expect(router.state.location.search).toBe(`?${params.toString()}`);
+ expect(router.state.location.search).toBeFalsy();
});
it("closes when cancelling and there are no unsaved changes", async () => {
const { router } = renderComponent(, { state, path, url });
await userEvent.click(
- await screen.findByRole("button", { name: Label.CANCEL_BUTTON }),
+ await screen.findByRole("button", {
+ name: ConfigPanelLabel.CANCEL_BUTTON,
+ }),
);
expect(
within(screen.getByRole("dialog", { name: "" })).queryByRole("heading", {
- name: Label.CANCEL_CONFIRM,
+ name: ConfirmationDialogLabel.CANCEL_CONFIRM,
}),
).not.toBeInTheDocument();
expect(router.state.location.search).toBeFalsy();
@@ -331,63 +302,18 @@ describe("ConfigPanel", () => {
within(await screen.findByTestId("email")).getByRole("textbox"),
"eggman@example.com",
);
- await userEvent.type(
- within(await screen.findByTestId("name")).getByRole("textbox"),
- "noteggman",
- );
- await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
- );
- expect(
- within(
- screen.getByRole("dialog", {
- name: Label.SAVE_CONFIRM,
- }),
- ).getByRole("heading", {
- name: Label.SAVE_CONFIRM,
- }),
- ).toBeInTheDocument();
- });
-
- it("can cancel the save confirmation", async () => {
- const setApplicationConfig = vi
- .fn()
- .mockImplementation(() => Promise.resolve());
- vi.spyOn(applicationHooks, "useSetApplicationConfig").mockImplementation(
- () => setApplicationConfig,
- );
- renderComponent(, { state, path, url });
- await userEvent.type(
- within(await screen.findByTestId("email")).getByRole("textbox"),
- "eggman@example.com",
- );
- await userEvent.type(
- within(await screen.findByTestId("name")).getByRole("textbox"),
- "noteggman",
- );
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
expect(
within(
screen.getByRole("dialog", {
- name: Label.SAVE_CONFIRM,
+ name: ConfirmationDialogLabel.SAVE_CONFIRM,
}),
).getByRole("heading", {
- name: Label.SAVE_CONFIRM,
+ name: ConfirmationDialogLabel.SAVE_CONFIRM,
}),
).toBeInTheDocument();
- await userEvent.click(
- within(
- screen.getByRole("dialog", {
- name: Label.SAVE_CONFIRM,
- }),
- ).getByRole("button", {
- name: Label.SAVE_CONFIRM_CANCEL_BUTTON,
- }),
- );
- expect(screen.queryByRole("dialog", { name: "" })).not.toBeInTheDocument();
- expect(setApplicationConfig).not.toHaveBeenCalled();
});
it("can save changes", async () => {
@@ -408,10 +334,12 @@ describe("ConfigPanel", () => {
"noteggman",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
expect(setApplicationConfig).toHaveBeenCalledWith("easyrsa", {
email: configFactory.build({
@@ -443,15 +371,13 @@ describe("ConfigPanel", () => {
within(await screen.findByTestId("email")).getByRole("textbox"),
"eggman@example.com",
);
- await userEvent.type(
- within(await screen.findByTestId("name")).getByRole("textbox"),
- "noteggman",
- );
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
expect(screen.getByText("That's not a name")).toBeInTheDocument();
expect(getApplicationConfig).toHaveBeenCalledTimes(1);
@@ -470,13 +396,16 @@ describe("ConfigPanel", () => {
expect(getApplicationConfig).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(console.error).toHaveBeenCalledWith(
- Label.GET_CONFIG_ERROR,
+ ConfigPanelLabel.GET_CONFIG_ERROR,
new Error("Error while calling getApplicationConfig"),
);
});
- const configErrorNotification = screen.getByText(Label.GET_CONFIG_ERROR, {
- exact: false,
- });
+ const configErrorNotification = screen.getByText(
+ ConfigPanelLabel.GET_CONFIG_ERROR,
+ {
+ exact: false,
+ },
+ );
expect(configErrorNotification).toBeInTheDocument();
expect(configErrorNotification.childElementCount).toBe(1);
const refetchButton = configErrorNotification.children[0];
@@ -513,36 +442,24 @@ describe("ConfigPanel", () => {
within(await screen.findByTestId("email")).getByRole("textbox"),
"eggman@example.com",
);
- await userEvent.type(
- within(await screen.findByTestId("name")).getByRole("textbox"),
- "noteggman",
- );
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
- );
- expect(setApplicationConfig).toHaveBeenCalledWith("easyrsa", {
- email: configFactory.build({
- name: "email",
- default: "",
- newValue: "eggman@example.com",
- }),
- name: configFactory.build({
- name: "name",
- default: "eggman",
- newValue: "noteggman",
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
}),
- });
+ );
await waitFor(() =>
expect(console.error).toHaveBeenCalledWith(
- Label.SUBMIT_TO_JUJU_ERROR,
+ ConfirmationDialogLabel.SUBMIT_TO_JUJU_ERROR,
new Error("Error while trying to save"),
),
);
expect(
- screen.getByText(Label.SUBMIT_TO_JUJU_ERROR, { exact: false }),
+ screen.getByText(ConfirmationDialogLabel.SUBMIT_TO_JUJU_ERROR, {
+ exact: false,
+ }),
).toBeInTheDocument();
});
@@ -561,14 +478,16 @@ describe("ConfigPanel", () => {
"secret:aabbccdd",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
expect(
screen.getByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ name: ConfirmationDialogLabel.GRANT_CONFIRM,
}),
).toBeInTheDocument();
});
@@ -601,14 +520,16 @@ describe("ConfigPanel", () => {
"secret:aabbccdd",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
expect(
screen.getByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ name: ConfirmationDialogLabel.GRANT_CONFIRM,
}),
).toBeInTheDocument();
});
@@ -620,14 +541,16 @@ describe("ConfigPanel", () => {
"notasecret",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
expect(
screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ name: ConfirmationDialogLabel.GRANT_CONFIRM,
}),
).not.toBeInTheDocument();
});
@@ -654,14 +577,16 @@ describe("ConfigPanel", () => {
"secret:aabbccdd",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
expect(
screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ name: ConfirmationDialogLabel.GRANT_CONFIRM,
}),
).not.toBeInTheDocument();
});
@@ -686,48 +611,18 @@ describe("ConfigPanel", () => {
"secret:aabbccdd",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
- );
- expect(
- screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
}),
- ).not.toBeInTheDocument();
- });
-
- it("can cancel the grant confirmation", async () => {
- state.juju.secrets = secretsStateFactory.build({
- abc123: modelSecretsFactory.build({
- items: [
- listSecretResultFactory.build({ access: [], uri: "secret:aabbccdd" }),
- ],
- loaded: true,
- }),
- });
- const { router } = renderComponent(, { state, path, url });
- expect(router.state.location.search).toBe(`?${params.toString()}`);
- await userEvent.type(
- within(await screen.findByTestId("email")).getByRole("textbox"),
- "secret:aabbccdd",
- );
- await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
- );
- await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
- );
- await userEvent.click(
- screen.getByRole("button", { name: Label.GRANT_CANCEL_BUTTON }),
);
expect(
screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ name: ConfirmationDialogLabel.GRANT_CONFIRM,
}),
).not.toBeInTheDocument();
- expect(router.state.location.search).toBe("");
});
it("can grant secrets", async () => {
@@ -757,19 +652,18 @@ describe("ConfigPanel", () => {
"secret:eeffgghh",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.GRANT_CONFIRM_BUTTON }),
- );
- expect(
- screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.GRANT_CONFIRM_BUTTON,
}),
- ).not.toBeInTheDocument();
+ );
expect(grantSecret).toHaveBeenCalledWith("secret:aabbccdd", ["easyrsa"]);
expect(grantSecret).toHaveBeenCalledWith("secret:eeffgghh", ["easyrsa"]);
expect(router.state.location.search).toBe("");
@@ -802,17 +696,21 @@ describe("ConfigPanel", () => {
"secret:aabbccdd",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.GRANT_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.GRANT_CONFIRM_BUTTON,
+ }),
);
expect(
screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ name: ConfirmationDialogLabel.GRANT_CONFIRM,
}),
).not.toBeInTheDocument();
expect(grantSecret).toHaveBeenCalledTimes(1);
@@ -841,24 +739,23 @@ describe("ConfigPanel", () => {
"secret:aabbccdd",
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON,
+ }),
);
await userEvent.click(
- screen.getByRole("button", { name: Label.GRANT_CONFIRM_BUTTON }),
- );
- expect(
- screen.queryByRole("dialog", {
- name: Label.GRANT_CONFIRM,
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.GRANT_CONFIRM_BUTTON,
}),
- ).not.toBeInTheDocument();
+ );
expect(router.state.location.search).toBe(`?${params.toString()}`);
await waitFor(() => {
expect(
document.querySelector(".p-notification--negative"),
- ).toHaveTextContent(Label.GRANT_ERROR);
+ ).toHaveTextContent(ConfirmationDialogLabel.GRANT_ERROR);
});
});
@@ -889,16 +786,16 @@ describe("ConfigPanel", () => {
"textbox",
);
await userEvent.type(input, "notasecret:aabbccdd");
- expect(screen.getByText(Label.SECRET_PREFIX_ERROR)).toHaveClass(
+ expect(screen.getByText(ConfigPanelLabel.SECRET_PREFIX_ERROR)).toHaveClass(
"p-form-validation__message",
);
expect(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
).toBeDisabled();
await userEvent.clear(input);
await userEvent.type(input, "secret:aabbccdd");
expect(
- screen.queryByText(Label.SECRET_PREFIX_ERROR),
+ screen.queryByText(ConfigPanelLabel.SECRET_PREFIX_ERROR),
).not.toBeInTheDocument();
});
@@ -916,16 +813,16 @@ describe("ConfigPanel", () => {
"textbox",
);
await userEvent.type(input, "secret:nothing");
- expect(screen.getByText(Label.INVALID_SECRET_ERROR)).toHaveClass(
+ expect(screen.getByText(ConfigPanelLabel.INVALID_SECRET_ERROR)).toHaveClass(
"p-form-validation__message",
);
expect(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
).toBeDisabled();
await userEvent.clear(input);
await userEvent.type(input, "secret:aabbccdd");
expect(
- screen.queryByText(Label.INVALID_SECRET_ERROR),
+ screen.queryByText(ConfigPanelLabel.INVALID_SECRET_ERROR),
).not.toBeInTheDocument();
});
@@ -947,11 +844,11 @@ describe("ConfigPanel", () => {
within(await screen.findByTestId("email")).getByRole("textbox"),
"secret:aabbccdd",
);
- expect(screen.getByText(Label.INVALID_SECRET_ERROR)).toHaveClass(
+ expect(screen.getByText(ConfigPanelLabel.INVALID_SECRET_ERROR)).toHaveClass(
"p-form-validation__message",
);
expect(
- screen.getByRole("button", { name: Label.SAVE_BUTTON }),
+ screen.getByRole("button", { name: ConfigPanelLabel.SAVE_BUTTON }),
).toBeDisabled();
});
});
diff --git a/src/panels/ConfigPanel/ConfigPanel.tsx b/src/panels/ConfigPanel/ConfigPanel.tsx
index c9cd047e5..7940968ae 100644
--- a/src/panels/ConfigPanel/ConfigPanel.tsx
+++ b/src/panels/ConfigPanel/ConfigPanel.tsx
@@ -1,34 +1,21 @@
import type { ListSecretResult } from "@canonical/jujulib/dist/api/facades/secrets/SecretsV2";
-import {
- ActionButton,
- Button,
- ConfirmationModal,
-} from "@canonical/react-components";
+import { ActionButton, Button } from "@canonical/react-components";
import classnames from "classnames";
import cloneDeep from "clone-deep";
import type { MouseEvent } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
-import usePortal from "react-useportal";
import FadeIn from "animations/FadeIn";
import CharmIcon from "components/CharmIcon";
import Panel from "components/Panel";
import type { EntityDetailsRoute } from "components/Routes";
-import SecretLabel from "components/secrets/SecretLabel";
import { isSet } from "components/utils";
import useAnalytics from "hooks/useAnalytics";
-import useCanManageSecrets from "hooks/useCanManageSecrets";
import useInlineErrors, { type SetError } from "hooks/useInlineErrors";
-import {
- useListSecrets,
- useGrantSecret,
- useSetApplicationConfig,
- useGetApplicationConfig,
-} from "juju/api-hooks";
+import { useListSecrets, useGetApplicationConfig } from "juju/api-hooks";
import PanelInlineErrors from "panels/PanelInlineErrors";
import { usePanelQueryParams } from "panels/hooks";
-import type { ConfirmTypes as DefaultConfirmTypes } from "panels/types";
import { ConfirmType as DefaultConfirmType } from "panels/types";
import bulbImage from "static/images/bulb.svg";
import boxImage from "static/images/no-config-params.svg";
@@ -38,40 +25,22 @@ import { useAppSelector, useAppDispatch } from "store/store";
import { secretIsAppOwned } from "utils";
import BooleanConfig from "./BooleanConfig";
-import ChangedKeyValues from "./ChangedKeyValues";
import type { SetNewValue, SetSelectedConfig } from "./ConfigField";
+import ConfirmationDialog from "./ConfirmationDialog";
import NumberConfig from "./NumberConfig";
import TextAreaConfig from "./TextAreaConfig";
+import type { ConfigQueryParams, ConfirmTypes } from "./types";
import {
+ InlineErrors,
Label,
TestId,
type Config,
type ConfigData,
type ConfigValue,
} from "./types";
-import { getRequiredGrants } from "./utils";
import "./_config-panel.scss";
-enum InlineErrors {
- FORM = "form",
- GET_CONFIG = "get-config",
- SUBMIT_TO_JUJU = "submit-to-juju",
-}
-
-enum ConfigConfirmType {
- GRANT = "grant",
-}
-
-type ConfirmTypes = DefaultConfirmTypes | ConfigConfirmType;
-
-type ConfigQueryParams = {
- panel: string | null;
- charm: string | null;
- entity: string | null;
- modelUUID: string | null;
-};
-
const hasChangedFields = (newConfig: Config): boolean => {
return Object.keys(newConfig).some(
(key) =>
@@ -113,7 +82,6 @@ export default function ConfigPanel(): JSX.Element {
});
const scrollArea = useRef(null);
const sendAnalytics = useAnalytics();
- const { Portal } = usePortal();
const updateConfig = useCallback((newConfig: Config) => {
setConfig(newConfig);
checkAllDefaults(newConfig);
@@ -135,11 +103,8 @@ export default function ConfigPanel(): JSX.Element {
const wsControllerURL = useAppSelector((state) =>
getModelByUUID(state, modelUUID),
)?.wsControllerURL;
- const canManageSecrets = useCanManageSecrets();
- const grantSecret = useGrantSecret(userName, modelName);
const listSecrets = useListSecrets(userName, modelName);
const getApplicationConfig = useGetApplicationConfig(userName, modelName);
- const setApplicationConfig = useSetApplicationConfig(userName, modelName);
useEffect(() => {
listSecrets();
@@ -224,164 +189,6 @@ export default function ConfigPanel(): JSX.Element {
setEnableSave(fieldChanged);
}
- async function _submitToJuju() {
- if (!modelUUID || !appName) {
- return;
- }
- setSavingConfig(true);
- const response = await setApplicationConfig(appName, config);
- const errors = response?.results?.reduce((collection, result) => {
- if (result.error) {
- collection.push(result.error.message);
- }
- return collection;
- }, []);
- setSavingConfig(false);
- setEnableSave(false);
- setConfirmType(null);
- if (errors?.length) {
- setInlineError(InlineErrors.FORM, errors);
- return;
- }
- sendAnalytics({
- category: "User",
- action: "Config values updated",
- });
- if (
- canManageSecrets &&
- getRequiredGrants(appName, config, secrets)?.length
- ) {
- setConfirmType(ConfigConfirmType.GRANT);
- } else {
- handleRemovePanelQueryParams();
- }
- }
-
- function generateConfirmationDialog(): JSX.Element | null {
- if (confirmType && appName) {
- if (confirmType === DefaultConfirmType.SUBMIT) {
- // Render the submit confirmation modal.
- return (
-
-
- You can revert back to the applications default settings by
- clicking the “Reset all values” button; or reset each edited
- field by clicking “Use default”.
-
- }
- cancelButtonLabel={Label.SAVE_CONFIRM_CANCEL_BUTTON}
- confirmButtonLabel={Label.SAVE_CONFIRM_CONFIRM_BUTTON}
- confirmButtonAppearance="positive"
- onConfirm={() => {
- setConfirmType(null);
- // Clear the form errors if there were any from a previous submit.
- setInlineError(InlineErrors.FORM, null);
- _submitToJuju().catch((error) => {
- setInlineError(
- InlineErrors.SUBMIT_TO_JUJU,
- Label.SUBMIT_TO_JUJU_ERROR,
- );
- console.error(Label.SUBMIT_TO_JUJU_ERROR, error);
- });
- }}
- close={() => setConfirmType(null)}
- >
-
-
-
- );
- }
- if (confirmType === ConfigConfirmType.GRANT) {
- // Render the grant confirmation modal.
- const requiredGrants = getRequiredGrants(appName, config, secrets);
- return (
-
- {
- setConfirmType(null);
- // Clear the form errors if there were any from a previous submit.
- setInlineError(InlineErrors.FORM, null);
- if (!appName || !requiredGrants) {
- // It is not possible to get to this point if these
- // variables aren't set.
- return;
- }
- void (async () => {
- try {
- for (const secretURI of requiredGrants) {
- await grantSecret(secretURI, [appName]);
- }
- setConfirmType(null);
- handleRemovePanelQueryParams();
- } catch (error) {
- setInlineError(
- InlineErrors.SUBMIT_TO_JUJU,
- Label.GRANT_ERROR,
- );
- console.error(Label.GRANT_ERROR, error);
- }
- })();
- }}
- close={() => {
- setConfirmType(null);
- handleRemovePanelQueryParams();
- }}
- >
-
- Would you like to grant access to this application for the
- following secrets?
-
-
- {requiredGrants?.map((secretURI) => {
- const secret = secrets?.find(({ uri }) => uri === secretURI);
- return (
- -
- {secret ? : secretURI}
-
- );
- })}
-
-
-
- );
- }
- if (confirmType === "cancel") {
- // Render the cancel confirmation modal.
- return (
-
- {
- setConfirmType(null);
- handleRemovePanelQueryParams();
- }}
- close={() => setConfirmType(null)}
- >
-
-
-
- );
- }
- }
- return null;
- }
-
function checkCanClose(event: KeyboardEvent | MouseEvent) {
if (!("code" in event)) {
const target = event.target as HTMLElement;
@@ -503,7 +310,16 @@ export default function ConfigPanel(): JSX.Element {
secrets,
)}
- {generateConfirmationDialog()}
+
>
) : (
diff --git a/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx
new file mode 100644
index 000000000..082914a24
--- /dev/null
+++ b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx
@@ -0,0 +1,514 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { Mock } from "vitest";
+import { vi } from "vitest";
+
+import * as useCanManageSecrets from "hooks/useCanManageSecrets";
+import * as applicationHooks from "juju/api-hooks/application";
+import * as secretHooks from "juju/api-hooks/secrets";
+import { ConfirmType as DefaultConfirmType } from "panels/types";
+import type { RootState } from "store/store";
+import { configFactory } from "testing/factories/juju/Application";
+import {
+ secretsStateFactory,
+ listSecretResultFactory,
+ modelSecretsFactory,
+} from "testing/factories/juju/juju";
+import { rootStateFactory } from "testing/factories/root";
+import { renderComponent } from "testing/utils";
+
+import type { Config } from "../types";
+import { ConfigConfirmType } from "../types";
+
+import ConfirmationDialog from "./ConfirmationDialog";
+import { InlineErrors, Label, Label as ConfirmationDialogLabel } from "./types";
+
+describe("ConfirmationDialog", () => {
+ let state: RootState;
+ const params = new URLSearchParams({
+ entity: "easyrsa",
+ charm: "cs:easyrsa",
+ modelUUID: "abc123",
+ panel: "config",
+ });
+ const url = `/models/eggman@external/hadoopspark?${params.toString()}`;
+ const consoleError = console.error;
+ let mockSetConfirmType: Mock;
+ let mockSetInlineError: Mock;
+ let mockHandleRemovePanelQueryParams: Mock;
+
+ const mockConfig = {
+ email: {
+ default: "",
+ description:
+ "Base64 encoded Certificate Authority (CA) bundle. Setting this config\n" +
+ "allows container runtimes to pull images from registries with TLS\n" +
+ "certificates signed by an external CA.\n",
+ source: "default",
+ type: "string",
+ value: "",
+ name: "email",
+ newValue: "secret:aabbccdd",
+ error: null,
+ },
+ name: {
+ default: "eggman",
+ description:
+ "Base64 encoded Certificate Authority (CA) bundle. Setting this config\n" +
+ "allows container runtimes to pull images from registries with TLS\n" +
+ "certificates signed by an external CA.\n",
+ source: "default",
+ type: "string",
+ value: "",
+ name: "name",
+ },
+ } as Config;
+ const mockQueryParams = {
+ panel: "config",
+ charm: "cs:easyrsa",
+ entity: "easyrsa",
+ modelUUID: "abc123",
+ };
+
+ beforeEach(() => {
+ console.error = vi.fn();
+ mockSetConfirmType = vi.fn();
+ mockSetInlineError = vi.fn();
+ mockHandleRemovePanelQueryParams = vi.fn();
+ vi.resetModules();
+ state = rootStateFactory.build();
+ const setApplicationConfig = vi
+ .fn()
+ .mockImplementation(() => Promise.resolve());
+ vi.spyOn(applicationHooks, "useSetApplicationConfig").mockImplementation(
+ () => setApplicationConfig,
+ );
+ });
+
+ afterEach(() => {
+ console.error = consoleError;
+ vi.restoreAllMocks();
+ });
+
+ it("should display submit confirmation dialog and can cancel submit", async () => {
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ expect(
+ screen.getByRole("dialog", { name: Label.SAVE_CONFIRM }),
+ ).toBeInTheDocument();
+ const cancelButton = screen.getByRole("button", {
+ name: Label.SAVE_CONFIRM_CANCEL_BUTTON,
+ });
+ expect(cancelButton).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ ).toBeInTheDocument();
+ // Check that the application name is displayed in the confirmation message.
+ expect(
+ screen.getByText(
+ "You have edited the following values to the easyrsa configuration:",
+ ),
+ ).toBeInTheDocument();
+ // Check that the changed values are displayed.
+ expect(
+ screen.getByText("email") && screen.getByText("secret:aabbccdd"),
+ ).toBeInTheDocument();
+ await userEvent.click(cancelButton);
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ });
+
+ it("should submit successfully and remove panel query params", async () => {
+ const setApplicationConfig = vi
+ .fn()
+ .mockImplementation(() => Promise.resolve());
+ vi.spyOn(applicationHooks, "useSetApplicationConfig").mockImplementation(
+ () => setApplicationConfig,
+ );
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ );
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(setApplicationConfig).toHaveBeenCalledWith("easyrsa", {
+ email: configFactory.build({
+ error: null,
+ name: "email",
+ default: "",
+ newValue: "secret:aabbccdd",
+ }),
+ name: configFactory.build({
+ name: "name",
+ default: "eggman",
+ }),
+ });
+ expect(console.error).not.toHaveBeenCalled();
+ expect(mockHandleRemovePanelQueryParams).toHaveBeenCalledOnce();
+ });
+
+ it("should submit successfully and open up grant confirmation dialog", async () => {
+ const setApplicationConfig = vi
+ .fn()
+ .mockImplementation(() => Promise.resolve());
+ vi.spyOn(applicationHooks, "useSetApplicationConfig").mockImplementation(
+ () => setApplicationConfig,
+ );
+ vi.spyOn(useCanManageSecrets, "default").mockImplementation(() => true);
+ state.juju.secrets = secretsStateFactory.build({
+ abc123: modelSecretsFactory.build({
+ items: [
+ listSecretResultFactory.build({ access: [], uri: "secret:aabbccdd" }),
+ ],
+ loaded: true,
+ }),
+ });
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ );
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(setApplicationConfig).toHaveBeenCalledWith("easyrsa", {
+ email: configFactory.build({
+ error: null,
+ name: "email",
+ default: "",
+ newValue: "secret:aabbccdd",
+ }),
+ name: configFactory.build({
+ name: "name",
+ default: "eggman",
+ }),
+ });
+ expect(console.error).not.toHaveBeenCalled();
+ expect(mockSetConfirmType).toHaveBeenCalledWith(ConfigConfirmType.GRANT);
+ });
+
+ it("should console error when trying to submit", async () => {
+ const setApplicationConfig = vi
+ .fn()
+ .mockImplementation(() =>
+ Promise.reject(new Error("Error while trying to save")),
+ );
+ vi.spyOn(applicationHooks, "useSetApplicationConfig").mockImplementation(
+ () => setApplicationConfig,
+ );
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: Label.SAVE_CONFIRM_CONFIRM_BUTTON }),
+ );
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(mockSetInlineError).toHaveBeenCalledWith(InlineErrors.FORM, null);
+ expect(setApplicationConfig).toHaveBeenCalledWith("easyrsa", {
+ email: configFactory.build({
+ error: null,
+ name: "email",
+ default: "",
+ newValue: "secret:aabbccdd",
+ }),
+ name: configFactory.build({
+ name: "name",
+ default: "eggman",
+ }),
+ });
+ await waitFor(() =>
+ expect(console.error).toHaveBeenCalledWith(
+ Label.SUBMIT_TO_JUJU_ERROR,
+ new Error("Error while trying to save"),
+ ),
+ );
+ expect(mockSetInlineError).toHaveBeenCalledWith(
+ InlineErrors.SUBMIT_TO_JUJU,
+ Label.SUBMIT_TO_JUJU_ERROR,
+ );
+ });
+
+ it("should display grant confirmation dialog and can cancel grant", async () => {
+ state.juju.secrets = secretsStateFactory.build({
+ abc123: modelSecretsFactory.build({
+ items: [
+ listSecretResultFactory.build({ access: [], uri: "secret:aabbccdd" }),
+ ],
+ loaded: true,
+ }),
+ });
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+
+ expect(
+ screen.getByRole("dialog", {
+ name: Label.GRANT_CONFIRM,
+ }),
+ ).toBeInTheDocument();
+ const cancelButton = screen.getByRole("button", {
+ name: Label.GRANT_CANCEL_BUTTON,
+ });
+ expect(cancelButton).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: Label.GRANT_CONFIRM_BUTTON }),
+ ).toBeInTheDocument();
+ expect(screen);
+ expect(
+ screen.getByText(
+ "Would you like to grant access to this application for the following secrets?",
+ ),
+ ).toBeInTheDocument();
+ // Check that the secret is displayed.
+ expect(screen.getByText("aabbccdd")).toBeInTheDocument();
+ await userEvent.click(cancelButton);
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(mockHandleRemovePanelQueryParams).toHaveBeenCalledOnce();
+ });
+
+ it("should grant secret successfully and remove panel query params", async () => {
+ const grantSecret = vi.fn().mockImplementation(() => Promise.resolve());
+ vi.spyOn(secretHooks, "useGrantSecret").mockImplementation(
+ () => grantSecret,
+ );
+ state.juju.secrets = secretsStateFactory.build({
+ abc123: modelSecretsFactory.build({
+ items: [
+ listSecretResultFactory.build({ access: [], uri: "secret:aabbccdd" }),
+ ],
+ loaded: true,
+ }),
+ });
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.GRANT_CONFIRM_BUTTON,
+ }),
+ );
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(mockSetInlineError).toHaveBeenCalledWith(InlineErrors.FORM, null);
+ expect(grantSecret).toHaveBeenCalledWith("secret:aabbccdd", ["easyrsa"]);
+ expect(console.error).not.toHaveBeenCalled();
+ expect(mockHandleRemovePanelQueryParams).toHaveBeenCalledOnce();
+ });
+
+ it("should console error when trying to grant access", async () => {
+ const grantSecret = vi
+ .fn()
+ .mockImplementation(() => Promise.reject(new Error("Caught error")));
+ vi.spyOn(secretHooks, "useGrantSecret").mockImplementation(
+ () => grantSecret,
+ );
+ state.juju.secrets = secretsStateFactory.build({
+ abc123: modelSecretsFactory.build({
+ items: [
+ listSecretResultFactory.build({ access: [], uri: "secret:aabbccdd" }),
+ ],
+ loaded: true,
+ }),
+ });
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: ConfirmationDialogLabel.GRANT_CONFIRM_BUTTON,
+ }),
+ );
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(mockSetInlineError).toHaveBeenCalledWith(InlineErrors.FORM, null);
+ expect(grantSecret).toHaveBeenCalledWith("secret:aabbccdd", ["easyrsa"]);
+ await waitFor(() =>
+ expect(console.error).toHaveBeenCalledWith(
+ Label.GRANT_ERROR,
+ new Error("Caught error"),
+ ),
+ );
+ expect(mockSetInlineError).toHaveBeenCalledWith(
+ InlineErrors.SUBMIT_TO_JUJU,
+ Label.GRANT_ERROR,
+ );
+ });
+
+ it("should display cancel confirmation dialog and can cancel the cancelation", async () => {
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: Label.CANCEL_CONFIRM_CONFIRM_BUTTON }),
+ );
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ expect(mockHandleRemovePanelQueryParams).toHaveBeenCalledOnce();
+ });
+
+ it("should cancel successfully", async () => {
+ renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ expect(
+ screen.getByRole("dialog", { name: Label.CANCEL_CONFIRM }),
+ ).toBeInTheDocument();
+ const cancelButton = screen.getByRole("button", {
+ name: Label.CANCEL_CONFIRM_CANCEL_BUTTON,
+ });
+ expect(cancelButton).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: Label.CANCEL_CONFIRM_CONFIRM_BUTTON }),
+ ).toBeInTheDocument();
+ // Check that the application name is displayed in the confirmation message.
+ expect(
+ screen.getByText(
+ "You have edited the following values to the easyrsa configuration:",
+ ),
+ ).toBeInTheDocument();
+ // Check that the changed values are displayed.
+ expect(
+ screen.getByText("email") && screen.getByText("secret:aabbccdd"),
+ ).toBeInTheDocument();
+ await userEvent.click(cancelButton);
+ expect(mockSetConfirmType).toHaveBeenCalledWith(null);
+ });
+
+ it("should display nothing if no application name is provided", () => {
+ const {
+ result: { container },
+ } = renderComponent(
+ ,
+ {
+ url,
+ state,
+ },
+ );
+ expect(container.tagName).toBe("DIV");
+ expect(container.children.length).toBe(1);
+ expect(container.firstChild).toBeEmptyDOMElement();
+ });
+});
diff --git a/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx
new file mode 100644
index 000000000..8cd74f0d0
--- /dev/null
+++ b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx
@@ -0,0 +1,201 @@
+import { ConfirmationModal } from "@canonical/react-components";
+import { useParams } from "react-router-dom";
+import usePortal from "react-useportal";
+
+import type { EntityDetailsRoute } from "components/Routes";
+import SecretLabel from "components/secrets/SecretLabel";
+import useAnalytics from "hooks/useAnalytics";
+import useCanManageSecrets from "hooks/useCanManageSecrets";
+import { type SetError } from "hooks/useInlineErrors";
+import { useGrantSecret, useSetApplicationConfig } from "juju/api-hooks";
+import type { usePanelQueryParams } from "panels/hooks";
+import { ConfirmType as DefaultConfirmType } from "panels/types";
+import { getModelSecrets } from "store/juju/selectors";
+import { useAppSelector } from "store/store";
+
+import ChangedKeyValues from "../ChangedKeyValues";
+import type { Config, ConfigQueryParams, ConfirmTypes } from "../types";
+import { ConfigConfirmType } from "../types";
+import { getRequiredGrants } from "../utils";
+
+import { InlineErrors, Label } from "./types";
+
+type Props = {
+ confirmType: ConfirmTypes;
+ queryParams: ConfigQueryParams;
+ setEnableSave: React.Dispatch>;
+ setSavingConfig: React.Dispatch>;
+ setConfirmType: React.Dispatch>;
+ setInlineError: SetError;
+ config: Config;
+ handleRemovePanelQueryParams: ReturnType[2];
+};
+
+const ConfirmationDialog = ({
+ confirmType,
+ queryParams,
+ setEnableSave,
+ setSavingConfig,
+ setConfirmType,
+ setInlineError,
+ config,
+ handleRemovePanelQueryParams,
+}: Props): JSX.Element | null => {
+ const { Portal } = usePortal();
+ const { userName, modelName } = useParams();
+ const { entity: appName, modelUUID } = queryParams;
+ const grantSecret = useGrantSecret(userName, modelName);
+ const setApplicationConfig = useSetApplicationConfig(userName, modelName);
+ const canManageSecrets = useCanManageSecrets();
+ const sendAnalytics = useAnalytics();
+ const secrets = useAppSelector((state) => getModelSecrets(state, modelUUID));
+
+ async function _submitToJuju(appName: string) {
+ setSavingConfig(true);
+ const response = await setApplicationConfig(appName, config);
+ const errors = response?.results?.reduce((collection, result) => {
+ if (result.error) {
+ collection.push(result.error.message);
+ }
+ return collection;
+ }, []);
+ setSavingConfig(false);
+ setEnableSave(false);
+ setConfirmType(null);
+ if (errors?.length) {
+ setInlineError(InlineErrors.FORM, errors);
+ return;
+ }
+ sendAnalytics({
+ category: "User",
+ action: "Config values updated",
+ });
+ if (
+ canManageSecrets &&
+ getRequiredGrants(appName, config, secrets)?.length
+ ) {
+ setConfirmType(ConfigConfirmType.GRANT);
+ } else {
+ handleRemovePanelQueryParams();
+ }
+ }
+ if (!appName) {
+ return null;
+ } else if (confirmType === DefaultConfirmType.SUBMIT) {
+ // Render the submit confirmation modal.
+ return (
+
+
+ You can revert back to the applications default settings by
+ clicking the “Reset all values” button; or reset each edited field
+ by clicking “Use default”.
+
+ }
+ cancelButtonLabel={Label.SAVE_CONFIRM_CANCEL_BUTTON}
+ confirmButtonLabel={Label.SAVE_CONFIRM_CONFIRM_BUTTON}
+ confirmButtonAppearance="positive"
+ onConfirm={() => {
+ setConfirmType(null);
+ // Clear the form errors if there were any from a previous submit.
+ setInlineError(InlineErrors.FORM, null);
+ _submitToJuju(appName).catch((error) => {
+ setInlineError(
+ InlineErrors.SUBMIT_TO_JUJU,
+ Label.SUBMIT_TO_JUJU_ERROR,
+ );
+ console.error(Label.SUBMIT_TO_JUJU_ERROR, error);
+ });
+ }}
+ close={() => setConfirmType(null)}
+ >
+
+
+
+ );
+ } else if (confirmType === ConfigConfirmType.GRANT) {
+ // Render the grant confirmation modal.
+ const requiredGrants = getRequiredGrants(appName, config, secrets);
+ return (
+
+ {
+ setConfirmType(null);
+ // Clear the form errors if there were any from a previous submit.
+ setInlineError(InlineErrors.FORM, null);
+ if (!requiredGrants) {
+ // It is not possible to get to this point if these
+ // variables aren't set.
+ return;
+ }
+ void (async () => {
+ try {
+ for (const secretURI of requiredGrants) {
+ await grantSecret(secretURI, [appName]);
+ }
+ setConfirmType(null);
+ handleRemovePanelQueryParams();
+ } catch (error) {
+ setInlineError(InlineErrors.SUBMIT_TO_JUJU, Label.GRANT_ERROR);
+ console.error(Label.GRANT_ERROR, error);
+ }
+ })();
+ }}
+ close={() => {
+ setConfirmType(null);
+ handleRemovePanelQueryParams();
+ }}
+ >
+
+ Would you like to grant access to this application for the following
+ secrets?
+
+
+ {requiredGrants?.map((secretURI) => {
+ const secret = secrets?.find(({ uri }) => uri === secretURI);
+ return (
+ -
+ {secret ? : secretURI}
+
+ );
+ })}
+
+
+
+ );
+ } else if (confirmType === DefaultConfirmType.CANCEL) {
+ // Render the cancel confirmation modal.
+ return (
+
+ {
+ setConfirmType(null);
+ handleRemovePanelQueryParams();
+ }}
+ close={() => setConfirmType(null)}
+ >
+
+
+
+ );
+ }
+ return null;
+};
+
+export default ConfirmationDialog;
diff --git a/src/panels/ConfigPanel/ConfirmationDialog/index.ts b/src/panels/ConfigPanel/ConfirmationDialog/index.ts
new file mode 100644
index 000000000..20bf0713d
--- /dev/null
+++ b/src/panels/ConfigPanel/ConfirmationDialog/index.ts
@@ -0,0 +1 @@
+export { default } from "./ConfirmationDialog";
diff --git a/src/panels/ConfigPanel/ConfirmationDialog/types.ts b/src/panels/ConfigPanel/ConfirmationDialog/types.ts
new file mode 100644
index 000000000..659b32132
--- /dev/null
+++ b/src/panels/ConfigPanel/ConfirmationDialog/types.ts
@@ -0,0 +1,18 @@
+export enum Label {
+ CANCEL_CONFIRM = "Are you sure you wish to cancel?",
+ CANCEL_CONFIRM_CANCEL_BUTTON = "Continue editing",
+ CANCEL_CONFIRM_CONFIRM_BUTTON = "Yes, I'm sure",
+ GRANT_CANCEL_BUTTON = "No",
+ GRANT_CONFIRM = "Grant secrets?",
+ GRANT_CONFIRM_BUTTON = "Yes",
+ GRANT_ERROR = "Unable to grant application access to secrets.",
+ SAVE_CONFIRM = "Are you sure you wish to apply these changes?",
+ SAVE_CONFIRM_CANCEL_BUTTON = "Cancel",
+ SAVE_CONFIRM_CONFIRM_BUTTON = "Yes, apply changes",
+ SUBMIT_TO_JUJU_ERROR = "Unable to submit config changes to Juju.",
+}
+
+export enum InlineErrors {
+ FORM = "form",
+ SUBMIT_TO_JUJU = "submit-to-juju",
+}
diff --git a/src/panels/ConfigPanel/types.ts b/src/panels/ConfigPanel/types.ts
index 241a6e609..62e103c74 100644
--- a/src/panels/ConfigPanel/types.ts
+++ b/src/panels/ConfigPanel/types.ts
@@ -1,3 +1,5 @@
+import type { ConfirmTypes as DefaultConfirmTypes } from "panels/types";
+
export type ConfigValue = string | number | boolean | undefined;
export type ConfigOption = {
@@ -22,25 +24,31 @@ export type Config = {
export enum Label {
CANCEL_BUTTON = "Cancel",
- CANCEL_CONFIRM = "Are you sure you wish to cancel?",
- CANCEL_CONFIRM_CANCEL_BUTTON = "Continue editing",
- CANCEL_CONFIRM_CONFIRM_BUTTON = "Yes, I'm sure",
- GRANT_CANCEL_BUTTON = "No",
- GRANT_CONFIRM = "Grant secrets?",
- GRANT_CONFIRM_BUTTON = "Yes",
- GRANT_ERROR = "Unable to grant application access to secrets.",
INVALID_SECRET_ERROR = "This is an invalid secret URI.",
SECRET_PREFIX_ERROR = "A secret URI must begin with the 'secret:' prefix.",
NONE = "This application doesn't have any configuration parameters",
RESET_BUTTON = "Reset all values",
SAVE_BUTTON = "Save and apply",
- SAVE_CONFIRM = "Are you sure you wish to apply these changes?",
- SAVE_CONFIRM_CANCEL_BUTTON = "Cancel",
- SAVE_CONFIRM_CONFIRM_BUTTON = "Yes, apply changes",
GET_CONFIG_ERROR = "Unable to get application config.",
- SUBMIT_TO_JUJU_ERROR = "Unable to submit config changes to Juju.",
}
export enum TestId {
PANEL = "config-panel",
}
+
+export enum InlineErrors {
+ GET_CONFIG = "get-config",
+}
+
+export enum ConfigConfirmType {
+ GRANT = "grant",
+}
+
+export type ConfirmTypes = DefaultConfirmTypes | ConfigConfirmType;
+
+export type ConfigQueryParams = {
+ panel: string | null;
+ charm: string | null;
+ entity: string | null;
+ modelUUID: string | null;
+};