Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(create secret) add file upload support #11

Merged
merged 1 commit into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-select": "^4.3.1",
Expand Down
2 changes: 1 addition & 1 deletion web/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { render } from "utils/test-utils";
import App from "./App";

test("renders app component", () => {
Expand Down
52 changes: 23 additions & 29 deletions web/src/components/CreateSecretForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Typography } from "@material-ui/core";
import { Typography } from "@material-ui/core";
import { TextField, Switch } from "formik-material-ui";
import { Field, Form, Formik } from "formik";
import { Autocomplete, AutocompleteRenderInputParams } from "formik-material-ui-lab";
Expand All @@ -8,35 +8,19 @@ import React, { useEffect } from "react";
import { CreateSecretFormProps } from "types/CreateSecretFormProps";
import { preventNonNumericalInput } from "utils/utils";
import { useStyles } from "styles/createSecretFormStyles";
import * as Yup from "yup";

const options: Lifetime[] = [
{ value: "5m", label: "5 min" },
{ value: "15m", label: "15 min" },
{ value: "30m", label: "30 min" },
{ value: "1h", label: "1 hour" },
{ value: "2h", label: "2 hours" },
{ value: "3h", label: "3 hours" },
{ value: "24h", label: "1 day" },
{ value: "48h", label: "2 days" },
{ value: "72h", label: "3 days" },
{ value: "168h", label: "7 days" }
];

const CreateSecretSchema = Yup.object().shape({
secret: Yup.string().required("You must add a secret"),
password: Yup.string(),
lifetime: Yup.object().shape({ value: Yup.string(), label: Yup.string() }).nullable(),
accessType: Yup.boolean(),
accesses: Yup.number().max(108, "The max number is 108")
});
import clsx from "clsx";
import Button from "./Button";
import StyledDropzone from "./Dropzone";
import { CreateSecretFileSchema, CreateSecretMessageSchema } from "utils/validation-schema";
import { LIFETIME_OPTIONS } from "constants/index";

const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
const classes = useStyles();
const CreateSecretSchema = props.type === "file" ? CreateSecretFileSchema : CreateSecretMessageSchema;

return (
<Formik initialValues={props.initialValues} validationSchema={CreateSecretSchema} onSubmit={props.onSubmit}>
{({ isSubmitting, errors, values, setFieldValue }) => {
{({ errors, values, setFieldValue }) => {
useEffect(() => {
if (values.accessType) {
setFieldValue("accesses", -1);
Expand All @@ -47,7 +31,7 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {

return (
<Form className={classes.form} noValidate>
<div>
<div className={clsx({ [classes.hide]: props.type === "file" })}>
<Field
component={TextField}
name="secret"
Expand All @@ -62,6 +46,10 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
maxRows={15}
/>
</div>
<div className={clsx({ [classes.hide]: props.type === "message" })}>
<Field component={StyledDropzone} />
</div>

<div>
<Field
component={TextField}
Expand Down Expand Up @@ -97,7 +85,7 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
name="lifetime"
component={Autocomplete}
size="small"
options={options}
options={LIFETIME_OPTIONS}
getOptionLabel={(option: Lifetime) => option.label}
getOptionSelected={(option: Lifetime, value: Lifetime) => option.value === value.value}
renderInput={(params: AutocompleteRenderInputParams) => (
Expand All @@ -116,9 +104,15 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
/>
</div>
<div>
<Button type="submit" variant="contained" color="primary" disabled={isSubmitting}>
Get Token
</Button>
<Button
label="Get Token"
type="submit"
variant="contained"
color="primary"
fullWidth
isLoading={props.loading}
disabled={props.loading}
/>
</div>
</Form>
);
Expand Down
87 changes: 87 additions & 0 deletions web/src/components/CreateSecretFormTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from "react";
import { makeStyles, Theme } from "@material-ui/core/styles";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import TabPanel from "./TabPanel";
import CreateSecretForm from "./CreateSecretForm";
import { FormikHelpers, FormikValues } from "formik";
import { Box, Divider, Typography } from "@material-ui/core";

type SecretType = "file" | "message";

function a11yProps(index: any) {
return {
id: `form-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}

const useStyles = makeStyles((theme: Theme) => ({
root: {
maxWidth: "500px",
margin: "0 auto",
backgroundColor: theme.palette.background.paper,
gap: theme.spacing(4),
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
width: "100%",
maxHeight: "80%",
height: "80%",
boxShadow: "rgba(0, 0, 0, 0.1) 0px 4px 12px",
background: "#f5f7f8",
borderRadius: "5px",
padding: theme.spacing(2)
},
container: {}
}));

type CreateSecretFormTabsProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSubmit: (values: FormikValues, helpers: FormikHelpers<any>) => void;
initialValues: FormikValues;
loading: boolean;
};

const CreateSecretFormTabs: React.FC<CreateSecretFormTabsProps> = props => {
const classes = useStyles();
const [value, setValue] = React.useState<"file" | "message">("message");

const handleChange = (event: React.ChangeEvent<any>, newValue: SecretType) => {
setValue(newValue);
};

return (
<div className={classes.root}>
<Box textAlign="center" marginY="1rem">
<Typography variant="body1" style={{ fontWeight: "bold" }}>
Which kind of secret do you want to share ?
</Typography>
</Box>
<Divider style={{ margin: "0 3rem" }} />
<Tabs value={value} onChange={handleChange} centered>
<Tab value="message" label="Secret message" {...a11yProps("message")} />
<Tab value="file" label="Secret file" {...a11yProps("file")} />
</Tabs>
<TabPanel value={value} index="message">
<CreateSecretForm
type={value}
onSubmit={props.onSubmit}
initialValues={props.initialValues}
loading={props.loading}
/>
</TabPanel>
<TabPanel value={value} index="file">
<CreateSecretForm
type={value}
onSubmit={props.onSubmit}
initialValues={props.initialValues}
loading={props.loading}
/>
</TabPanel>
</div>
);
};

export default CreateSecretFormTabs;
85 changes: 85 additions & 0 deletions web/src/components/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Typography } from "@material-ui/core";
import clsx from "clsx";
import { makeStyles } from "@material-ui/styles";
import { ErrorMessage, FieldProps } from "formik";
import { useMemo } from "react";
import { useDropzone } from "react-dropzone";

const useStyles = makeStyles(() => ({
error: {
borderColor: "#f44336 !important"
}
}));

const baseStyle = {
flex: 1,
display: "flex",
alignItems: "center",
padding: "20px",
borderWidth: 2,
borderRadius: 2,
height: "175px",
borderColor: "#eeeeee",
borderStyle: "dashed",
backgroundColor: "#fafafa",
color: "#bdbdbd",
outline: "none",
transition: "border .24s ease-in-out"
};

const activeStyle = {
borderColor: "#2196f3"
};

const acceptStyle = {
borderColor: "#00e676"
};

type StyledDropzoneProps = FieldProps;

const FILE_SIZE = 64 * 1024;

const StyledDropzone: React.FC<StyledDropzoneProps> = ({ form }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I very much like the dropzone for the file upload - nice work!

const classes = useStyles();
const { getRootProps, getInputProps, acceptedFiles, isDragActive, isDragAccept } = useDropzone({
maxSize: FILE_SIZE,
onDrop: (_acceptedFiles: File[]) => {
const file = _acceptedFiles[0];
form.setFieldValue("file", file);
form.setFieldValue("filename", file.name);
form.setFieldValue("is_base64", true);
}
});

const files = acceptedFiles.map(file => <Typography key={file.name}>{file.name}</Typography>);

const style = useMemo(
() => ({
...baseStyle,
...(isDragActive ? activeStyle : {}),
...(isDragAccept ? acceptStyle : {})
}),
[isDragActive, isDragAccept]
);

return (
<div className="container">
<div
{...getRootProps({ style })}
className={clsx({ [classes.error]: form.touched["file"] && !!form.errors["file"] })}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here ...</p>
) : (
<>{!files.length ? "Drag'n drop your file here, or click to select the file" : files}</>
)}
</div>
<small className="MuiFormHelperText-root MuiFormHelperText-contained Mui-error Mui-required">
<ErrorMessage name="file" />
</small>
</div>
);
};

export default StyledDropzone;
11 changes: 10 additions & 1 deletion web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { makeStyles } from "@material-ui/core";
import Footer from "./Footer";

type LayoutProps = {
children: React.ReactNode;
};

const useStyles = makeStyles(() => ({
children: {
height: "100vh",
width: "100%"
}
}));

const Layout: React.FC<LayoutProps> = ({ children }) => {
const classes = useStyles();
return (
<div>
{children}
<div className={classes.children}>{children}</div>
<Footer />
</div>
);
Expand Down
63 changes: 63 additions & 0 deletions web/src/components/ShowFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import AttachmentIcon from "@material-ui/icons/Attachment";
import { Box, Button, makeStyles, Theme, Typography } from "@material-ui/core";

const useStyles = makeStyles((theme: Theme) => ({
container: {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "100%",
maxWidth: "500px",
background: "#f7f5f5",
padding: theme.spacing(2)
}
}));

type ShowFileProps = {
file?: File;
uploadedAt?: Date;
};

const ShowFile: React.FC<ShowFileProps> = ({ file }) => {
const classes = useStyles();

const handleDownloadClick = () => {
if (file) {
const url = window.URL.createObjectURL(new Blob([file]));
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
link.setAttribute("download", file.name);
document.body.appendChild(link);
link.click();
link.parentNode && link.parentNode.removeChild(link);
}
};

return (
<div className={classes.container}>
<Box display="flex" alignItems="center" justifyContent="space-between">
<div>
<AttachmentIcon fontSize="large" />
</div>
<div>
<Typography variant="caption">Name</Typography>
<Typography>{file?.name}</Typography>
</div>
<div>
<Typography variant="caption">Size</Typography>
<Typography>{`${file?.size && (file?.size / 1024).toFixed(2)} Mb`}</Typography>
</div>
<div>
<Button variant="contained" color="primary" onClick={handleDownloadClick} fullWidth>
Download
</Button>
</div>
</Box>
</div>
);
};

export default ShowFile;
Loading