Skip to content

Commit

Permalink
feat: import stage (#2985)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Jan 25, 2023
1 parent e2e7f64 commit decb7f3
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 38 deletions.
67 changes: 46 additions & 21 deletions frontend/src/component/project/Project/Import/ImportModal.tsx
Expand Up @@ -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,
Expand All @@ -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 }) => ({
Expand Down Expand Up @@ -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');

const close = () => {
setOpen(false);
};

useEffect(() => {
if (open === true) {
setInitialState();
}
}, [open]);

const setInitialState = () => {
setImportStage('configure');
setEnvironment('');
setImportPayload('');
setActiveTab('file');
};

return (
<SidebarModal
open={open}
onClose={() => {
setOpen(false);
}}
label="Import toggles"
>
<SidebarModal open={open} onClose={close} label="Import toggles">
<ModalContentContainer>
<TimelineContainer>
<TimelineHeader>Process</TimelineHeader>
Expand Down Expand Up @@ -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' ? (
<ValidationStage
project={project}
environment={environment}
payload={JSON.parse(importPayload)}
onBack={() => setImportStage('configure')}
onClose={() => setOpen(false)}
/>
) : (
''
)}
<ConditionallyRender
condition={importStage === 'validate'}
show={
<ValidationStage
project={project}
environment={environment}
payload={importPayload}
onBack={() => setImportStage('configure')}
onSubmit={() => setImportStage('import')}
onClose={close}
/>
}
/>
<ConditionallyRender
condition={importStage === 'import'}
show={
<ImportStage
project={project}
environment={environment}
payload={importPayload}
onClose={close}
/>
}
/>
</ModalContentContainer>
</SidebarModal>
);
Expand Down

This file was deleted.

Expand Up @@ -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`]: {
Expand Down Expand Up @@ -55,7 +55,7 @@ const TimelineItemDescription = styled(Box)(({ theme }) => ({
}));

export const ImportTimeline: FC<{
stage: ImportStage;
stage: StageName;
}> = ({ stage }) => {
return (
<StyledTimeline>
Expand Down
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions frontend/src/component/project/Project/Import/StageName.ts
@@ -0,0 +1 @@
export type StageName = 'configure' | 'validate' | 'import';
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
Expand Down
Expand Up @@ -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>
Expand All @@ -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
/>
Expand Down
127 changes: 127 additions & 0 deletions 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<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>
);
};
Expand Up @@ -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>(
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions 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({
Expand Down
@@ -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> }>;
Expand Down
11 changes: 7 additions & 4 deletions src/lib/services/export-import-service.ts
Expand Up @@ -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,
Expand Down

0 comments on commit decb7f3

Please sign in to comment.