• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DLR-SC / ESID / 16340435677

17 Jul 2025 08:42AM UTC coverage: 52.615% (-1.5%) from 54.09%
16340435677

push

github

web-flow
Merge pull request #416 from DLR-SC/feature/fix-filters

Fix Filters

473 of 607 branches covered (77.92%)

Branch coverage included in aggregate %.

0 of 153 new or added lines in 5 files covered. (0.0%)

194 existing lines in 7 files now uncovered.

4527 of 8896 relevant lines covered (50.89%)

10.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/ScenarioComponents/ScenarioLibrary.tsx
1
// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR)
2
// SPDX-License-Identifier: Apache-2.0
3

4
import React, {useCallback, useContext, useMemo, useRef, useState} from 'react';
×
5
import Paper from '@mui/material/Paper';
×
6
import Popper from '@mui/material/Popper';
×
7
import Box from '@mui/material/Box';
×
8
import Button from '@mui/material/Button';
×
9
import useTheme from '@mui/material/styles/useTheme';
×
10
import {useTranslation} from 'react-i18next';
×
11
import Typography from '@mui/material/Typography';
×
12
import Divider from '@mui/material/Divider';
×
13
import ClickAwayListener from '@mui/material/ClickAwayListener';
×
14
import {useAppDispatch, useAppSelector} from 'store/hooks';
×
15
import IconButton from '@mui/material/IconButton';
×
16
import Close from '@mui/icons-material/Close';
×
17
import CardTitle from './CardsComponents/MainCard/CardTitle';
×
18
import WebAssetOff from '@mui/icons-material/WebAssetOff';
×
19
import LibraryAddOutlined from '@mui/icons-material/LibraryAddOutlined';
×
20
import Dialog from '@mui/material/Dialog';
×
21
import NewScenarioDialog, {NewScenarioData} from './NewScenarioDialog';
×
22
import {InterventionTemplates, Models, NodeLists, Scenario} from 'store/services/APITypes';
23
import {useGetMultiScenariosQuery} from 'store/services/scenarioApi';
×
24
import {ScenarioVisibility, updateScenario} from 'store/DataSelectionSlice';
×
25
import {setScenarioColors} from 'store/UserPreferenceSlice';
×
26
import {DataContext} from 'context/SelectedDataContext';
×
27
import {AuthContext, IAuthContext} from 'react-oauth2-code-pkce';
×
UNCOV
28
import ScenarioDescription from 'components/ScenarioComponents/ScenarioDescription';
×
29

30
type ResourceAccess = {
31
  [clientId: string]: {
32
    roles: string[];
33
  };
34
};
35

