Skip to content

Commit

Permalink
Merge branch 'donskov/v2' into aslobodian/v2-add-import-certificate-d…
Browse files Browse the repository at this point in the history
…ialog
  • Loading branch information
aleksandr-slobodian committed May 17, 2024
2 parents 412f0b3 + ab4a8d6 commit 7f0b5e9
Show file tree
Hide file tree
Showing 28 changed files with 1,853 additions and 952 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Main CI
on: [workflow_dispatch, push]
jobs:
tests_checks_build:
runs-on: ubuntu-latest
name: Tests, checks, build
steps:
- uses: actions/checkout@v4
name: Checkout
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
name: Setup node
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run linter
run: yarn lint
- name: Run tests
run: yarn test:ci
- name: Run build
run: yarn build
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ yarn-debug.log*
yarn-error.log*
.vercel

*storybook.log
*storybook.log

src/coverage
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,29 @@
"@storybook/react": "^8.0.9",
"@storybook/react-vite": "^8.0.9",
"@storybook/test": "^8.0.9",
"@testing-library/react": "^15.0.7",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.19",
"eslint": "^9.1.1",
"eslint-config-prettier": "^9.1.0",
"jsdom": "^24.0.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"sass": "^1.75.0",
"storybook": "^8.0.9",
"typescript": "^5.4.5",
"typescript-eslint": "^7.7.1",
"vite": "^5.2.10",
"vite-plugin-svgr": "^4.2.0"
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest-dom": "^0.1.1"
},
"dependencies": {
"@peculiar/certificates-viewer-react": "^4.2.1",
"@peculiar/certificates-viewer-react": "^4.2.2",
"@peculiar/fortify-webcomponents-react": "^4.0.3",
"@peculiar/react-components": "^0.6.0",
"@peculiar/x509": "^1.9.7",
Expand All @@ -52,6 +58,9 @@
"build": "tsc && vite build --emptyOutDir",
"preview": "vite preview --port 3005",
"format": "prettier --write 'src/**/*.{ts,tsx}'",
"storybook": "storybook dev -p 6006"
"storybook": "storybook dev -p 6006",
"test:ci": "vitest run --root src/",
"test:unit": "vitest --root src/",
"coverage": "vitest run --coverage --root src/"
}
}
5 changes: 5 additions & 0 deletions setup-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as matchers from "vitest-dom/matchers";
import { expect } from "vitest";
import i18n from "./src/i18n";
i18n.init();
expect.extend(matchers);
23 changes: 22 additions & 1 deletion src/components/certificates-list/CertificatesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import { CertificateTypeLabel } from "../certificate-type-label";
import { Date } from "../date";
import { CertificateName } from "../certificate-name";
import { CertificateSerialNumber } from "../certificate-serial-number";
import { downloadCertificate } from "../../utils/download-certificate";
import { CopyIconButton } from "../copy-icon-button";
import { certificateRawToPem } from "../../utils/certificate";

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

import DeleteIcon from "../../icons/delete.svg?react";
import DownloadIcon from "../../icons/download-20.svg?react";

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

