diff --git a/src/app/preferences/components/Routes/Routes.tsx b/src/app/preferences/components/Routes/Routes.tsx index bb98816247..305345a0c9 100644 --- a/src/app/preferences/components/Routes/Routes.tsx +++ b/src/app/preferences/components/Routes/Routes.tsx @@ -2,6 +2,7 @@ import { Redirect } from "react-router"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom-v5-compat"; import DeleteSSHKey from "../../views/SSHKeys/DeleteSSHKey"; +import DeleteSSLKey from "../../views/SSLKeys/DeleteSSLKey"; import PageContent from "@/app/base/components/PageContent"; import urls from "@/app/base/urls"; @@ -125,6 +126,17 @@ const Routes = (): JSX.Element => { } path={getRelativeRoute(urls.preferences.sslKeys.add, base)} /> + } + sidePanelTitle="Delete SSL key" + > + + + } + path={getRelativeRoute(urls.preferences.sslKeys.delete(null), base)} + /> } path="*" /> ); diff --git a/src/app/preferences/urls.ts b/src/app/preferences/urls.ts index d0b9b4d6b9..6ec279a839 100644 --- a/src/app/preferences/urls.ts +++ b/src/app/preferences/urls.ts @@ -1,3 +1,5 @@ +import type { SSLKey, SSLKeyMeta } from "../store/sslkey/types"; + import type { Token } from "@/app/store/token/types"; import { argPath } from "@/app/utils"; @@ -17,6 +19,9 @@ const urls = { }, sslKeys: { add: "/account/prefs/ssl-keys/add", + delete: argPath<{ id: SSLKey[SSLKeyMeta.PK] }>( + "/account/prefs/ssl-keys/:id/delete" + ), index: "/account/prefs/ssl-keys", }, }; diff --git a/src/app/preferences/views/SSLKeys/DeleteSSLKey/DeleteSSLKey.test.tsx b/src/app/preferences/views/SSLKeys/DeleteSSLKey/DeleteSSLKey.test.tsx new file mode 100644 index 0000000000..f985119061 --- /dev/null +++ b/src/app/preferences/views/SSLKeys/DeleteSSLKey/DeleteSSLKey.test.tsx @@ -0,0 +1,81 @@ +import configureStore from "redux-mock-store"; + +import DeleteSSLKey from "./DeleteSSLKey"; + +import { Label as SSLKeyListLabels } from "@/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList"; +import type { RootState } from "@/app/store/root/types"; +import { + sslKey as sslKeyFactory, + sslKeyState as sslKeyStateFactory, + rootState as rootStateFactory, +} from "@/testing/factories"; +import { screen, renderWithBrowserRouter, userEvent } from "@/testing/utils"; + +const mockStore = configureStore(); + +let state: RootState; + +beforeEach(() => { + state = rootStateFactory({ + sslkey: sslKeyStateFactory({ + loading: false, + loaded: true, + items: [ + sslKeyFactory({ + id: 1, + key: "ssh-rsa aabb", + }), + sslKeyFactory({ + id: 2, + key: "ssh-rsa ccdd", + }), + sslKeyFactory({ + id: 3, + key: "ssh-rsa eeff", + }), + sslKeyFactory({ + id: 4, + key: "ssh-rsa gghh", + }), + sslKeyFactory({ id: 5, key: "ssh-rsa gghh" }), + ], + }), + }); +}); + +it("can show a delete confirmation", () => { + renderWithBrowserRouter(, { + route: "/account/prefs/ssl-keys/1/delete", + routePattern: "/account/prefs/ssl-keys/:id/delete", + state, + }); + expect(screen.getByRole("form", { name: SSLKeyListLabels.DeleteConfirm })); + expect( + screen.getByText(/Are you sure you want to delete this SSL key?/i) + ).toBeInTheDocument(); +}); + +it("can delete an SSL key", async () => { + const store = mockStore(state); + renderWithBrowserRouter(, { + route: "/account/prefs/ssl-keys/1/delete", + routePattern: "/account/prefs/ssl-keys/:id/delete", + store, + }); + + await userEvent.click(screen.getByRole("button", { name: "Delete" })); + expect( + store.getActions().find((action) => action.type === "sslkey/delete") + ).toEqual({ + type: "sslkey/delete", + payload: { + params: { + id: 1, + }, + }, + meta: { + model: "sslkey", + method: "delete", + }, + }); +}); diff --git a/src/app/preferences/views/SSLKeys/DeleteSSLKey/DeleteSSLKey.tsx b/src/app/preferences/views/SSLKeys/DeleteSSLKey/DeleteSSLKey.tsx new file mode 100644 index 0000000000..27cc0da516 --- /dev/null +++ b/src/app/preferences/views/SSLKeys/DeleteSSLKey/DeleteSSLKey.tsx @@ -0,0 +1,45 @@ +import { useOnEscapePressed } from "@canonical/react-components"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom-v5-compat"; + +import ModelActionForm from "@/app/base/components/ModelActionForm"; +import { useAddMessage, useGetURLId } from "@/app/base/hooks"; +import urls from "@/app/preferences/urls"; +import { Label } from "@/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList"; +import { actions as sslkeyActions } from "@/app/store/sslkey"; +import sslkeySelectors from "@/app/store/sslkey/selectors"; +import { isId } from "@/app/utils"; + +const DeleteSSLKey = () => { + const id = useGetURLId("id"); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const saved = useSelector(sslkeySelectors.saved); + const saving = useSelector(sslkeySelectors.saving); + const onClose = () => navigate({ pathname: urls.sslKeys.index }); + useOnEscapePressed(() => onClose()); + useAddMessage(saved, sslkeyActions.cleanup, "SSL key removed successfully."); + + if (!isId(id)) { + return

SSL key not found

; + } + + return ( + { + dispatch(sslkeyActions.delete(id)); + }} + saved={saved} + savedRedirect={urls.sslKeys.index} + saving={saving} + submitAppearance="negative" + submitLabel="Delete" + /> + ); +}; + +export default DeleteSSLKey; diff --git a/src/app/preferences/views/SSLKeys/DeleteSSLKey/index.ts b/src/app/preferences/views/SSLKeys/DeleteSSLKey/index.ts new file mode 100644 index 0000000000..5630e6e049 --- /dev/null +++ b/src/app/preferences/views/SSLKeys/DeleteSSLKey/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteSSLKey"; diff --git a/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.test.tsx b/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.test.tsx index 7c77c04265..2109973995 100644 --- a/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.test.tsx +++ b/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.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 SSLKeyList, { Label as SSLKeyListLabels } from "./SSLKeyList"; @@ -12,16 +10,11 @@ import { rootState as rootStateFactory, } from "@/testing/factories"; import { - userEvent, screen, - render, - within, renderWithMockStore, renderWithBrowserRouter, } from "@/testing/utils"; -const mockStore = configureStore(); - describe("SSLKeyList", () => { let state: RootState; @@ -103,108 +96,6 @@ describe("SSLKeyList", () => { expect(screen.getByRole("grid", { name: SSLKeyListLabels.Title })); }); - it("can show a delete confirmation", async () => { - renderWithMockStore( - - - - - , - { state } - ); - let row = screen.getByRole("row", { name: "ssh-rsa aabb" }); - expect(row).not.toHaveClass("is-active"); - // Click on the delete button: - await userEvent.click(within(row).getByRole("button", { name: "Delete" })); - row = screen.getByRole("row", { name: "ssh-rsa aabb" }); - expect(row).toHaveClass("is-active"); - }); - - it("can delete a SSL key", async () => { - const store = mockStore(state); - render( - - - - - - - - ); - let row = screen.getByRole("row", { name: "ssh-rsa aabb" }); - - // 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).getByLabelText(SSLKeyListLabels.DeleteConfirm) - ).getByRole("button", { - name: "Delete", - }) - ); - expect( - store.getActions().find((action) => action.type === "sslkey/delete") - ).toEqual({ - type: "sslkey/delete", - payload: { - params: { - id: 1, - }, - }, - meta: { - model: "sslkey", - method: "delete", - }, - }); - }); - - it("can add a message when a SSL key is deleted", async () => { - state.sslkey.saved = true; - const store = mockStore(state); - render( - - - - - - - - ); - let row = screen.getByRole("row", { name: "ssh-rsa aabb" }); - - // 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).getByLabelText(SSLKeyListLabels.DeleteConfirm) - ).getByRole("button", { - name: "Delete", - }) - ); - - const actions = store.getActions(); - expect(actions.some((action) => action.type === "sslkey/cleanup")).toBe( - true - ); - expect(actions.some((action) => action.type === "message/add")).toBe(true); - }); - it("displays an empty state message", () => { state.sslkey.items = []; renderWithBrowserRouter(, { diff --git a/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.tsx b/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.tsx index 813a1b0bc7..cdd4f94ee1 100644 --- a/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.tsx +++ b/src/app/preferences/views/SSLKeys/SSLKeyList/SSLKeyList.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 { useFetchActions, useWindowTitle } from "@/app/base/hooks"; import urls from "@/app/base/urls"; import SettingsTable from "@/app/settings/components/SettingsTable"; import { actions as sslkeyActions } from "@/app/store/sslkey"; import sslkeySelectors from "@/app/store/sslkey/selectors"; -import type { SSLKey, SSLKeyMeta, SSLKeyState } from "@/app/store/sslkey/types"; +import type { SSLKey } from "@/app/store/sslkey/types"; export enum Label { Title = "SSL keys", DeleteConfirm = "Confirm or cancel deletion of SSL key", } -const generateRows = ( - sslkeys: SSLKey[], - expandedId: SSLKey[SSLKeyMeta.PK] | null, - setExpandedId: (id: SSLKey[SSLKeyMeta.PK] | null) => void, - hideExpanded: () => void, - dispatch: Dispatch, - saved: SSLKeyState["saved"], - saving: SSLKeyState["saving"], - setDeleting: (deleting: boolean) => void -) => +const generateRows = (sslkeys: SSLKey[]) => sslkeys.map(({ id, display, key }) => { - const expanded = expandedId === id; return { "aria-label": key, - className: expanded ? "p-table__row is-active" : null, + className: "p-table__row is-active", columns: [ { className: "u-truncate", @@ -45,28 +27,15 @@ const generateRows = ( }, { content: ( - setExpandedId(id)} /> + ), className: "u-align--right", }, ], "data-testid": "sslkey-row", - expanded: expanded, - expandedContent: expanded && ( -
- { - setDeleting(true); - dispatch(sslkeyActions.delete(id)); - }} - /> -
- ), key: id, sortData: { key: display, @@ -75,31 +44,13 @@ const generateRows = ( }); const SSLKeyList = (): JSX.Element => { - const [expandedId, setExpandedId] = useState( - null - ); const sslkeyErrors = useSelector(sslkeySelectors.errors); const sslkeyLoading = useSelector(sslkeySelectors.loading); const sslkeyLoaded = useSelector(sslkeySelectors.loaded); const sslkeys = useSelector(sslkeySelectors.all); - const saved = useSelector(sslkeySelectors.saved); - const saving = useSelector(sslkeySelectors.saving); - const [deleting, setDeleting] = useState(false); - const dispatch = useDispatch(); useWindowTitle(Label.Title); - useAddMessage( - saved && deleting, - sslkeyActions.cleanup, - "SSL key removed successfully.", - () => setDeleting(false) - ); - - const hideExpanded = () => { - setExpandedId(null); - }; - useFetchActions([sslkeyActions.fetch]); return ( @@ -125,16 +76,7 @@ const SSLKeyList = (): JSX.Element => { ]} loaded={sslkeyLoaded} loading={sslkeyLoading} - rows={generateRows( - sslkeys, - expandedId, - setExpandedId, - hideExpanded, - dispatch, - saved, - saving, - setDeleting - )} + rows={generateRows(sslkeys)} tableClassName="sslkey-list" />