36
export default function ScenarioLibrary(): JSX.Element {
×
37
  const dispatch = useAppDispatch();
×
38
  const {t} = useTranslation();
×
UNCOV
39
  const theme = useTheme();
×
40

UNCOV
41
  const {tokenData} = useContext<IAuthContext>(AuthContext);
×
42

43
  // *Warning, brute force cast, should be replaced with a more type-safe solution
44
  /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access*/
45
  const roles: string[] =
×
46
    (tokenData &&
×
47
      tokenData.resource_access &&
×
48
      (tokenData.resource_access as ResourceAccess)[import.meta.env.VITE_OAUTH_CLIENT_ID]?.roles) ??
×
UNCOV
49
    [];
×
50
  /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access*/
UNCOV
51
  const canCreateScenarios = roles.includes('lha-user');
×
52

53
  const {scenarios, simulationModels, npis, nodeLists} = useContext(DataContext)!;
×
UNCOV
54
  const scenariosState = useAppSelector((state) => state.dataSelection.scenarios);
×
55

UNCOV
56
  const {data: completeScenarios} = useGetMultiScenariosQuery(scenarios?.map((s) => s.id) ?? [], {skip: !scenarios});
×
57

58
  const scenarioCreated = useCallback(
×
59
    (data: NewScenarioData) => {
×
60
      const result = Object.values(completeScenarios ?? {}).find((scenario: Scenario) => {
×
61
        if (scenario.name === 'casedata') {
×
62
          return false;
×
63
        }
×
64
        if (scenario.linkedInterventions.length === data.npis.length) {
×
65
          return data.npis.every((id) =>
×
66
            scenario.linkedInterventions.find((intervention) => intervention.interventionId === id)
×
67
          );
×
68
        }
×
69
        return false;
×
UNCOV
70
      });
×
71

72
      if (result) {
×
73
        dispatch(
×
74
          updateScenario({
×
75
            id: result.id,
×
76
            state: {
×
77
              name: data.name,
×
78
              description: data.description,
×
79
              visibility: ScenarioVisibility.FaceUp,
×
80
              colors: data.colors,
×
81
            },
×
82
          })
×
83
        );
×
84
        dispatch(
×
85
          setScenarioColors({
×
86
            scenarioId: result.id,
×
87
            colors: data.colors,
×
88
          })
×
89
        );
×
90
      }
×
91
    },
×
92
    [completeScenarios, dispatch]
×
UNCOV
93
  );
×
94

95
  const hiddenScenarios = useMemo(() => {
×
96
    return Object.entries(scenariosState)
×
97
      .filter(([_, value]) => value.visibility === ScenarioVisibility.InLibrary)
×
98
      .map(([key, scenario]) => ({
×
99
        id: key,
×
100
        name: scenario.name,
×
101
        description: scenario.description,
×
UNCOV
102
      }));
×
103
  }, [scenariosState]);
×
104

105
  const anchorRef = useRef<HTMLButtonElement>(null);
×
106

107
  const [open, setOpen] = useState(false);
×
108
  const handleToggle = () => {
×
UNCOV
109
    setOpen((prevOpen) => !prevOpen);
×
110
  };
×
111

112
  const id = open ? 'simple-popper' : undefined;
×
113
  const handleClose = (event: Event | React.SyntheticEvent) => {
×
114
    if (anchorRef.current?.contains(event.target as HTMLElement)) {
×
UNCOV
115
      return;
×
116
    }
×
117

UNCOV
118
    setOpen(false);
×
119
  };
×
120

121
  return (
×
122
    <Box>
×
123
      <Button
×
124
        ref={anchorRef}
×
125
        aria-describedby={id}
×
126
        type='button'
×
127
        onClick={handleToggle}
×
128
        id='scenario-add-button'
×
129
        variant='outlined'
×
130
        color='success'
×
131
        sx={{
×
132
          height: '244px',
×
133
          width: '200px',
×
134
          margin: theme.spacing(3),
×
135
          marginTop: theme.spacing(2),
×
136
          fontWeight: 'bolder',
×
137
          fontSize: '3rem',
×
138
          border: `2px ${theme.palette.primary.light} dashed`,
×
139
          borderRadius: '3px',
×
UNCOV
140
          color: theme.palette.primary.light,
×
141
          alignSelf: 'top',
×
142

143
          '&:hover': {
×
144
            border: `2px ${theme.palette.primary.light} dashed`,
×
145
            background: '#E7E7E7',
×
146
          },
×
147
        }}
×
UNCOV
148
        aria-label={t('scenario-library.add')}
×
149
      >
×
150
        +
151
      </Button>
×
152
      <Popper id={id} open={open} anchorEl={anchorRef.current} placement='bottom-start' sx={{zIndex: 100}}>
×
153
        <Paper>
×
154
          <ClickAwayListener onClickAway={handleClose}>
×
155
            <Box
×
156
              sx={{
×
157
                width: '930px',
×
158
                margin: theme.spacing(2),
×
159
                display: 'flex',
×
160
                flexDirection: 'column',
×
161
                flexGrow: '1',
×
162
                padding: theme.spacing(4),
×
UNCOV
163
                alignItems: 'center',
×
164
              }}
×
165
            >
166
              <Box
×
167
                id='group-filter-dialog-title-bar'
×
168
                sx={{
×
169
                  display: 'grid',
×
170
                  gridTemplateColumns: '1fr auto 1fr',
×
171
                  gridColumnGap: '5px',
×
172
                  alignItems: 'center',
×
173
                  justifyItems: 'center',
×
174
                  width: '100%',
×
UNCOV
175
                  marginBottom: theme.spacing(2),
×
176
                }}
×
177
              >
178
                <div />
×
179
                <Typography variant='h1'>{t('scenario-library.title')}</Typography>
×
180
                <IconButton color='primary' sx={{marginLeft: 'auto'}} onClick={() => setOpen(false)}>
×
181
                  <Close />
×
182
                </IconButton>
×
183
              </Box>
×
184
              <Divider orientation='horizontal' variant='middle' flexItem />
×
185
              <Box
×
186
                sx={{
×
187
                  display: 'grid',
×
188
                  gridTemplateColumns: 'repeat(auto-fill, 204px)',
×
189
                  gridGap: '1rem',
×
190
                  justifyContent: 'space-between',
×
191
                  width: '100%',
×
192
                  marginTop: theme.spacing(2),
×
193
                  maxHeight: '500px',
×
UNCOV
194
                  overflowY: 'auto',
×
195
                }}
×
196
              >
197
                {canCreateScenarios && (
×
198
                  <NewScenarioCard
×
199
                    models={simulationModels ?? []}
×
200
                    npis={npis ?? []}
×
201
                    nodeLists={nodeLists ?? []}
×
UNCOV
202
                    scenarioCreated={scenarioCreated}
×
203
                  />
×
204
                )}
UNCOV
205
                {hiddenScenarios.length > 0 ? (
×
206
                  hiddenScenarios.map((scenario) => (
×
207
                    <LibraryCard
×
208
                      key={scenario.id}
×
209
                      id={scenario.id}
×
210
                      name={scenario.name}
×
211
                      description={scenario.description}
×
212
                      completeScenarios={completeScenarios}
×
213
                      simulationModels={simulationModels}
×
UNCOV
214
                      nodeLists={nodeLists}
×
215
                      npis={npis}
×
216
                    />
×
217
                  ))
×
218
                ) : (
219
                  <Box
×
220
                    sx={{
×
221
                      margin: 'auto',
×
222
                      display: 'flex',
×
223
                      alignItems: 'center',
×
224
                      justifyContent: 'center',
×
UNCOV
225
                      flexDirection: 'column',
×
226
                    }}
×
227
                  >
228
                    <WebAssetOff color='primary' fontSize='large' />
×
229
                    <Typography variant='body1'>{t('scenario-library.no-scenarios')}</Typography>
×
230
                  </Box>
×
231
                )}
232
              </Box>
×
UNCOV
233
            </Box>
×
234
          </ClickAwayListener>
×
235
        </Paper>
×
236
      </Popper>
×
237
    </Box>
×
238
  );
239
}
×
240

