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:
parent
85566b1431
commit
e2e7f64b5b
@ -1,11 +1,18 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ImportTimeline } from './ImportTimeline';
|
import { ImportTimeline } from './ImportTimeline';
|
||||||
import { ImportStage } from './ImportStage';
|
import { ImportStage } from './ImportStage';
|
||||||
import { ConfigurationStage } from './configure/ConfigurationStage';
|
import {
|
||||||
|
Actions,
|
||||||
|
ConfigurationStage,
|
||||||
|
ConfigurationTabs,
|
||||||
|
ImportArea,
|
||||||
|
ImportMode,
|
||||||
|
} from './configure/ConfigurationStage';
|
||||||
import { ValidationStage } from './validate/ValidationStage';
|
import { ValidationStage } from './validate/ValidationStage';
|
||||||
|
import { ImportOptions } from './configure/ImportOptions';
|
||||||
|
|
||||||
const ModalContentContainer = styled('div')(({ theme }) => ({
|
const ModalContentContainer = styled('div')(({ theme }) => ({
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
@ -27,6 +34,15 @@ const TimelineHeader = styled('div')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const isValidJSON = (json: string) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(json);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface IImportModalProps {
|
interface IImportModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (value: boolean) => void;
|
setOpen: (value: boolean) => void;
|
||||||
@ -34,9 +50,10 @@ interface IImportModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
||||||
const [importStage, setImportStage] = useState<ImportStage>({
|
const [importStage, setImportStage] = useState<ImportStage>('configure');
|
||||||
name: 'configure',
|
const [environment, setEnvironment] = useState('');
|
||||||
});
|
const [importPayload, setImportPayload] = useState('');
|
||||||
|
const [activeTab, setActiveTab] = useState<ImportMode>('file');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarModal
|
<SidebarModal
|
||||||
@ -49,29 +66,49 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
|
|||||||
<ModalContentContainer>
|
<ModalContentContainer>
|
||||||
<TimelineContainer>
|
<TimelineContainer>
|
||||||
<TimelineHeader>Process</TimelineHeader>
|
<TimelineHeader>Process</TimelineHeader>
|
||||||
<ImportTimeline stage={importStage.name} />
|
<ImportTimeline stage={importStage} />
|
||||||
</TimelineContainer>
|
</TimelineContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={importStage.name === 'configure'}
|
condition={importStage === 'configure'}
|
||||||
show={
|
show={
|
||||||
<ConfigurationStage
|
<ConfigurationStage
|
||||||
project={project}
|
tabs={
|
||||||
onClose={() => setOpen(false)}
|
<ConfigurationTabs
|
||||||
onSubmit={configuration =>
|
activeTab={activeTab}
|
||||||
setImportStage({
|
setActiveTab={setActiveTab}
|
||||||
name: 'validate',
|
/>
|
||||||
...configuration,
|
}
|
||||||
})
|
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
|
<ValidationStage
|
||||||
project={project}
|
project={project}
|
||||||
environment={importStage.environment}
|
environment={environment}
|
||||||
payload={JSON.parse(importStage.payload)}
|
payload={JSON.parse(importPayload)}
|
||||||
onBack={() => setImportStage({ name: 'configure' })}
|
onBack={() => setImportStage('configure')}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,4 +1 @@
|
|||||||
export type ImportStage =
|
export type ImportStage = 'configure' | 'validate' | 'import';
|
||||||
| { name: 'configure' }
|
|
||||||
| { name: 'validate'; environment: string; payload: string }
|
|
||||||
| { name: 'import' };
|
|
||||||
|
@ -55,7 +55,7 @@ const TimelineItemDescription = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const ImportTimeline: FC<{
|
export const ImportTimeline: FC<{
|
||||||
stage: ImportStage['name'];
|
stage: ImportStage;
|
||||||
}> = ({ stage }) => {
|
}> = ({ stage }) => {
|
||||||
return (
|
return (
|
||||||
<StyledTimeline>
|
<StyledTimeline>
|
||||||
|
@ -7,13 +7,12 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ImportOptions } from './ImportOptions';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { StyledFileDropZone } from './StyledFileDropZone';
|
import { StyledFileDropZone } from './StyledFileDropZone';
|
||||||
import { PulsingAvatar } from './PulsingAvatar';
|
import { PulsingAvatar } from './PulsingAvatar';
|
||||||
import { ArrowUpward } from '@mui/icons-material';
|
import { ArrowUpward } from '@mui/icons-material';
|
||||||
import { ImportExplanation } from './ImportExplanation';
|
import { ImportExplanation } from './ImportExplanation';
|
||||||
import React, { FC, useState } from 'react';
|
import React, { FC, ReactNode, useState } from 'react';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { ImportLayoutContainer } from '../ImportLayoutContainer';
|
import { ImportLayoutContainer } from '../ImportLayoutContainer';
|
||||||
import { ActionsContainer } from '../ActionsContainer';
|
import { ActionsContainer } from '../ActionsContainer';
|
||||||
@ -38,129 +37,133 @@ const MaxSizeMessage = styled(Typography)(({ theme }) => ({
|
|||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type ImportMode = 'file' | 'code';
|
export type ImportMode = 'file' | 'code';
|
||||||
|
|
||||||
const isValidJSON = (json: string) => {
|
export const ConfigurationTabs: FC<{
|
||||||
try {
|
activeTab: ImportMode;
|
||||||
JSON.parse(json);
|
setActiveTab: (mode: ImportMode) => void;
|
||||||
return true;
|
}> = ({ activeTab, setActiveTab }) => (
|
||||||
} catch (e) {
|
<Box
|
||||||
return false;
|
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 {
|
export const ImportArea: FC<{
|
||||||
environment: string;
|
activeTab: ImportMode;
|
||||||
payload: string;
|
setActiveTab: (mode: ImportMode) => void;
|
||||||
}
|
importPayload: string;
|
||||||
|
setImportPayload: (payload: string) => void;
|
||||||
export const ConfigurationStage: FC<{
|
}> = ({ activeTab, setActiveTab, importPayload, setImportPayload }) => {
|
||||||
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');
|
|
||||||
const [dragActive, setDragActive] = useState(false);
|
const [dragActive, setDragActive] = useState(false);
|
||||||
const { setToastData } = useToast();
|
const { setToastData } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportLayoutContainer>
|
<ConditionallyRender
|
||||||
<Box
|
condition={activeTab === 'file'}
|
||||||
sx={{
|
show={
|
||||||
borderBottom: 1,
|
<StyledFileDropZone
|
||||||
borderColor: 'divider',
|
onSuccess={data => {
|
||||||
}}
|
setImportPayload(data);
|
||||||
>
|
setActiveTab('code');
|
||||||
<Tabs value={activeTab}>
|
setToastData({
|
||||||
<Tab
|
type: 'success',
|
||||||
label="Upload file"
|
title: 'File uploaded',
|
||||||
value="file"
|
});
|
||||||
onClick={() => setActiveTab('file')}
|
}}
|
||||||
/>
|
onError={error => {
|
||||||
<Tab
|
setImportPayload('');
|
||||||
label="Code editor"
|
setToastData({
|
||||||
value="code"
|
type: 'error',
|
||||||
onClick={() => setActiveTab('code')}
|
title: error,
|
||||||
/>
|
});
|
||||||
</Tabs>
|
}}
|
||||||
</Box>
|
onDragStatusChange={setDragActive}
|
||||||
<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)}
|
|
||||||
>
|
>
|
||||||
Validate
|
<PulsingAvatar active={dragActive}>
|
||||||
</Button>
|
<ArrowUpward fontSize="large" />
|
||||||
<Button
|
</PulsingAvatar>
|
||||||
sx={{ position: 'static', ml: 2 }}
|
<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"
|
variant="outlined"
|
||||||
type="submit"
|
onChange={event => setImportPayload(event.target.value)}
|
||||||
onClick={onClose}
|
value={importPayload}
|
||||||
>
|
multiline
|
||||||
Cancel import
|
minRows={13}
|
||||||
</Button>
|
maxRows={13}
|
||||||
</ActionsContainer>
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
</ImportLayoutContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -39,10 +39,6 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
|
|||||||
title: environment.name,
|
title: environment.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChange(environmentOptions[0]?.key);
|
|
||||||
}, [JSON.stringify(environmentOptions)]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportOptionsContainer>
|
<ImportOptionsContainer>
|
||||||
<ImportOptionsHeader>Import options</ImportOptionsHeader>
|
<ImportOptionsHeader>Import options</ImportOptionsHeader>
|
||||||
@ -54,7 +50,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
|
|||||||
options={environmentOptions}
|
options={environmentOptions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label={'Environment'}
|
label={'Environment'}
|
||||||
value={environment}
|
value={environment || environmentOptions[0]?.key}
|
||||||
IconComponent={KeyboardArrowDownOutlined}
|
IconComponent={KeyboardArrowDownOutlined}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
@ -92,16 +92,18 @@ export const ValidationStage: FC<{
|
|||||||
const [validationResult, setValidationResult] = useState<IValidationSchema>(
|
const [validationResult, setValidationResult] = useState<IValidationSchema>(
|
||||||
{ errors: [], warnings: [] }
|
{ errors: [], warnings: [] }
|
||||||
);
|
);
|
||||||
|
const [validJSON, setValidJSON] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
validateImport({ environment, project, data: payload })
|
validateImport({ environment, project, data: payload })
|
||||||
.then(setValidationResult)
|
.then(setValidationResult)
|
||||||
.catch(error =>
|
.catch(error => {
|
||||||
|
setValidJSON(false);
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: formatUnknownError(error),
|
title: formatUnknownError(error),
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -185,7 +187,7 @@ export const ValidationStage: FC<{
|
|||||||
sx={{ position: 'static' }}
|
sx={{ position: 'static' }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={validationResult.errors.length > 0}
|
disabled={validationResult.errors.length > 0 || !validJSON}
|
||||||
>
|
>
|
||||||
Import configuration
|
Import configuration
|
||||||
</Button>
|
</Button>
|
||||||
|
Loading…
Reference in New Issue
Block a user