mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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 { useDropzone } from 'react-dropzone';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
const formatJSON = (json: string) => {
|
const formatJSON = (json: string) => JSON.stringify(JSON.parse(json), null, 4);
|
||||||
try {
|
|
||||||
return JSON.stringify(JSON.parse(json), null, 4);
|
interface IFileDropZoneProps {
|
||||||
} catch (e) {
|
onSuccess: (message: string) => void;
|
||||||
console.error(e);
|
onError: (error: string) => void;
|
||||||
return '';
|
}
|
||||||
}
|
|
||||||
};
|
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
|
// This component has no styling on purpose
|
||||||
export const FileDropZone: FC<{ onChange: (content: string) => void }> = ({
|
export const FileDropZone: FC<IFileDropZoneProps> = ({
|
||||||
onChange,
|
onSuccess,
|
||||||
|
onError,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const onDrop = useCallback(([file]) => {
|
const onDrop = useCallback(([file]) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
reader.onload = onFileDropped({ onSuccess, onError });
|
||||||
const contents = e?.target?.result;
|
|
||||||
if (typeof contents === 'string') {
|
|
||||||
onChange(formatJSON(contents));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}, []);
|
}, []);
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
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 { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
import { ArrowUpward } from '@mui/icons-material';
|
||||||
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
|
import React, { useState } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useImportApi } from 'hooks/api/actions/useImportApi/useImportApi';
|
import { useImportApi } from 'hooks/api/actions/useImportApi/useImportApi';
|
||||||
import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
|
import { StyledFileDropZone } from './StyledFileDropZone';
|
||||||
import { StyledFileDropZone } from './ImportTogglesDropZone';
|
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { ImportOptions } from './ImportOptions';
|
||||||
|
|
||||||
const StyledDiv = styled('div')(({ theme }) => ({
|
const LayoutContainer = styled('div')(({ theme }) => ({
|
||||||
backgroundColor: '#efefef',
|
backgroundColor: '#fff',
|
||||||
height: '100vh',
|
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 }) => ({
|
const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||||
width: '100%',
|
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 {
|
interface IImportModalProps {
|
||||||
@ -25,74 +62,116 @@ interface IImportModalProps {
|
|||||||
project: string;
|
project: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImportMode = 'file' | 'code';
|
||||||
|
|
||||||
export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
||||||
const { environments } = useProjectEnvironments(project);
|
|
||||||
const { createImport } = useImportApi();
|
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 [environment, setEnvironment] = useState('');
|
||||||
const [data, setData] = useState('');
|
const [importPayload, setImportPayload] = useState('');
|
||||||
|
const [activeTab, setActiveTab] = useState<ImportMode>('file');
|
||||||
useEffect(() => {
|
|
||||||
setEnvironment(environmentOptions[0]?.key);
|
|
||||||
}, [JSON.stringify(environmentOptions)]);
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
await createImport({
|
await createImport({
|
||||||
data: JSON.parse(data),
|
data: JSON.parse(importPayload),
|
||||||
environment,
|
environment,
|
||||||
project,
|
project,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { setToastData } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarModal
|
<SidebarModal
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
label={'New service account'}
|
label="Import toggles"
|
||||||
>
|
>
|
||||||
<StyledDiv>
|
<LayoutContainer>
|
||||||
<GeneralSelect
|
<Box
|
||||||
sx={{ width: '140px' }}
|
sx={{
|
||||||
options={environmentOptions}
|
borderBottom: 1,
|
||||||
onChange={setEnvironment}
|
borderColor: 'divider',
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
Import
|
<Tabs value={activeTab}>
|
||||||
</Button>{' '}
|
<Tab
|
||||||
</StyledDiv>
|
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>
|
</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