Skip to content

Commit

Permalink
Add sorting to certificate list
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksandr-slobodian committed May 20, 2024
1 parent 4296d2f commit e89e5ff
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 15 deletions.
1 change: 1 addition & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Preview } from "@storybook/react";
import { ThemeProvider, ToastProvider } from "@peculiar/react-components";
import { theme } from "../src/config/theme";
import i18n from "../src/i18n";
import "../public/assets/styles/reset.css";
import "../src/global.scss";
import "./storybook.css";

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"devDependencies": {
"@eslint/js": "^9.1.1",
"@faker-js/faker": "^8.4.1",
"@storybook/addon-actions": "^8.0.9",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
Expand Down Expand Up @@ -43,6 +44,7 @@
"clsx": "^2.1.1",
"i18next": "^23.11.2",
"intl-messageformat": "^10.5.11",
"lodash": "^4.17.21",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-dropzone": "^14.2.3",
Expand Down
8 changes: 8 additions & 0 deletions src/app.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@
background-color: var(--pv-color-gray-2);
}
}
.certificate_list {
background-color: var(--pv-color-gray-2);
th {
position: sticky;
top: var(--pv-top-header-height);
z-index: 99;
}
}
14 changes: 13 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CertificatesProvidersList } from "./components/certificates-providers-l
import { CertificatesTopbar } from "./components/certificates-topbar";
import { CertificateDeleteDialog } from "./components/certificate-delete-dialog";
import { CertificateViewerDialog } from "./components/certificate-viewer-dialog";
import { useSortList } from "./hooks/sort-list";
import { useCertificateImportDialog } from "./dialogs/certificate-import-dialog";

import styles from "./app.module.scss";
Expand All @@ -31,6 +32,13 @@ export function App() {
handleCertificateViewerClose,
} = useApp();

const {
list: sortedCertificates,
name: currentSortName,
derection: currentSortDir,
handleSort,
} = useSortList(certificates, "notAfter");

