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:
parent
a8a910a39f
commit
287d28e91d
@ -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({
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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),
|
||||
}));
|
@ -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',
|
||||
}));
|
Loading…
Reference in New Issue
Block a user