241
function LibraryCard(
×
242
  props: Readonly<{
×
243
    id: string;
244
    name: string;
245
    description: string;
246
    completeScenarios: Record<string, Scenario> | undefined;
247
    simulationModels: Models;
248
    nodeLists: NodeLists;
249
    npis: InterventionTemplates;
250
  }>
251
): JSX.Element {
×
252
  const dispatch = useAppDispatch();
×
253
  const theme = useTheme();
×
254
  const {t: tBackend, i18n} = useTranslation('backend');
×
255
  const scenarioColors = useAppSelector((state) => state.userPreference.scenarioColors);
×
UNCOV
256
  const [hover, setHover] = useState(false);
×
257

258
  // Add mouse event handlers to track hover state
259
  const handleMouseEnter = () => {
×
260
    setHover(true);
×
261
  };
×
262

263
  const handleMouseLeave = () => {
×
264
    setHover(false);
×
265
  };
×
266

267
  const handleRestore = () => {
×
268
    const savedColors = scenarioColors[props.id];
×
269
    dispatch(
×
UNCOV
270
      updateScenario({
×
271
        id: props.id,
×
272
        state: {
×
273
          visibility: ScenarioVisibility.FaceUp,
×
274
          colors: savedColors,
×
275
        },
×
276
      })
×
277
    );
×
278
  };
×
279

280
  // Get the complete scenario data from backend
281
  const backendScenario = props.completeScenarios?.[props.id];
×
282
  const model = props.simulationModels.find((model) => model.id === backendScenario?.modelId);
×
283
  const nodeList = props.nodeLists.find((nodeList) => nodeList.id === backendScenario?.nodeListId);
×
284
  const npiList = props.npis
×
UNCOV
285
    .filter((npi) =>
×
286
      backendScenario?.linkedInterventions.find((intervention) => intervention.interventionId === npi.id)
×
287
    )
×
288
    .map((npi) => tBackend(`interventions.${npi.name}`));
×
289

290
  return (
×
UNCOV
291
    <Box
×
292
      id={`card-root-${props.id}`}
×
293
      sx={{
×
294
        display: 'flex',
×
295
        flexDirection: 'row',
×
296
        color: theme.palette.divider,
×
297
        width: 'min-content',
×
298
      }}
×
299
      onMouseEnter={handleMouseEnter}
×
300
      onMouseLeave={handleMouseLeave}
×
301
    >
302
      <Box
×
303
        id='card-container'
×
304
        sx={{
×
305
          position: 'relative',
×
306
          zIndex: 0,
×
307
          flexGrow: 0,
×
UNCOV
308
          flexShrink: 0,
×
309
          width: '200px',
×
310
          boxSizing: 'border-box',
×
311
          marginX: '2px',
×
312
          marginY: theme.spacing(2),
×
313
          marginBottom: 0,
×
314
        }}
×
315
      >
316
        <Box
×
317
          id={`card-main-card-${props.id}`}
×
318
          sx={{
×
319
            position: 'relative',
×
320
            zIndex: 0,
×
321
            boxSizing: 'border-box',
×
322
            height: '244px',
×
UNCOV
323
            paddingTop: theme.spacing(2),
×
324
            paddingBottom: theme.spacing(2),
×
325
            border: `2px solid ${theme.palette.secondary.light}`,
×
326
            borderRadius: '3px',
×
327
            background: theme.palette.background.paper,
×
328
            color: theme.palette.secondary.main,
×
UNCOV
329
            cursor: 'pointer',
×
330

UNCOV
331
            '&:hover': {
×
332
              background: '#EEEEEEEE',
×
333
            },
×
334
          }}
×
335
          onClick={handleRestore}
×
336
        >
337
          <Box
×
UNCOV
338
            id={`card-front-${props.id}`}
×
UNCOV
339
            sx={{
×
UNCOV
340
              marginTop: '6px',
×
UNCOV
341
              marginBottom: '6px',
×
342
              height: '100%',
×
343
              display: 'flex',
×
344
              flexDirection: 'column',
×
345
            }}
×
346
          >
347
            <CardTitle
×
348
              color={theme.palette.secondary.light}
×
349
              label={
×
350
                i18n.exists(`scenario-names.${props.name}`, {ns: 'backend'})
×
351
                  ? tBackend(`scenario-names.${props.name}`)
×
352
                  : props.name
×
353
              }
354
            />
×
355
            <Box
×
UNCOV
356
              sx={{
×
357
                flexGrow: 1,
×
358
                display: 'flex',
×
359
                overflow: 'hidden',
×
360
              }}
×
361
            >
362
              {backendScenario && hover ? (
×
363
                <Box sx={{color: theme.palette.text.primary}}>
×
364
                  <ScenarioDescription
×
365
                    description={props.description}
×
366
                    startDate={new Date(backendScenario.startDate).toLocaleDateString(i18n.language)}
×
367
                    endDate={new Date(backendScenario.endDate).toLocaleDateString(i18n.language)}
×
368
                    model={tBackend(`models.${model?.name}`)}
×
369
                    nodeList={tBackend(`regions.${nodeList?.name}`)}
×
UNCOV
370
                    linkedInterventions={npiList}
×
371
                  />
×
372
                </Box>
×
373
              ) : (
374
                <Box
×
375
                  sx={{
×
376
                    fontWeight: 'bolder',
×
377
                    fontSize: '4rem',
×
378
                    color: theme.palette.secondary.light,
×
379
                    flexGrow: 1,
×
380
                    display: 'flex',
×
381
                    justifyContent: 'center',
×
382
                    alignItems: 'center',
×
383
                  }}
×
384
                  aria-label='+'
×
UNCOV
385
                >
×
386
                  +
387
                </Box>
×
388
              )}
389
            </Box>
×
390
          </Box>
×
UNCOV
391
        </Box>
×
392
      </Box>
×
393
    </Box>
×
394
  );
395
}
×
396