const {
open: handleCertificateImportDialogOpen,
dialog: certificateImportDialog,
Expand Down Expand Up @@ -61,7 +69,11 @@ export function App() {
></CertificatesTopbar>
{fetching.certificates ? (
<CertificatesList
certificates={certificates}
currentSortName={currentSortName}
currentSortDir={currentSortDir}
onSort={handleSort}
className={styles.certificate_list}
certificates={sortedCertificates}
onDelete={handleCertificateDeleteDialogOpen}
onViewDetails={handleCertificateViewerOpen}
/>
Expand Down
34 changes: 34 additions & 0 deletions src/components/certificates-list/CertificatesList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CertificatesList } from "./CertificatesList";
import { faker } from "@faker-js/faker";

const meta: Meta<typeof CertificatesList> = {
title: "Components/CertificatesList",
component: CertificatesList,
};

export default meta;
type Story = StoryObj<typeof CertificatesList>;

export const Default: Story = {
args: {
certificates: Array.from(Array(10)).map(() => ({
index: faker.string.uuid(),
issuerName: "",
notAfter: faker.date.future(),
notBefore: faker.date.past(),
providerID: faker.string.uuid(),
publicKey: "" as unknown as CryptoKey,
raw: new ArrayBuffer(0),
serialNumber: faker.string.uuid(),
subjectName: "",
type: "x509",
id: faker.string.uuid(),
label: faker.internet.domainName(),
privateKeyId: "",
subject: {
commonName: "",
},
})),
},
};
64 changes: 57 additions & 7 deletions src/components/certificates-list/CertificatesList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { ComponentProps, useState } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { Button, IconButton, Typography } from "@peculiar/react-components";
Expand All @@ -18,6 +18,7 @@ import { CertificateSerialNumber } from "../certificate-serial-number";
import { downloadCertificate } from "../../utils/download-certificate";
import { CopyIconButton } from "../copy-icon-button";
import { certificateRawToPem } from "../../utils/certificate";
import { SortButton } from "../sort-button";

import { CertificateProps } from "../../types";

Expand All @@ -26,16 +27,34 @@ import DownloadIcon from "../../icons/download-20.svg?react";

import styles from "./styles/index.module.scss";

type TSortColumnName = keyof Pick<
CertificateProps,
"label" | "serialNumber" | "notAfter"
>;
type TSortColumnDir = "asc" | "desc";

interface CertificatesListProps {
certificates: CertificateProps[];
currentSortName: TSortColumnName | string;
currentSortDir: TSortColumnDir;
onSort: (name: TSortColumnName, dir: TSortColumnDir) => void;
onDelete: (id: string, name: string) => void;
onViewDetails: (certificate: CertificateProps) => void;
className?: ComponentProps<"table">["className"];
}

export const CertificatesList: React.FunctionComponent<
CertificatesListProps
> = (props) => {
const { certificates, onViewDetails, onDelete } = props;
const {
certificates,
className,
currentSortName,
currentSortDir,
onSort,
onViewDetails,
onDelete,
} = props;

const { t } = useTranslation();

Expand All @@ -54,15 +73,43 @@ export const CertificatesList: React.FunctionComponent<
);
}

const renderSortTitle = (name: TSortColumnName, title: string) => (
<SortButton
active={currentSortName === name}
order={currentSortName === name ? currentSortDir : "desc"}
onClick={() =>
onSort(
name,
currentSortName === name
? currentSortDir === "asc"
? "desc"
: "asc"
: "asc"
)
}
>
{t(title)}
</SortButton>
);

return (
<div className={styles.table_wrapper}>
<Table className={styles.list_table}>
<Table className={clsx(styles.list_table, className)}>
<TableHeader>
<TableRow>
<TableHead>{t("certificates.list.header.type")}</TableHead>
<TableHead>{t("certificates.list.header.name")}</TableHead>
<TableHead>{t("certificates.list.header.serial-number")}</TableHead>
<TableHead>{t("certificates.list.header.expires")}</TableHead>
<TableHead>
{renderSortTitle("label", "certificates.list.header.name")}
</TableHead>
<TableHead>
{renderSortTitle(
"serialNumber",
"certificates.list.header.serial-number"
)}
</TableHead>
<TableHead>
{renderSortTitle("notAfter", "certificates.list.header.expires")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand Down Expand Up @@ -112,13 +159,16 @@ export const CertificatesList: React.FunctionComponent<
{t("certificates.list.action.view-details")}
</Button>
<CopyIconButton
value={certificateRawToPem(raw, type)}
value={
raw.byteLength ? certificateRawToPem(raw, type) : ""
}
className={styles.action_icon_button}
/>
<IconButton
tabIndex={0}
title={t("certificates.list.action.download")}
onClick={() =>
raw.byteLength &&
downloadCertificate(label as string, raw, type)
}
size="small"
Expand Down
7 changes: 0 additions & 7 deletions src/components/certificates-list/styles/index.module.scss
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
.list_table {
background-color: var(--pv-color-gray-2);
table-layout: fixed;

thead th,
tbody {
background-color: var(--pv-color-white);
}

th {
position: sticky;
top: var(--pv-top-header-height);
z-index: 99;
}

th:nth-child(1) {
width: 30%;
}
Expand Down
1 change: 1 addition & 0 deletions src/hooks/sort-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useSortList";
26 changes: 26 additions & 0 deletions src/hooks/sort-list/useSortList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState } from "react";
import _orderBy from "lodash/orderBy";

type TSortDir = "asc" | "desc";

export function useSortList(
list: object,
name: string,
direction: TSortDir = "desc"
) {
const [currentName, setCurrentName] = useState(name);
const [currentDerection, setCurrentDirection] = useState(direction);
const handleSort = (name: string, dir: TSortDir) => {
setCurrentName(name);
setCurrentDirection(dir);
};

const sortedList = _orderBy(list, [currentName], [currentDerection]);

return {
list: sortedList,
name: currentName,
derection: currentDerection,
handleSort,
};
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.2.0.tgz#b0a9123e8e91a3d9a2eed3a04a6ed44fdab639aa"
integrity sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==

"@faker-js/faker@^8.4.1":
version "8.4.1"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.4.1.tgz#5d5e8aee8fce48f5e189bf730ebd1f758f491451"
integrity sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==

"@fal-works/esbuild-plugin-global-externals@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4"
Expand Down

0 comments on commit e89e5ff

Please sign in to comment.