Skip to content

Throbber and data upload #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/locales/de-global.json5
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
menu: {
label: 'Anwendungsmenü',
login: 'Anmelden',
upload: 'Daten Hochladen',
imprint: 'Impressum',
'privacy-policy': 'Datenschutzerklärung',
accessibility: 'Barrierefreiheit',
Expand Down Expand Up @@ -77,4 +78,10 @@
'no-data': 'Keine Daten',
'loki-logo': 'LOKI-Logo',
okay: 'Okay',
upload: {
header: 'Falldaten hochladen',
dragNotice: 'Ziehen Sie ihre Datei(en) in dieses Feld oder nutzen sie den Knopf um Falldaten für ESID hochzuladen.',
button: 'Daten hochladen',
dropNotice: 'Hier loslassen um die Daten hochzuladen.',
},
}
7 changes: 7 additions & 0 deletions frontend/locales/en-global.json5
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
menu: {
label: 'Application menu',
login: 'Login',
upload: 'Upload Data',
imprint: 'Imprint',
'privacy-policy': 'Privacy Policy',
accessibility: 'Accessibility',
Expand Down Expand Up @@ -92,4 +93,10 @@
sanctus est Lorem ipsum dolor sit amet.',
WIP: 'This functionality is still work in progress.',
okay: 'Okay',
upload: {
header: 'Upload Case Data',
dragNotice: 'Drag and drop your file(s) in here to or use the button below to uplad your case data to ESID.',
button: 'Upload Data',
dropNotice: 'Drop here to upload.',
},
}
6 changes: 6 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"i18next-browser-languagedetector": "^7.2.0",
"i18next-http-backend": "^2.4.2",
"json5": "^2.2.3",
"ldrs": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0",
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ import {selectDistrict} from './store/DataSelectionSlice';
import {I18nextProvider, useTranslation} from 'react-i18next';
import i18n from './util/i18n';
import {MUILocalization} from './components/shared/MUILocalization';
import LoadingOverlay from 'components/shared/LoadingOverlay';

/**
* This is the root element of the React application. It divides the main screen area into the three main components.
* The top bar, the sidebar and the main content area.
*/
export default function App(): JSX.Element {
return (
<Suspense fallback='loading'>
<Suspense
// Use Loading Overlay with default background and primary color (theme isn't loaded at this point)
fallback={<LoadingOverlay show={true} overlayColor={'#F0F0F2'} throbberColor={'#543CF0'}></LoadingOverlay>}
>
<Provider store={Store}>
<ThemeProvider theme={Theme}>
<PersistGate loading={null} persistor={Persistor}>
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/components/TopBar/ApplicationMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Snackbar from '@mui/material/Snackbar';
import Box from '@mui/system/Box';

// Let's import pop-ups only once they are opened.
const DataUploadDialog = React.lazy(() => import('./PopUps/DataUploadDialog'));
const ChangelogDialog = React.lazy(() => import('./PopUps/ChangelogDialog'));
const ImprintDialog = React.lazy(() => import('./PopUps/ImprintDialog'));
const PrivacyPolicyDialog = React.lazy(() => import('./PopUps/PrivacyPolicyDialog'));
Expand All @@ -33,6 +34,7 @@ export default function ApplicationMenu(): JSX.Element {
const [accessibilityOpen, setAccessibilityOpen] = React.useState(false);
const [attributionsOpen, setAttributionsOpen] = React.useState(false);
const [changelogOpen, setChangelogOpen] = React.useState(false);
const [uploadOpen, setUploadOpen] = React.useState(false);
const [snackbarOpen, setSnackbarOpen] = React.useState(false);

/** Calling this method opens the application menu. */
Expand All @@ -51,6 +53,12 @@ export default function ApplicationMenu(): JSX.Element {
setSnackbarOpen(true);
};

/** This method gets called, when the login menu entry was clicked. */
const uploadClicked = () => {
closeMenu();
setUploadOpen(true);
};

/** This method gets called, when the imprint menu entry was clicked. It opens a dialog showing the legal text. */
const imprintClicked = () => {
closeMenu();
Expand Down Expand Up @@ -93,7 +101,12 @@ export default function ApplicationMenu(): JSX.Element {
<MenuIcon />
</Button>
<Menu id='application-menu' anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={closeMenu}>
<MenuItem onClick={loginClicked}>{t('topBar.menu.login')}</MenuItem>
<MenuItem disabled onClick={loginClicked}>
{t('topBar.menu.login')}
</MenuItem>
<MenuItem disabled onClick={uploadClicked}>
{t('topBar.menu.upload')}
</MenuItem>
<Divider />
<MenuItem onClick={imprintClicked}>{t('topBar.menu.imprint')}</MenuItem>
<MenuItem onClick={privacyPolicyClicked}>{t('topBar.menu.privacy-policy')}</MenuItem>
Expand All @@ -102,6 +115,10 @@ export default function ApplicationMenu(): JSX.Element {
<MenuItem onClick={changelogClicked}>{t('topBar.menu.changelog')}</MenuItem>
</Menu>

<Dialog maxWidth='lg' fullWidth={true} open={uploadOpen} onClose={() => setUploadOpen(false)}>
<DataUploadDialog />
</Dialog>

<Dialog maxWidth='lg' fullWidth={true} open={imprintOpen} onClose={() => setImprintOpen(false)}>
<ImprintDialog />
</Dialog>
Expand Down
184 changes: 184 additions & 0 deletions frontend/src/components/TopBar/PopUps/DataUploadDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
// SPDX-License-Identifier: Apache-2.0

import React, {useCallback, useEffect} from 'react';
import {useTheme} from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import {useTranslation} from 'react-i18next';
import {Button, List, ListItem, ListItemText} from '@mui/material';
import {Clear, CloudUpload, Done} from '@mui/icons-material';
import {helix} from 'ldrs';

/**
* This component displays the accessibility legal text.
*/
export default function DataUploadDialog(): JSX.Element {
const {t} = useTranslation();
const theme = useTheme();
const [dragActive, setDragActive] = React.useState(false);

enum UploadStatus {
Started,
Error,
Done,
}

// Register throbber for later use.
useEffect(() => {
helix.register();
}, []);

const [uploadStat, setUploadStat] = React.useState<{filename: string; status: UploadStatus}[]>([]);

const fileTypes: string[] = [];

// Function to handle data upload.
const handleFiles = useCallback(
(filelist: FileList) => {
// Function to increase readability of file size appended behind filename.
const fileSizeToString = (size: number) => {
if (size < 1024) {
return `${size} B`;
} else if (size >= 1024 && size < 1048576) {
return `${(size / 1024).toFixed(1)} KB`;
} else {
return `${(size / 1048576).toFixed(1)} MB`;
}
};
// Update file display with new files.
const displaylist: {filename: string; status: UploadStatus}[] = [];
for (let i = 0; i < filelist.length; i++) {
const file = filelist[i];

displaylist.push({
filename: `${file.name} (${fileSizeToString(file.size)})`,
status: UploadStatus.Started,
});
}
setUploadStat(displaylist);

// TODO: init file upload, adjust UploadStat as needed
},
[UploadStatus]
);

// Callback for drag event (to modify styling).
const handleDrag = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true);
} else if (e.type === 'dragleave') {
setDragActive(false);
}
}, []);

// Callback for files selected through drag & drop.
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFiles(e.dataTransfer.files);
}
},
[handleFiles]
);

