From a3404328eaf6da093daa124dfa46dd8cfd7d82da Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 23 Jan 2023 20:02:05 +0100 Subject: [PATCH] validation stage (#2970) --- .../Project/Import/ActionsContainer.tsx | 10 + .../project/Project/Import/ImportModal.tsx | 5 +- .../Import/configure/ConfigurationStage.tsx | 10 +- .../Import/validate/ValidationStage.tsx | 201 ++++++++++++++++++ .../Import/validate/ValidationState.tsx | 44 ---- .../useValidateImportApi.ts | 37 ++++ 6 files changed, 253 insertions(+), 54 deletions(-) create mode 100644 frontend/src/component/project/Project/Import/ActionsContainer.tsx create mode 100644 frontend/src/component/project/Project/Import/validate/ValidationStage.tsx delete mode 100644 frontend/src/component/project/Project/Import/validate/ValidationState.tsx create mode 100644 frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts diff --git a/frontend/src/component/project/Project/Import/ActionsContainer.tsx b/frontend/src/component/project/Project/Import/ActionsContainer.tsx new file mode 100644 index 0000000000..eabfa961a7 --- /dev/null +++ b/frontend/src/component/project/Project/Import/ActionsContainer.tsx @@ -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', +})); diff --git a/frontend/src/component/project/Project/Import/ImportModal.tsx b/frontend/src/component/project/Project/Import/ImportModal.tsx index 84a1264615..ad07bf8c7c 100644 --- a/frontend/src/component/project/Project/Import/ImportModal.tsx +++ b/frontend/src/component/project/Project/Import/ImportModal.tsx @@ -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) => { setImportStage({ name: 'configure' })} + onClose={() => setOpen(false)} /> ) : ( '' diff --git a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx index 3aa4cf95e6..cee58ba6ce 100644 --- a/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx +++ b/frontend/src/component/project/Project/Import/configure/ConfigurationStage.tsx @@ -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) => { diff --git a/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx new file mode 100644 index 0000000000..56901012d1 --- /dev/null +++ b/frontend/src/component/project/Project/Import/validate/ValidationStage.tsx @@ -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( + { errors: [], warnings: [] } + ); + + useEffect(() => { + validateImport({ environment, project, data: payload }) + .then(setValidationResult) + .catch(error => + setToastData({ + type: 'error', + title: formatUnknownError(error), + }) + ); + }, []); + + return ( + + + + You are importing this configuration in: + + + + + {environment} + + + + {project} + + + + 0} + show={ + + + Conflict! There are some + configurations that don't exist in the current + instance and need to be created before importing + this configuration + + {validationResult.errors.map(error => ( + + {error.message} + + {error.affectedItems.map(item => ( + {item} + ))} + + + ))} + + } + /> + 0} + show={ + + + Warning! 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 + + {validationResult.warnings.map(error => ( + + {error.message} + + {error.affectedItems.map(item => ( + {item} + ))} + + + ))} + + } + /> + + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/Import/validate/ValidationState.tsx b/frontend/src/component/project/Project/Import/validate/ValidationState.tsx deleted file mode 100644 index fc0d014d67..0000000000 --- a/frontend/src/component/project/Project/Import/validate/ValidationState.tsx +++ /dev/null @@ -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 ( - - - - You are importing this configuration in: - - - - - {environment} - - - - {project} - - - - - ); -}; diff --git a/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts b/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts new file mode 100644 index 0000000000..8d2292102a --- /dev/null +++ b/frontend/src/hooks/api/actions/useValidateImportApi/useValidateImportApi.ts @@ -0,0 +1,37 @@ +import useAPI from '../useApi/useApi'; + +export interface ImportQuerySchema {} +export interface IValidationSchema { + errors: Array<{ message: string; affectedItems: Array }>; + warnings: Array<{ message: string; affectedItems: Array }>; +} + +export const useValidateImportApi = () => { + const { makeRequest, createRequest, errors, loading } = useAPI({ + propagateErrors: true, + }); + + const validateImport = async ( + payload: ImportQuerySchema + ): Promise => { + 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, + }; +};