Skip to content
Open
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 locales/de-global.json5
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
label: 'Anwendungsmenü',
login: 'Anmelden',
logout: 'Abmelden',
upload: 'Daten Hochladen',
imprint: 'Impressum',
'privacy-policy': 'Datenschutzerklärung',
accessibility: 'Barrierefreiheit',
Expand Down Expand Up @@ -134,4 +135,10 @@
'loki-logo': 'LOKI-Logo',
okay: 'Okay',
yAxisLabel: 'Wert',
upload: {
header: 'Falldaten Hochladen',
dragNotice: 'Ziehen sie Ihre Datei(en) in diesen Bereich oder nutzen Sie den Knopf unten, um Ihre Falldaten an ESID zu senden.',
button: 'Daten hochladen',
dropNotice: 'Hier ablegen um die Datei(en) hochzuladen.',
},
}
7 changes: 7 additions & 0 deletions locales/en-global.json5
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
label: 'Application menu',
login: 'Login',
logout: 'Logout',
upload: 'Upload Data',
imprint: 'Imprint',
'privacy-policy': 'Privacy Policy',
accessibility: 'Accessibility',
Expand Down Expand Up @@ -149,4 +150,10 @@
WIP: 'This functionality is still work in progress.',
okay: 'Okay',
yAxisLabel: 'Value',
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 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"i18next-browser-languagedetector": "7.2.0",
"i18next-http-backend": "2.4.2",
"json5": "2.2.3",
"ldrs": "1.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "13.5.0",
Expand Down
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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';
import AuthProvider from './components/AuthProvider';
import BaseDataContext from 'context/BaseDataContext';
/**
Expand All @@ -29,7 +29,10 @@ import BaseDataContext from 'context/BaseDataContext';
*/
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}>
<AuthProvider>
<ThemeProvider theme={Theme}>
Expand Down
9 changes: 7 additions & 2 deletions src/components/LineChartContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export default function LineChartContainer() {
const sum = pattern.reduce((acc, curr) => acc + curr, 0);
for (let x = 0; x <= width; x += sum) {
for (let i = 0; i < pattern.length; i++) {
html += `<div style="height: 2px; width: ${pattern[i]}px; background: ${i % 2 === 0 ? color : 'transparent'}"></div>`;
html += `<div style="height: 2px; width: ${pattern[i]}px; background: ${
i % 2 === 0 ? color : 'transparent'
}"></div>`;
}
}
return html + '</div>';
Expand All @@ -132,7 +134,10 @@ export default function LineChartContainer() {
seriesId: `${id}-${filter.id}`,
name: `
<div style="display: flex; align-items: center; gap: 5px">
${generateHTMLStrokePattern(STROKE_PATTERNS[index % STROKE_PATTERNS.length], scenariosState[id]?.colors[0] ?? 'transparent')}
${generateHTMLStrokePattern(
STROKE_PATTERNS[index % STROKE_PATTERNS.length],
scenariosState[id]?.colors[0] ?? 'transparent'
)}
${filter.name}
</div>`,
visible: true,
Expand Down
5 changes: 3 additions & 2 deletions src/components/ScenarioComponents/ScenarioContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ export default function ScenarioContainer({minCompartmentsRows = 4, maxCompartme
const model = simulationModels.find((model) => model.id === backendScenario?.modelId);
const nodeList = nodeLists.find((nodeList) => nodeList.id === backendScenario?.nodeListId);
const npiList = npis
.filter((npi) =>
backendScenario?.linkedInterventions.find((intervention) => intervention.interventionId === npi.id)
.filter(
(npi) =>
backendScenario?.linkedInterventions.find((intervention) => intervention.interventionId === npi.id)
)
.map((npi) => tBackend(`interventions.${npi.name}`));

Expand Down
4 changes: 2 additions & 2 deletions src/components/ScenarioComponents/ScenarioLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ function LibraryCard(
const model = props.simulationModels.find((model) => model.id === backendScenario?.modelId);
const nodeList = props.nodeLists.find((nodeList) => nodeList.id === backendScenario?.nodeListId);
const npiList = props.npis
.filter((npi) =>
backendScenario?.linkedInterventions.find((intervention) => intervention.interventionId === npi.id)
.filter(
(npi) => backendScenario?.linkedInterventions.find((intervention) => intervention.interventionId === npi.id)
)
.map((npi) => tBackend(`interventions.${npi.name}`));

Expand Down
22 changes: 21 additions & 1 deletion src/components/TopBar/ApplicationMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {useAppSelector} from 'store/hooks';
import {AuthContext, IAuthContext} from 'react-oauth2-code-pkce';
import CircularProgress from '@mui/material/CircularProgress';

// 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 Down Expand Up @@ -50,10 +52,15 @@ 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 keycloakLogout = () => {
window.location.assign(
`${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURI(`${import.meta.env.VITE_OAUTH_REDIRECT_URL}`)}&id_token_hint=${idToken}`
`${
import.meta.env.VITE_OAUTH_API_URL
}/realms/${realm}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURI(
`${import.meta.env.VITE_OAUTH_REDIRECT_URL}`
)}&id_token_hint=${idToken}`
);
};

Expand All @@ -80,6 +87,12 @@ export default function ApplicationMenu(): JSX.Element {
keycloakLogout();
};

/** 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 @@ -135,13 +148,20 @@ export default function ApplicationMenu(): JSX.Element {
</MenuItem>
)}
<Divider />
<MenuItem onClick={uploadClicked} disabled={!isAuthenticated}>
{t('topBar.menu.upload')}
</MenuItem>
<MenuItem onClick={imprintClicked}>{t('topBar.menu.imprint')}</MenuItem>
<MenuItem onClick={privacyPolicyClicked}>{t('topBar.menu.privacy-policy')}</MenuItem>
<MenuItem onClick={accessibilityClicked}>{t('topBar.menu.accessibility')}</MenuItem>
<MenuItem onClick={attributionClicked}>{t('topBar.menu.attribution')}</MenuItem>
<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)}>
<Suspense
fallback={
Expand Down
187 changes: 187 additions & 0 deletions src/components/TopBar/PopUps/DataUploadDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// 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 from '@mui/material/Button';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Clear from '@mui/icons-material/Clear';
import CloudUpload from '@mui/icons-material/CloudUpload';
import Done from '@mui/icons-material/Done';
import {helix} from 'ldrs';
import {useSendCasedataFileQuery} from 'store/services/utilsApi';

/**
* 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);

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

const [uploadList, setUploadList] = React.useState<{fileInfo: string; file: File}[]>([]);

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: {fileInfo: string; file: File}[] = [];
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];

displayList.push({
fileInfo: `${file.name} (${fileSizeToString(file.size)})`,
file: file,
});
}
setUploadList(displayList);
}, []);

// 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>
{uploadList.length > 0 && (
<List>
{uploadList.map((item) => (
// Create a list item for each file.
<FileItem key={item.fileInfo} fileInfo={item.fileInfo} file={item.file} />
))}
</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>
);
}

function FileItem({fileInfo, file}: {fileInfo: string; file: File}): JSX.Element {
const theme = useTheme();
const {isSuccess, isError} = useSendCasedataFileQuery(file);

return (
<ListItem
disableGutters
secondaryAction={
isSuccess ? (
<Done sx={{color: theme.palette.primary.main, fontSize: 45}} />
) : isError ? (
<Clear sx={{color: theme.palette.error.main, fontSize: 45}} />
) : (
<l-helix size={45} speed={2.5} color={theme.palette.divider}></l-helix>
)
}
>
<ListItemText primary={fileInfo} />
</ListItem>
);
}
Loading