Expand Down Expand Up @@ -63,7 +67,8 @@ export const CertificatesList: React.FunctionComponent<
</TableHeader>
<TableBody>
{certificates.map((certificate) => {
const { id, serialNumber, type, label, notAfter } = certificate;
const { id, serialNumber, type, label, notAfter, raw } =
certificate;
return (
<TableRow
tabIndex={0}
Expand Down Expand Up @@ -106,11 +111,27 @@ export const CertificatesList: React.FunctionComponent<
>
{t("certificates.list.action.view-details")}
</Button>
<CopyIconButton
value={certificateRawToPem(raw, type)}
className={styles.action_icon_button}
/>
<IconButton
tabIndex={0}
title={t("certificates.list.action.download")}
onClick={() =>
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(id as string, label as string)}
size="small"
className={styles.action_icon_button}
>
<DeleteIcon />
</IconButton>
Expand Down
4 changes: 4 additions & 0 deletions src/components/certificates-list/styles/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
.view_details_button {
margin-right: var(--pv-size-base-2);
}

.action_icon_button {
color: var(--pv-color-gray-10);
}
}
&[class~="current"] .list_table_actions,
&:hover .list_table_actions {
Expand Down
12 changes: 12 additions & 0 deletions src/components/certificates-topbar/CertificatesTopbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CertificatesTopbar } from "./CertificatesTopbar";

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

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

export const Default: Story = {};
45 changes: 35 additions & 10 deletions src/components/certificates-topbar/CertificatesTopbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ import React, { ComponentProps, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
PlusIcon,
Menu,
TextField,
useDebounceCallback,
} from "@peculiar/react-components";
import ImportIcon from "../../icons/import.svg?react";
import SearchIcon from "../../icons/search.svg?react";
import PlusIcon from "../../icons/plus-20.svg?react";
import CertificatCSRIcon from "../../icons/csr-30.svg?react";
import CertificatSSCIcon from "../../icons/certificate-30.svg?react";

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

interface CertificatesTopbarProps {
className?: ComponentProps<"div">["className"];
onSearch: (value: string) => void;
onImport: () => void;
onCreate: () => void;
onCreate: (type: "csr" | "x509") => void;
}
export const CertificatesTopbar: React.FunctionComponent<
CertificatesTopbarProps
Expand Down Expand Up @@ -60,15 +64,36 @@ export const CertificatesTopbar: React.FunctionComponent<
</Button>
</div>
<div>
<Button
color="primary"
variant="contained"
size="large"
startIcon={<PlusIcon />}
onClick={onCreate}
<Menu
popoverProps={{
className: styles.creation_menu,
}}
options={[
{
label: t("topbar.create-certificate-scr"),
startIcon: (
<CertificatCSRIcon className={styles.creation_menu_icon} />
),
onClick: () => onCreate("csr"),
},
{
label: t("topbar.create-certificate-ssc"),
startIcon: (
<CertificatSSCIcon className={styles.creation_menu_icon} />
),
onClick: () => onCreate("x509"),
},
]}
>
{t("topbar.create-certificate")}
</Button>
<Button
color="primary"
variant="contained"
size="large"
startIcon={<PlusIcon />}
>
{t("topbar.create-certificate")}
</Button>
</Menu>
</div>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/certificates-topbar/styles/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@
}
}
}
.creation_menu {
margin: 0 !important;

.creation_menu_icon {
color: var(--pv-color-gray-10);
}
}
16 changes: 16 additions & 0 deletions src/components/copy-icon-button/CopyIconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CopyIconButton } from "./CopyIconButton";

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

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

export const Default: Story = {
args: {
value: "Copied text",
},
};
26 changes: 26 additions & 0 deletions src/components/copy-icon-button/CopyIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import {
IconButton,
IconButtonProps,
useClipboard,
} from "@peculiar/react-components";

import CopyIcon from "../../icons/copy-20.svg?react";
import CheckIcon from "../../icons/check-20.svg?react";

interface CopyIconButtonProps extends Omit<IconButtonProps, "children"> {
value: string;
}

export const CopyIconButton: React.FunctionComponent<CopyIconButtonProps> = (
props
) => {
const { copy, isCopied } = useClipboard();
const { value, size = "small", ...restProps } = props;

return (
<IconButton size={size} {...restProps} onClick={() => copy(value)}>
{isCopied ? <CheckIcon /> : <CopyIcon />}
</IconButton>
);
};
1 change: 1 addition & 0 deletions src/components/copy-icon-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CopyIconButton";
12 changes: 12 additions & 0 deletions src/components/table/table/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, it, expect, render } from "@testing";

import { Table } from "./Table";

describe("<Table />", () => {
it("should render", () => {
const { getByTestId } = render(<Table data-testid="table" />);
const tableElement = getByTestId("table");

expect(tableElement).toBeInTheDocument();
});
});
7 changes: 5 additions & 2 deletions src/i18n/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
},
"action": {
"view-details": "View details",
"delete": "Delete certificate"
"delete": "Delete certificate",
"download": "Download certificate"
},
"empty-text": "There are no certificates yet."
},
Expand Down Expand Up @@ -98,7 +99,9 @@
"topbar": {
"search-placeholder": "Search",
"import-certificate": "Import certificate",
"create-certificate": "Create"
"create-certificate": "Create",
"create-certificate-scr": "Certificate signing request (CSR)",
"create-certificate-ssc": "Self-signed certificate"
},
"certificate-viewer-dialog": {
"title": "“{{name}}” details"
Expand Down
6 changes: 6 additions & 0 deletions src/icons/certificate-30.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/icons/check-20.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/icons/copy-20.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/icons/csr-30.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions src/icons/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/icons/download-20.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/icons/plus-20.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/utils/certificate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Convert } from "pvtsutils";
import { Pkcs10CertificateRequest, X509Certificate } from "@peculiar/x509";

export function certificateRawToPem(raw: ArrayBuffer, type: "x509" | "csr") {
let pem;
switch (type) {
case "x509": {
const cert = new X509Certificate(Convert.ToBase64(raw));
pem = cert.toString("pem");
break;
}
case "csr": {
const req = new Pkcs10CertificateRequest(Convert.ToBase64(raw));
pem = req.toString("pem");
break;
}
default:
throw new Error(`Unsupported certificate type: ${type}`);
}
return pem;
}
Loading

0 comments on commit 7f0b5e9

Please sign in to comment.