From 331539953147a9796e387a3b0587771b19ebf1a2 Mon Sep 17 00:00:00 2001 From: Oleksandr Slobodian <129727832+OleksandrSPV@users.noreply.github.com> Date: Fri, 31 May 2024 12:34:09 +0300 Subject: [PATCH] Version 2: Create Certificate creation Dialog (#144) * Add CertificateTypeSelect component * Add KeyUsagesCheckboxGroup component * Add CountrySelect component * Fix naming * Add CertificateKeyPropertiesSelect * Add CertificateCreateDialog layout * Add CertificateTypeSelect to dialog * Fix CertificateTypeSelect * Add type selection * Add Card component * Add CertificateCreateByEmail block * Fix text field requrid styles * Add CertificateCreateByCname block * Add CertificateAlgorithmInfo component * Add CertificateCreateByCustom component * Add more fields CertificateCreateByCustom component * Add CertificateCreateByCustom to CertificateCreateDialog * Add useCertificateCreateDialog * Fix useCertificateCreateDialog * Fix useCertificateCreateDialog * Fix useCertificateCreateDialog * Fix dialog content width * Add create dialog to app * Fix Certificate Algorithm * Add type onCreate * Fix types * Add certificateSubjectToString method * Fix scroll in provider list * Fix overscroll behavior * Add scroll lock for import & create dialogs * Fix certificateKeyUsageExtensions * Fix form * Fix CertificateTypeSelect styles * Fix CertificateCreateDialog * Fix forms & validations * Fix CertificateCreateByCname * Fix CertificateCreateByEmail * Fix CertificateCreateByCustom * Fix ExtendedKeyUsages --------- Co-authored-by: alex-slobodian --- package.json | 3 +- src/app.tsx | 13 +- src/components/card/Card.stories.tsx | 16 + src/components/card/Card.tsx | 14 + src/components/card/index.ts | 1 + src/components/card/styles/index.module.scss | 9 + .../CertificateAlgorithmInfo.stories.tsx | 21 + .../CertificateAlgorithmInfo.tsx | 34 + .../certificate-algorithm-info/index.ts | 1 + .../styles/index.module.scss | 4 + .../CertificateCreateByCname.stories.tsx | 17 + .../CertificateCreateByCname.tsx | 85 ++ .../certificate-create-by-cname/index.ts | 1 + .../styles/index.module.scss | 14 + .../CertificateCreateByCustom.stories.tsx | 17 + .../CertificateCreateByCustom.tsx | 188 ++++ .../certificate-create-by-custom/index.ts | 1 + .../styles/index.module.scss | 63 ++ .../CertificateCreateByEmail.stories.tsx | 17 + .../CertificateCreateByEmail.tsx | 109 ++ .../certificate-create-by-email/index.ts | 1 + .../styles/index.module.scss | 14 + .../CertificateCreateDialog.stories.tsx | 39 + .../CertificateCreateDialog.tsx | 156 +++ .../certificate-create-dialog/index.ts | 1 + .../styles/index.module.scss | 72 ++ ...CertificateKeyPropertiesSelect.stories.tsx | 12 + .../CertificateKeyPropertiesSelect.tsx | 50 + .../index.ts | 1 + .../styles/index.module.scss | 15 + .../CertificateTypeSelect.stories.tsx | 12 + .../CertificateTypeSelect.tsx | 65 ++ .../certificate-type-select/index.ts | 1 + .../styles/index.module.scss | 19 + .../styles/index.module.scss | 3 +- .../KeyUsagesCheckboxGroup.stories.tsx | 12 + .../KeyUsagesCheckboxGroup.tsx | 44 + .../key-usages-checkbox-group/index.ts | 1 + .../styles/index.module.scss | 17 + .../data/certificate-key-usage-extensions.ts | 13 + src/config/data/countries.ts | 974 ++++++++++++++++++ src/config/data/index.ts | 2 + .../certificate-create-dialog/index.ts | 1 + .../useCertificateCreateDialog.tsx | 73 ++ .../useCertificateImportDialog.tsx | 9 +- src/global.scss | 18 + src/hooks/app/useApp.tsx | 6 - src/i18n/locales/en/main.json | 74 ++ src/types.ts | 22 +- src/utils/certificate.ts | 16 +- src/utils/download-certificate.ts | 3 +- yarn.lock | 196 +++- 52 files changed, 2554 insertions(+), 16 deletions(-) create mode 100644 src/components/card/Card.stories.tsx create mode 100644 src/components/card/Card.tsx create mode 100644 src/components/card/index.ts create mode 100644 src/components/card/styles/index.module.scss create mode 100644 src/components/certificate-algorithm-info/CertificateAlgorithmInfo.stories.tsx create mode 100644 src/components/certificate-algorithm-info/CertificateAlgorithmInfo.tsx create mode 100644 src/components/certificate-algorithm-info/index.ts create mode 100644 src/components/certificate-algorithm-info/styles/index.module.scss create mode 100644 src/components/certificate-create-by-cname/CertificateCreateByCname.stories.tsx create mode 100644 src/components/certificate-create-by-cname/CertificateCreateByCname.tsx create mode 100644 src/components/certificate-create-by-cname/index.ts create mode 100644 src/components/certificate-create-by-cname/styles/index.module.scss create mode 100644 src/components/certificate-create-by-custom/CertificateCreateByCustom.stories.tsx create mode 100644 src/components/certificate-create-by-custom/CertificateCreateByCustom.tsx create mode 100644 src/components/certificate-create-by-custom/index.ts create mode 100644 src/components/certificate-create-by-custom/styles/index.module.scss create mode 100644 src/components/certificate-create-by-email/CertificateCreateByEmail.stories.tsx create mode 100644 src/components/certificate-create-by-email/CertificateCreateByEmail.tsx create mode 100644 src/components/certificate-create-by-email/index.ts create mode 100644 src/components/certificate-create-by-email/styles/index.module.scss create mode 100644 src/components/certificate-create-dialog/CertificateCreateDialog.stories.tsx create mode 100644 src/components/certificate-create-dialog/CertificateCreateDialog.tsx create mode 100644 src/components/certificate-create-dialog/index.ts create mode 100644 src/components/certificate-create-dialog/styles/index.module.scss create mode 100644 src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.stories.tsx create mode 100644 src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.tsx create mode 100644 src/components/certificate-key-properties-select/index.ts create mode 100644 src/components/certificate-key-properties-select/styles/index.module.scss create mode 100644 src/components/certificate-type-select/CertificateTypeSelect.stories.tsx create mode 100644 src/components/certificate-type-select/CertificateTypeSelect.tsx create mode 100644 src/components/certificate-type-select/index.ts create mode 100644 src/components/certificate-type-select/styles/index.module.scss create mode 100644 src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.stories.tsx create mode 100644 src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.tsx create mode 100644 src/components/key-usages-checkbox-group/index.ts create mode 100644 src/components/key-usages-checkbox-group/styles/index.module.scss create mode 100644 src/config/data/certificate-key-usage-extensions.ts create mode 100644 src/config/data/countries.ts create mode 100644 src/config/data/index.ts create mode 100644 src/dialogs/certificate-create-dialog/index.ts create mode 100644 src/dialogs/certificate-create-dialog/useCertificateCreateDialog.tsx diff --git a/package.json b/package.json index a17f4e1d..c65af615 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "react": "^18.3.0", "react-dom": "^18.3.0", "react-dropzone": "^14.2.3", - "react-i18next": "^14.1.1" + "react-i18next": "^14.1.1", + "react-use": "^17.5.0" }, "resolutions": { "@webcrypto-local/client": "1.7.7" diff --git a/src/app.tsx b/src/app.tsx index e975a0c8..9f08710d 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -9,6 +9,7 @@ import { CertificatesTopbar } from "./components/certificates-topbar"; import { CertificateDeleteDialog } from "./components/certificate-delete-dialog"; import { CertificateViewerDialog } from "./components/certificate-viewer-dialog"; import { useCertificateImportDialog } from "./dialogs/certificate-import-dialog"; +import { useCertificateCreateDialog } from "./dialogs/certificate-create-dialog"; import styles from "./app.module.scss"; @@ -23,7 +24,6 @@ export function App() { currentCertificateViewerValue, handleProviderChange, handleCertificatesSearch, - handleCertificateCreate, handleCertificateDeleteDialogOpen, handleCertificateDeleteDialogClose, handleCertificateDelete, @@ -39,6 +39,14 @@ export function App() { currentProviderId, }); + const { + open: handleCertificateCreateDialogOpen, + dialog: certificateCreateDialog, + } = useCertificateCreateDialog({ + providers, + currentProviderId, + }); + return ( <> @@ -53,7 +61,7 @@ export function App() { className={styles.top_bar} onSearch={handleCertificatesSearch} onImport={handleCertificateImportDialogOpen} - onCreate={handleCertificateCreate} + onCreate={handleCertificateCreateDialogOpen} > {fetching.certificates ? ( ) : null} {certificateImportDialog()} + {certificateCreateDialog()} ); } diff --git a/src/components/card/Card.stories.tsx b/src/components/card/Card.stories.tsx new file mode 100644 index 00000000..89648422 --- /dev/null +++ b/src/components/card/Card.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Card } from "./Card"; + +const meta: Meta = { + title: "Components/Card", + component: Card, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: "Content", + }, +}; diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx new file mode 100644 index 00000000..71827bb5 --- /dev/null +++ b/src/components/card/Card.tsx @@ -0,0 +1,14 @@ +import React, { ComponentProps, PropsWithChildren } from "react"; +import clsx from "clsx"; + +import styles from "./styles/index.module.scss"; + +interface CardProps extends PropsWithChildren { + className?: ComponentProps<"div">["className"]; +} + +export const Card: React.FunctionComponent = (props) => { + const { className, children } = props; + + return
{children}
; +}; diff --git a/src/components/card/index.ts b/src/components/card/index.ts new file mode 100644 index 00000000..24d32124 --- /dev/null +++ b/src/components/card/index.ts @@ -0,0 +1 @@ +export * from "./Card"; diff --git a/src/components/card/styles/index.module.scss b/src/components/card/styles/index.module.scss new file mode 100644 index 00000000..50bd4025 --- /dev/null +++ b/src/components/card/styles/index.module.scss @@ -0,0 +1,9 @@ +.card { + background-color: var(--pv-color-white); + padding: var(--pv-size-base-6) var(--pv-size-base-6) var(--pv-size-base-8); + border: 1px solid var(--pv-color-gray-4); + border-radius: var(--pv-border-radius-base); + display: flex; + flex-direction: column; + gap: var(--pv-size-base-4); +} diff --git a/src/components/certificate-algorithm-info/CertificateAlgorithmInfo.stories.tsx b/src/components/certificate-algorithm-info/CertificateAlgorithmInfo.stories.tsx new file mode 100644 index 00000000..16ba49a0 --- /dev/null +++ b/src/components/certificate-algorithm-info/CertificateAlgorithmInfo.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CertificateAlgorithmInfo } from "./CertificateAlgorithmInfo"; +import { + EHashAlgorithm, + ESignatureAlgorithm, +} from "@peculiar/fortify-client-core"; + +const meta: Meta = { + title: "Components/CertificateAlgorithmInfo", + component: CertificateAlgorithmInfo, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + algorithmSignature: ESignatureAlgorithm.RSA4096, + algorithmHash: EHashAlgorithm.SHA_256, + }, +}; diff --git a/src/components/certificate-algorithm-info/CertificateAlgorithmInfo.tsx b/src/components/certificate-algorithm-info/CertificateAlgorithmInfo.tsx new file mode 100644 index 00000000..2f91774a --- /dev/null +++ b/src/components/certificate-algorithm-info/CertificateAlgorithmInfo.tsx @@ -0,0 +1,34 @@ +import React, { ComponentProps } from "react"; +import { Typography } from "@peculiar/react-components"; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; +import { + EHashAlgorithm, + ESignatureAlgorithm, +} from "@peculiar/fortify-client-core"; + +import styles from "./styles/index.module.scss"; + +interface CertificateAlgorithmInfoProps { + className?: ComponentProps<"div">["className"]; + algorithmSignature: ESignatureAlgorithm; + algorithmHash: EHashAlgorithm; +} + +export const CertificateAlgorithmInfo: React.FunctionComponent< + CertificateAlgorithmInfoProps +> = (props) => { + const { className, algorithmSignature, algorithmHash } = props; + const { t } = useTranslation(); + + return ( +
+ + {t("certificates.signature-algorithm")}: {algorithmSignature} + + + {t("certificates.hash-algorithm")}: {algorithmHash} + +
+ ); +}; diff --git a/src/components/certificate-algorithm-info/index.ts b/src/components/certificate-algorithm-info/index.ts new file mode 100644 index 00000000..4f06ed36 --- /dev/null +++ b/src/components/certificate-algorithm-info/index.ts @@ -0,0 +1 @@ +export * from "./CertificateAlgorithmInfo"; diff --git a/src/components/certificate-algorithm-info/styles/index.module.scss b/src/components/certificate-algorithm-info/styles/index.module.scss new file mode 100644 index 00000000..9b345f67 --- /dev/null +++ b/src/components/certificate-algorithm-info/styles/index.module.scss @@ -0,0 +1,4 @@ +.algorithm_info { + display: flex; + gap: var(--pv-size-base-6); +} diff --git a/src/components/certificate-create-by-cname/CertificateCreateByCname.stories.tsx b/src/components/certificate-create-by-cname/CertificateCreateByCname.stories.tsx new file mode 100644 index 00000000..aea5736b --- /dev/null +++ b/src/components/certificate-create-by-cname/CertificateCreateByCname.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import { CertificateCreateByCname } from "./CertificateCreateByCname"; + +const meta: Meta = { + title: "Components/CertificateCreateByCname", + component: CertificateCreateByCname, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + onCreateButtonClick: fn(), + }, +}; diff --git a/src/components/certificate-create-by-cname/CertificateCreateByCname.tsx b/src/components/certificate-create-by-cname/CertificateCreateByCname.tsx new file mode 100644 index 00000000..9c3673e0 --- /dev/null +++ b/src/components/certificate-create-by-cname/CertificateCreateByCname.tsx @@ -0,0 +1,85 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, TextField } from "@peculiar/react-components"; +import { CertificateAlgorithmInfo } from "../certificate-algorithm-info"; +import { Card } from "../card"; +import { + EHashAlgorithm, + ESignatureAlgorithm, +} from "@peculiar/fortify-client-core"; +import { CertificateAlgorithmProps, CertificateType } from "../../types"; + +import styles from "./styles/index.module.scss"; + +export interface ICertificateCreateByCnameData { + subject: { + CN: string; + }; + algorithm: CertificateAlgorithmProps; + type: CertificateType; +} + +interface CertificateCreateByCnameProps { + type: CertificateType; + onCreateButtonClick: (data: ICertificateCreateByCnameData) => void; +} + +export const CertificateCreateByCname: React.FunctionComponent< + CertificateCreateByCnameProps +> = (props) => { + const { type = "x509", onCreateButtonClick } = props; + + const { t } = useTranslation(); + const [isFormValid, setIsFormValid] = useState(false); + + const algorithm: CertificateAlgorithmProps = { + hash: EHashAlgorithm.SHA_256, + signature: ESignatureAlgorithm.RSA2048, + }; + + const handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + onCreateButtonClick({ + subject: { + CN: formData.get("CN") as string, + }, + algorithm, + type, + }); + }; + + return ( +
setIsFormValid(event.currentTarget.checkValidity())} + > + + + + + +
+ +
+
+ ); +}; diff --git a/src/components/certificate-create-by-cname/index.ts b/src/components/certificate-create-by-cname/index.ts new file mode 100644 index 00000000..c1a450ef --- /dev/null +++ b/src/components/certificate-create-by-cname/index.ts @@ -0,0 +1 @@ +export * from "./CertificateCreateByCname"; diff --git a/src/components/certificate-create-by-cname/styles/index.module.scss b/src/components/certificate-create-by-cname/styles/index.module.scss new file mode 100644 index 00000000..daff2de2 --- /dev/null +++ b/src/components/certificate-create-by-cname/styles/index.module.scss @@ -0,0 +1,14 @@ +.form_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-6); + + .button_group { + display: flex; + justify-content: end; + } + .algorithm { + display: flex; + gap: var(--pv-size-base-6); + } +} diff --git a/src/components/certificate-create-by-custom/CertificateCreateByCustom.stories.tsx b/src/components/certificate-create-by-custom/CertificateCreateByCustom.stories.tsx new file mode 100644 index 00000000..40a02c29 --- /dev/null +++ b/src/components/certificate-create-by-custom/CertificateCreateByCustom.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import { CertificateCreateByCustom } from "./CertificateCreateByCustom"; + +const meta: Meta = { + title: "Components/CertificateCreateByCustom", + component: CertificateCreateByCustom, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + onCreateButtonClick: fn(), + }, +}; diff --git a/src/components/certificate-create-by-custom/CertificateCreateByCustom.tsx b/src/components/certificate-create-by-custom/CertificateCreateByCustom.tsx new file mode 100644 index 00000000..4648fd84 --- /dev/null +++ b/src/components/certificate-create-by-custom/CertificateCreateByCustom.tsx @@ -0,0 +1,188 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + CertificateAlgorithmProps, + CertificateSubjectProps, + CertificateType, +} from "../../types"; +import { + Autocomplete, + Button, + TextField, + Typography, +} from "@peculiar/react-components"; +import { CertificateKeyPropertiesSelect } from "../certificate-key-properties-select"; +import { Card } from "../card"; +import { KeyUsagesCheckboxGroup } from "../key-usages-checkbox-group"; + +import { ICertificateExtendedKeyUsages } from "../../config/data"; +import { countries } from "../../config/data"; + +import styles from "./styles/index.module.scss"; + +export interface ICertificateCreateByCustomData { + subject: CertificateSubjectProps; + algorithm?: CertificateAlgorithmProps; + extendedKeyUsages: ICertificateExtendedKeyUsages[]; + type: CertificateType; +} + +interface CertificateCreateByCustomProps { + type: CertificateType; + onCreateButtonClick: (data: ICertificateCreateByCustomData) => void; +} + +export const CertificateCreateByCustom: React.FunctionComponent< + CertificateCreateByCustomProps +> = (props) => { + const { type = "x509", onCreateButtonClick } = props; + + const { t } = useTranslation(); + + const [isFormValid, setIsFormValid] = useState(false); + const [emailAddressErrorMessage, setEmailAddressErrorMessage] = useState< + string | undefined + >(undefined); + + const validateEmailAddress = ( + event: React.SyntheticEvent + ) => { + if (!event.currentTarget.checkValidity()) { + setEmailAddressErrorMessage( + t("certificates.subject.email-address.error.type") + ); + return; + } + setEmailAddressErrorMessage(undefined); + }; + + const handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const { hashAlgorithm, signatureAlgorithm, C, keyUsage, ...subject } = + Object.fromEntries(formData); + + const extendedKeyUsages = keyUsage + ? (formData.getAll("keyUsage") as ICertificateExtendedKeyUsages[]) + : []; + + const hash = hashAlgorithm + .toString() + .replace(/"/g, "") as CertificateAlgorithmProps["hash"]; + const signature = signatureAlgorithm + .toString() + .replace(/"/g, "") as CertificateAlgorithmProps["signature"]; + + const country = C ? JSON.parse(C as string)?.code : ""; + + const submitedData: ICertificateCreateByCustomData = { + subject: { + ...(subject as unknown as ICertificateCreateByCustomData["subject"]), + C: country, + }, + extendedKeyUsages, + type, + algorithm: { + hash, + signature, + }, + }; + onCreateButtonClick(submitedData); + }; + + return ( +
setIsFormValid(event.currentTarget.checkValidity())} + > + +
+ + {t("certificates.general.title")} + +
+ + +
+
+
+
+ + {t("certificates.subject.title")} + +
+ + + + + value} + options={countries} + name="C" + placeholder={t("certificates.subject.country-name.placeholder")} + label={t("certificates.subject.country-name.label")} + /> + + +
+
+
+ +
+ +
+ +
+ +
+
+ ); +}; diff --git a/src/components/certificate-create-by-custom/index.ts b/src/components/certificate-create-by-custom/index.ts new file mode 100644 index 00000000..4e9469c9 --- /dev/null +++ b/src/components/certificate-create-by-custom/index.ts @@ -0,0 +1 @@ +export * from "./CertificateCreateByCustom"; diff --git a/src/components/certificate-create-by-custom/styles/index.module.scss b/src/components/certificate-create-by-custom/styles/index.module.scss new file mode 100644 index 00000000..4f81a887 --- /dev/null +++ b/src/components/certificate-create-by-custom/styles/index.module.scss @@ -0,0 +1,63 @@ +.form_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-6); + + .card { + gap: var(--pv-size-base-6); + } + + .subject_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-2); + + .subject_fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--pv-size-base-2) var(--pv-size-base-6); + } + } + + .general_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-2); + + .general_fields { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-2); + } + } + + .divider { + height: 1px; + background-color: var(--pv-color-gray-4); + position: relative; + &:after, + &:before { + content: ""; + height: 1px; + width: var(--pv-size-base-6); + background-color: var(--pv-color-gray-4); + position: absolute; + top: 0; + } + &:after { + left: 100%; + } + &:before { + right: 100%; + } + } + + .key_properties_select { + margin-bottom: var(--pv-size-base-2); + } + + .button_group { + display: flex; + justify-content: end; + } +} diff --git a/src/components/certificate-create-by-email/CertificateCreateByEmail.stories.tsx b/src/components/certificate-create-by-email/CertificateCreateByEmail.stories.tsx new file mode 100644 index 00000000..3ec89c7a --- /dev/null +++ b/src/components/certificate-create-by-email/CertificateCreateByEmail.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import { CertificateCreateByEmail } from "./CertificateCreateByEmail"; + +const meta: Meta = { + title: "Components/CertificateCreateByEmail", + component: CertificateCreateByEmail, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + onCreateButtonClick: fn(), + }, +}; diff --git a/src/components/certificate-create-by-email/CertificateCreateByEmail.tsx b/src/components/certificate-create-by-email/CertificateCreateByEmail.tsx new file mode 100644 index 00000000..d79a8f71 --- /dev/null +++ b/src/components/certificate-create-by-email/CertificateCreateByEmail.tsx @@ -0,0 +1,109 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, TextField } from "@peculiar/react-components"; +import { CertificateAlgorithmInfo } from "../certificate-algorithm-info"; +import { Card } from "../card"; +import { + EHashAlgorithm, + ESignatureAlgorithm, +} from "@peculiar/fortify-client-core"; +import { CertificateAlgorithmProps, CertificateType } from "../../types"; + +import styles from "./styles/index.module.scss"; + +export interface ICertificateCreateByEmailData { + subject: { + E: string; + CN: string; + }; + algorithm: CertificateAlgorithmProps; + type: CertificateType; +} + +interface CertificateCreateByEmailProps { + type: CertificateType; + onCreateButtonClick: (data: ICertificateCreateByEmailData) => void; +} + +export const CertificateCreateByEmail: React.FunctionComponent< + CertificateCreateByEmailProps +> = (props) => { + const { type = "x509", onCreateButtonClick } = props; + + const { t } = useTranslation(); + + const [isFormValid, setIsFormValid] = useState(false); + const [emailAddressErrorMessage, setEmailAddressErrorMessage] = useState< + string | undefined + >(undefined); + + const algorithm: CertificateAlgorithmProps = { + hash: EHashAlgorithm.SHA_256, + signature: ESignatureAlgorithm.RSA2048, + }; + + const validateEmailAddress = ( + event: React.SyntheticEvent + ) => { + if (!event.currentTarget.checkValidity()) { + setEmailAddressErrorMessage( + t("certificates.subject.email-address.error.type") + ); + return; + } + setEmailAddressErrorMessage(undefined); + }; + + const handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + const emailAddress = formData.get("email") as string; + + onCreateButtonClick({ + subject: { + CN: emailAddress, + E: emailAddress, + }, + algorithm, + type, + }); + }; + + return ( +
setIsFormValid(event.currentTarget.checkValidity())} + > + + + + + +
+ +
+
+ ); +}; diff --git a/src/components/certificate-create-by-email/index.ts b/src/components/certificate-create-by-email/index.ts new file mode 100644 index 00000000..9c50eb1b --- /dev/null +++ b/src/components/certificate-create-by-email/index.ts @@ -0,0 +1 @@ +export * from "./CertificateCreateByEmail"; diff --git a/src/components/certificate-create-by-email/styles/index.module.scss b/src/components/certificate-create-by-email/styles/index.module.scss new file mode 100644 index 00000000..daff2de2 --- /dev/null +++ b/src/components/certificate-create-by-email/styles/index.module.scss @@ -0,0 +1,14 @@ +.form_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-6); + + .button_group { + display: flex; + justify-content: end; + } + .algorithm { + display: flex; + gap: var(--pv-size-base-6); + } +} diff --git a/src/components/certificate-create-dialog/CertificateCreateDialog.stories.tsx b/src/components/certificate-create-dialog/CertificateCreateDialog.stories.tsx new file mode 100644 index 00000000..32f89651 --- /dev/null +++ b/src/components/certificate-create-dialog/CertificateCreateDialog.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import { CertificateCreateDialog } from "./CertificateCreateDialog"; + +const meta: Meta = { + title: "Components/CertificateCreateDialog", + component: CertificateCreateDialog, +}; + +export default meta; +type Story = StoryObj; + +const providers = [ + { + id: "1", + name: "Provider 1", + }, + { + id: "2", + name: "Provider 2", + }, +]; + +export const Default: Story = { + args: { + currentProviderId: "2", + providers, + onProviderSelect: fn(), + onCreateButtonClick: fn(), + }, +}; + +export const isLoading: Story = { + args: { + loading: true, + providers, + onProviderSelect: fn(), + }, +}; diff --git a/src/components/certificate-create-dialog/CertificateCreateDialog.tsx b/src/components/certificate-create-dialog/CertificateCreateDialog.tsx new file mode 100644 index 00000000..ecc12a67 --- /dev/null +++ b/src/components/certificate-create-dialog/CertificateCreateDialog.tsx @@ -0,0 +1,156 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { IProviderInfo } from "@peculiar/fortify-client-core"; +import clsx from "clsx"; +import { + CertificateTypeSelect, + ICertificateTypeSelectValue, +} from "../certificate-type-select"; +import { + Dialog, + ArrowRightIcon, + IconButton, + Typography, + CircularProgress, +} from "@peculiar/react-components"; +import { Card } from "../card"; +import { + CertificateCreateByEmail, + ICertificateCreateByEmailData, +} from "../certificate-create-by-email"; +import { + CertificateCreateByCname, + ICertificateCreateByCnameData, +} from "../certificate-create-by-cname"; +import { + CertificateCreateByCustom, + ICertificateCreateByCustomData, +} from "../certificate-create-by-custom"; +import { CertificatesProvidersSelectList } from "../certificates-providers-select-list"; +import { CertificateType } from "../../types"; + +import styles from "./styles/index.module.scss"; + +export type CertificateCreateDataProps = + | ICertificateCreateByCnameData + | ICertificateCreateByEmailData + | ICertificateCreateByCustomData; + +interface CertificateCreateDialogProps { + type: CertificateType; + currentProviderId?: string; + providers: Pick[]; + loading?: boolean; + onProviderSelect: (id: string) => void; + onDialogClose: () => void; + onCreateButtonClick: (data: CertificateCreateDataProps) => void; +} + +export const CertificateCreateDialog: React.FunctionComponent< + CertificateCreateDialogProps +> = (props) => { + const { + loading, + type = "x509", + providers, + currentProviderId, + onProviderSelect, + onDialogClose, + onCreateButtonClick, + } = props; + + const [currentTypeSelect, setCurrentTypeSelect] = React.useState< + ICertificateTypeSelectValue | undefined + >(undefined); + + const { t } = useTranslation(); + + const renderContent = () => { + if (currentTypeSelect) { + if ( + ["emailProtection", "codeSigning", "documentSigning"].includes( + currentTypeSelect.value + ) + ) { + return ( + + ); + } + + if (["clientAuth", "serverAuth"].includes(currentTypeSelect.value)) { + return ( + + ); + } + + if (currentTypeSelect.value === "custom") { + return ( + + ); + } + } + }; + + return ( + +
+
+
+ + + +
+
+ + {t(`certificates.dialog.create.title.${type}`)} + +
+
+ +
+
+
+
+
+ + + {t(`certificates.dialog.create.select-type.${type}`)} + + + + {renderContent()} +
+
+ {loading ? ( +
+ + + {t("certificates.dialog.create.loading-text")} + +
+ ) : null} +
+ ); +}; diff --git a/src/components/certificate-create-dialog/index.ts b/src/components/certificate-create-dialog/index.ts new file mode 100644 index 00000000..dd4cb455 --- /dev/null +++ b/src/components/certificate-create-dialog/index.ts @@ -0,0 +1 @@ +export * from "./CertificateCreateDialog"; diff --git a/src/components/certificate-create-dialog/styles/index.module.scss b/src/components/certificate-create-dialog/styles/index.module.scss new file mode 100644 index 00000000..e143ac15 --- /dev/null +++ b/src/components/certificate-create-dialog/styles/index.module.scss @@ -0,0 +1,72 @@ +.dialog { + background-color: var(--pv-color-gray-2) !important; + + .title { + width: 100%; + height: 80px; + min-height: 80px; + border-bottom: 1px solid var(--pv-color-gray-5); + .centered { + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--pv-size-base-2); + .title_label { + flex-grow: 1; + } + } + } + + .content { + padding: var(--pv-size-base-6) 0; + overflow-y: auto; + } + + .centered { + max-width: 740px; + margin: 0 auto; + width: 100% !important; + padding-left: var(--pv-size-base-6); + padding-right: var(--pv-size-base-6); + } + + .content_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-2); + } + + .provider_select { + width: 200px; + } + .button_back { + width: 30px; + height: 30px; + .arrow_back { + transform: scale(-1, -1); + display: block; + width: 30px; + height: 30px; + color: var(--pv-color-gray-10); + } + } + + .loading { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: var(--pv-color-gray-2); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--pv-size-base-6); + } +} + +.provider_select_popover { + min-width: 200px !important; +} diff --git a/src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.stories.tsx b/src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.stories.tsx new file mode 100644 index 00000000..cb8990ab --- /dev/null +++ b/src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CertificateKeyPropertiesSelect } from "./CertificateKeyPropertiesSelect"; + +const meta: Meta = { + title: "Components/CertificateKeyPropertiesSelect", + component: CertificateKeyPropertiesSelect, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.tsx b/src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.tsx new file mode 100644 index 00000000..c60043ab --- /dev/null +++ b/src/components/certificate-key-properties-select/CertificateKeyPropertiesSelect.tsx @@ -0,0 +1,50 @@ +import React, { ComponentProps } from "react"; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; +import { + EHashAlgorithm, + ESignatureAlgorithm, +} from "@peculiar/fortify-client-core"; +import { Autocomplete, Typography } from "@peculiar/react-components"; + +import styles from "./styles/index.module.scss"; + +interface CertificateKeyPropertiesSelectProps { + className?: ComponentProps<"select">["className"]; +} + +export const CertificateKeyPropertiesSelect: React.FunctionComponent< + CertificateKeyPropertiesSelectProps +> = (props) => { + const { className } = props; + const { t } = useTranslation(); + + const signatureAlgorithm = Object.values(ESignatureAlgorithm); + const hashAlgorithm = Object.values(EHashAlgorithm); + + return ( +
+ + {t("certificates.key-properties")} + +
+ + +
+
+ ); +}; diff --git a/src/components/certificate-key-properties-select/index.ts b/src/components/certificate-key-properties-select/index.ts new file mode 100644 index 00000000..996a789b --- /dev/null +++ b/src/components/certificate-key-properties-select/index.ts @@ -0,0 +1 @@ +export * from "./CertificateKeyPropertiesSelect"; diff --git a/src/components/certificate-key-properties-select/styles/index.module.scss b/src/components/certificate-key-properties-select/styles/index.module.scss new file mode 100644 index 00000000..24fa3bb3 --- /dev/null +++ b/src/components/certificate-key-properties-select/styles/index.module.scss @@ -0,0 +1,15 @@ +.certificate_key_prop_select_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-3); + + .certificate_key_prop_selects { + display: flex; + width: 100%; + gap: var(--pv-size-base-3); + + .certificate_key_prop_select { + flex: 1; + } + } +} diff --git a/src/components/certificate-type-select/CertificateTypeSelect.stories.tsx b/src/components/certificate-type-select/CertificateTypeSelect.stories.tsx new file mode 100644 index 00000000..fa661801 --- /dev/null +++ b/src/components/certificate-type-select/CertificateTypeSelect.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CertificateTypeSelect } from "./CertificateTypeSelect"; + +const meta: Meta = { + title: "Components/CertificateTypeSelect", + component: CertificateTypeSelect, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/certificate-type-select/CertificateTypeSelect.tsx b/src/components/certificate-type-select/CertificateTypeSelect.tsx new file mode 100644 index 00000000..ee7c623a --- /dev/null +++ b/src/components/certificate-type-select/CertificateTypeSelect.tsx @@ -0,0 +1,65 @@ +import React, { ComponentProps } from "react"; +import { Autocomplete, useControllableState } from "@peculiar/react-components"; +import { useTranslation } from "react-i18next"; +import { + ICertificateExtendedKeyUsages, + certificateKeyUsageExtensions, +} from "../../config/data"; +import { CertificateType } from "../../types"; + +import styles from "./styles/index.module.scss"; + +type IExtendedKeyUsages = ICertificateExtendedKeyUsages | "custom"; + +export type ICertificateTypeSelectValue = { + value: IExtendedKeyUsages; + label: string; +}; + +interface CertificateTypeSelectProps { + className?: ComponentProps<"select">["className"]; + type: CertificateType; + onChange: (data: ICertificateTypeSelectValue) => void; +} + +export const CertificateTypeSelect: React.FunctionComponent< + CertificateTypeSelectProps +> = (props) => { + const { className, type = "csr", onChange } = props; + + const [currentValue, setCurrentValue] = + useControllableState({ onChange }); + + const { t } = useTranslation(); + const list: ICertificateTypeSelectValue[] = [ + ...Object.keys(certificateKeyUsageExtensions).map((key) => ({ + value: key as IExtendedKeyUsages, + label: t(`certificates.key-usage-extension.${key}`), + })), + { + label: t( + type === "x509" + ? "certificates.custom-certificate-option" + : "certificates.custom-csr-option" + ), + value: "custom", + }, + ]; + + return ( + + setCurrentValue(value as ICertificateTypeSelectValue) + } + getOptionLabel={({ label }) => label} + placeholder={t("certificates.select-type-placeholder")} + popoverProps={{ + className: styles.certificate_type_select_popover, + }} + /> + ); +}; diff --git a/src/components/certificate-type-select/index.ts b/src/components/certificate-type-select/index.ts new file mode 100644 index 00000000..cf8855aa --- /dev/null +++ b/src/components/certificate-type-select/index.ts @@ -0,0 +1 @@ +export * from "./CertificateTypeSelect"; diff --git a/src/components/certificate-type-select/styles/index.module.scss b/src/components/certificate-type-select/styles/index.module.scss new file mode 100644 index 00000000..af153039 --- /dev/null +++ b/src/components/certificate-type-select/styles/index.module.scss @@ -0,0 +1,19 @@ +.certificate_type_select_popover { + & > ul[role="listbox"] { + padding: 7px 0; + + li:last-child { + margin-top: 11px; + position: relative; + &:before { + content: ""; + height: 1px; + background-color: var(--pv-color-gray-5); + position: absolute; + top: -6px; + left: 0; + right: 0; + } + } + } +} diff --git a/src/components/certificates-providers-list/styles/index.module.scss b/src/components/certificates-providers-list/styles/index.module.scss index d1fd1a4e..6e1e493a 100644 --- a/src/components/certificates-providers-list/styles/index.module.scss +++ b/src/components/certificates-providers-list/styles/index.module.scss @@ -11,7 +11,8 @@ .list_wrapper { padding: 1px var(--pv-size-base-2); max-height: 100%; - overflow-y: scroll; + overflow-y: auto; + overscroll-behavior: none; .list { display: flex; flex-direction: column; diff --git a/src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.stories.tsx b/src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.stories.tsx new file mode 100644 index 00000000..ab4fb5ff --- /dev/null +++ b/src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { KeyUsagesCheckboxGroup } from "./KeyUsagesCheckboxGroup"; + +const meta: Meta = { + title: "Components/KeyUsagesCheckboxGroup", + component: KeyUsagesCheckboxGroup, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.tsx b/src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.tsx new file mode 100644 index 00000000..d1efa512 --- /dev/null +++ b/src/components/key-usages-checkbox-group/KeyUsagesCheckboxGroup.tsx @@ -0,0 +1,44 @@ +import React, { ComponentProps } from "react"; +import clsx from "clsx"; +import { Checkbox, Typography } from "@peculiar/react-components"; +import { useTranslation } from "react-i18next"; +import { certificateKeyUsageExtensions } from "../../config/data"; + +import styles from "./styles/index.module.scss"; + +interface KeyUsagesCheckboxGroupProps { + className?: ComponentProps<"div">["className"]; +} + +export const KeyUsagesCheckboxGroup: React.FunctionComponent< + KeyUsagesCheckboxGroupProps +> = (props) => { + const { className } = props; + const { t } = useTranslation(); + + return ( +
+ + {t("certificates.extended-key-usages")} + +
+ {Object.entries(certificateKeyUsageExtensions).map(([key, value]) => ( + + ))} +
+
+ ); +}; diff --git a/src/components/key-usages-checkbox-group/index.ts b/src/components/key-usages-checkbox-group/index.ts new file mode 100644 index 00000000..7e7977ce --- /dev/null +++ b/src/components/key-usages-checkbox-group/index.ts @@ -0,0 +1 @@ +export * from "./KeyUsagesCheckboxGroup"; diff --git a/src/components/key-usages-checkbox-group/styles/index.module.scss b/src/components/key-usages-checkbox-group/styles/index.module.scss new file mode 100644 index 00000000..da72684a --- /dev/null +++ b/src/components/key-usages-checkbox-group/styles/index.module.scss @@ -0,0 +1,17 @@ +.key_usages_checkbox_group_box { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-3); + + .key_usages_checkbox_group { + display: flex; + flex-direction: column; + gap: var(--pv-size-base-2); + align-items: flex-start; + + .key_usages_checkbox { + display: flex; + gap: var(--pv-size-base-2); + } + } +} diff --git a/src/config/data/certificate-key-usage-extensions.ts b/src/config/data/certificate-key-usage-extensions.ts new file mode 100644 index 00000000..f2f3b20d --- /dev/null +++ b/src/config/data/certificate-key-usage-extensions.ts @@ -0,0 +1,13 @@ +import { ExtendedKeyUsage } from "@peculiar/x509"; + +export const certificateKeyUsageExtensions = { + emailProtection: ExtendedKeyUsage.emailProtection, + codeSigning: ExtendedKeyUsage.codeSigning, + // http://oid-info.com/get/1.3.6.1.5.5.7.3.36 + documentSigning: "1.3.6.1.5.5.7.3.36", + clientAuth: ExtendedKeyUsage.clientAuth, + serverAuth: ExtendedKeyUsage.serverAuth, +}; + +export type ICertificateExtendedKeyUsages = + keyof typeof certificateKeyUsageExtensions; diff --git a/src/config/data/countries.ts b/src/config/data/countries.ts new file mode 100644 index 00000000..63a8a6d0 --- /dev/null +++ b/src/config/data/countries.ts @@ -0,0 +1,974 @@ +export const countries = [ + { + code: "AF", + value: "Afghanistan", + }, + { + code: "AX", + value: "Åland Islands", + }, + { + code: "AL", + value: "Albania", + }, + { + code: "DZ", + value: "Algeria", + }, + { + code: "AS", + value: "American Samoa", + }, + { + code: "AD", + value: "AndorrA", + }, + { + code: "AO", + value: "Angola", + }, + { + code: "AI", + value: "Anguilla", + }, + { + code: "AQ", + value: "Antarctica", + }, + { + code: "AG", + value: "Antigua and Barbuda", + }, + { + code: "AR", + value: "Argentina", + }, + { + code: "AM", + value: "Armenia", + }, + { + code: "AW", + value: "Aruba", + }, + { + code: "AU", + value: "Australia", + }, + { + code: "AT", + value: "Austria", + }, + { + code: "AZ", + value: "Azerbaijan", + }, + { + code: "BS", + value: "Bahamas", + }, + { + code: "BH", + value: "Bahrain", + }, + { + code: "BD", + value: "Bangladesh", + }, + { + code: "BB", + value: "Barbados", + }, + { + code: "BY", + value: "Belarus", + }, + { + code: "BE", + value: "Belgium", + }, + { + code: "BZ", + value: "Belize", + }, + { + code: "BJ", + value: "Benin", + }, + { + code: "BM", + value: "Bermuda", + }, + { + code: "BT", + value: "Bhutan", + }, + { + code: "BO", + value: "Bolivia", + }, + { + code: "BA", + value: "Bosnia and Herzegovina", + }, + { + code: "BW", + value: "Botswana", + }, + { + code: "BV", + value: "Bouvet Island", + }, + { + code: "BR", + value: "Brazil", + }, + { + code: "IO", + value: "British Indian Ocean Territory", + }, + { + code: "CA", + value: "Canada", + }, + { + code: "BN", + value: "Brunei Darussalam", + }, + { + code: "BG", + value: "Bulgaria", + }, + { + code: "BF", + value: "Burkina Faso", + }, + { + code: "BI", + value: "Burundi", + }, + { + code: "KH", + value: "Cambodia", + }, + { + code: "CM", + value: "Cameroon", + }, + { + code: "CV", + value: "Cape Verde", + }, + { + code: "KY", + value: "Cayman Islands", + }, + { + code: "CF", + value: "Central African Republic", + }, + { + code: "TD", + value: "Chad", + }, + { + code: "CL", + value: "Chile", + }, + { + code: "CN", + value: "China", + }, + { + code: "CX", + value: "Christmas Island", + }, + { + code: "CC", + value: "Cocos (Keeling) Islands", + }, + { + code: "CO", + value: "Colombia", + }, + { + code: "KM", + value: "Comoros", + }, + { + code: "CG", + value: "Congo", + }, + { + code: "CD", + value: "Congo, The Democratic Republic of the", + }, + { + code: "CK", + value: "Cook Islands", + }, + { + code: "CR", + value: "Costa Rica", + }, + { + code: "CI", + value: "Cote D'Ivoire", + }, + { + code: "HR", + value: "Croatia", + }, + { + code: "CU", + value: "Cuba", + }, + { + code: "CY", + value: "Cyprus", + }, + { + code: "CZ", + value: "Czech Republic", + }, + { + code: "DK", + value: "Denmark", + }, + { + code: "DJ", + value: "Djibouti", + }, + { + code: "DM", + value: "Dominica", + }, + { + code: "DO", + value: "Dominican Republic", + }, + { + code: "EC", + value: "Ecuador", + }, + { + code: "EG", + value: "Egypt", + }, + { + code: "SV", + value: "El Salvador", + }, + { + code: "GQ", + value: "Equatorial Guinea", + }, + { + code: "ER", + value: "Eritrea", + }, + { + code: "EE", + value: "Estonia", + }, + { + code: "ET", + value: "Ethiopia", + }, + { + code: "FK", + value: "Falkland Islands (Malvinas)", + }, + { + code: "FO", + value: "Faroe Islands", + }, + { + code: "FJ", + value: "Fiji", + }, + { + code: "FI", + value: "Finland", + }, + { + code: "FR", + value: "France", + }, + { + code: "GF", + value: "French Guiana", + }, + { + code: "PF", + value: "French Polynesia", + }, + { + code: "TF", + value: "French Southern Territories", + }, + { + code: "GA", + value: "Gabon", + }, + { + code: "GM", + value: "Gambia", + }, + { + code: "GE", + value: "Georgia", + }, + { + code: "DE", + value: "Germany", + }, + { + code: "GH", + value: "Ghana", + }, + { + code: "GI", + value: "Gibraltar", + }, + { + code: "GR", + value: "Greece", + }, + { + code: "GL", + value: "Greenland", + }, + { + code: "GD", + value: "Grenada", + }, + { + code: "GP", + value: "Guadeloupe", + }, + { + code: "GU", + value: "Guam", + }, + { + code: "GT", + value: "Guatemala", + }, + { + code: "GG", + value: "Guernsey", + }, + { + code: "GN", + value: "Guinea", + }, + { + code: "GW", + value: "Guinea-Bissau", + }, + { + code: "GY", + value: "Guyana", + }, + { + code: "HT", + value: "Haiti", + }, + { + code: "HM", + value: "Heard Island and Mcdonald Islands", + }, + { + code: "VA", + value: "Holy See (Vatican City State)", + }, + { + code: "HN", + value: "Honduras", + }, + { + code: "HK", + value: "Hong Kong", + }, + { + code: "HU", + value: "Hungary", + }, + { + code: "IS", + value: "Iceland", + }, + { + code: "IN", + value: "India", + }, + { + code: "ID", + value: "Indonesia", + }, + { + code: "IR", + value: "Iran, Islamic Republic Of", + }, + { + code: "IQ", + value: "Iraq", + }, + { + code: "IE", + value: "Ireland", + }, + { + code: "IM", + value: "Isle of Man", + }, + { + code: "IL", + value: "Israel", + }, + { + code: "IT", + value: "Italy", + }, + { + code: "JM", + value: "Jamaica", + }, + { + code: "JP", + value: "Japan", + }, + { + code: "JE", + value: "Jersey", + }, + { + code: "JO", + value: "Jordan", + }, + { + code: "KZ", + value: "Kazakhstan", + }, + { + code: "KE", + value: "Kenya", + }, + { + code: "KI", + value: "Kiribati", + }, + { + code: "KP", + value: "Korea, Democratic People'S Republic of", + }, + { + code: "KR", + value: "Korea, Republic of", + }, + { + code: "KW", + value: "Kuwait", + }, + { + code: "KG", + value: "Kyrgyzstan", + }, + { + code: "LA", + value: "Lao People'S Democratic Republic", + }, + { + code: "LV", + value: "Latvia", + }, + { + code: "LB", + value: "Lebanon", + }, + { + code: "LS", + value: "Lesotho", + }, + { + code: "LR", + value: "Liberia", + }, + { + code: "LY", + value: "Libyan Arab Jamahiriya", + }, + { + code: "LI", + value: "Liechtenstein", + }, + { + code: "LT", + value: "Lithuania", + }, + { + code: "LU", + value: "Luxembourg", + }, + { + code: "MO", + value: "Macao", + }, + { + code: "MK", + value: "Macedonia, The Former Yugoslav Republic of", + }, + { + code: "MG", + value: "Madagascar", + }, + { + code: "MW", + value: "Malawi", + }, + { + code: "MY", + value: "Malaysia", + }, + { + code: "MV", + value: "Maldives", + }, + { + code: "ML", + value: "Mali", + }, + { + code: "MT", + value: "Malta", + }, + { + code: "MH", + value: "Marshall Islands", + }, + { + code: "MQ", + value: "Martinique", + }, + { + code: "MR", + value: "Mauritania", + }, + { + code: "MU", + value: "Mauritius", + }, + { + code: "YT", + value: "Mayotte", + }, + { + code: "MX", + value: "Mexico", + }, + { + code: "FM", + value: "Micronesia, Federated States of", + }, + { + code: "MD", + value: "Moldova, Republic of", + }, + { + code: "MC", + value: "Monaco", + }, + { + code: "MN", + value: "Mongolia", + }, + { + code: "MS", + value: "Montserrat", + }, + { + code: "MA", + value: "Morocco", + }, + { + code: "MZ", + value: "Mozambique", + }, + { + code: "MM", + value: "Myanmar", + }, + { + code: "NA", + value: "Namibia", + }, + { + code: "NR", + value: "Nauru", + }, + { + code: "NP", + value: "Nepal", + }, + { + code: "NL", + value: "Netherlands", + }, + { + code: "AN", + value: "Netherlands Antilles", + }, + { + code: "NC", + value: "New Caledonia", + }, + { + code: "NZ", + value: "New Zealand", + }, + { + code: "NI", + value: "Nicaragua", + }, + { + code: "NE", + value: "Niger", + }, + { + code: "NG", + value: "Nigeria", + }, + { + code: "NU", + value: "Niue", + }, + { + code: "NF", + value: "Norfolk Island", + }, + { + code: "MP", + value: "Northern Mariana Islands", + }, + { + code: "NO", + value: "Norway", + }, + { + code: "OM", + value: "Oman", + }, + { + code: "PK", + value: "Pakistan", + }, + { + code: "PW", + value: "Palau", + }, + { + code: "PS", + value: "Palestinian Territory, Occupied", + }, + { + code: "PA", + value: "Panama", + }, + { + code: "PG", + value: "Papua New Guinea", + }, + { + code: "PY", + value: "Paraguay", + }, + { + code: "PE", + value: "Peru", + }, + { + code: "PH", + value: "Philippines", + }, + { + code: "PN", + value: "Pitcairn", + }, + { + code: "PL", + value: "Poland", + }, + { + code: "PT", + value: "Portugal", + }, + { + code: "PR", + value: "Puerto Rico", + }, + { + code: "QA", + value: "Qatar", + }, + { + code: "RE", + value: "Reunion", + }, + { + code: "RO", + value: "Romania", + }, + { + code: "RU", + value: "Russian Federation", + }, + { + code: "RW", + value: "RWANDA", + }, + { + code: "SH", + value: "Saint Helena", + }, + { + code: "KN", + value: "Saint Kitts and Nevis", + }, + { + code: "LC", + value: "Saint Lucia", + }, + { + code: "PM", + value: "Saint Pierre and Miquelon", + }, + { + code: "VC", + value: "Saint Vincent and the Grenadines", + }, + { + code: "WS", + value: "Samoa", + }, + { + code: "SM", + value: "San Marino", + }, + { + code: "ST", + value: "Sao Tome and Principe", + }, + { + code: "SA", + value: "Saudi Arabia", + }, + { + code: "SN", + value: "Senegal", + }, + { + code: "CS", + value: "Serbia and Montenegro", + }, + { + code: "SC", + value: "Seychelles", + }, + { + code: "SL", + value: "Sierra Leone", + }, + { + code: "SG", + value: "Singapore", + }, + { + code: "SK", + value: "Slovakia", + }, + { + code: "SI", + value: "Slovenia", + }, + { + code: "SB", + value: "Solomon Islands", + }, + { + code: "SO", + value: "Somalia", + }, + { + code: "ZA", + value: "South Africa", + }, + { + code: "GS", + value: "South Georgia and the South Sandwich Islands", + }, + { + code: "ES", + value: "Spain", + }, + { + code: "LK", + value: "Sri Lanka", + }, + { + code: "SD", + value: "Sudan", + }, + { + code: "SR", + value: "Suriname", + }, + { + code: "SJ", + value: "Svalbard and Jan Mayen", + }, + { + code: "SZ", + value: "Swaziland", + }, + { + code: "SE", + value: "Sweden", + }, + { + code: "CH", + value: "Switzerland", + }, + { + code: "SY", + value: "Syrian Arab Republic", + }, + { + code: "TW", + value: "Taiwan, Province of China", + }, + { + code: "TJ", + value: "Tajikistan", + }, + { + code: "TZ", + value: "Tanzania, United Republic of", + }, + { + code: "TH", + value: "Thailand", + }, + { + code: "TL", + value: "Timor-Leste", + }, + { + code: "TG", + value: "Togo", + }, + { + code: "TK", + value: "Tokelau", + }, + { + code: "TO", + value: "Tonga", + }, + { + code: "TT", + value: "Trinidad and Tobago", + }, + { + code: "TN", + value: "Tunisia", + }, + { + code: "TR", + value: "Turkey", + }, + { + code: "TM", + value: "Turkmenistan", + }, + { + code: "TC", + value: "Turks and Caicos Islands", + }, + { + code: "TV", + value: "Tuvalu", + }, + { + code: "UG", + value: "Uganda", + }, + { + code: "UA", + value: "Ukraine", + }, + { + code: "AE", + value: "United Arab Emirates", + }, + { + code: "GB", + value: "United Kingdom", + }, + { + code: "US", + value: "United States", + }, + { + code: "UM", + value: "United States Minor Outlying Islands", + }, + { + code: "UY", + value: "Uruguay", + }, + { + code: "UZ", + value: "Uzbekistan", + }, + { + code: "VU", + value: "Vanuatu", + }, + { + code: "VE", + value: "Venezuela", + }, + { + code: "VN", + value: "Viet Nam", + }, + { + code: "VG", + value: "Virgin Islands, British", + }, + { + code: "VI", + value: "Virgin Islands, U.S.", + }, + { + code: "WF", + value: "Wallis and Futuna", + }, + { + code: "EH", + value: "Western Sahara", + }, + { + code: "YE", + value: "Yemen", + }, + { + code: "ZM", + value: "Zambia", + }, + { + code: "ZW", + value: "Zimbabwe", + }, +]; diff --git a/src/config/data/index.ts b/src/config/data/index.ts new file mode 100644 index 00000000..6c416d54 --- /dev/null +++ b/src/config/data/index.ts @@ -0,0 +1,2 @@ +export * from "./certificate-key-usage-extensions"; +export * from "./countries"; diff --git a/src/dialogs/certificate-create-dialog/index.ts b/src/dialogs/certificate-create-dialog/index.ts new file mode 100644 index 00000000..e481cd3a --- /dev/null +++ b/src/dialogs/certificate-create-dialog/index.ts @@ -0,0 +1 @@ +export * from "./useCertificateCreateDialog"; diff --git a/src/dialogs/certificate-create-dialog/useCertificateCreateDialog.tsx b/src/dialogs/certificate-create-dialog/useCertificateCreateDialog.tsx new file mode 100644 index 00000000..9892fd4c --- /dev/null +++ b/src/dialogs/certificate-create-dialog/useCertificateCreateDialog.tsx @@ -0,0 +1,73 @@ +import React, { useRef } from "react"; +import { IProviderInfo } from "@peculiar/fortify-client-core"; +import { useToast } from "@peculiar/react-components"; +import { useTranslation } from "react-i18next"; +import { useLockBodyScroll } from "react-use"; +import { certificateSubjectToString } from "../../utils/certificate"; +import { + CertificateCreateDataProps, + CertificateCreateDialog, +} from "../../components/certificate-create-dialog"; +import { CertificateType } from "../../types"; + +export function useCertificateCreateDialog(props: { + providers: IProviderInfo[]; + currentProviderId?: string; +}) { + const { providers, currentProviderId } = props; + const { addToast } = useToast(); + const { t } = useTranslation(); + + const [isOpen, setIsOpen] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + + const localCurrentProviderId = useRef(currentProviderId); + + const dialogType = useRef("x509"); + + const handleCertificateCreate = (data: CertificateCreateDataProps) => { + // Check provider + if (!localCurrentProviderId?.current) { + localCurrentProviderId.current = currentProviderId; + } + // TODO: add logic + console.log("Create", data); + console.log("localCurrentProviderId", localCurrentProviderId.current); + const subject = certificateSubjectToString(data.subject); + console.log("subject => ", subject); + // temporary behaviour + setIsLoading(true); + setTimeout(function () { + setIsLoading(false); + addToast({ + message: t("certificates.dialog.create.failure-message"), + variant: "wrong", + disableIcon: true, + isClosable: true, + }); + }, 1000); + }; + + useLockBodyScroll(isOpen); + + return { + open: (type: CertificateType) => { + dialogType.current = type; + setIsOpen(true); + }, + dialog: () => + isOpen ? ( + setIsOpen(false)} + onProviderSelect={(id) => { + localCurrentProviderId.current = id; + }} + providers={providers} + currentProviderId={currentProviderId} + loading={isLoading} + /> + ) : null, + }; +} diff --git a/src/dialogs/certificate-import-dialog/useCertificateImportDialog.tsx b/src/dialogs/certificate-import-dialog/useCertificateImportDialog.tsx index 1acf724b..164fd498 100644 --- a/src/dialogs/certificate-import-dialog/useCertificateImportDialog.tsx +++ b/src/dialogs/certificate-import-dialog/useCertificateImportDialog.tsx @@ -3,6 +3,7 @@ import { IProviderInfo } from "@peculiar/fortify-client-core"; import { X509Certificate } from "@peculiar/x509"; import { useToast } from "@peculiar/react-components"; import { useTranslation } from "react-i18next"; +import { useLockBodyScroll } from "react-use"; import { CertificateImportDialog } from "../../components/certificate-import-dialog"; export function useCertificateImportDialog(props: { @@ -22,6 +23,10 @@ export function useCertificateImportDialog(props: { const localCurrentProviderId = useRef(currentProviderId); const handleCertificateImport = () => { + // Check provider + if (!localCurrentProviderId?.current) { + localCurrentProviderId.current = currentProviderId; + } // TODO: add logic console.log("Import", certificate); console.log("localCurrentProviderId", localCurrentProviderId?.current); @@ -45,6 +50,8 @@ export function useCertificateImportDialog(props: { setIsTextAreaError(false); }; + useLockBodyScroll(isOpen); + return { open: () => setIsOpen(true), dialog: () => @@ -58,7 +65,7 @@ export function useCertificateImportDialog(props: { }} onTextAreaBlur={() => { try { - // certificate?.length && new X509Certificate(certificate); + certificate?.length && new X509Certificate(certificate); setIsTextAreaError(false); } catch (error) { diff --git a/src/global.scss b/src/global.scss index 12fb006d..e17844da 100644 --- a/src/global.scss +++ b/src/global.scss @@ -17,6 +17,11 @@ body { background-color: var(--pv-color-gray-2); } +html, +body { + overscroll-behavior: none; +} + #root { margin: 0 40px 0 340px; min-height: 100%; @@ -32,3 +37,16 @@ a { text-decoration: underline; } } + +.required_text_field { + position: relative; + &:after { + content: ""; + position: absolute; + width: 6px; + height: 6px; + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNiIgaGVpZ2h0PSI2IiB2aWV3Qm94PSIwIDAgNiA2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAuNjExMTM3IDBDMC4yNzM4MzQgMCAwIDAuMjcxMDQgMCAwLjYwNTE0OUMwIDAuNzg0Mzc5IDAuMDc4NjM3NSAwLjk0NTY0NiAwLjIwMzk3OCAxLjA1NjIyQzAuMjA2NzczIDEuMDU5MDEgMC4yMDM1NzkgMS4wNjMgMC4yMDM5NzggMS4wNjU4TDQuOTQyOTggNS44MTY3OEw0Ljk0OTc3IDUuODE3NThDNS4wNTk1NCA1LjkyOTc1IDUuMjEyODMgNiA1LjM4Mjg4IDZDNS43MTY5OCA2IDUuOTg4MDIgNS43Mjg5NiA1Ljk4ODAyIDUuMzk1MjVDNS45ODc2MyA1LjM4MjQ4IDYgMS41OTY3IDYgMS41OTY3QzYgMC43MTQ5MjMgNS4yODUwOCAwIDQuNDAzMyAwSDAuNjExMTM3WiIgZmlsbD0iI0RFNDY0MSIvPgo8L3N2Zz4K); + right: 2px; + top: 28px; + } +} diff --git a/src/hooks/app/useApp.tsx b/src/hooks/app/useApp.tsx index e0eb797a..a292b2bb 100644 --- a/src/hooks/app/useApp.tsx +++ b/src/hooks/app/useApp.tsx @@ -204,11 +204,6 @@ export function useApp() { console.log(value); }; - const handleCertificateCreate = () => { - // TODO: add logic - console.log("Create"); - }; - const handleCertificateDeleteDialogOpen = (id: string, name: string) => { setCurrentCetificateDelete({ id, @@ -255,7 +250,6 @@ export function useApp() { currentCertificateViewerValue, handleProviderChange, handleCertificatesSearch, - handleCertificateCreate, handleCertificateDeleteDialogOpen, handleCertificateDeleteDialogClose, handleCertificateDelete, diff --git a/src/i18n/locales/en/main.json b/src/i18n/locales/en/main.json index 528fb148..4d533419 100644 --- a/src/i18n/locales/en/main.json +++ b/src/i18n/locales/en/main.json @@ -27,6 +27,19 @@ "empty-text": "There are no certificates yet." }, "dialog": { + "create": { + "title": { + "x509": "Create Self-signed certificate", + "csr": "Create Certificate Signing Request (CSR)" + }, + "select-type": { + "x509": "What do you need certificate for?", + "csr": "What do you need CSR for?" + }, + "loading-text": "Creating certificate...", + "success-message": "Certificate created.", + "failure-message": "Can't create certificate" + }, "delete": { "title": "Delete certificate", "message": "Are you sure you want to delete “{{name}}”?", @@ -60,6 +73,67 @@ "success-message": "Certificate imported.", "failure-message": "Failed to import certificate because of error. Please try again." } + }, + "key-usage-extension": { + "emailProtection": "S/MIME", + "codeSigning": "Code signing", + "documentSigning": "Document signing", + "clientAuth": "TLS Client Authentication", + "serverAuth": "TLS Server Authentication" + }, + "custom-csr-option": "Custom CSR", + "custom-certificate-option": "Custom certificate", + "select-type-placeholder": "Select type", + "extended-key-usages": "Extended key usages", + "key-properties": "Key properties", + "signature-algorithm": "Signature Algorithm", + "hash-algorithm": "Hash Algorithm", + "key-named-curve": "Named curve", + "general": { + "title": "General", + "friendly-name": "Friendly name", + "description": "Description" + }, + "subject": { + "title": "Subject", + "cname": { + "label": "Common name", + "placeholder": "company.com", + "placeholder-2": "example.com" + }, + "email-address": { + "label": "Email address", + "placeholder": "company@example.com", + "error": { + "type": "Please enter valid email address" + } + }, + "organization-name": { + "label": "Organization", + "placeholder": "Company name" + }, + "organization-unit-name": { + "label": "Organization unit" + }, + "locality-name": { + "label": "Locality", + "placeholder": "Town, city, village, etc." + }, + "state-or-province-name": { + "label": "State", + "placeholder": "Province, region, county or state" + }, + "country-name": { + "label": "Country", + "placeholder": "Select country" + } + }, + "button-create": { + "text": { + "x509": "Create certificate", + "csr": "Create CSR" + }, + "title": "Please fill required fields" } }, "connection": { diff --git a/src/types.ts b/src/types.ts index be213899..ef3893b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,25 @@ -import { ICertificate } from "@peculiar/fortify-client-core"; +import { + ICertificate, + EHashAlgorithm, + ESignatureAlgorithm, +} from "@peculiar/fortify-client-core"; export interface CertificateProps extends ICertificate { id?: string; label?: string; } +export interface CertificateSubjectProps { + CN: string; + E?: string; + O?: string; + OU?: string; + L?: string; + ST?: string; + C?: string; +} + +export type CertificateAlgorithmProps = { + signature: ESignatureAlgorithm; + hash: EHashAlgorithm; +}; + +export type CertificateType = "x509" | "csr"; diff --git a/src/utils/certificate.ts b/src/utils/certificate.ts index 4328cd0b..f045bb58 100644 --- a/src/utils/certificate.ts +++ b/src/utils/certificate.ts @@ -1,7 +1,8 @@ import { Convert } from "pvtsutils"; import { Pkcs10CertificateRequest, X509Certificate } from "@peculiar/x509"; +import { CertificateSubjectProps, CertificateType } from "../types"; -export function certificateRawToPem(raw: ArrayBuffer, type: "x509" | "csr") { +export function certificateRawToPem(raw: ArrayBuffer, type: CertificateType) { let pem; switch (type) { case "x509": { @@ -19,3 +20,16 @@ export function certificateRawToPem(raw: ArrayBuffer, type: "x509" | "csr") { } return pem; } + +export function certificateSubjectToString( + attrs: CertificateSubjectProps +): string { + const parts: string[] = []; + + for (const key in attrs) { + const val = attrs[key as keyof CertificateSubjectProps]; + val?.length && parts.push(`${key}=${val}`); + } + + return parts.join(", "); +} diff --git a/src/utils/download-certificate.ts b/src/utils/download-certificate.ts index 1290f052..298906a2 100644 --- a/src/utils/download-certificate.ts +++ b/src/utils/download-certificate.ts @@ -1,11 +1,12 @@ import { Convert } from "pvtsutils"; import { Pkcs10CertificateRequest, X509Certificate } from "@peculiar/x509"; import { Download } from "@peculiar/certificates-viewer"; +import { CertificateType } from "../types"; export function downloadCertificate( label: string, certRaw: ArrayBuffer, - type: "x509" | "csr" + type: CertificateType ) { switch (type) { case "x509": { diff --git a/yarn.lock b/yarn.lock index 937b91e8..8a2fdabd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1015,7 +1015,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== @@ -3123,6 +3123,11 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== +"@types/js-cookie@^2.2.6": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" + integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== + "@types/lodash@^4.14.167": version "4.17.4" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7" @@ -3494,6 +3499,11 @@ tslib "^2.4.0" tsprotobuf "^1.0.18" +"@xobotyi/scrollbar-width@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== + "@yarnpkg/esbuild-plugin-pnp@^3.0.0-rc.10": version "3.0.0-rc.15" resolved "https://registry.yarnpkg.com/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.15.tgz#4e40e7d2eb28825c9a35ab9d04c363931d7c0e67" @@ -4169,6 +4179,13 @@ cookie@0.6.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +copy-to-clipboard@^3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + core-js-compat@^3.31.0, core-js-compat@^3.36.1: version "3.37.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" @@ -4216,6 +4233,21 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-in-js-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb" + integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A== + dependencies: + hyphenate-style-name "^1.0.3" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -4228,7 +4260,7 @@ cssstyle@^4.0.1: dependencies: rrweb-cssom "^0.6.0" -csstype@^3.0.2: +csstype@^3.0.2, csstype@^3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -4539,6 +4571,13 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" @@ -4854,6 +4893,21 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-loops@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75" + integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g== + +fast-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" + integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== + +fastest-stable-stringify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76" + integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q== + fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -5409,6 +5463,11 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +hyphenate-style-name@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.5.tgz#70b68605ee601b7142362239a0236159a8b2dc33" + integrity sha512-fedL7PRwmeVkgyhu9hLeTBaI6wcGk7JGJswdaRsa5aUbkXI1kr1xZwTPBtaYPpwf56878iDek6VbVnuWMebJmw== + i18next@^23.11.2: version "23.11.4" resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.4.tgz#3f0e620fd2cff3825324191615d0ab0a1eec3baf" @@ -5486,6 +5545,14 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inline-style-prefixer@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz#991d550735d42069f528ac1bcdacd378d1305442" + integrity sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ== + dependencies: + css-in-js-utils "^3.1.0" + fast-loops "^1.1.3" + internal-slot@^1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" @@ -5841,6 +5908,11 @@ jake@^10.8.5: filelist "^1.0.4" minimatch "^3.1.2" +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6160,6 +6232,11 @@ markdown-to-jsx@7.3.2: resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.3.2.tgz#f286b4d112dad3028acc1e77dfe1f653b347e131" integrity sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q== +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -6318,6 +6395,20 @@ ms@2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +nano-css@^5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.6.1.tgz#964120cb1af6cccaa6d0717a473ccd876b34c197" + integrity sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + css-tree "^1.1.2" + csstype "^3.1.2" + fastest-stable-stringify "^2.0.2" + inline-style-prefixer "^7.0.0" + rtl-css-js "^1.16.1" + stacktrace-js "^2.0.2" + stylis "^4.3.0" + nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -7107,6 +7198,31 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + +react-use@^17.5.0: + version "17.5.0" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.5.0.tgz#1fae45638828a338291efa0f0c61862db7ee6442" + integrity sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.6.1" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + "react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.3.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -7275,6 +7391,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -7351,6 +7472,13 @@ rrweb-cssom@^0.6.0: resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== +rtl-css-js@^1.16.1: + version "1.16.1" + resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80" + integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg== + dependencies: + "@babel/runtime" "^7.1.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -7396,6 +7524,11 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" +screenfull@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -7462,6 +7595,11 @@ set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +set-harmonic-interval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249" + integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -7547,6 +7685,11 @@ source-map-support@^0.5.16: buffer-from "^1.0.0" source-map "^0.6.0" +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA== + source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -7588,11 +7731,40 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== +stack-generator@^2.0.5: + version "2.0.10" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d" + integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ== + dependencies: + stackframe "^1.3.4" + stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-gps@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz#0c40b24a9b119b20da4525c398795338966a2fb0" + integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ== + dependencies: + source-map "0.5.6" + stackframe "^1.3.4" + +stacktrace-js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b" + integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg== + dependencies: + error-stack-parser "^2.0.6" + stack-generator "^2.0.5" + stacktrace-gps "^3.0.4" + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -7735,6 +7907,11 @@ stylis@4.2.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== +stylis@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7846,6 +8023,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +throttle-debounce@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" + integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== + through2@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -7891,6 +8073,11 @@ tocbot@^4.20.1: resolved "https://registry.yarnpkg.com/tocbot/-/tocbot-4.27.20.tgz#c7ba627585894fa306d65b08f53f624949becf19" integrity sha512-6M78FT20+FA5edtx7KowLvhG3gbZ6GRcEkL/0b2TcPbn6Ba+1ayI3SEVxe25zjkWGs0jd04InImaO81Hd8Hukw== +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -7928,6 +8115,11 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0: resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + tsconfck@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.3.tgz#d9bda0e87d05b1c360e996c9050473c7e6f8084f"