Skip to content

Commit e21f078

Browse files
committed
Merge remote-tracking branch 'origin/feature/throbbers-and-data-upload' into loki-f2f-demo-2025-12-01
2 parents 1493509 + 654e1f2 commit e21f078

11 files changed

Lines changed: 306 additions & 17 deletions

File tree

locales/de-global.json5

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
label: 'Anwendungsmenü',
1414
login: 'Anmelden',
1515
logout: 'Abmelden',
16+
upload: 'Daten Hochladen',
1617
imprint: 'Impressum',
1718
'privacy-policy': 'Datenschutzerklärung',
1819
accessibility: 'Barrierefreiheit',
@@ -154,4 +155,10 @@
154155
'selected-value': 'Wert Ausgewähltes Datum',
155156
},
156157
},
158+
upload: {
159+
header: 'Falldaten Hochladen',
160+
dragNotice: 'Ziehen sie Ihre Datei(en) in diesen Bereich oder nutzen Sie den Knopf unten, um Ihre Falldaten an ESID zu senden.',
161+
button: 'Daten hochladen',
162+
dropNotice: 'Hier ablegen um die Datei(en) hochzuladen.',
163+
},
157164
}

locales/en-global.json5

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
label: 'Application menu',
1414
login: 'Login',
1515
logout: 'Logout',
16+
upload: 'Upload Data',
1617
imprint: 'Imprint',
1718
'privacy-policy': 'Privacy Policy',
1819
accessibility: 'Accessibility',
@@ -169,4 +170,10 @@
169170
'selected-value': 'Value Selected Date',
170171
},
171172
},
173+
upload: {
174+
header: 'Upload Case Data',
175+
dragNotice: 'Drag and drop your file(s) in here to or use the button below to uplad your case data to ESID.',
176+
button: 'Upload Data',
177+
dropNotice: 'Drop here to upload.',
178+
},
172179
}

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"i18next-browser-languagedetector": "7.2.0",
5454
"i18next-http-backend": "2.4.2",
5555
"json5": "2.2.3",
56+
"ldrs": "1.0.1",
5657
"react": "18.3.1",
5758
"react-dom": "18.3.1",
5859
"react-i18next": "13.5.0",