// Callback for files selected through dialog.
const handleClick = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
if (e.target.files && e.target.files[0]) {
handleFiles(e.target.files);
}
},
[handleFiles]
);

return (
<form id='upload-form' onDragEnter={handleDrag} onDragLeave={handleDrag} onSubmit={(e) => e.preventDefault()}>
<input type='file' id='upload-input' multiple={true} accept={fileTypes.join(',')} onChange={handleClick} hidden />
<Box
sx={{
margin: theme.spacing(4),
padding: theme.spacing(4),
minHeight: '30vw',
background: theme.palette.background.paper,
border: `${theme.palette.divider} ${dragActive ? 'solid' : 'dashed'} 2px`,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
alignItems: 'center',
}}
>
<Typography variant='h1'>{t('upload.header')}</Typography>
<div>{t('upload.dragNotice')}</div>
{uploadStat.length > 0 && (
<List>
{uploadStat.map((file) => (
// Create a list item for each file.
<ListItem
key={file.filename}
disableGutters
secondaryAction={
file.status === UploadStatus.Done ? (
<Done sx={{color: theme.palette.primary.main, fontSize: 45}} />
) : file.status === UploadStatus.Error ? (
<Clear sx={{color: theme.palette.error.main, fontSize: 45}} />
) : (
<l-helix size={45} speed={2.5} color={theme.palette.divider}></l-helix>
)
}
>
<ListItemText primary={file.filename} />
</ListItem>
))}
</List>
)}
<label htmlFor='upload-input'>
<Button variant='contained' startIcon={<CloudUpload />} component='span'>
{t('upload.button')}
</Button>
</label>
</Box>
{dragActive && (
// Add an overlay on top of the popup to display a notice and make handling the drag events smoother.
<div
id='upload-drop-notice'
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
style={{
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
bottom: 0,
right: 0,
background: 'rgba(255, 255, 255, 0.6)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography
variant='h1'
sx={{
background: 'white',
border: `solid ${theme.palette.divider} 1px`,
borderRadius: '1em',
padding: '1em',
}}
>
{t('upload.dropNotice')}
</Typography>
</div>
)}
</form>
);
}
12 changes: 11 additions & 1 deletion frontend/src/components/shared/LoadingContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import React from 'react';
import Box from '@mui/material/Box';
import LoadingOverlay from './LoadingOverlay';
import {SxProps} from '@mui/system';
import {useTheme} from '@mui/material/styles';

/**
* This is a wrapper component for a container that can have a loading indicator overlayed.
*/
export default function LoadingContainer(props: LoadingContainerProps): JSX.Element {
const theme = useTheme();

return (
<Box sx={{...props.sx, position: 'relative'}}>
{props.children}
<LoadingOverlay show={props.show} overlayColor={props.overlayColor} />
<LoadingOverlay
show={props.show}
overlayColor={props.overlayColor}
throbberColor={props.throbberColor ? props.throbberColor : theme.palette.primary.main}
/>
</Box>
);
}
Expand All @@ -28,6 +35,9 @@ interface LoadingContainerProps {
/** The color of the overlay. */
overlayColor: string;

/** The color of the throbber. Theme primary color by default. */
throbberColor?: string;

/** React prop to allow nesting components. Do not set manually. */
children: React.ReactNode;
}
Loading
Loading