Skip to content

Commit

Permalink
Batch import styling (#2959)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Jan 20, 2023
1 parent a8a910a commit 287d28e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 81 deletions.
42 changes: 26 additions & 16 deletions frontend/src/component/project/Project/Import/FileDropZone.tsx
Expand Up @@ -2,29 +2,39 @@ import React, { FC, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { Box } from '@mui/material';

const formatJSON = (json: string) => {
try {
return JSON.stringify(JSON.parse(json), null, 4);
} catch (e) {
console.error(e);
return '';
}
};
const formatJSON = (json: string) => JSON.stringify(JSON.parse(json), null, 4);

interface IFileDropZoneProps {
onSuccess: (message: string) => void;
onError: (error: string) => void;
}

const onFileDropped =
({ onSuccess, onError }: IFileDropZoneProps) =>
(e: ProgressEvent<FileReader>) => {
const contents = e?.target?.result;
if (typeof contents === 'string') {
try {
const json = formatJSON(contents);
onSuccess(json);
} catch (e) {
onError('Cannot format as valid JSON');
}
} else {
onError('Unsupported format');
}
};

// This component has no styling on purpose
export const FileDropZone: FC<{ onChange: (content: string) => void }> = ({
onChange,
export const FileDropZone: FC<IFileDropZoneProps> = ({
onSuccess,
onError,
children,
...props
}) => {
const onDrop = useCallback(([file]) => {
const reader = new FileReader();
reader.onload = function (e) {
const contents = e?.target?.result;
if (typeof contents === 'string') {
onChange(formatJSON(contents));
}
};
reader.onload = onFileDropped({ onSuccess, onError });
reader.readAsText(file);
}, []);
const { getRootProps, getInputProps } = useDropzone({
Expand Down
191 changes: 135 additions & 56 deletions frontend/src/component/project/Project/Import/ImportModal.tsx
@@ -1,21 +1,58 @@
import { Button, styled, TextField } from '@mui/material';
import {
Button,
styled,
Tabs,
Tab,
TextField,
Box,
Typography,
Avatar,
} from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
import React, { useEffect, useState } from 'react';
import { ArrowUpward } from '@mui/icons-material';
import React, { useState } from 'react';
import { useImportApi } from 'hooks/api/actions/useImportApi/useImportApi';
import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
import { StyledFileDropZone } from './ImportTogglesDropZone';
import { StyledFileDropZone } from './StyledFileDropZone';
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
import useToast from 'hooks/useToast';
import { ImportOptions } from './ImportOptions';

const StyledDiv = styled('div')(({ theme }) => ({
backgroundColor: '#efefef',
const LayoutContainer = styled('div')(({ theme }) => ({
backgroundColor: '#fff',
height: '100vh',
padding: theme.spacing(2),
padding: theme.spacing(4, 8, 4, 8),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
}));

const StyledTextField = styled(TextField)(({ theme }) => ({
width: '100%',
margin: theme.spacing(2, 0),
}));

const DropMessage = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(4),
fontSize: theme.fontSizes.mainHeader,
}));

const SelectFileMessage = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(2),
marginBottom: theme.spacing(1.5),
color: theme.palette.text.secondary,
}));

const MaxSizeMessage = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(9),
color: theme.palette.text.secondary,
}));

const ActionsContainer = styled(Box)(({ theme }) => ({
width: '100%',
borderTop: `1px solid ${theme.palette.dividerAlternative}`,
marginTop: 'auto',
paddingTop: theme.spacing(4),
display: 'flex',
justifyContent: 'flex-end',
}));

interface IImportModalProps {
Expand All @@ -25,74 +62,116 @@ interface IImportModalProps {
project: string;
}

type ImportMode = 'file' | 'code';

export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
const { environments } = useProjectEnvironments(project);
const { createImport } = useImportApi();

const environmentOptions = environments
.filter(environment => environment.enabled)
.map(environment => ({
key: environment.name,
label: environment.name,
title: environment.name,
}));

const [environment, setEnvironment] = useState('');
const [data, setData] = useState('');

useEffect(() => {
setEnvironment(environmentOptions[0]?.key);
}, [JSON.stringify(environmentOptions)]);
const [importPayload, setImportPayload] = useState('');
const [activeTab, setActiveTab] = useState<ImportMode>('file');

const onSubmit = async () => {
await createImport({
data: JSON.parse(data),
data: JSON.parse(importPayload),
environment,
project,
});
};

const { setToastData } = useToast();

return (
<SidebarModal
open={open}
onClose={() => {
setOpen(false);
}}
label={'New service account'}
label="Import toggles"
>
<StyledDiv>
<GeneralSelect
sx={{ width: '140px' }}
options={environmentOptions}
<LayoutContainer>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider',
}}
>
<Tabs value={activeTab}>
<Tab
label="Upload file"
value="file"
onClick={() => setActiveTab('file')}
/>
<Tab
label="Code editor"
value="code"
onClick={() => setActiveTab('code')}
/>
</Tabs>
</Box>
<ImportOptions
project={project}
environment={environment}
onChange={setEnvironment}
label={'Environment'}
value={environment}
IconComponent={KeyboardArrowDownOutlined}
fullWidth
/>
<StyledFileDropZone onChange={setData}>
<p>
Drag 'n' drop some files here, or click to select files
</p>
</StyledFileDropZone>
<StyledTextField
label="Exported toggles"
variant="outlined"
onChange={event => setData(event.target.value)}
value={data}
multiline
minRows={20}
maxRows={20}
<ConditionallyRender
condition={activeTab === 'file'}
show={
<StyledFileDropZone
onSuccess={data => {
setImportPayload(data);
setActiveTab('code');
setToastData({
type: 'success',
title: 'File uploaded',
});
}}
onError={error => {
setToastData({
type: 'error',
title: error,
});
}}
>
<Avatar sx={{ width: 80, height: 80 }}>
<ArrowUpward fontSize="large" />
</Avatar>
<DropMessage>Drop your file here</DropMessage>
<SelectFileMessage>
or select a file from your device
</SelectFileMessage>
<Button variant="outlined">Select file</Button>
<MaxSizeMessage>
JSON format: max 500 kB
</MaxSizeMessage>
</StyledFileDropZone>
}
elseShow={
<StyledTextField
label="Exported toggles"
variant="outlined"
onChange={event =>
setImportPayload(event.target.value)
}
value={importPayload}
multiline
minRows={17}
maxRows={17}
/>
}
/>
<Button
variant="contained"
color="primary"
type="submit"
onClick={onSubmit}
>
Import
</Button>{' '}
</StyledDiv>
<ActionsContainer>
<Button
sx={{ position: 'static' }}
variant="contained"
color="primary"
type="submit"
onClick={onSubmit}
>
Import
</Button>
</ActionsContainer>
</LayoutContainer>
</SidebarModal>
);
};
63 changes: 63 additions & 0 deletions frontend/src/component/project/Project/Import/ImportOptions.tsx
@@ -0,0 +1,63 @@
import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect';
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
import React, { FC, useEffect, useState } from 'react';
import { useProjectEnvironments } from '../../../../hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
import { Box, styled, Typography } from '@mui/material';

