diff --git a/frontend/src/component/project/Project/Import/FileDropZone.tsx b/frontend/src/component/project/Project/Import/FileDropZone.tsx index e57bd042d6..8edcc798c1 100644 --- a/frontend/src/component/project/Project/Import/FileDropZone.tsx +++ b/frontend/src/component/project/Project/Import/FileDropZone.tsx @@ -2,29 +2,39 @@ import React, { FC, useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { Box } from '@mui/material'; -const formatJSON = (json: string) => { - try { - return JSON.stringify(JSON.parse(json), null, 4); - } catch (e) { - console.error(e); - return ''; - } -}; +const formatJSON = (json: string) => JSON.stringify(JSON.parse(json), null, 4); + +interface IFileDropZoneProps { + onSuccess: (message: string) => void; + onError: (error: string) => void; +} + +const onFileDropped = + ({ onSuccess, onError }: IFileDropZoneProps) => + (e: ProgressEvent) => { + const contents = e?.target?.result; + if (typeof contents === 'string') { + try { + const json = formatJSON(contents); + onSuccess(json); + } catch (e) { + onError('Cannot format as valid JSON'); + } + } else { + onError('Unsupported format'); + } + }; // This component has no styling on purpose -export const FileDropZone: FC<{ onChange: (content: string) => void }> = ({ - onChange, +export const FileDropZone: FC = ({ + onSuccess, + onError, children, ...props }) => { const onDrop = useCallback(([file]) => { const reader = new FileReader(); - reader.onload = function (e) { - const contents = e?.target?.result; - if (typeof contents === 'string') { - onChange(formatJSON(contents)); - } - }; + reader.onload = onFileDropped({ onSuccess, onError }); reader.readAsText(file); }, []); const { getRootProps, getInputProps } = useDropzone({ diff --git a/frontend/src/component/project/Project/Import/ImportModal.tsx b/frontend/src/component/project/Project/Import/ImportModal.tsx index 3494059a01..2a200ab85a 100644 --- a/frontend/src/component/project/Project/Import/ImportModal.tsx +++ b/frontend/src/component/project/Project/Import/ImportModal.tsx @@ -1,21 +1,58 @@ -import { Button, styled, TextField } from '@mui/material'; +import { + Button, + styled, + Tabs, + Tab, + TextField, + Box, + Typography, + Avatar, +} from '@mui/material'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; -import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; -import { KeyboardArrowDownOutlined } from '@mui/icons-material'; -import React, { useEffect, useState } from 'react'; +import { ArrowUpward } from '@mui/icons-material'; +import React, { useState } from 'react'; import { useImportApi } from 'hooks/api/actions/useImportApi/useImportApi'; -import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments'; -import { StyledFileDropZone } from './ImportTogglesDropZone'; +import { StyledFileDropZone } from './StyledFileDropZone'; +import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender'; +import useToast from 'hooks/useToast'; +import { ImportOptions } from './ImportOptions'; -const StyledDiv = styled('div')(({ theme }) => ({ - backgroundColor: '#efefef', +const LayoutContainer = styled('div')(({ theme }) => ({ + backgroundColor: '#fff', height: '100vh', - padding: theme.spacing(2), + padding: theme.spacing(4, 8, 4, 8), + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), })); const StyledTextField = styled(TextField)(({ theme }) => ({ width: '100%', - margin: theme.spacing(2, 0), +})); + +const DropMessage = styled(Typography)(({ theme }) => ({ + marginTop: theme.spacing(4), + fontSize: theme.fontSizes.mainHeader, +})); + +const SelectFileMessage = styled(Typography)(({ theme }) => ({ + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1.5), + color: theme.palette.text.secondary, +})); + +const MaxSizeMessage = styled(Typography)(({ theme }) => ({ + marginTop: theme.spacing(9), + color: theme.palette.text.secondary, +})); + +const ActionsContainer = styled(Box)(({ theme }) => ({ + width: '100%', + borderTop: `1px solid ${theme.palette.dividerAlternative}`, + marginTop: 'auto', + paddingTop: theme.spacing(4), + display: 'flex', + justifyContent: 'flex-end', })); interface IImportModalProps { @@ -25,74 +62,116 @@ interface IImportModalProps { project: string; } +type ImportMode = 'file' | 'code'; + export const ImportModal = ({ open, setOpen, project }: IImportModalProps) => { - const { environments } = useProjectEnvironments(project); const { createImport } = useImportApi(); - const environmentOptions = environments - .filter(environment => environment.enabled) - .map(environment => ({ - key: environment.name, - label: environment.name, - title: environment.name, - })); - const [environment, setEnvironment] = useState(''); - const [data, setData] = useState(''); - - useEffect(() => { - setEnvironment(environmentOptions[0]?.key); - }, [JSON.stringify(environmentOptions)]); + const [importPayload, setImportPayload] = useState(''); + const [activeTab, setActiveTab] = useState('file'); const onSubmit = async () => { await createImport({ - data: JSON.parse(data), + data: JSON.parse(importPayload), environment, project, }); }; + const { setToastData } = useToast(); + return ( { setOpen(false); }} - label={'New service account'} + label="Import toggles" > - - - -

- Drag 'n' drop some files here, or click to select files -

-
- setData(event.target.value)} - value={data} - multiline - minRows={20} - maxRows={20} - /> - {' '} -
+ + setActiveTab('file')} + /> + setActiveTab('code')} + /> + + + + { + setImportPayload(data); + setActiveTab('code'); + setToastData({ + type: 'success', + title: 'File uploaded', + }); + }} + onError={error => { + setToastData({ + type: 'error', + title: error, + }); + }} + > + + + + Drop your file here + + or select a file from your device + + + + JSON format: max 500 kB + + + } + elseShow={ + + setImportPayload(event.target.value) + } + value={importPayload} + multiline + minRows={17} + maxRows={17} + /> + } + /> + + + +
); }; diff --git a/frontend/src/component/project/Project/Import/ImportOptions.tsx b/frontend/src/component/project/Project/Import/ImportOptions.tsx new file mode 100644 index 0000000000..14c32fa764 --- /dev/null +++ b/frontend/src/component/project/Project/Import/ImportOptions.tsx @@ -0,0 +1,63 @@ +import GeneralSelect from '../../../common/GeneralSelect/GeneralSelect'; +import { KeyboardArrowDownOutlined } from '@mui/icons-material'; +import React, { FC, useEffect, useState } from 'react'; +import { useProjectEnvironments } from '../../../../hooks/api/getters/useProjectEnvironments/useProjectEnvironments'; +import { Box, styled, Typography } from '@mui/material'; + +const ImportOptionsContainer = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.secondaryContainer, + borderRadius: theme.shape.borderRadiusLarge, + padding: theme.spacing(3), +})); + +const ImportOptionsHeader = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(3), + fontWeight: theme.typography.fontWeightBold, +})); + +const ImportOptionsDescription = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(1.5), +})); + +interface IImportOptionsProps { + project: string; + environment: string; + onChange: (value: string) => void; +} + +export const ImportOptions: FC = ({ + project, + environment, + onChange, +}) => { + const { environments } = useProjectEnvironments(project); + const environmentOptions = environments + .filter(environment => environment.enabled) + .map(environment => ({ + key: environment.name, + label: environment.name, + title: environment.name, + })); + + useEffect(() => { + onChange(environmentOptions[0]?.key); + }, [JSON.stringify(environmentOptions)]); + + return ( + + Import options + + Choose the environment to import the configuration for + + + + ); +}; diff --git a/frontend/src/component/project/Project/Import/ImportTogglesDropZone.tsx b/frontend/src/component/project/Project/Import/ImportTogglesDropZone.tsx deleted file mode 100644 index aedb3d1316..0000000000 --- a/frontend/src/component/project/Project/Import/ImportTogglesDropZone.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { styled } from '@mui/material'; -import { FileDropZone } from './FileDropZone'; -import React from 'react'; - -export const StyledFileDropZone = styled(FileDropZone)(({ theme }) => ({ - padding: theme.spacing(4), - border: '1px solid black', - margin: theme.spacing(2, 0), -})); diff --git a/frontend/src/component/project/Project/Import/StyledFileDropZone.tsx b/frontend/src/component/project/Project/Import/StyledFileDropZone.tsx new file mode 100644 index 0000000000..78989945ce --- /dev/null +++ b/frontend/src/component/project/Project/Import/StyledFileDropZone.tsx @@ -0,0 +1,12 @@ +import { styled } from '@mui/material'; +import { FileDropZone } from './FileDropZone'; +import React from 'react'; + +export const StyledFileDropZone = styled(FileDropZone)(({ theme }) => ({ + padding: theme.spacing(10, 2, 2, 2), + border: `1px dashed ${theme.palette.secondary.border}`, + borderRadius: theme.shape.borderRadiusLarge, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +}));