mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: import stage (#2985)
This commit is contained in:
		
							parent
							
								
									e2e7f64b5b
								
							
						
					
					
						commit
						decb7f320d
					
				@ -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<ImportStage>('configure');
 | 
			
		||||
    const [importStage, setImportStage] = useState<StageName>('configure');
 | 
			
		||||
    const [environment, setEnvironment] = useState('');
 | 
			
		||||
    const [importPayload, setImportPayload] = useState('');
 | 
			
		||||
    const [activeTab, setActiveTab] = useState<ImportMode>('file');
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <SidebarModal
 | 
			
		||||
            open={open}
 | 
			
		||||
            onClose={() => {
 | 
			
		||||
    const close = () => {
 | 
			
		||||
        setOpen(false);
 | 
			
		||||
            }}
 | 
			
		||||
            label="Import toggles"
 | 
			
		||||
        >
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (open === true) {
 | 
			
		||||
            setInitialState();
 | 
			
		||||
        }
 | 
			
		||||
    }, [open]);
 | 
			
		||||
 | 
			
		||||
    const setInitialState = () => {
 | 
			
		||||
        setImportStage('configure');
 | 
			
		||||
        setEnvironment('');
 | 
			
		||||
        setImportPayload('');
 | 
			
		||||
        setActiveTab('file');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <SidebarModal open={open} onClose={close} label="Import toggles">
 | 
			
		||||
            <ModalContentContainer>
 | 
			
		||||
                <TimelineContainer>
 | 
			
		||||
                    <TimelineHeader>Process</TimelineHeader>
 | 
			
		||||
@ -97,23 +109,36 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
 | 
			
		||||
                                <Actions
 | 
			
		||||
                                    disabled={!isValidJSON(importPayload)}
 | 
			
		||||
                                    onSubmit={() => setImportStage('validate')}
 | 
			
		||||
                                    onClose={() => setOpen(false)}
 | 
			
		||||
                                    onClose={close}
 | 
			
		||||
                                />
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                {importStage === 'validate' ? (
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={importStage === 'validate'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ValidationStage
 | 
			
		||||
                            project={project}
 | 
			
		||||
                            environment={environment}
 | 
			
		||||
                        payload={JSON.parse(importPayload)}
 | 
			
		||||
                            payload={importPayload}
 | 
			
		||||
                            onBack={() => setImportStage('configure')}
 | 
			
		||||
                        onClose={() => setOpen(false)}
 | 
			
		||||
                            onSubmit={() => setImportStage('import')}
 | 
			
		||||
                            onClose={close}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={importStage === 'import'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ImportStage
 | 
			
		||||
                            project={project}
 | 
			
		||||
                            environment={environment}
 | 
			
		||||
                            payload={importPayload}
 | 
			
		||||
                            onClose={close}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    ''
 | 
			
		||||
                )}
 | 
			
		||||
            </ModalContentContainer>
 | 
			
		||||
        </SidebarModal>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
export type ImportStage = 'configure' | 'validate' | 'import';
 | 
			
		||||
@ -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 (
 | 
			
		||||
        <StyledTimeline>
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
export type StageName = 'configure' | 'validate' | 'import';
 | 
			
		||||
@ -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}
 | 
			
		||||
                >
 | 
			
		||||
                    <PulsingAvatar active={dragActive}>
 | 
			
		||||
                    <PulsingAvatar
 | 
			
		||||
                        sx={{ width: 80, height: 80 }}
 | 
			
		||||
                        active={dragActive}
 | 
			
		||||
                    >
 | 
			
		||||
                        <ArrowUpward fontSize="large" />
 | 
			
		||||
                    </PulsingAvatar>
 | 
			
		||||
                    <DropMessage>
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,12 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
 | 
			
		||||
            title: environment.name,
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (environment === '' && environmentOptions[0]) {
 | 
			
		||||
            onChange(environmentOptions[0].key);
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ImportOptionsContainer>
 | 
			
		||||
            <ImportOptionsHeader>Import options</ImportOptionsHeader>
 | 
			
		||||
@ -50,7 +56,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
 | 
			
		||||
                options={environmentOptions}
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                label={'Environment'}
 | 
			
		||||
                value={environment || environmentOptions[0]?.key}
 | 
			
		||||
                value={environment}
 | 
			
		||||
                IconComponent={KeyboardArrowDownOutlined}
 | 
			
		||||
                fullWidth
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
@ -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<string, string> }
 | 
			
		||||
    | { status: 'loading' };
 | 
			
		||||
 | 
			
		||||
const toApiStatus = (
 | 
			
		||||
    loading: boolean,
 | 
			
		||||
    errors: Record<string, string>
 | 
			
		||||
): 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 (
 | 
			
		||||
        <ImportLayoutContainer>
 | 
			
		||||
            <ImportStatusArea>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={importStatus.status === 'loading'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <PulsingAvatar
 | 
			
		||||
                            sx={{ width: 80, height: 80 }}
 | 
			
		||||
                            active={true}
 | 
			
		||||
                        >
 | 
			
		||||
                            <Pending fontSize="large" />
 | 
			
		||||
                        </PulsingAvatar>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={importStatus.status === 'success'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <SuccessAvatar sx={{ width: 80, height: 80 }}>
 | 
			
		||||
                            <Check fontSize="large" />
 | 
			
		||||
                        </SuccessAvatar>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={importStatus.status === 'error'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ErrorAvatar sx={{ width: 80, height: 80 }}>
 | 
			
		||||
                            <Error fontSize="large" />
 | 
			
		||||
                        </ErrorAvatar>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <ImportMessage>
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={importStatus.status === 'loading'}
 | 
			
		||||
                        show={'Importing...'}
 | 
			
		||||
                    />
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={importStatus.status === 'success'}
 | 
			
		||||
                        show={'Import completed'}
 | 
			
		||||
                    />
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={importStatus.status === 'error'}
 | 
			
		||||
                        show={'Import failed'}
 | 
			
		||||
                    />
 | 
			
		||||
                </ImportMessage>
 | 
			
		||||
            </ImportStatusArea>
 | 
			
		||||
 | 
			
		||||
            <ActionsContainer>
 | 
			
		||||
                <Button
 | 
			
		||||
                    sx={{ position: 'static' }}
 | 
			
		||||
                    variant="contained"
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                    onClick={onClose}
 | 
			
		||||
                >
 | 
			
		||||
                    Close
 | 
			
		||||
                </Button>
 | 
			
		||||
            </ActionsContainer>
 | 
			
		||||
        </ImportLayoutContainer>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -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<IValidationSchema>(
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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({
 | 
			
		||||
 | 
			
		||||
@ -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<string> }>;
 | 
			
		||||
    warnings: Array<{ message: string; affectedItems: Array<string> }>;
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user