Skip to content

Commit

Permalink
feat: move SSH Key delete form to sidepanel (#5291)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jay-Topher authored Jan 19, 2024
1 parent 32db438 commit 74ecb1c
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 148 deletions.
77 changes: 11 additions & 66 deletions src/app/base/components/SSHKeyList/SSHKeyList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import configureStore from "redux-mock-store";

import SSHKeyList from "./SSHKeyList";

import * as sidePanelHooks from "@/app/base/side-panel-context";
import urls from "@/app/preferences/urls";
import type { RootState } from "@/app/store/root/types";
import {
sshKey as sshKeyFactory,
Expand All @@ -19,8 +21,15 @@ const mockStore = configureStore<RootState>();

describe("SSHKeyList", () => {
let state: RootState;
const setSidePanelContent = vi.fn();

beforeEach(() => {
vi.spyOn(sidePanelHooks, "useSidePanel").mockReturnValue({
setSidePanelContent,
sidePanelContent: null,
setSidePanelSize: vi.fn(),
sidePanelSize: "regular",
});
state = rootStateFactory({
sshkey: sshKeyStateFactory({
loading: false,
Expand Down Expand Up @@ -116,7 +125,7 @@ describe("SSHKeyList", () => {
).toBeInTheDocument();
});

it("can show a delete confirmation", async () => {
it("can trigger a delete confirmation form", async () => {
renderWithBrowserRouter(<SSHKeyList />, {
route: "/account/prefs/ssh-keys",
state,
Expand All @@ -125,71 +134,7 @@ describe("SSHKeyList", () => {
expect(row).not.toHaveClass("is-active");
// Click on the delete button:
await userEvent.click(screen.getAllByRole("button", { name: "Delete" })[0]);
row = screen.getAllByTestId("sshkey-row")[0];
expect(row).toHaveClass("is-active");
expect(
screen.getByText("Are you sure you want to delete this SSH key?")
).toBeInTheDocument();
});

it("can delete a SSH key", async () => {
const store = mockStore(state);
renderWithBrowserRouter(<SSHKeyList />, {
route: "/account/prefs/ssh-keys",
store,
});
// Click on the delete button:
await userEvent.click(screen.getAllByText("Delete")[0]);
// Click on the delete confirm button
await userEvent.click(screen.getByTestId("action-confirm"));
expect(
store.getActions().find((action) => action.type === "sshkey/delete")
).toEqual({
type: "sshkey/delete",
payload: {
params: {
id: 1,
},
},
meta: {
model: "sshkey",
method: "delete",
},
});
});

it("can delete a group of SSH keys", async () => {
const store = mockStore(state);
renderWithBrowserRouter(<SSHKeyList />, {
route: "/account/prefs/ssh-keys",
store,
});
// Click on the delete button:
await userEvent.click(screen.getAllByRole("button", { name: "Delete" })[1]);
// Click on the delete confirm button
await userEvent.click(screen.getByTestId("action-confirm"));
expect(
store.getActions().filter((action) => action.type === "sshkey/delete")
.length
).toEqual(2);
});

it("can add a message when a SSH key is deleted", async () => {
state.sshkey.saved = true;
const store = mockStore(state);
renderWithBrowserRouter(<SSHKeyList />, {
route: "/account/prefs/ssh-keys",
store,
});
// Click on the delete button:
await userEvent.click(screen.getAllByRole("button", { name: "Delete" })[0]);
// Simulate clicking on the delete confirm button.
await userEvent.click(screen.getByTestId("action-confirm"));
const actions = store.getActions();
expect(actions.some((action) => action.type === "sshkey/cleanup")).toBe(
true
);
expect(actions.some((action) => action.type === "message/add")).toBe(true);
expect(window.location.pathname).toBe(urls.sshKeys.delete);
});

it("displays a message if there are no SSH keys", () => {
Expand Down
99 changes: 18 additions & 81 deletions src/app/base/components/SSHKeyList/SSHKeyList.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import type { ReactNode } from "react";
import { useState } from "react";

import { Button, Notification } from "@canonical/react-components";
import { useDispatch, useSelector } from "react-redux";
import type { Dispatch } from "redux";
import { useSelector } from "react-redux";
import type { NavigateFunction } from "react-router-dom-v5-compat";
import { useNavigate } from "react-router-dom-v5-compat";

import TableDeleteConfirm from "@/app/base/components/TableDeleteConfirm";
import { useFetchActions, useAddMessage } from "@/app/base/hooks";
import { useFetchActions } from "@/app/base/hooks";
import urls from "@/app/preferences/urls";
import SettingsTable from "@/app/settings/components/SettingsTable";
import type { Props as SettingsTableProps } from "@/app/settings/components/SettingsTable/SettingsTable";
import { actions as sshkeyActions } from "@/app/store/sshkey";
import sshkeySelectors from "@/app/store/sshkey/selectors";
import type {
KeySource,
SSHKey,
SSHKeyMeta,
SSHKeyState,
} from "@/app/store/sshkey/types";
import type { KeySource, SSHKey } from "@/app/store/sshkey/types";

type Props = {
sidebar?: boolean;
} & Partial<SettingsTableProps>;
type Props = Partial<SettingsTableProps>;

const formatKey = (key: SSHKey["key"]) => {
const parts = key.split(" ");
Expand Down Expand Up @@ -79,21 +72,10 @@ const generateKeyCols = (keys: SSHKey[], deleteButton: ReactNode) => {
);
};

const generateRows = (
sshkeys: SSHKey[],
expandedId: SSHKey[SSHKeyMeta.PK] | null,
setExpandedId: (id: SSHKey[SSHKeyMeta.PK] | null) => void,
hideExpanded: () => void,
dispatch: Dispatch,
saved: SSHKeyState["saved"],
saving: SSHKeyState["saving"],
sidebar: boolean,
setDeleting: (ids: SSHKey[SSHKeyMeta.PK][]) => void
) =>
const generateRows = (sshkeys: SSHKey[], navigate: NavigateFunction) =>
groupBySource(sshkeys).map(([id, group]) => {
const expanded = expandedId === id;
const ids: number[] = group.keys.map((key: SSHKey) => key.id);
return {
className: expanded ? "p-table__row is-active" : null,
columns: [
{
content: group.source,
Expand All @@ -108,34 +90,19 @@ const generateRows = (
appearance="base"
className="is-dense u-table-cell-padding-overlap"
hasIcon
onClick={() => {
setExpandedId(id);
}}
onClick={() =>
navigate({
pathname: urls.sshKeys.delete,
search: `?ids=${ids.join()}`,
})
}
>
<i className="p-icon--delete">Delete</i>
</Button>
),
},
],
"data-testid": "sshkey-row",
expanded: expanded,
expandedContent: expanded && (
<TableDeleteConfirm
deleted={saved}
deleting={saving}
message={`Are you sure you want to delete ${
group.keys.length > 1 ? "these SSH keys" : "this SSH key"
}?`}
onClose={hideExpanded}
onConfirm={() => {
group.keys.forEach((key: SSHKey) => {
dispatch(sshkeyActions.delete(key.id));
});
setDeleting(group.keys.map(({ id }: SSHKey) => id));
}}
sidebar={sidebar}
/>
),
key: id,
sortData: {
source: group.source,
Expand All @@ -144,32 +111,12 @@ const generateRows = (
};
});

const SSHKeyList = ({ sidebar = true, ...tableProps }: Props): JSX.Element => {
const [expandedId, setExpandedId] = useState<SSHKey[SSHKeyMeta.PK] | null>(
null
);
const [deleting, setDeleting] = useState<SSHKey[SSHKeyMeta.PK][]>([]);
const SSHKeyList = ({ ...tableProps }: Props): JSX.Element => {
const sshkeyErrors = useSelector(sshkeySelectors.errors);
const sshkeyLoading = useSelector(sshkeySelectors.loading);
const sshkeyLoaded = useSelector(sshkeySelectors.loaded);
const sshkeys = useSelector(sshkeySelectors.all);
const saved = useSelector(sshkeySelectors.saved);
const saving = useSelector(sshkeySelectors.saving);
const dispatch = useDispatch();
const sshKeysDeleted =
deleting.length > 0 &&
!deleting.some((id) => !sshkeys.find((key) => key.id === id));

useAddMessage(
saved && sshKeysDeleted,
sshkeyActions.cleanup,
"SSH key removed successfully.",
() => setDeleting([])
);

const hideExpanded = () => {
setExpandedId(null);
};
const navigate = useNavigate();

useFetchActions([sshkeyActions.fetch]);

Expand Down Expand Up @@ -203,17 +150,7 @@ const SSHKeyList = ({ sidebar = true, ...tableProps }: Props): JSX.Element => {
]}
loaded={sshkeyLoaded}
loading={sshkeyLoading}
rows={generateRows(
sshkeys,
expandedId,
setExpandedId,
hideExpanded,
dispatch,
saved,
saving,
sidebar,
setDeleting
)}
rows={generateRows(sshkeys, navigate)}
tableClassName="sshkey-list"
{...tableProps}
/>
Expand Down
4 changes: 4 additions & 0 deletions src/app/base/side-panel-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
NetworkDiscoverySidePanelViews,
type NetworkDiscoverySidePanelContent,
} from "@/app/networkDiscovery/views/constants";
import { PreferenceSidePanelViews } from "@/app/preferences/constants";
import type { PreferenceSidePanelContent } from "@/app/preferences/types";
import {
SubnetSidePanelViews,
type SubnetSidePanelContent,
Expand Down Expand Up @@ -64,6 +66,7 @@ export type SidePanelContent =
| VLANDetailsSidePanelContent
| FabricDetailsSidePanelContent
| ImageSidePanelContent
| PreferenceSidePanelContent
| SubnetDetailsSidePanelContent
| SpaceDetailsSidePanelContent
| null;
Expand Down Expand Up @@ -93,6 +96,7 @@ export const SidePanelViews = {
...VLANDetailsSidePanelViews,
...FabricDetailsSidePanelViews,
...ImageSidePanelViews,
...PreferenceSidePanelViews,
...SubnetDetailsSidePanelViews,
...SpaceDetailsSidePanelViews,
} as const;
Expand Down
2 changes: 1 addition & 1 deletion src/app/intro/views/UserIntro/UserIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const UserIntro = (): JSX.Element => {
Add multiple keys from Launchpad and Github or enter them manually.
</p>
<h4>Keys</h4>
{hasSSHKeys ? <SSHKeyList sidebar={false} /> : null}
{hasSSHKeys ? <SSHKeyList /> : null}
<SSHKeyForm
onSaveAnalytics={{
action: "Import",
Expand Down
13 changes: 13 additions & 0 deletions src/app/preferences/components/Routes/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Redirect } from "react-router";
import { Route, Routes as ReactRouterRoutes } from "react-router-dom-v5-compat";

import DeleteSSHKey from "../../views/SSHKeys/DeleteSSHKey";

import PageContent from "@/app/base/components/PageContent";
import urls from "@/app/base/urls";
import NotFound from "@/app/base/views/NotFound";
Expand Down Expand Up @@ -93,6 +95,17 @@ const Routes = (): JSX.Element => {
}
path={getRelativeRoute(urls.preferences.sshKeys.add, base)}
/>
<Route
element={
<PageContent
sidePanelContent={<DeleteSSHKey />}
sidePanelTitle="Delete SSH key"
>
<SSHKeyList />
</PageContent>
}
path={getRelativeRoute(urls.preferences.sshKeys.delete, base)}
/>
<Route
element={
<PageContent sidePanelContent={null} sidePanelTitle={null}>
Expand Down
4 changes: 4 additions & 0 deletions src/app/preferences/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export const preferencesNavItems: NavItem[] = [
label: "SSL keys",
},
];

export const PreferenceSidePanelViews = {
DELETE_SSH_KEYS: ["", "deleteSSHKeys"],
} as const;
17 changes: 17 additions & 0 deletions src/app/preferences/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ValueOf } from "@canonical/react-components";

import type { PreferenceSidePanelViews } from "./constants";

import type { SidePanelContent, SetSidePanelContent } from "@/app/base/types";
import type { SSHKey } from "@/app/store/sshkey/types";

type SSHKeyGroup = {
keys: SSHKey[];
};
export type PreferenceSidePanelContent = SidePanelContent<
ValueOf<typeof PreferenceSidePanelViews>,
{ group?: SSHKeyGroup }
>;

export type PreferenceSetSidePanelContent =
SetSidePanelContent<PreferenceSidePanelContent>;
1 change: 1 addition & 0 deletions src/app/preferences/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const urls = {
index: "/account/prefs",
sshKeys: {
add: "/account/prefs/ssh-keys/add",
delete: "/account/prefs/ssh-keys/delete",
index: "/account/prefs/ssh-keys",
},
sslKeys: {
Expand Down
Loading

0 comments on commit 74ecb1c

Please sign in to comment.