mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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