1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

validation stage (#2970)

This commit is contained in:
Mateusz Kwasniewski 2023-01-23 20:02:05 +01:00 committed by GitHub
parent 515845edd1
commit a3404328ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 253 additions and 54 deletions

View File

@ -0,0 +1,10 @@
import { Box, styled } from '@mui/material';
export const ActionsContainer = styled(Box)(({ theme }) => ({
width: '100%',
borderTop: `1px solid ${theme.palette.dividerAlternative}`,
marginTop: 'auto',
paddingTop: theme.spacing(3),
display: 'flex',
justifyContent: 'flex-end',
}));

View File

@ -5,7 +5,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { ImportTimeline } from './ImportTimeline';
import { ImportStage } from './ImportStage';
import { ConfigurationStage } from './configure/ConfigurationStage';
import { ValidationStage } from './validate/ValidationState';
import { ValidationStage } from './validate/ValidationStage';
const ModalContentContainer = styled('div')(({ theme }) => ({
minHeight: '100vh',
@ -70,6 +70,9 @@ export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => {
<ValidationStage
project={project}
environment={importStage.environment}
payload={JSON.parse(importStage.payload)}
onBack={() => setImportStage({ name: 'configure' })}
onClose={() => setOpen(false)}
/>
) : (
''

View File

@ -16,6 +16,7 @@ import { ImportExplanation } from './ImportExplanation';
import React, { FC, useState } from 'react';
import useToast from 'hooks/useToast';
import { ImportLayoutContainer } from '../ImportLayoutContainer';
import { ActionsContainer } from '../ActionsContainer';
const StyledTextField = styled(TextField)(({ theme }) => ({
width: '100%',
@ -37,15 +38,6 @@ const MaxSizeMessage = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const ActionsContainer = styled(Box)(({ theme }) => ({
width: '100%',
borderTop: `1px solid ${theme.palette.dividerAlternative}`,
marginTop: 'auto',
paddingTop: theme.spacing(3),
display: 'flex',
justifyContent: 'flex-end',
}));
type ImportMode = 'file' | 'code';
const isValidJSON = (json: string) => {

View File

@ -0,0 +1,201 @@
import { ImportLayoutContainer } from '../ImportLayoutContainer';
import { Box, Button, styled, Typography } from '@mui/material';
import React, { FC, useEffect, useState } from 'react';
import {
IValidationSchema,
useValidateImportApi,
} from 'hooks/api/actions/useValidateImportApi/useValidateImportApi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ActionsContainer } from '../ActionsContainer';
const ImportInfoContainer = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.secondaryContainer,
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(3),
}));
const Label = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
}));
const Value = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.primary,
fontWeight: theme.fontWeight.bold,
}));
const ErrorContainer = styled(Box)(({ theme }) => ({
border: `1px solid ${theme.palette.error.border}`,
borderRadius: theme.shape.borderRadiusLarge,
paddingBottom: theme.spacing(2),
}));
const WarningContainer = styled(Box)(({ theme }) => ({
border: `1px solid ${theme.palette.warning.border}`,
borderRadius: theme.shape.borderRadiusLarge,
paddingBottom: theme.spacing(2),
}));
const ErrorHeader = styled(Box)(({ theme }) => ({
color: theme.palette.error.dark,
backgroundColor: theme.palette.error.light,
fontSize: theme.fontSizes.smallBody,
borderBottom: `1px solid ${theme.palette.error.border}`,
borderTopLeftRadius: theme.shape.borderRadiusLarge,
borderTopRightRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(2),
}));
const WarningHeader = styled(Box)(({ theme }) => ({
color: theme.palette.warning.dark,
backgroundColor: theme.palette.warning.light,
fontSize: theme.fontSizes.smallBody,
borderBottom: `1px solid ${theme.palette.warning.border}`,
borderTopLeftRadius: theme.shape.borderRadiusLarge,
borderTopRightRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(2),
}));
const ErrorMessage = styled(Box)(({ theme }) => ({
color: theme.palette.error.dark,
fontSize: theme.fontSizes.smallBody,
}));
const WarningMessage = styled(Box)(({ theme }) => ({
color: theme.palette.warning.dark,
fontSize: theme.fontSizes.smallBody,
}));
const StyledItems = styled('ul')(({ theme }) => ({
marginTop: theme.spacing(1),
marginBottom: theme.spacing(0),
paddingLeft: theme.spacing(3),
paddingBottom: theme.spacing(3),
borderBottom: `1px dashed ${theme.palette.neutral.border}`,
}));
const StyledItem = styled('li')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
export const ValidationStage: FC<{
environment: string;
project: string;
payload: string;
onClose: () => void;
onBack: () => void;
}> = ({ environment, project, payload, onClose, onBack }) => {
const { validateImport } = useValidateImportApi();
const { setToastData } = useToast();
const [validationResult, setValidationResult] = useState<IValidationSchema>(
{ errors: [], warnings: [] }
);
useEffect(() => {
validateImport({ environment, project, data: payload })
.then(setValidationResult)
.catch(error =>
setToastData({
type: 'error',
title: formatUnknownError(error),
})
);
}, []);
return (
<ImportLayoutContainer>
<ImportInfoContainer>
<Typography sx={{ mb: 1.5 }}>
You are importing this configuration in:
</Typography>
<Box sx={{ display: 'flex', gap: 3 }}>
<span>
<Label>Environment: </Label>
<Value>{environment}</Value>
</span>
<span>
<Label>Project: </Label>
<Value>{project}</Value>
</span>
</Box>
</ImportInfoContainer>
<ConditionallyRender
condition={validationResult.errors.length > 0}
show={
<ErrorContainer>
<ErrorHeader>
<strong>Conflict!</strong> There are some
configurations that don't exist in the current
instance and need to be created before importing
this configuration
</ErrorHeader>
{validationResult.errors.map(error => (
<Box sx={{ p: 2 }}>
<ErrorMessage>{error.message}</ErrorMessage>
<StyledItems>
{error.affectedItems.map(item => (
<StyledItem>{item}</StyledItem>
))}
</StyledItems>
</Box>
))}
</ErrorContainer>
}
/>
<ConditionallyRender
condition={validationResult.warnings.length > 0}
show={
<WarningContainer>
<WarningHeader>
<strong>Warning!</strong> There are existing feature
toggles in the current instance and if you continue
the import, they will be overwritten with the
configuration from this import file
</WarningHeader>
{validationResult.warnings.map(error => (
<Box sx={{ p: 2 }}>
<WarningMessage>{error.message}</WarningMessage>
<StyledItems>
{error.affectedItems.map(item => (
<StyledItem>{item}</StyledItem>
))}
</StyledItems>
</Box>
))}
</WarningContainer>
}
/>
<ActionsContainer>
<Button
sx={{
position: 'static',
mr: 'auto',
}}
variant="outlined"
type="submit"
onClick={onBack}
>
Back
</Button>
<Button
sx={{ position: 'static' }}
variant="contained"
type="submit"
disabled={validationResult.errors.length > 0}
>
Import configuration
</Button>
<Button
sx={{ position: 'static', ml: 2 }}
variant="outlined"
type="submit"
onClick={onClose}
>
Cancel import
</Button>
</ActionsContainer>
</ImportLayoutContainer>
);
};

