Skip to content

Commit

Permalink
Version 2: Add import certificate dialog (#132)
Browse files Browse the repository at this point in the history
* Add CertificatesProvidersSelectList component

* Add CertificateImportDialog component

* Certificate Import Dialog open/close

* Add file drop zone

* Styles drop zone

* Certificate processing

* Fix certificate processing

* Add actions

* Fix title border

* Fix styles

* Fix drop zone styles & behaviour

* Fix styles

* Fix dialog title

* Fix CertificatesProvidersSelectList style

* Add useCertificateImportDialog

* Fix provider selection

* Fix provider select popover

* Fix upload certificate

* Lock fix

---------

Co-authored-by: alex-slobodian <[email protected]>
  • Loading branch information
OleksandrSPV and aleksandr-slobodian authored May 20, 2024
1 parent ab4a8d6 commit c970d5a
Show file tree
Hide file tree
Showing 17 changed files with 1,752 additions and 941 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@
"@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",
"clsx": "^2.1.1",
"i18next": "^23.11.2",
"intl-messageformat": "^10.5.11",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^14.1.1"
},
"resolutions": {
Expand Down
23 changes: 12 additions & 11 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CertificatesProvidersList } from "./components/certificates-providers-l
import { CertificatesTopbar } from "./components/certificates-topbar";
import { CertificateDeleteDialog } from "./components/certificate-delete-dialog";
import { CertificateViewerDialog } from "./components/certificate-viewer-dialog";
import { useCertificateImportDialog } from "./dialogs/certificate-import-dialog";

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