const ImportOptionsContainer = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.secondaryContainer,
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(3),
}));

const ImportOptionsHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(3),
fontWeight: theme.typography.fontWeightBold,
}));

const ImportOptionsDescription = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(1.5),
}));

interface IImportOptionsProps {
project: string;
environment: string;
onChange: (value: string) => void;
}

export const ImportOptions: FC<IImportOptionsProps> = ({
project,
environment,
onChange,
}) => {
const { environments } = useProjectEnvironments(project);
const environmentOptions = environments
.filter(environment => environment.enabled)
.map(environment => ({
key: environment.name,
label: environment.name,
title: environment.name,
}));

useEffect(() => {
onChange(environmentOptions[0]?.key);
}, [JSON.stringify(environmentOptions)]);

return (
<ImportOptionsContainer>
<ImportOptionsHeader>Import options</ImportOptionsHeader>
<ImportOptionsDescription>
Choose the environment to import the configuration for
</ImportOptionsDescription>
<GeneralSelect
sx={{ width: '180px' }}
options={environmentOptions}
onChange={onChange}
label={'Environment'}
value={environment}
IconComponent={KeyboardArrowDownOutlined}
fullWidth
/>
</ImportOptionsContainer>
);
};

This file was deleted.

@@ -0,0 +1,12 @@
import { styled } from '@mui/material';
import { FileDropZone } from './FileDropZone';
import React from 'react';

export const StyledFileDropZone = styled(FileDropZone)(({ theme }) => ({
padding: theme.spacing(10, 2, 2, 2),
border: `1px dashed ${theme.palette.secondary.border}`,
borderRadius: theme.shape.borderRadiusLarge,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}));

0 comments on commit 287d28e

Please sign in to comment.