View File

@ -1,44 +0,0 @@
import { ImportLayoutContainer } from '../ImportLayoutContainer';
import { Box, styled, Typography } from '@mui/material';
import { FC } from 'react';
const ImportInfoContainer = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.secondaryContainer,
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(3),
}));
const Label = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
}));
const Value = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.primary,
fontWeight: theme.fontWeight.bold,
}));
export const ValidationStage: FC<{ environment: string; project: string }> = ({
environment,
project,
}) => {
return (
<ImportLayoutContainer>
<ImportInfoContainer>
<Typography sx={{ mb: 1.5 }}>
You are importing this configuration in:
</Typography>
<Box sx={{ display: 'flex', gap: 3 }}>
<span>
<Label>Environment: </Label>
<Value>{environment}</Value>
</span>
<span>
<Label>Project: </Label>
<Value>{project}</Value>
</span>
</Box>
</ImportInfoContainer>
</ImportLayoutContainer>
);
};

View File

@ -0,0 +1,37 @@
import useAPI from '../useApi/useApi';
export interface ImportQuerySchema {}
export interface IValidationSchema {
errors: Array<{ message: string; affectedItems: Array<string> }>;
warnings: Array<{ message: string; affectedItems: Array<string> }>;
}
export const useValidateImportApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const validateImport = async (
payload: ImportQuerySchema
): Promise<IValidationSchema> => {
const path = `api/admin/features-batch/full-validate`;
const req = createRequest(path, {
method: 'POST',
body: JSON.stringify(payload),
});
try {
const res = await makeRequest(req.caller, req.id);
return res.json();
} catch (e) {
throw e;
}
};
return {
loading,
errors,
validateImport,
};
};