Expand All @@ -22,7 +23,6 @@ export function App() {
currentCertificateViewerValue,
handleProviderChange,
handleCertificatesSearch,
handleCertificateImport,
handleCertificateCreate,
handleCertificateDeleteDialogOpen,
handleCertificateDeleteDialogClose,
Expand All @@ -31,6 +31,14 @@ export function App() {
handleCertificateViewerClose,
} = useApp();

const {
open: handleCertificateImportDialogOpen,
dialog: certificateImportDialog,
} = useCertificateImportDialog({
providers,
currentProviderId,
});

return (
<>
<CertificatesSidebar className={styles.sidebar}>
Expand All @@ -41,22 +49,14 @@ export function App() {
<CertificatesProvidersList
providers={providers}
currentProviderId={currentProviderId}
onSelect={(id) => {
if (
currentProviderId === id ||
fetching.certificates === "pending"
) {
return;
}
handleProviderChange(id);
}}
onSelect={handleProviderChange}
/>
)}
</CertificatesSidebar>
<CertificatesTopbar
className={styles.top_bar}
onSearch={handleCertificatesSearch}
onImport={handleCertificateImport}
onImport={handleCertificateImportDialogOpen}
onCreate={handleCertificateCreate}
></CertificatesTopbar>
{fetching.certificates ? (
Expand All @@ -83,6 +83,7 @@ export function App() {
onClose={handleCertificateViewerClose}
/>
) : null}
{certificateImportDialog()}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { CertificateImportDialog } from "./CertificateImportDialog";

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

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

const providers = [
{
id: "1",
name: "Provider 1",
},
{
id: "2",
name: "Provider 2",
},
];

export const Default: Story = {
args: {
currentProviderId: "2",
providers,
onProviderSelect: fn(),
onTextAreaChange: fn(),
},
};

export const isLoading: Story = {
args: {
loading: true,
providers,
onProviderSelect: fn(),
},
};
225 changes: 225 additions & 0 deletions src/components/certificate-import-dialog/CertificateImportDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import React from "react";
import { useDropzone } from "react-dropzone";
import { Trans, useTranslation } from "react-i18next";
import { IProviderInfo } from "@peculiar/fortify-client-core";
import clsx from "clsx";
import {
Dialog,
ArrowRightIcon,
IconButton,
Typography,
TextareaField,
Button,
CircularProgress,
} from "@peculiar/react-components";
import { CertificatesProvidersSelectList } from "../certificates-providers-select-list";
import { formatBytes } from "../../utils";

import CrossIcon from "../../icons/cross.svg?react";

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

import {
APP_CERTIFICATE_ALLOWED_MIMES,
APP_CERTIFICATE_MAX_SIZE_BYTES,
} from "../../config";

interface CertificateImportDialogProps {
currentProviderId?: string;
providers: Pick<IProviderInfo, "id" | "name">[];
loading?: boolean;
certificate: string;
isTextAreaError: boolean;
onTextAreaChange: (certificate: string) => void;
onTextAreaBlur: () => void;
onProviderSelect: (id: string) => void;
onDialogClose: () => void;
onImportButtonClick: () => void;
onDropError: (error?: unknown) => void;
onDropRejected: (error: string) => void;
onDropAccepted: (fileContent: ArrayBuffer) => void;
onClearButtonClick: () => void;
}

export const CertificateImportDialog: React.FunctionComponent<
CertificateImportDialogProps
> = (props) => {
const {
loading,
providers,
currentProviderId,
certificate,
isTextAreaError,
onTextAreaChange,
onTextAreaBlur,
onProviderSelect,
onDialogClose,
onImportButtonClick,
onDropError,
onDropRejected,
onDropAccepted,
onClearButtonClick,
} = props;

const { t } = useTranslation();

const { getRootProps, isDragActive, isDragReject } = useDropzone({
multiple: false,
accept: APP_CERTIFICATE_ALLOWED_MIMES,
maxSize: APP_CERTIFICATE_MAX_SIZE_BYTES,
onDropRejected: ([file]) => {
file.errors.forEach((err) => {
let msg;
if (err.code === "file-too-large") {
msg = t("certificates.dialog.import.file.error.too-large", {
size: formatBytes(APP_CERTIFICATE_MAX_SIZE_BYTES),
});
}
if (err.code === "file-invalid-type") {
msg = t("certificates.dialog.import.file.error.invalid-type");
}

if (msg) {
onDropRejected(msg);
}
});
},
onDropAccepted: ([file]) => {
if (!file) {
return false;
}
const reader = new FileReader();

reader.readAsArrayBuffer(file);

reader.onload = (event) => {
try {
onDropAccepted(event.target?.result as ArrayBuffer);
} catch (error) {
onDropError(error);
}
};

reader.onerror = onDropError;
},
onError: onDropError,
});

return (
<Dialog open fullScreen className={styles.dialog} onClose={onDialogClose}>
<>
<div className={styles.title}>
<div className={styles.centered}>
<div>
<IconButton
onClick={onDialogClose}
className={styles.button_back}
size="small"
>
<ArrowRightIcon className={styles.arrow_back} />
</IconButton>
</div>
<div className={styles.title_label}>
<Typography variant="h4" color="black">
{t("certificates.dialog.import.title")}
</Typography>
</div>
<div>
<CertificatesProvidersSelectList
providers={providers}
currentProviderId={currentProviderId}
onSelect={onProviderSelect}
className={styles.provider_select}
popoverClassName={styles.provider_select_popover}
/>
</div>
</div>
</div>
<div className={styles.content}>
<div className={clsx(styles.centered, styles.content_box)}>
<div
{...getRootProps({
className: clsx(styles.drop_zone, {
[styles.drop_zone_active]: isDragActive,
[styles.drop_zone_reject]: isDragReject,
}),
})}
>
<Typography variant="s2" color="gray-10">
<Trans
i18nKey="certificates.dialog.import.drop-zone.title"
components={[
<Typography
className={styles.drop_zone_title_link}
color="primary"
variant="s2"
component="button"
>
{0}
</Typography>,
]}
/>
</Typography>
<Typography variant="c2" color="gray-9">
{t("certificates.dialog.import.drop-zone.description")}
</Typography>
</div>
<div className={styles.divider_form}>
<Typography
variant="s2"
className={styles.divider_form_inner}
color="gray-10"
>
{t("certificates.dialog.import.divider-label")}
</Typography>
</div>
<div>
<TextareaField
className={styles.text_area}
value={certificate}
onChange={(event) => onTextAreaChange(event.target.value)}
size="large"
onBlur={onTextAreaBlur}
error={isTextAreaError}
errorText={
isTextAreaError
? t(
"certificates.dialog.import.certificate.error.invalid-data"
)
: undefined
}
/>
</div>
<div className={styles.buttons_group}>
<Button
variant="outlined"
startIcon={<CrossIcon />}
disabled={!certificate?.length}
onClick={onClearButtonClick}
className={styles.cancel_button}
>
{t("button.clear")}
</Button>
<Button
variant="contained"
color="primary"
disabled={!certificate?.length || isTextAreaError}
onClick={onImportButtonClick}
>
{t("button.import-certificate")}
</Button>
</div>
</div>
</div>
{loading ? (
<div className={styles.loading}>
<CircularProgress />
<Typography variant="b2" color="gray-9">
{t("certificates.dialog.import.loading-text")}
</Typography>
</div>
) : null}
</>
</Dialog>
);
};
1 change: 1 addition & 0 deletions src/components/certificate-import-dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CertificateImportDialog";
Loading

0 comments on commit c970d5a

Please sign in to comment.