diff --git a/frontend/src/component/project/Project/Import/ImportModal.tsx b/frontend/src/component/project/Project/Import/ImportModal.tsx index f689d822a9..cef316970b 100644 --- a/frontend/src/component/project/Project/Import/ImportModal.tsx +++ b/frontend/src/component/project/Project/Import/ImportModal.tsx @@ -3,7 +3,7 @@ import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; import React, { useEffect, useState } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ImportTimeline } from './ImportTimeline'; -import { ImportStage } from './ImportStage'; +import { StageName } from './StageName'; import { Actions, ConfigurationStage, @@ -12,6 +12,7 @@ import { ImportMode, } from './configure/ConfigurationStage'; import { ValidationStage } from './validate/ValidationStage'; +import { ImportStage } from './import/ImportStage'; import { ImportOptions } from './configure/ImportOptions'; const ModalContentContainer = styled('div')(({ theme }) => ({ @@ -50,19 +51,30 @@ interface IImportModalProps { } export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => { - const [importStage, setImportStage] = useState('configure'); + const [importStage, setImportStage] = useState('configure'); const [environment, setEnvironment] = useState(''); const [importPayload, setImportPayload] = useState(''); const [activeTab, setActiveTab] = useState('file'); + const close = () => { + setOpen(false); + }; + + useEffect(() => { + if (open === true) { + setInitialState(); + } + }, [open]); + + const setInitialState = () => { + setImportStage('configure'); + setEnvironment(''); + setImportPayload(''); + setActiveTab('file'); + }; + return ( - { - setOpen(false); - }} - label="Import toggles" - > + Process @@ -97,23 +109,36 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => { setImportStage('validate')} - onClose={() => setOpen(false)} + onClose={close} /> } /> } /> - {importStage === 'validate' ? ( - setImportStage('configure')} - onClose={() => setOpen(false)} - /> - ) : ( - '' - )} + setImportStage('configure')} + onSubmit={() => setImportStage('import')} + onClose={close} + /> + } + /> + + } + /> ); diff --git a/frontend/src/component/project/Project/Import/ImportStage.ts b/frontend/src/component/project/Project/Import/ImportStage.ts deleted file mode 100644 index 82a5e3cbad..0000000000 --- a/frontend/src/component/project/Project/Import/ImportStage.ts +++ /dev/null @@ -1 +0,0 @@ -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 d68ec5d9be..95138d6230 100644 --- a/frontend/src/component/project/Project/Import/ImportTimeline.tsx +++ b/frontend/src/component/project/Project/Import/ImportTimeline.tsx @@ -6,7 +6,7 @@ import TimelineConnector from '@mui/lab/TimelineConnector'; import TimelineDot from '@mui/lab/TimelineDot'; import TimelineContent from '@mui/lab/TimelineContent'; import Timeline from '@mui/lab/Timeline'; -import { ImportStage } from './ImportStage'; +import { StageName } from './StageName'; const StyledTimeline = styled(Timeline)(() => ({ [`& .${timelineItemClasses.root}:before`]: { @@ -55,7 +55,7 @@ const TimelineItemDescription = styled(Box)(({ theme }) => ({ })); export const ImportTimeline: FC<{ - stage: ImportStage; + stage: StageName; }> = ({ stage }) => { return ( diff --git a/frontend/src/component/project/Project/Import/configure/PulsingAvatar.tsx b/frontend/src/component/project/Project/Import/PulsingAvatar.tsx similarity index 94% rename from frontend/src/component/project/Project/Import/configure/PulsingAvatar.tsx rename to frontend/src/component/project/Project/Import/PulsingAvatar.tsx index 1400639e4a..782410aa24 100644 --- a/frontend/src/component/project/Project/Import/configure/PulsingAvatar.tsx +++ b/frontend/src/component/project/Project/Import/PulsingAvatar.tsx @@ -3,8 +3,6 @@ import { alpha, Avatar, styled } from '@mui/material'; export const PulsingAvatar = styled(Avatar, { shouldForwardProp: prop => prop !== 'active', })<{ active: boolean }>(({ theme, active }) => ({ - width: '80px', - height: '80px', transition: 'background-color 0.5s ease', backgroundColor: active ? theme.palette.primary.main diff --git a/frontend/src/component/project/Project/Import/StageName.ts b/frontend/src/component/project/Project/Import/StageName.ts new file mode 100644 index 0000000000..aca8114b70 --- /dev/null +++ b/frontend/src/component/project/Project/Import/StageName.ts @@ -0,0 +1 @@ +export type StageName = 'configure' | 'validate' | 'import'; diff --git a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx index ea2ebce2f3..f8638a6ec0 100644 --- a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx +++ b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx @@ -9,7 +9,7 @@ import { } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { StyledFileDropZone } from './StyledFileDropZone'; -import { PulsingAvatar } from './PulsingAvatar'; +import { PulsingAvatar } from '../PulsingAvatar'; import { ArrowUpward } from '@mui/icons-material'; import { ImportExplanation } from './ImportExplanation'; import React, { FC, ReactNode, useState } from 'react'; @@ -95,7 +95,10 @@ export const ImportArea: FC<{ }} onDragStatusChange={setDragActive} > - + diff --git a/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx b/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx index 1caf2db4d4..ca394d2888 100644 --- a/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx +++ b/frontend/src/component/project/Project/Import/configure/ImportOptions.tsx @@ -39,6 +39,12 @@ export const ImportOptions: FC = ({ title: environment.name, })); + useEffect(() => { + if (environment === '' && environmentOptions[0]) { + onChange(environmentOptions[0].key); + } + }, []); + return ( Import options @@ -50,7 +56,7 @@ export const ImportOptions: FC = ({ options={environmentOptions} onChange={onChange} label={'Environment'} - value={environment || environmentOptions[0]?.key} + value={environment} IconComponent={KeyboardArrowDownOutlined} fullWidth /> diff --git a/frontend/src/component/project/Project/Import/import/ImportStage.tsx b/frontend/src/component/project/Project/Import/import/ImportStage.tsx new file mode 100644 index 0000000000..9a46178883 --- /dev/null +++ b/frontend/src/component/project/Project/Import/import/ImportStage.tsx @@ -0,0 +1,127 @@ +import React, { FC, useEffect } from 'react'; +import { ImportLayoutContainer } from '../ImportLayoutContainer'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import { useImportApi } from 'hooks/api/actions/useImportApi/useImportApi'; +import useToast from 'hooks/useToast'; +import { Avatar, Button, styled, Typography } from '@mui/material'; +import { ActionsContainer } from '../ActionsContainer'; +import { Pending, Check, Error } from '@mui/icons-material'; +import { PulsingAvatar } from '../PulsingAvatar'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Box } from '@mui/system'; + +export const ImportStatusArea = styled(Box)(({ theme }) => ({ + padding: theme.spacing(4, 2, 2, 2), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: theme.spacing(8), +})); + +const ImportMessage = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.mainHeader, +})); + +export const SuccessAvatar = styled(Avatar)(({ theme }) => ({ + backgroundColor: theme.palette.primary.main, +})); + +export const ErrorAvatar = styled(Avatar)(({ theme }) => ({ + backgroundColor: theme.palette.error.main, +})); + +type ApiStatus = + | { status: 'success' } + | { status: 'error'; errors: Record } + | { status: 'loading' }; + +const toApiStatus = ( + loading: boolean, + errors: Record +): ApiStatus => { + if (loading) return { status: 'loading' }; + if (Object.keys(errors).length > 0) return { status: 'error', errors }; + return { status: 'success' }; +}; + +export const ImportStage: FC<{ + environment: string; + project: string; + payload: string; + onClose: () => void; +}> = ({ environment, project, payload, onClose }) => { + const { createImport, loading, errors } = useImportApi(); + const { setToastData } = useToast(); + + useEffect(() => { + createImport({ environment, project, data: JSON.parse(payload) }).catch( + error => { + setToastData({ + type: 'error', + title: formatUnknownError(error), + }); + } + ); + }, []); + + const importStatus = toApiStatus(loading, errors); + + return ( + + + + + + } + /> + + + + } + /> + + + + } + /> + + + + + + + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx index 58d11a0b3e..ad0ab59eb5 100644 --- a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx +++ b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx @@ -85,8 +85,9 @@ export const ValidationStage: FC<{ project: string; payload: string; onClose: () => void; + onSubmit: () => void; onBack: () => void; -}> = ({ environment, project, payload, onClose, onBack }) => { +}> = ({ environment, project, payload, onClose, onBack, onSubmit }) => { const { validateImport } = useValidateImportApi(); const { setToastData } = useToast(); const [validationResult, setValidationResult] = useState( @@ -95,7 +96,7 @@ export const ValidationStage: FC<{ const [validJSON, setValidJSON] = useState(true); useEffect(() => { - validateImport({ environment, project, data: payload }) + validateImport({ environment, project, data: JSON.parse(payload) }) .then(setValidationResult) .catch(error => { setValidJSON(false); @@ -187,6 +188,7 @@ export const ValidationStage: FC<{ sx={{ position: 'static' }} variant="contained" type="submit" + onClick={onSubmit} disabled={validationResult.errors.length > 0 || !validJSON} > Import configuration diff --git a/frontend/src/hooks/api/actions/useImportApi/useImportApi.ts b/frontend/src/hooks/api/actions/useImportApi/useImportApi.ts index a3e171549f..858daf7aeb 100644 --- a/frontend/src/hooks/api/actions/useImportApi/useImportApi.ts +++ b/frontend/src/hooks/api/actions/useImportApi/useImportApi.ts @@ -1,7 +1,10 @@ -import { ExportQuerySchema } from 'openapi'; import useAPI from '../useApi/useApi'; -export interface ImportQuerySchema {} +export interface ImportQuerySchema { + project: string; + environment: string; + data: object; +} export const useImportApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ diff --git a/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts b/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts index 8d2292102a..366c226f42 100644 --- a/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts +++ b/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts @@ -1,6 +1,10 @@ import useAPI from '../useApi/useApi'; -export interface ImportQuerySchema {} +export interface ImportQuerySchema { + project: string; + environment: string; + data: object; +} export interface IValidationSchema { errors: Array<{ message: string; affectedItems: Array }>; warnings: Array<{ message: string; affectedItems: Array }>; diff --git a/src/lib/services/export-import-service.ts b/src/lib/services/export-import-service.ts index 849b20d4b8..61b68d52b9 100644 --- a/src/lib/services/export-import-service.ts +++ b/src/lib/services/export-import-service.ts @@ -127,10 +127,13 @@ export default class ExportImportService { const { createdAt, archivedAt, lastSeenAt, ...rest } = item; return rest; }), - featureStrategies: featureStrategies.map((item) => ({ - name: item.strategyName, - ...item, - })), + featureStrategies: featureStrategies.map((item) => { + const { createdAt, ...rest } = item; + return { + name: rest.strategyName, + ...rest, + }; + }), featureEnvironments: featureEnvironments.map((item) => ({ ...item, name: item.featureName,