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 { 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)}
/> />
) : ( ) : (

View File

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

View File

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

View File

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

View File

@ -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
/> />

View File

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