From e2e7f64b5b391a587d6ab52567064dd636c8efdf Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 25 Jan 2023 10:11:08 +0100 Subject: [PATCH] feat: back transition from validate to configure (#2982) --- .../project/Project/Import/ImportModal.tsx | 73 ++++-- .../project/Project/Import/ImportStage.ts | 5 +- .../project/Project/Import/ImportTimeline.tsx | 2 +- .../Import/configure/ConfigurationStage.tsx | 237 +++++++++--------- .../Import/configure/ImportOptions.tsx | 6 +- .../Import/validate/ValidationStage.tsx | 10 +- 6 files changed, 184 insertions(+), 149 deletions(-) diff --git a/frontend/src/component/project/Project/Import/ImportModal.tsx b/frontend/src/component/project/Project/Import/ImportModal.tsx index ad07bf8c7c..f689d822a9 100644 --- a/frontend/src/component/project/Project/Import/ImportModal.tsx +++ b/frontend/src/component/project/Project/Import/ImportModal.tsx @@ -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({ - name: 'configure', - }); + const [importStage, setImportStage] = useState('configure'); + const [environment, setEnvironment] = useState(''); + const [importPayload, setImportPayload] = useState(''); + const [activeTab, setActiveTab] = useState('file'); return ( { Process - + setOpen(false)} - onSubmit={configuration => - setImportStage({ - name: 'validate', - ...configuration, - }) + tabs={ + + } + importOptions={ + + } + importArea={ + + } + actions={ + setImportStage('validate')} + onClose={() => setOpen(false)} + /> } /> } /> - {importStage.name === 'validate' ? ( + {importStage === 'validate' ? ( setImportStage({ name: 'configure' })} + environment={environment} + payload={JSON.parse(importPayload)} + onBack={() => setImportStage('configure')} onClose={() => setOpen(false)} /> ) : ( diff --git a/frontend/src/component/project/Project/Import/ImportStage.ts b/frontend/src/component/project/Project/Import/ImportStage.ts index bf902b65f1..82a5e3cbad 100644 --- a/frontend/src/component/project/Project/Import/ImportStage.ts +++ b/frontend/src/component/project/Project/Import/ImportStage.ts @@ -1,4 +1 @@ -export type ImportStage = - | { name: 'configure' } - | { name: 'validate'; environment: string; payload: string } - | { name: 'import' }; +export type ImportStage = 'configure' | 'validate' | 'import'; diff --git a/frontend/src/component/project/Project/Import/ImportTimeline.tsx b/frontend/src/component/project/Project/Import/ImportTimeline.tsx index 322b8e8957..d68ec5d9be 100644 --- a/frontend/src/component/project/Project/Import/ImportTimeline.tsx +++ b/frontend/src/component/project/Project/Import/ImportTimeline.tsx @@ -55,7 +55,7 @@ const TimelineItemDescription = styled(Box)(({ theme }) => ({ })); export const ImportTimeline: FC<{ - stage: ImportStage['name']; + stage: ImportStage; }> = ({ stage }) => { return ( diff --git a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx index cee58ba6ce..ea2ebce2f3 100644 --- a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx +++ b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx @@ -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 }) => ( + + + setActiveTab('file')} + /> + setActiveTab('code')} + /> + + +); -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('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 ( - - - - setActiveTab('file')} - /> - setActiveTab('code')} - /> - - - - { - setImportPayload(data); - setActiveTab('code'); - setToastData({ - type: 'success', - title: 'File uploaded', - }); - }} - onError={error => { - setImportPayload(''); - setToastData({ - type: 'error', - title: error, - }); - }} - onDragStatusChange={setDragActive} - > - - - - - {dragActive - ? 'Drop your file to upload' - : 'Drop your file here'} - - - or select a file from your device - - - JSON format: max 500 kB - - } - elseShow={ - setImportPayload(event.target.value)} - value={importPayload} - multiline - minRows={13} - maxRows={13} - /> - } - /> - - - - + JSON format: max 500 kB + + } + elseShow={ + - Cancel import - - + 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 }) => ( + + + + +); + +export const ConfigurationStage: FC<{ + tabs: ReactNode; + importOptions: ReactNode; + importArea: ReactNode; + actions: ReactNode; +}> = ({ tabs, importOptions, importArea, actions }) => { + return ( + + {tabs} + {importOptions} + {importArea} + + {actions} ); }; diff --git a/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx b/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx index 48f1ef14de..1caf2db4d4 100644 --- a/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx +++ b/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx @@ -39,10 +39,6 @@ export const ImportOptions: FC = ({ title: environment.name, })); - useEffect(() => { - onChange(environmentOptions[0]?.key); - }, [JSON.stringify(environmentOptions)]); - return ( Import options @@ -54,7 +50,7 @@ export const ImportOptions: FC = ({ options={environmentOptions} onChange={onChange} label={'Environment'} - value={environment} + value={environment || environmentOptions[0]?.key} IconComponent={KeyboardArrowDownOutlined} fullWidth /> diff --git a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx index c528d2c415..58d11a0b3e 100644 --- a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx +++ b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx @@ -92,16 +92,18 @@ export const ValidationStage: FC<{ const [validationResult, setValidationResult] = useState( { 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