397
function NewScenarioCard({
×
398
  models,
×
399
  npis,
×
400
  nodeLists,
×
UNCOV
401
  scenarioCreated,
×
402
}: {
×
403
  models: Models;
404
  npis: InterventionTemplates;
405
  nodeLists: NodeLists;
406
  scenarioCreated: (data: NewScenarioData) => void;
407
}): JSX.Element {
×
408
  const theme = useTheme();
×
409
  const {t} = useTranslation();
×
410
  const [newScenarioDialogOpen, setNewScenarioDialogOpen] = useState(false);
×
411

412
  return (
×
413
    <Box
×
414
      id={`new-scenario-card-root`}
×
UNCOV
415
      sx={{
×
416
        display: 'flex',
×
417
        flexDirection: 'row',
×
418
        color: theme.palette.divider,
×
419
        width: 'min-content',
×
420
      }}
×
421
    >
422
      <Box
×
423
        id='new-scenario-card-container'
×
424
        sx={{
×
425
          position: 'relative',
×
426
          zIndex: 0,
×
427
          flexGrow: 0,
×
428
          flexShrink: 0,
×
429
          width: '200px',
×
430
          boxSizing: 'border-box',
×
431
          marginX: '2px',
×
432
          marginY: theme.spacing(2),
×
433
          marginBottom: 0,
×
434
        }}
×
435
      >
436
        <Box
×
437
          id={`new-scenario-card-main-card`}
×
438
          sx={{
×
439
            position: 'relative',
×
440
            zIndex: 0,
×
UNCOV
441
            boxSizing: 'border-box',
×
442
            height: '244px',
×
UNCOV
443
            paddingTop: theme.spacing(2),
×
UNCOV
444
            paddingBottom: theme.spacing(2),
×
UNCOV
445
            border: `2px solid ${theme.palette.primary.main}`,
×
UNCOV
446
            borderRadius: '3px',
×
UNCOV
447
            background: theme.palette.background.paper,
×
UNCOV
448
            color: theme.palette.primary.main,
×
UNCOV
449
            cursor: 'pointer',
×
450

UNCOV
451
            '&:hover': {
×
UNCOV
452
              background: '#EEEEEEEE',
×
UNCOV
453
            },
×
UNCOV
454
          }}
×
UNCOV
455
          onClick={() => setNewScenarioDialogOpen(true)}
×
456
        >
UNCOV
457
          <Box
×
UNCOV
458
            id={`new-scenario-card-front`}
×
UNCOV
459
            sx={{
×
UNCOV
460
              marginTop: '6px',
×
UNCOV
461
              marginBottom: '6px',
×
UNCOV
462
              height: '100%',
×
UNCOV
463
              display: 'flex',
×
UNCOV
464
              flexDirection: 'column',
×
UNCOV
465
            }}
×
466
          >
UNCOV
467
            <CardTitle label={t('scenario-library.new-scenario')} />
×
UNCOV
468
            <Box
×
UNCOV
469
              sx={{
×
UNCOV
470
                fontWeight: 'bolder',
×
UNCOV
471
                fontSize: '3rem',
×
UNCOV
472
                color: theme.palette.primary.main,
×
UNCOV
473
                textAlign: 'center',
×
UNCOV
474
                flexGrow: 1,
×
UNCOV
475
                display: 'flex',
×
UNCOV
476
                justifyContent: 'center',
×
UNCOV
477
                alignItems: 'center',
×
UNCOV
478
              }}
×
UNCOV
479
              aria-label={t('scenario-library.new-scenario') as unknown as string}
×
480
            >
UNCOV
481
              <LibraryAddOutlined color='primary' fontSize='large' />
×
UNCOV
482
            </Box>
×
UNCOV
483
          </Box>
×
UNCOV
484
        </Box>
×
UNCOV
485
      </Box>
×
UNCOV
486
      <Dialog open={newScenarioDialogOpen} maxWidth='sm' fullWidth={true}>
×
UNCOV
487
        <NewScenarioDialog
×
UNCOV
488
          models={[models[0]?.name ?? 'SECIRVV']}
×
UNCOV
489
          npiOptions={npis.map((npi) => {
×
UNCOV
490
            return {
×
UNCOV
491
              id: npi.id,
×
UNCOV
492
              name: npi.name,
×
UNCOV
493
            };
×
UNCOV
494
          })}
×
UNCOV
495
          nodeOptions={[nodeLists[0]?.name ?? 'Districts of Germany']}
×
UNCOV
496
          colorOptions={theme.custom.scenarios.slice(1)}
×
UNCOV
497
          onSubmit={(data) => {
×
UNCOV
498
            if (data) {
×
UNCOV
499
              scenarioCreated(data);
×
UNCOV
500
            }
×
UNCOV
501
            setNewScenarioDialogOpen(false);
×
UNCOV
502
          }}
×
UNCOV
503
        />
×
UNCOV
504
      </Dialog>
×
UNCOV
505
    </Box>
×
506
  );
UNCOV
507
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc