Skip to content

Commit

Permalink
Version 2: Certificates list improvements (#158)
Browse files Browse the repository at this point in the history
* Add HighlightedText component

* Add highlight to CertificateName

* Add highlighted text for certificate name in CertificatesList

* Add CertificatesListLoading

* Add CertificatesListLoading

* Add Empty Search to  CertificatesList

---------

Co-authored-by: alex-slobodian <[email protected]>
  • Loading branch information
OleksandrSPV and aleksandr-slobodian authored Jul 4, 2024
1 parent d98f207 commit 9f6a477
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 99 deletions.
3 changes: 3 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export function App() {
certificates={sortedCertificates}
onDelete={handleCertificateDeleteDialogOpen}
onViewDetails={handleCertificateViewerOpen}
loading={
!fetching.certificates || fetching.certificates === "pending"
}
/>
) : null}

Expand Down
14 changes: 11 additions & 3 deletions src/components/certificate-name/CertificateName.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
import React, { ComponentProps } from "react";
import clsx from "clsx";
import { Tooltip, Typography } from "@peculiar/react-components";
import { HighlightedText } from "../highlighted-text";

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

interface CertificateNameProps {
name?: string;
highlight?: string;
className?: ComponentProps<"div">["className"];
}

export const CertificateName: React.FunctionComponent<CertificateNameProps> = (
props
) => {
const { className, name } = props;
const { className, name, highlight } = props;
if (!name) {
return null;
}

const text = highlight ? (
<HighlightedText text={name} highlight={highlight} />
) : (
name
);

return (
<Tooltip placement="bottom-start" offset={5} title={name}>
<Tooltip placement="bottom-start" offset={5} title={text}>
<Typography
className={clsx(className, styles.certificate_name)}
variant="b2"
color="black"
>
{name}
{text}
</Typography>
</Tooltip>
);
Expand Down
13 changes: 13 additions & 0 deletions src/components/certificates-list/CertificatesList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,16 @@ export const Default: Story = {
})),
},
};

export const Empty: Story = {
args: {
certificates: [],
},
};

export const EmptySearch: Story = {
args: {
certificates: [],
highlightedText: "test",
},
};
247 changes: 152 additions & 95 deletions src/components/certificates-list/CertificatesList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React, { ComponentProps, useState } from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import clsx from "clsx";
import { Button, IconButton, Typography } from "@peculiar/react-components";
import {
Button,
IconButton,
Skeleton,
Typography,
} from "@peculiar/react-components";
import {
Table,
TableBody,
Expand All @@ -10,7 +15,7 @@ import {
TableHeader,
TableRow,
} from "../table";
import CertificatesIcon from "../../icons/certificates.svg?react";

import { CertificateTypeLabel } from "../certificate-type-label";
import { Date } from "../date";
import { CertificateName } from "../certificate-name";
Expand All @@ -24,6 +29,8 @@ import { CertificateProps } from "../../types";

import DeleteIcon from "../../icons/delete.svg?react";
import DownloadIcon from "../../icons/download-20.svg?react";
import CertificatesIcon from "../../icons/certificates.svg?react";
import SearchIcon from "../../icons/search-120.svg?react";

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

Expand All @@ -45,6 +52,8 @@ interface CertificatesListProps {
}) => void;
onViewDetails: (certificate: CertificateProps) => void;
className?: ComponentProps<"table">["className"];
highlightedText?: string;
loading?: boolean;
}

export const CertificatesList: React.FunctionComponent<
Expand All @@ -55,6 +64,8 @@ export const CertificatesList: React.FunctionComponent<
className,
currentSortName,
currentSortDir,
highlightedText,
loading,
onSort,
onViewDetails,
onDelete,
Expand All @@ -64,7 +75,33 @@ export const CertificatesList: React.FunctionComponent<

const [currentRow, setCurrentRow] = useState<string>();

if (!certificates?.length) {
if (highlightedText && !certificates?.length && !loading) {
return (
<div className={clsx(styles.empty_list, styles.empty_search_list)}>
<div className={styles.empty_search_list_icon}>
<SearchIcon />
</div>
<Typography variant="b2" color="gray-9">
<Trans
i18nKey="certificates.list.empty-search-text"
values={{ text: highlightedText }}
components={[
<Typography
className={styles.empty_list_search_text}
variant="b2"
color="black"
component="span"
>
{0}
</Typography>,
]}
/>
</Typography>
</div>
);
}

if (!certificates?.length && !loading) {
return (
<div className={styles.empty_list}>
<div className={styles.empty_list_icon}>
Expand All @@ -87,6 +124,7 @@ export const CertificatesList: React.FunctionComponent<
currentSortName === name && currentSortDir === "asc" ? "desc" : "asc"
)
}
disabled={loading}
>
{t(title)}
</SortButton>
Expand All @@ -113,100 +151,119 @@ export const CertificatesList: React.FunctionComponent<
</TableRow>
</TableHeader>
<TableBody>
{certificates.map((certificate) => {
const {
id,
providerID,
serialNumber,
type,
label,
notAfter,
raw,
index,
} = certificate;

return (
<TableRow
tabIndex={0}
key={id}
onClick={() => onViewDetails(certificate)}
onFocus={() => setCurrentRow(id)}
onBlur={() => setCurrentRow(undefined)}
onKeyDown={(event) =>
["Space", "Enter"].includes(event.code) &&
onViewDetails(certificate)
}
onMouseOver={() => currentRow && setCurrentRow(undefined)}
className={clsx({
["current"]: currentRow === id,
})}
>
<TableCell>
<CertificateTypeLabel type={type} />
</TableCell>
{/* // TODO: not sure about label as name */}
<TableCell>
<CertificateName name={label} />
</TableCell>
<TableCell>
<CertificateSerialNumber value={serialNumber} />
</TableCell>
<TableCell>
<Date date={notAfter} />
<div
className={styles.list_table_actions}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
<Button
tabIndex={0}
className={styles.view_details_button}
variant="outlined"
size="small"
onClick={() => onViewDetails(certificate)}
{loading ? (
<CertificatesListLoading />
) : (
certificates.map((certificate) => {
const {
id,
providerID,
serialNumber,
type,
label,
notAfter,
raw,
index,
} = certificate;
return (
<TableRow
tabIndex={0}
key={id}
onClick={() => onViewDetails(certificate)}
onFocus={() => setCurrentRow(id)}
onBlur={() => setCurrentRow(undefined)}
onKeyDown={(event) =>
["Space", "Enter"].includes(event.code) &&
onViewDetails(certificate)
}
onMouseOver={() => currentRow && setCurrentRow(undefined)}
className={clsx({
["current"]: currentRow === id,
})}
>
<TableCell>
<CertificateTypeLabel type={type} />
</TableCell>
{/* // TODO: not sure about label as name */}
<TableCell>
<CertificateName highlight={highlightedText} name={label} />
</TableCell>
<TableCell>
<CertificateSerialNumber value={serialNumber} />
</TableCell>
<TableCell>
<Date date={notAfter} />
<div
className={styles.list_table_actions}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
{t("certificates.list.action.view-details")}
</Button>
<CopyIconButton
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"
className={styles.action_icon_button}
>
<DownloadIcon />
</IconButton>
<IconButton
tabIndex={0}
title={t("certificates.list.action.delete")}
onClick={() =>
onDelete({
certificateIndex: index,
providerId: providerID,
label: label as string,
})
}
size="small"
className={styles.action_icon_button}
>
<DeleteIcon />
</IconButton>
</div>
</TableCell>
</TableRow>
);
})}
<Button
tabIndex={0}
className={styles.view_details_button}
variant="outlined"
size="small"
onClick={() => onViewDetails(certificate)}
>
{t("certificates.list.action.view-details")}
</Button>
<CopyIconButton
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"
className={styles.action_icon_button}
>
<DownloadIcon />
</IconButton>
<IconButton
tabIndex={0}
title={t("certificates.list.action.delete")}
onClick={() =>
onDelete({
certificateIndex: index,
providerId: providerID,
label: label as string,
})
}
size="small"
className={styles.action_icon_button}
>
<DeleteIcon />
</IconButton>
</div>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
);
};

function CertificatesListLoading() {
return [...Array(12).keys()].map((index) => (
<TableRow className={styles.skeleton_tr} key={`skel-row-${index}`}>
{[...Array(4).keys()].map((index) => (
<TableCell className={styles.skeleton_td}>
<Skeleton
key={`skel-td-${index}`}
className={styles.skeleton_td_item}
height={31}
/>
</TableCell>
))}
</TableRow>
));
}
Loading

0 comments on commit 9f6a477

Please sign in to comment.