1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Batch import styling (#2959)

This commit is contained in:
Mateusz Kwasniewski 2023-01-20 15:01:40 +01:00 committed by GitHub
parent a8a910a39f
commit 287d28e91d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 238 additions and 83 deletions

View File

@ -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({

View File

@ -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 {
@ -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}
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}
/>
<Button
variant="contained"
color="primary"
type="submit"
onClick={onSubmit}
<LayoutContainer>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider',
}}
>
Import
</Button>{' '}
</StyledDiv>
<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}
/>
<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}
/>
}
/>
<ActionsContainer>
<Button
sx={{ position: 'static' }}
variant="contained"
color="primary"
type="submit"
onClick={onSubmit}
>
Import
</Button>
</ActionsContainer>
</LayoutContainer>
</SidebarModal>
);
};

View File

@ -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>
);
};

View File

@ -1,9 +0,0 @@
import { styled } from '@mui/material';
import { FileDropZone } from './FileDropZone';
import React from 'react';
export const StyledFileDropZone = styled(FileDropZone)(({ theme }) => ({
padding: theme.spacing(4),
border: '1px solid black',
margin: theme.spacing(2, 0),
}));

View File

@ -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',
}));