1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-29 01:15:48 +02:00

feat: back transition from validate to configure (#2982)

This commit is contained in:
Mateusz Kwasniewski 2023-01-25 10:11:08 +01:00 committed by GitHub
parent 85566b1431
commit e2e7f64b5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 184 additions and 149 deletions

View File

@ -1,11 +1,18 @@
import { styled } from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ImportTimeline } from './ImportTimeline';
import { ImportStage } from './ImportStage';
import { ConfigurationStage } from './configure/ConfigurationStage';
import {
Actions,
ConfigurationStage,
ConfigurationTabs,
ImportArea,
ImportMode,
} from './configure/ConfigurationStage';
import { ValidationStage } from './validate/ValidationStage';
import { ImportOptions } from './configure/ImportOptions';
const ModalContentContainer = styled('div')(({ theme }) => ({
minHeight: '100vh',
@ -27,6 +34,15 @@ const TimelineHeader = styled('div')(({ theme }) => ({
marginBottom: theme.spacing(4),
}));
const isValidJSON = (json: string) => {
try {
JSON.parse(json);
return true;
} catch (e) {
return false;
}
};
interface IImportModalProps {
open: boolean;
setOpen: (value: boolean) => void;
@ -34,9 +50,10 @@ interface IImportModalProps {
}
export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
const [importStage, setImportStage] = useState<ImportStage>({
name: 'configure',
});
const [importStage, setImportStage] = useState<ImportStage>('configure');
const [environment, setEnvironment] = useState('');
const [importPayload, setImportPayload] = useState('');
const [activeTab, setActiveTab] = useState<ImportMode>('file');
return (
<SidebarModal
@ -49,29 +66,49 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
<ModalContentContainer>
<TimelineContainer>
<TimelineHeader>Process</TimelineHeader>
<ImportTimeline stage={importStage.name} />
<ImportTimeline stage={importStage} />
</TimelineContainer>
<ConditionallyRender
condition={importStage.name === 'configure'}
condition={importStage === 'configure'}
show={
<ConfigurationStage
project={project}
onClose={() => setOpen(false)}
onSubmit={configuration =>
setImportStage({
name: 'validate',
...configuration,
})
tabs={
<ConfigurationTabs
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
}
importOptions={
<ImportOptions
project={project}
environment={environment}
onChange={setEnvironment}
/>
}
importArea={
<ImportArea
activeTab={activeTab}
setActiveTab={setActiveTab}
importPayload={importPayload}
setImportPayload={setImportPayload}
/>
}
actions={
<Actions
disabled={!isValidJSON(importPayload)}
onSubmit={() => setImportStage('validate')}
onClose={() => setOpen(false)}
/>
}
/>
}
/>
{importStage.name === 'validate' ? (
{importStage === 'validate' ? (
<ValidationStage
project={project}
environment={importStage.environment}
payload={JSON.parse(importStage.payload)}
onBack={() => setImportStage({ name: 'configure' })}
environment={environment}
payload={JSON.parse(importPayload)}
onBack={() => setImportStage('configure')}
onClose={() => setOpen(false)}
/>
) : (

View File

@ -1,4 +1 @@
export type ImportStage =
| { name: 'configure' }
| { name: 'validate'; environment: string; payload: string }
| { name: 'import' };
export type ImportStage = 'configure' | 'validate' | 'import';

View File

@ -55,7 +55,7 @@ const TimelineItemDescription = styled(Box)(({ theme }) => ({
}));
export const ImportTimeline: FC<{
stage: ImportStage['name'];
stage: ImportStage;
}> = ({ stage }) => {
return (
<StyledTimeline>

View File

@ -7,13 +7,12 @@ import {
TextField,
Typography,
} from '@mui/material';
import { ImportOptions } from './ImportOptions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StyledFileDropZone } from './StyledFileDropZone';
import { PulsingAvatar } from './PulsingAvatar';
import { ArrowUpward } from '@mui/icons-material';
import { ImportExplanation } from './ImportExplanation';
import React, { FC, useState } from 'react';
import React, { FC, ReactNode, useState } from 'react';
import useToast from 'hooks/useToast';
import { ImportLayoutContainer } from '../ImportLayoutContainer';
import { ActionsContainer } from '../ActionsContainer';
@ -38,129 +37,133 @@ const MaxSizeMessage = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
type ImportMode = 'file' | 'code';
export type ImportMode = 'file' | 'code';
const isValidJSON = (json: string) => {
try {
JSON.parse(json);
return true;
} catch (e) {
return false;
}
};
export const ConfigurationTabs: FC<{
activeTab: ImportMode;
setActiveTab: (mode: ImportMode) => void;
}> = ({ activeTab, setActiveTab }) => (
<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>
);
interface IConfigurationSettings {
environment: string;
payload: string;
}
export const ConfigurationStage: FC<{
project: string;
onClose: () => void;
onSubmit: (props: IConfigurationSettings) => void;
}> = ({ project, onClose, onSubmit }) => {
const [environment, setEnvironment] = useState('');
const [importPayload, setImportPayload] = useState('');
const [activeTab, setActiveTab] = useState<ImportMode>('file');
export const ImportArea: FC<{
activeTab: ImportMode;
setActiveTab: (mode: ImportMode) => void;
importPayload: string;
setImportPayload: (payload: string) => void;
}> = ({ activeTab, setActiveTab, importPayload, setImportPayload }) => {
const [dragActive, setDragActive] = useState(false);
const { setToastData } = useToast();
return (
<ImportLayoutContainer>
<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}
/>
<ConditionallyRender
condition={activeTab === 'file'}
show={
<StyledFileDropZone
onSuccess={data => {
setImportPayload(data);
setActiveTab('code');
setToastData({
type: 'success',
title: 'File uploaded',
});
}}
onError={error => {
setImportPayload('');
setToastData({
type: 'error',
title: error,
});
}}
onDragStatusChange={setDragActive}
>
<PulsingAvatar active={dragActive}>
<ArrowUpward fontSize="large" />
</PulsingAvatar>
<DropMessage>
{dragActive
? 'Drop your file to upload'
: '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={13}
maxRows={13}
/>
}
/>
<ImportExplanation />
<ActionsContainer>
<Button
sx={{ position: 'static' }}
variant="contained"
type="submit"
onClick={() =>
onSubmit({ payload: importPayload, environment })
}
disabled={!isValidJSON(importPayload)}
<ConditionallyRender
condition={activeTab === 'file'}
show={
<StyledFileDropZone
onSuccess={data => {
setImportPayload(data);
setActiveTab('code');
setToastData({
type: 'success',
title: 'File uploaded',
});
}}
onError={error => {
setImportPayload('');
setToastData({
type: 'error',
title: error,
});
}}
onDragStatusChange={setDragActive}
>
Validate
</Button>
<Button
sx={{ position: 'static', ml: 2 }}
<PulsingAvatar active={dragActive}>
<ArrowUpward fontSize="large" />
</PulsingAvatar>
<DropMessage>
{dragActive
? 'Drop your file to upload'
: '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"
type="submit"
onClick={onClose}
>
Cancel import
</Button>
</ActionsContainer>
onChange={event => setImportPayload(event.target.value)}
value={importPayload}
multiline
minRows={13}
maxRows={13}
/>
}
/>
);
};
export const Actions: FC<{
onSubmit: () => void;
onClose: () => void;
disabled: boolean;
}> = ({ onSubmit, onClose, disabled }) => (
<ActionsContainer>
<Button
sx={{ position: 'static' }}
variant="contained"
type="submit"
onClick={onSubmit}
disabled={disabled}
>
Validate
</Button>
<Button
sx={{ position: 'static', ml: 2 }}
variant="outlined"
type="submit"
onClick={onClose}
>
Cancel import
</Button>
</ActionsContainer>
);
export const ConfigurationStage: FC<{
tabs: ReactNode;
importOptions: ReactNode;
importArea: ReactNode;
actions: ReactNode;
}> = ({ tabs, importOptions, importArea, actions }) => {
return (
<ImportLayoutContainer>
{tabs}
{importOptions}
{importArea}
<ImportExplanation />
{actions}
</ImportLayoutContainer>
);
};

View File

@ -39,10 +39,6 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
title: environment.name,
}));
useEffect(() => {
onChange(environmentOptions[0]?.key);
}, [JSON.stringify(environmentOptions)]);
return (
<ImportOptionsContainer>
<ImportOptionsHeader>Import options</ImportOptionsHeader>
@ -54,7 +50,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
options={environmentOptions}
onChange={onChange}
label={'Environment'}
value={environment}
value={environment || environmentOptions[0]?.key}
IconComponent={KeyboardArrowDownOutlined}
fullWidth
/>

View File

@ -92,16 +92,18 @@ export const ValidationStage: FC<{
const [validationResult, setValidationResult] = useState<IValidationSchema>(
{ errors: [], warnings: [] }
);
const [validJSON, setValidJSON] = useState(true);
useEffect(() => {
validateImport({ environment, project, data: payload })
.then(setValidationResult)
.catch(error =>
.catch(error => {
setValidJSON(false);
setToastData({
type: 'error',
title: formatUnknownError(error),
})
);
});
});
}, []);
return (
@ -185,7 +187,7 @@ export const ValidationStage: FC<{
sx={{ position: 'static' }}
variant="contained"
type="submit"
disabled={validationResult.errors.length > 0}
disabled={validationResult.errors.length > 0 || !validJSON}
>
Import configuration
</Button>