From 9b1cc5b202abb882fd32ac386a4aa5933a10d58d Mon Sep 17 00:00:00 2001
From: Huw Wilkins
Date: Tue, 3 Dec 2024 13:28:48 +1100
Subject: [PATCH] chore(tests): create shared delete button and add tests
---
.../DeletePanelButton.test.tsx | 224 ++++++++++++++++++
.../DeletePanelButton/DeletePanelButton.tsx | 62 +++++
ui/src/components/DeletePanelButton/index.ts | 2 +
ui/src/components/DeletePanelButton/types.ts | 17 ++
.../DeleteClientBtn/DeleteClientBtn.test.tsx | 27 +++
.../DeleteClientBtn/DeleteClientBtn.tsx | 63 ++---
ui/src/pages/clients/DeleteClientBtn/types.ts | 3 +
.../DeleteIdentityBtn.test.tsx | 27 +++
.../DeleteIdentityBtn/DeleteIdentityBtn.tsx | 66 ++----
.../identities/DeleteIdentityBtn/types.ts | 3 +
.../DeleteProviderBtn.test.tsx | 62 +++++
.../DeleteProviderBtn/DeleteProviderBtn.tsx | 135 ++++-------
.../providers/DeleteProviderBtn/types.ts | 3 +
13 files changed, 504 insertions(+), 190 deletions(-)
create mode 100644 ui/src/components/DeletePanelButton/DeletePanelButton.test.tsx
create mode 100644 ui/src/components/DeletePanelButton/DeletePanelButton.tsx
create mode 100644 ui/src/components/DeletePanelButton/index.ts
create mode 100644 ui/src/components/DeletePanelButton/types.ts
create mode 100644 ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.test.tsx
create mode 100644 ui/src/pages/clients/DeleteClientBtn/types.ts
create mode 100644 ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.test.tsx
create mode 100644 ui/src/pages/identities/DeleteIdentityBtn/types.ts
create mode 100644 ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.test.tsx
create mode 100644 ui/src/pages/providers/DeleteProviderBtn/types.ts
diff --git a/ui/src/components/DeletePanelButton/DeletePanelButton.test.tsx b/ui/src/components/DeletePanelButton/DeletePanelButton.test.tsx
new file mode 100644
index 000000000..baa677660
--- /dev/null
+++ b/ui/src/components/DeletePanelButton/DeletePanelButton.test.tsx
@@ -0,0 +1,224 @@
+import { screen, waitFor } from "@testing-library/dom";
+import * as reactQuery from "@tanstack/react-query";
+import userEvent from "@testing-library/user-event";
+import {
+ NotificationConsumer,
+ NotificationProvider,
+} from "@canonical/react-components";
+
+import { renderComponent } from "test/utils";
+
+import DeletePanelButton from "./DeletePanelButton";
+import { Label } from "./types";
+import { Location } from "react-router-dom";
+
+vi.mock("@tanstack/react-query", async () => {
+ const actual = await vi.importActual("@tanstack/react-query");
+ return {
+ ...actual,
+ useQueryClient: vi.fn(),
+ };
+});
+
+beforeEach(() => {
+ vi.spyOn(reactQuery, "useQueryClient").mockReturnValue({
+ invalidateQueries: vi.fn(),
+ } as unknown as reactQuery.QueryClient);
+});
+
+test("displays the delete button", () => {
+ renderComponent(
+ Promise.resolve()}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />,
+ );
+ expect(
+ screen.getByRole("button", { name: Label.DELETE }),
+ ).toBeInTheDocument();
+});
+
+test("displays a confirmation", async () => {
+ renderComponent(
+ Promise.resolve()}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ expect(screen.getByRole("dialog", { name: "Define" })).toBeInTheDocument();
+});
+
+test("can disable the confirm button", async () => {
+ renderComponent(
+ Promise.resolve()}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ expect(screen.getByRole("button", { name: "Confirm" })).toBeDisabled();
+});
+
+test("starts deletion", async () => {
+ renderComponent(
+ Promise.resolve()}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
+ expect(document.querySelector(".u-animation--spin")).toBeInTheDocument();
+});
+
+test("calls the delete method", async () => {
+ const onDelete = vi.fn().mockImplementation(() => Promise.resolve());
+ renderComponent(
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
+ expect(onDelete).toHaveBeenCalled();
+});
+
+test("handles a successful delete call", async () => {
+ let location: Location | null = null;
+ renderComponent(
+
+
+ Promise.resolve()}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />
+ ,
+ {
+ setLocation: (newLocation) => {
+ location = newLocation;
+ },
+ },
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
+ expect(
+ screen
+ .getByText("successfully formed")
+ .closest(".p-notification--positive"),
+ ).toBeInTheDocument();
+ expect((location as Location | null)?.pathname).toBe("/nebulous");
+});
+
+test("notifies on error", async () => {
+ renderComponent(
+
+
+ Promise.reject("Oops")}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
+ expect(
+ screen
+ .getByText("Nebulous deletion failed")
+ .closest(".p-notification--negative"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Oops")).toHaveClass("p-notification__message");
+});
+
+test("notifies on error object", async () => {
+ renderComponent(
+
+
+ Promise.reject(new Error("Oops"))}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
+ expect(
+ screen
+ .getByText("Nebulous deletion failed")
+ .closest(".p-notification--negative"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Oops")).toHaveClass("p-notification__message");
+});
+
+test("invlidates queries and hides the spinner on success", async () => {
+ const invalidateQueries = vi.fn();
+ vi.spyOn(reactQuery, "useQueryClient").mockReturnValue({
+ invalidateQueries,
+ } as unknown as reactQuery.QueryClient);
+ renderComponent(
+ Promise.resolve()}
+ successPath="/nebulous"
+ successMessage="successfully formed"
+ />,
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
+ await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
+ await waitFor(() =>
+ expect(invalidateQueries).toHaveBeenCalledWith({
+ queryKey: ["nebulous"],
+ }),
+ );
+ await waitFor(() =>
+ expect(
+ document.querySelector(".u-animation--spin"),
+ ).not.toBeInTheDocument(),
+ );
+});
diff --git a/ui/src/components/DeletePanelButton/DeletePanelButton.tsx b/ui/src/components/DeletePanelButton/DeletePanelButton.tsx
new file mode 100644
index 000000000..8b939f07c
--- /dev/null
+++ b/ui/src/components/DeletePanelButton/DeletePanelButton.tsx
@@ -0,0 +1,62 @@
+import { FC, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useQueryClient } from "@tanstack/react-query";
+import { ConfirmationButton, useNotify } from "@canonical/react-components";
+import { Label, Props } from "./types";
+
+const DeletePanelButton: FC = ({
+ confirmButtonDisabled,
+ confirmButtonLabel,
+ confirmContent,
+ confirmTitle = "Confirm delete",
+ invalidateQuery,
+ entityName,
+ onDelete,
+ successMessage,
+ successPath,
+}) => {
+ const notify = useNotify();
+ const queryClient = useQueryClient();
+ const [isLoading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ const handleDelete = () => {
+ setLoading(true);
+ onDelete()
+ .then(() => {
+ navigate(successPath, notify.queue(notify.success(successMessage)));
+ })
+ .catch((error: unknown) => {
+ notify.failure(
+ `${entityName} deletion failed`,
+ error instanceof Error ? error : null,
+ typeof error === "string" ? error : null,
+ );
+ })
+ .finally(() => {
+ setLoading(false);
+ void queryClient.invalidateQueries({
+ queryKey: [invalidateQuery],
+ });
+ });
+ };
+
+ return (
+
+ {Label.DELETE}
+
+ );
+};
+
+export default DeletePanelButton;
diff --git a/ui/src/components/DeletePanelButton/index.ts b/ui/src/components/DeletePanelButton/index.ts
new file mode 100644
index 000000000..8bfe6fd14
--- /dev/null
+++ b/ui/src/components/DeletePanelButton/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./DeletePanelButton";
+export { Label as DeletePanelButtonLabel } from "./types";
diff --git a/ui/src/components/DeletePanelButton/types.ts b/ui/src/components/DeletePanelButton/types.ts
new file mode 100644
index 000000000..33dfc7bf4
--- /dev/null
+++ b/ui/src/components/DeletePanelButton/types.ts
@@ -0,0 +1,17 @@
+import { ReactNode } from "react";
+
+export type Props = {
+ confirmButtonLabel: string;
+ confirmButtonDisabled?: boolean;
+ confirmTitle?: string;
+ confirmContent: ReactNode;
+ entityName: string;
+ onDelete: () => Promise;
+ successMessage: string;
+ successPath: string;
+ invalidateQuery: string;
+};
+
+export enum Label {
+ DELETE = "Delete",
+}
diff --git a/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.test.tsx b/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.test.tsx
new file mode 100644
index 000000000..e8dca66a6
--- /dev/null
+++ b/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.test.tsx
@@ -0,0 +1,27 @@
+import { screen } from "@testing-library/dom";
+import userEvent from "@testing-library/user-event";
+
+import { renderComponent } from "test/utils";
+
+import DeleteClientBtn from "./DeleteClientBtn";
+import { mockClient } from "test/mocks/clients";
+import { DeletePanelButtonLabel } from "components/DeletePanelButton";
+import MockAdapter from "axios-mock-adapter";
+import { axiosInstance } from "api/axios";
+import { Label } from "./types";
+
+const mock = new MockAdapter(axiosInstance);
+
+beforeEach(() => {
+ mock.reset();
+});
+
+test("deletes the client", async () => {
+ const client = mockClient();
+ renderComponent( );
+ await userEvent.click(
+ screen.getByRole("button", { name: DeletePanelButtonLabel.DELETE }),
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.CONFIRM }));
+ expect(mock.history.delete[0].url).toBe(`/clients/${client.client_id}`);
+});
diff --git a/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.tsx b/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.tsx
index 75bba83e5..ef99a8fbc 100644
--- a/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.tsx
+++ b/ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.tsx
@@ -1,59 +1,30 @@
-import { FC, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { FC } from "react";
import { queryKeys } from "util/queryKeys";
-import { useQueryClient } from "@tanstack/react-query";
-import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { deleteClient } from "api/client";
import { Client } from "types/client";
+import DeletePanelButton from "components/DeletePanelButton";
+import { urls } from "urls";
+import { Label } from "./types";
interface Props {
client: Client;
}
const DeleteClientBtn: FC = ({ client }) => {
- const notify = useNotify();
- const queryClient = useQueryClient();
- const [isLoading, setLoading] = useState(false);
- const navigate = useNavigate();
-
- const handleDelete = () => {
- setLoading(true);
- deleteClient(client.client_id)
- .then(() => {
- navigate(
- "/client",
- notify.queue(notify.success(`Client ${client.client_name} deleted.`)),
- );
- })
- .catch((e) => {
- notify.failure("Client deletion failed", e);
- })
- .finally(() => {
- setLoading(false);
- void queryClient.invalidateQueries({
- queryKey: [queryKeys.clients],
- });
- });
- };
-
return (
-
- This will permanently delete client {client.client_name} .
-
- ),
- confirmButtonLabel: "Delete client",
- onConfirm: handleDelete,
- }}
- title="Confirm delete"
- >
- Delete
-
+
+ This will permanently delete client {client.client_name} .
+
+ }
+ entityName="Client"
+ invalidateQuery={queryKeys.clients}
+ onDelete={() => deleteClient(client.client_id)}
+ successPath={urls.clients.index}
+ successMessage={`Client ${client.client_name} deleted.`}
+ />
);
};
diff --git a/ui/src/pages/clients/DeleteClientBtn/types.ts b/ui/src/pages/clients/DeleteClientBtn/types.ts
new file mode 100644
index 000000000..daae85977
--- /dev/null
+++ b/ui/src/pages/clients/DeleteClientBtn/types.ts
@@ -0,0 +1,3 @@
+export enum Label {
+ CONFIRM = "Delete client",
+}
diff --git a/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.test.tsx b/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.test.tsx
new file mode 100644
index 000000000..7d0e8575f
--- /dev/null
+++ b/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.test.tsx
@@ -0,0 +1,27 @@
+import { screen } from "@testing-library/dom";
+import userEvent from "@testing-library/user-event";
+
+import { renderComponent } from "test/utils";
+
+import DeleteIdentityBtn from "./DeleteIdentityBtn";
+import { Label } from "./types";
+import { mockIdentity } from "test/mocks/identities";
+import { DeletePanelButtonLabel } from "components/DeletePanelButton";
+import MockAdapter from "axios-mock-adapter";
+import { axiosInstance } from "api/axios";
+
+const mock = new MockAdapter(axiosInstance);
+
+beforeEach(() => {
+ mock.reset();
+});
+
+test("deletes the identity", async () => {
+ const identity = mockIdentity();
+ renderComponent( );
+ await userEvent.click(
+ screen.getByRole("button", { name: DeletePanelButtonLabel.DELETE }),
+ );
+ await userEvent.click(screen.getByRole("button", { name: Label.CONFIRM }));
+ expect(mock.history.delete[0].url).toBe(`/identities/${identity.id}`);
+});
diff --git a/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.tsx b/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.tsx
index 9cca023e7..73e4fded3 100644
--- a/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.tsx
+++ b/ui/src/pages/identities/DeleteIdentityBtn/DeleteIdentityBtn.tsx
@@ -1,62 +1,30 @@
-import { FC, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { FC } from "react";
import { queryKeys } from "util/queryKeys";
-import { useQueryClient } from "@tanstack/react-query";
-import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { Identity } from "types/identity";
import { deleteIdentity } from "api/identities";
+import { urls } from "urls";
+import DeletePanelButton from "components/DeletePanelButton";
+import { Label } from "./types";
interface Props {
identity: Identity;
}
const DeleteIdentityBtn: FC = ({ identity }) => {
- const notify = useNotify();
- const queryClient = useQueryClient();
- const [isLoading, setLoading] = useState(false);
- const navigate = useNavigate();
-
- const handleDelete = () => {
- setLoading(true);
- deleteIdentity(identity.id)
- .then(() => {
- navigate(
- "/identity",
- notify.queue(
- notify.success(`Identity ${identity.traits?.email} deleted.`),
- ),
- );
- })
- .catch((e) => {
- notify.failure("Identity deletion failed", e);
- })
- .finally(() => {
- setLoading(false);
- void queryClient.invalidateQueries({
- queryKey: [queryKeys.identities],
- });
- });
- };
-
return (
-
- This will permanently delete identity{" "}
- {identity.traits?.email} .
-
- ),
- confirmButtonLabel: "Delete identity",
- onConfirm: handleDelete,
- }}
- title="Confirm delete"
- >
- Delete
-
+
+ This will permanently delete identity {identity.traits?.email} .
+
+ }
+ entityName="Identity"
+ invalidateQuery={queryKeys.identities}
+ onDelete={() => deleteIdentity(identity.id)}
+ successPath={urls.identities.index}
+ successMessage={`Identity ${identity.traits?.email} deleted.`}
+ />
);
};
diff --git a/ui/src/pages/identities/DeleteIdentityBtn/types.ts b/ui/src/pages/identities/DeleteIdentityBtn/types.ts
new file mode 100644
index 000000000..d0fc23632
--- /dev/null
+++ b/ui/src/pages/identities/DeleteIdentityBtn/types.ts
@@ -0,0 +1,3 @@
+export enum Label {
+ CONFIRM = "Delete identity",
+}
diff --git a/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.test.tsx b/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.test.tsx
new file mode 100644
index 000000000..5ef3725c0
--- /dev/null
+++ b/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.test.tsx
@@ -0,0 +1,62 @@
+import { screen } from "@testing-library/dom";
+import userEvent from "@testing-library/user-event";
+
+import { renderComponent } from "test/utils";
+
+import DeleteProviderBtn from "./DeleteProviderBtn";
+import { Label } from "./types";
+import { mockIdentityProvider } from "test/mocks/providers";
+import { DeletePanelButtonLabel } from "components/DeletePanelButton";
+import MockAdapter from "axios-mock-adapter";
+import { axiosInstance } from "api/axios";
+import {
+ NotificationConsumer,
+ NotificationProvider,
+} from "@canonical/react-components";
+
+const mock = new MockAdapter(axiosInstance);
+
+beforeEach(() => {
+ mock.reset();
+});
+
+test("disables confirmation if the confirm text hasn't been entered", async () => {
+ const provider = mockIdentityProvider({ id: "provider1" });
+ renderComponent( );
+ await userEvent.click(
+ screen.getByRole("button", { name: DeletePanelButtonLabel.DELETE }),
+ );
+ expect(screen.getByRole("button", { name: Label.CONFIRM })).toBeDisabled();
+});
+
+test("deletes the provider", async () => {
+ const provider = mockIdentityProvider({ id: "provider1" });
+ renderComponent( );
+ await userEvent.click(
+ screen.getByRole("button", { name: DeletePanelButtonLabel.DELETE }),
+ );
+ await userEvent.type(screen.getByRole("textbox"), `remove ${provider.id}`);
+ await userEvent.click(screen.getByRole("button", { name: Label.CONFIRM }));
+ expect(mock.history.delete[0].url).toBe(`/idps/${provider.id}`);
+});
+
+test("displays an error if the provider doesn't have an id", async () => {
+ const provider = mockIdentityProvider({ id: undefined });
+ renderComponent(
+
+
+
+ ,
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: DeletePanelButtonLabel.DELETE }),
+ );
+ await userEvent.type(screen.getByRole("textbox"), "remove ");
+ await userEvent.click(screen.getByRole("button", { name: Label.CONFIRM }));
+ expect(mock.history.delete).toHaveLength(0);
+ expect(
+ screen
+ .getByText("Provider deletion failed")
+ .closest(".p-notification--negative"),
+ ).toBeInTheDocument();
+});
diff --git a/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.tsx b/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.tsx
index bc660f81b..fdb6e02fb 100644
--- a/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.tsx
+++ b/ui/src/pages/providers/DeleteProviderBtn/DeleteProviderBtn.tsx
@@ -1,114 +1,59 @@
import { FC, useState } from "react";
-import { useNavigate } from "react-router-dom";
import { queryKeys } from "util/queryKeys";
-import { useQueryClient } from "@tanstack/react-query";
-import {
- ActionButton,
- Button,
- Icon,
- Input,
- Modal,
- useNotify,
-} from "@canonical/react-components";
+import { Input } from "@canonical/react-components";
import { deleteProvider } from "api/provider";
import { IdentityProvider } from "types/provider";
-import usePortal from "react-useportal";
+import { urls } from "urls";
+import DeletePanelButton from "components/DeletePanelButton";
+import { Label } from "./types";
interface Props {
provider: IdentityProvider;
}
const DeleteProviderBtn: FC = ({ provider }) => {
- const notify = useNotify();
- const queryClient = useQueryClient();
- const [isLoading, setLoading] = useState(false);
- const navigate = useNavigate();
- const { openPortal, closePortal, isOpen, Portal } = usePortal();
const [confirmText, setConfirmText] = useState("");
-
- const handleDelete = () => {
- setLoading(true);
- if (!provider.id) {
- console.error("Cannot delete provider without id", provider);
- return;
- }
- deleteProvider(provider.id)
- .then(() => {
- navigate(
- "/provider",
- notify.queue(notify.success(`Provider ${provider.id} deleted.`)),
- );
- })
- .catch((e) => {
- notify.failure("Provider deletion failed", e);
- })
- .finally(() => {
- setLoading(false);
- void queryClient.invalidateQueries({
- queryKey: [queryKeys.providers],
- });
- });
- };
-
- const expectedConfirmText = `remove ${provider.id}`;
+ const expectedConfirmText = `remove ${provider.id || ""}`;
return (
- <>
- {isOpen && (
-
-
+
+ Are you sure you want to remove {'"'}
+ {provider.id}
+ {'"'} as an ID provider? The removal of {provider.id} as an ID
+ provider is irreversible and might adversely affect your system.
+
+ setConfirmText(e.target.value)}
+ value={confirmText}
+ type="text"
+ placeholder={expectedConfirmText}
+ label={
<>
-
- Cancel
-
-
-
- Remove
-
+ Type {expectedConfirmText} to confirm
>
}
- >
-
- Are you sure you want to remove {'"'}
- {provider.id}
- {'"'} as an ID provider? The removal of {provider.id} as an ID
- provider is irreversible and might adversely affect your system.
-
- setConfirmText(e.target.value)}
- value={confirmText}
- type="text"
- placeholder={expectedConfirmText}
- label={
- <>
- Type {expectedConfirmText} to confirm
- >
- }
- />
-
-
- )}
-
- Delete
-
- >
+ />
+ >
+ }
+ confirmTitle="Remove ID provider"
+ entityName="Provider"
+ invalidateQuery={queryKeys.providers}
+ onDelete={() => {
+ if (!provider.id) {
+ const error = "Cannot delete provider without id";
+ console.error(error, provider);
+ return Promise.reject(error);
+ }
+ return deleteProvider(provider.id);
+ }}
+ successPath={urls.providers.index}
+ successMessage={`Provider ${provider.id} deleted.`}
+ />
);
};
diff --git a/ui/src/pages/providers/DeleteProviderBtn/types.ts b/ui/src/pages/providers/DeleteProviderBtn/types.ts
new file mode 100644
index 000000000..bc7a26ff2
--- /dev/null
+++ b/ui/src/pages/providers/DeleteProviderBtn/types.ts
@@ -0,0 +1,3 @@
+export enum Label {
+ CONFIRM = "Remove",
+}