src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {selectDistrict} from 'store/DataSelectionSlice';
2020
import {I18nextProvider, useTranslation} from 'react-i18next';
2121
import i18n from './util/i18n';
2222
import {MUILocalization} from 'components/shared/MUILocalization';
23-
23+
import LoadingOverlay from './components/shared/LoadingOverlay';
2424
import AuthProvider from './components/AuthProvider';
2525
import BaseDataContext from 'context/BaseDataContext';
2626
import ExportingRegistry from 'context/ExportContext';
@@ -30,7 +30,10 @@ import ExportingRegistry from 'context/ExportContext';
3030
*/
3131
export default function App(): JSX.Element {
3232
return (
33-
<Suspense fallback='loading'>
33+
<Suspense
34+
// Use Loading Overlay with default background and primary color (theme isn't loaded at this point)
35+
fallback={<LoadingOverlay show={true} overlayColor={'#F0F0F2'} throbberColor={'#543CF0'}></LoadingOverlay>}
36+
>
3437
<Provider store={Store}>
3538
<AuthProvider>
3639
<ThemeProvider theme={Theme}>

src/components/TopBar/ApplicationMenu.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {useAppSelector} from 'store/hooks';
1414
import {AuthContext, IAuthContext} from 'react-oauth2-code-pkce';
1515
import CircularProgress from '@mui/material/CircularProgress';
1616

17+
// Let's import pop-ups only once they are opened.
18+
const DataUploadDialog = React.lazy(() => import('./PopUps/DataUploadDialog'));
1719
const ChangelogDialog = React.lazy(() => import('./PopUps/ChangelogDialog'));
1820
const ImprintDialog = React.lazy(() => import('./PopUps/ImprintDialog'));
1921
const PrivacyPolicyDialog = React.lazy(() => import('./PopUps/PrivacyPolicyDialog'));
@@ -53,6 +55,7 @@ export default function ApplicationMenu(): JSX.Element {
5355
const [changelogOpen, setChangelogOpen] = React.useState(false);
5456
const [exportOpen, setExportOpen] = React.useState(false);
5557
const [exportAnchorElement, setExportAnchorElement] = React.useState<Element | null>(null);
58+
const [uploadOpen, setUploadOpen] = React.useState(false);
5659

5760
const keycloakLogout = () => {
5861
window.location.assign(
@@ -93,6 +96,12 @@ export default function ApplicationMenu(): JSX.Element {
9396
keycloakLogout();
9497
};
9598

99+
/** This method gets called, when the login menu entry was clicked. */
100+
const uploadClicked = () => {
101+
closeMenu();
102+
setUploadOpen(true);
103+
};
104+
96105
/** This method gets called, when the imprint menu entry was clicked. It opens a dialog showing the legal text. */
97106
const imprintClicked = () => {
98107
closeMenu();
@@ -148,6 +157,9 @@ export default function ApplicationMenu(): JSX.Element {
148157
</MenuItem>
149158
)}
150159
<Divider />
160+
<MenuItem onClick={uploadClicked} disabled={!isAuthenticated}>
161+
{t('topBar.menu.upload')}
162+
</MenuItem>
151163
<MenuItem onClick={imprintClicked}>{t('topBar.menu.imprint')}</MenuItem>
152164
<MenuItem onClick={privacyPolicyClicked}>{t('topBar.menu.privacy-policy')}</MenuItem>
153165
<MenuItem onClick={accessibilityClicked}>{t('topBar.menu.accessibility')}</MenuItem>
@@ -165,6 +177,10 @@ export default function ApplicationMenu(): JSX.Element {
165177
</MenuItem>
166178
</Menu>
167179

180+
<Dialog maxWidth='lg' fullWidth={true} open={uploadOpen} onClose={() => setUploadOpen(false)}>
181+
<DataUploadDialog />
182+
</Dialog>
183+
168184
<Dialog maxWidth='lg' fullWidth={true} open={imprintOpen} onClose={() => setImprintOpen(false)}>
169185
<Suspense
170186
fallback={
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, {useCallback, useEffect} from 'react';
5+
import {useTheme} from '@mui/material/styles';
6+
import Box from '@mui/material/Box';
7+
import Typography from '@mui/material/Typography';
8+
import {useTranslation} from 'react-i18next';
9+
import Button from '@mui/material/Button';
10+
import List from '@mui/material';
11+
import ListItem from '@mui/material';
12+
import ListItemText from '@mui/material';
13+
import Clear from '@mui/icons-material/Clear';
14+
import CloudUpload from '@mui/icons-material/CloudUpload';
15+
import Done from '@mui/icons-material/Done';
16+
import {helix} from 'ldrs';
17+
import {useSendCasedataFileQuery} from 'store/services/utilsApi';
18+
19+
/**
20+
* This component displays the accessibility legal text.
21+
*/
22+
export default function DataUploadDialog(): JSX.Element {
23+
const {t} = useTranslation();
24+
const theme = useTheme();
25+
const [dragActive, setDragActive] = React.useState(false);
26+
27+
// Register throbber for later use.
28+
useEffect(() => {
29+
helix.register();
30+
}, []);
31+
32+
const [uploadList, setUploadList] = React.useState<{fileinfo: string; file: File}[]>([]);
33+
34+
const fileTypes: string[] = [];
35+
36+
// Function to handle data upload.
37+
const handleFiles = useCallback((fileList: FileList) => {
38+
// Function to increase readability of file size appended behind filename.
39+
const fileSizeToString = (size: number) => {
40+
if (size < 1024) {
41+
return `${size} B`;
42+
} else if (size >= 1024 && size < 1048576) {
43+
return `${(size / 1024).toFixed(1)} KB`;
44+
} else {
45+
return `${(size / 1048576).toFixed(1)} MB`;
46+
}
47+
};
48+
// Update file display with new files.
49+
const displayList: {fileInfo: string; file: File}[] = [];
50+
for (let i = 0; i < fileList.length; i++) {
51+
const file = fileList[i];
52+
53+
displayList.push({
54+
fileInfo: `${file.name} (${fileSizeToString(file.size)})`,
55+
file: file,
56+
});
57+
}
58+
setUploadList(displayList);
59+
}, []);
60+
61+
// Callback for drag event (to modify styling).
62+
const handleDrag = useCallback((e: React.DragEvent) => {
63+
e.preventDefault();
64+
e.stopPropagation();
65+
if (e.type === 'dragenter' || e.type === 'dragover') {
66+
setDragActive(true);
67+
} else if (e.type === 'dragleave') {
68+
setDragActive(false);
69+
}
70+
}, []);
71+
72+
// Callback for files selected through drag & drop.
73+
const handleDrop = useCallback(
74+
(e: React.DragEvent) => {
75+
e.preventDefault();
76+
e.stopPropagation();
77+
setDragActive(false);
78+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
79+
handleFiles(e.dataTransfer.files);
80+
}
81+
},
82+
[handleFiles]
83+
);
84+
85+
// Callback for files selected through dialog.
86+
const handleClick = useCallback(
87+
(e: React.ChangeEvent<HTMLInputElement>) => {
88+
e.preventDefault();
89+
if (e.target.files && e.target.files[0]) {
90+
handleFiles(e.target.files);
91+
}
92+
},
93+
[handleFiles]
94+
);
95+
96+
return (
97+
<form id='upload-form' onDragEnter={handleDrag} onDragLeave={handleDrag} onSubmit={(e) => e.preventDefault()}>
98+
<input type='file' id='upload-input' multiple={true} accept={fileTypes.join(',')} onChange={handleClick} hidden />
99+
<Box
100+
sx={{
101+
margin: theme.spacing(4),
102+
padding: theme.spacing(4),
103+
minHeight: '30vw',
104+
background: theme.palette.background.paper,
105+
border: `${theme.palette.divider} ${dragActive ? 'solid' : 'dashed'} 2px`,
106+
display: 'flex',
107+
flexDirection: 'column',
108+
justifyContent: 'space-around',
109+
alignItems: 'center',
110+
}}
111+
>
112+
<Typography variant='h1'>{t('upload.header')}</Typography>
113+
<div>{t('upload.dragNotice')}</div>
114+
{uploadList.length > 0 && (
115+
<List>
116+
{uploadList.map((item) => (
117+
// Create a list item for each file.
118+
<FileItem key={item.fileinfo} fileinfo={item.fileinfo} file={item.file} />
119+
))}
120+
</List>
121+
)}
122+
<label htmlFor='upload-input'>
123+
<Button variant='contained' startIcon={<CloudUpload />} component='span'>
124+
{t('upload.button')}
125+
</Button>
126+
</label>
127+
</Box>
128+
{dragActive && (
129+
// Add an overlay on top of the popup to display a notice and make handling the drag events smoother.
130+
<div
131+
id='upload-drop-notice'
132+
onDragEnter={handleDrag}
133+
onDragLeave={handleDrag}
134+
onDragOver={handleDrag}
135+
onDrop={handleDrop}
136+
style={{
137+
position: 'absolute',
138+
width: '100%',
139+
height: '100%',
140+
top: 0,
141+
left: 0,
142+
bottom: 0,
143+
right: 0,
144+
background: 'rgba(255, 255, 255, 0.6)',
145+
display: 'flex',
146+
justifyContent: 'center',
147+
alignItems: 'center',
148+
}}
149+
>
150+
<Typography
151+
variant='h1'
152+
sx={{
153+
background: 'white',
154+
border: `solid ${theme.palette.divider} 1px`,
155+
borderRadius: '1em',
156+
padding: '1em',
157+
}}
158+
>
159+
{t('upload.dropNotice')}
160+
</Typography>
161+
</div>
162+
)}
163+
</form>
164+
);
165+
}
166+
167+
function FileItem({fileInfo, file}: {fileInfo: string; file: File}): JSX.Element {
168+
const theme = useTheme();
169+
const {isSuccess, isError} = useSendCasedataFileQuery(file);
170+
171+
return (
172+
<ListItem
173+
disableGutters
174+
secondaryAction={
175+
isSuccess ? (
176+
<Done sx={{color: theme.palette.primary.main, fontSize: 45}} />
177+
) : isError ? (
178+
<Clear sx={{color: theme.palette.error.main, fontSize: 45}} />
179+
) : (
180+
<l-helix size={45} speed={2.5} color={theme.palette.divider}></l-helix>
181+
)
182+
}
183+
>
184+
<ListItemText primary={fileInfo} />
185+
</ListItem>
186+
);
187+
}

src/components/shared/LoadingContainer.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import React from 'react';
55
import Box from '@mui/material/Box';
66
import LoadingOverlay from './LoadingOverlay';
77
import {SxProps} from '@mui/system';
8+
import {useTheme} from '@mui/material/styles';
89

910
/**
1011
* This is a wrapper component for a container that can have a loading indicator overlayed.
1112
*/
1213
export default function LoadingContainer(props: LoadingContainerProps): JSX.Element {
14+
const theme = useTheme();
15+
1316
return (
1417
<Box sx={{...props.sx, position: 'relative'}}>
1518
{props.children}
16-
<LoadingOverlay show={props.show} overlayColor={props.overlayColor} />
19+
<LoadingOverlay
20+
show={props.show}
21+
overlayColor={props.overlayColor}
22+
throbberColor={props.throbberColor ? props.throbberColor : theme.palette.primary.main}
23+
/>
1724
</Box>
1825
);
1926
}
@@ -28,6 +35,9 @@ interface LoadingContainerProps {
2835
/** The color of the overlay. */
2936
overlayColor: string;
3037

38+
/** The color of the throbber. Theme primary color by default. */
39+
throbberColor?: string;
40+
3141
/** React prop to allow nesting components. Do not set manually. */
3242
children: React.ReactNode;
3343
}

0 commit comments

Comments
 (0)