Skip to content

Commit

Permalink
build a signing key table with modals
Browse files Browse the repository at this point in the history
  • Loading branch information
chillkang committed Jul 31, 2023
1 parent c0f55c8 commit f69b7ae
Show file tree
Hide file tree
Showing 12 changed files with 567 additions and 8 deletions.
20 changes: 17 additions & 3 deletions static/js/brand-store/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { atom } from "recoil";
import { atom, RecoilState } from "recoil";
import type { Store, Model, Policy, SigningKey } from "../types/shared";

const brandStoresState = atom({
Expand Down Expand Up @@ -34,9 +34,21 @@ const policiesListFilterState = atom({
default: "" as string,
});

const signingKeysListState = atom({
const signingKeysListState = atom<SigningKey[]>({
key: "signingKeysList",
default: [] as Array<SigningKey>,
default: [],
});

const signingKeysListFilterState = atom({
key: "signingKeysListFilter",
default: "" as string,
});

const newSigningKeyState = atom({
key: "newSigningKey",
default: {
name: "",
},
});

export {
Expand All @@ -47,4 +59,6 @@ export {
policiesListState,
policiesListFilterState,
signingKeysListState,
signingKeysListFilterState,
newSigningKeyState,
};
1 change: 1 addition & 0 deletions static/js/brand-store/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function App() {
element={<Policies />}
/>
<Route path="/admin/:id/signing-keys" element={<SigningKeys />} />
<Route path="/admin/:id/signing-keys/create" element={<SigningKeys />} />
</Routes>
</div>
</QueryClientProvider>
Expand Down
58 changes: 58 additions & 0 deletions static/js/brand-store/components/SigningKeys/DisableModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";
import { Modal, Button } from "@canonical/react-components";
import { useRecoilValue } from "recoil";

import { filteredSigningKeysListState } from "../../selectors";

import type { SigningKey } from "../../types/shared";

type Props = {
disableModalOpen: boolean;
setDisableModalOpen: Function;
handleDisable: Function;
isDeleting: boolean;
signingKey: SigningKey;
};

function DisableModal({
disableModalOpen,
setDisableModalOpen,
handleDisable,
isDeleting,
signingKey,
}: Props) {

const closeHandler = () => setDisableModalOpen(false);
const signingKeysList = useRecoilValue<Array<SigningKey>>(filteredSigningKeysListState);

if (!disableModalOpen) {
return null;
}

return (
<Modal
close={closeHandler}
title="Confirm disable"
buttonRow={
<>
<Button dense className="u-no-margin--bottom" onClick={closeHandler}>
Cancel
</Button>

<Button
dense
className="p-button--negative u-no-margin--bottom"
onClick={() => handleDisable()}
disabled={isDeleting}
>
Disable
</Button>
</>
}
>
<p>{`Warning: This will permanently disable the signing key ${signingKey.name}.`}</p>
</Modal>
);
}

export default DisableModal;
63 changes: 63 additions & 0 deletions static/js/brand-store/components/SigningKeys/ModelsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import { Modal, Icon } from "@canonical/react-components";
import { useRecoilValue } from "recoil";

import { filteredModelsListState, filteredPoliciesListState } from "../../selectors";

import type { SigningKey, Model, Policy } from "../../types/shared";

type Props = {
modelsModalOpen: boolean;
setModelsModalOpen: Function;
signingKey: SigningKey;
};

function ModelsModal({
modelsModalOpen,
setModelsModalOpen,
signingKey,
}: Props) {

const closeHandler = () => setModelsModalOpen(false);
const filteredModels = useRecoilValue<Array<Model>>(filteredModelsListState);
const filteredPolicies = useRecoilValue<Array<Policy>>(filteredPoliciesListState);

if (!modelsModalOpen) {
return null;
}

const relatedModels = filteredModels.filter((model) =>
signingKey.models.includes(model.name)
);

const relatedPolicies = filteredPolicies.filter((policy) =>
signingKey.models.includes(policy["model-name"])
);

return (
<Modal
title={
<React.Fragment>
<Icon name="warning" />
{` Deactivate ${signingKey.name}`}
</React.Fragment>}
close={closeHandler}
>
<h3>{signingKey.name} is used in :</h3>
<ul>
{relatedModels.map((model) => {
const relatedPolicy = relatedPolicies.find((policy) => policy["model-name"] === model.name);
return (
<React.Fragment key={model.name}>
<li>{model.name}</li>
<li>{relatedPolicy ? relatedPolicy["created-by"] : "No policy found"}</li>
</React.Fragment>
);
})}
</ul>
<p>You need to update each policy with a new key first to be able to delete this one.</p>
</Modal>
);
}

export default ModelsModal;
47 changes: 47 additions & 0 deletions static/js/brand-store/components/SigningKeys/SigningKeys.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { RecoilRoot } from "recoil";
import { QueryClient, QueryClientProvider } from "react-query";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import "@testing-library/jest-dom";

import SigningKeys from "./SigningKeys";

let mockFilterQuery = "signing-key-1";

jest.mock("react-router-dom", () => {
return {
...jest.requireActual("react-router-dom"),
useSearchParams: () => [new URLSearchParams({ filter: mockFilterQuery })],
};
});

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
},
});

function renderComponent() {
return render(
<RecoilRoot>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<SigningKeys />
</QueryClientProvider>
</BrowserRouter>
</RecoilRoot>
);
}

describe("Models", () => {
it("displays a table of models", () => {
renderComponent();
expect(screen.getByTestId("signing-keys-table")).toBeInTheDocument();
});
});
96 changes: 92 additions & 4 deletions static/js/brand-store/components/SigningKeys/SigningKeys.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,108 @@
import React from "react";
import React, { useState } from "react";
import { useQuery } from "react-query";
import { useSetRecoilState, useRecoilState } from "recoil";
import {
Link,
useParams,
useLocation,
useNavigate,
useSearchParams,
} from "react-router-dom";
import { Row, Col, Notification } from "@canonical/react-components";

import {
signingKeysListState,
signingKeysListFilterState,
policiesListState,
} from "../../atoms";

import SectionNav from "../SectionNav";
import SigningKeysTable from "./SigningKeysTable";

import { isClosedPanel } from "../../utils";

import type { SigningKey, Policy, Query } from "../../types/shared";

function SigningKeys() {
const getSigningKeys = async () => {
const response = await fetch(`/admin/store/${id}/signing-keys`);

if (!response.ok) {
throw new Error("There was a problem fetching signing keys");
}

const signingKeysData = await response.json();

if (!signingKeysData.success) {
throw new Error(signingKeysData.message);
}

setSigningKeysList(signingKeysData.data);
}

const { id } = useParams();
const location = useLocation();
const navigate = useNavigate();
const { isLoading, isError, error }: Query = useQuery(
"signingKeys",
getSigningKeys
);
const setSigningKeysList = useSetRecoilState<Array<SigningKey>>(signingKeysListState);
const setFilter = useSetRecoilState<string>(signingKeysListFilterState);
const [searchParams] = useSearchParams();
const [showNotification, setShowNotification] = useState<boolean>(false);
const [showErrorNotification, setShowErrorNotification] = useState<boolean>(
false
);

return (
<main className="l-main">
<div className="p-panel">
<div className="p-panel__content">
<div className="u-fixed-width">
<SectionNav sectionName="signing-keys" />
</div>
<div className="u-fixed-width">
<p>This is where the signing keys table will be</p>
{showNotification && (
<div className="u-fixed-width">
<Notification
severity="positive"
onDismiss={() => {
setShowNotification(false);
}}
>
New signing key created
</Notification>
</div>
)}
{showErrorNotification && (
<div className="u-fixed-width">
<Notification
severity="negative"
onDismiss={() => {
setShowErrorNotification(false);
}}
>
Unable to create signing key
</Notification>
</div>
)}
<Row>
<Col size={6}>
<Link className="p-button" to={`/admin/${id}/signing-keys/create`}>
Create new signing key
</Link>
</Col>
<Col size={6}>

</Col>
</Row>
<div className="u-fixed-width">
{isLoading && <p>Fetching signing keys...</p>}
{isError && error && <p>Error: {error.message}</p>}
<SigningKeysTable />
</div>
</div>
</div>
</div>
</main>
);
}
Expand Down
Loading

0 comments on commit f69b7ae

Please sign in to comment.