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

DLR-SC / ESID / 15379197426

01 Jun 2025 08:38PM UTC coverage: 50.694%. First build
15379197426

push

github

Kanakanajm
:tada: Hide create scenario button in case of non lha users

397 of 506 branches covered (78.46%)

Branch coverage included in aggregate %.

5 of 20 new or added lines in 2 files covered. (25.0%)

3767 of 7708 relevant lines covered (48.87%)

4.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';
×
NEW
27
import {AuthContext, IAuthContext} from 'react-oauth2-code-pkce';
×
28

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

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

NEW
40
  const {tokenData} = useContext<IAuthContext>(AuthContext);
×
41

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

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

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

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

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

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

103
  const anchorRef = useRef<HTMLButtonElement>(null);
×
104

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

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

116
    setOpen(false);
×
117
  };
×
118

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

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

228
function LibraryCard(props: Readonly<{id: string; name: string}>): JSX.Element {
×
229
  const dispatch = useAppDispatch();
×
230
  const theme = useTheme();
×
231
  const {t: tBackend, i18n} = useTranslation('backend');
×
232
  const scenarioColors = useAppSelector((state) => state.userPreference.scenarioColors);
×
233

234
  const handleRestore = () => {
×
235
    const savedColors = scenarioColors[props.id];
×
236
    dispatch(
×
237
      updateScenario({
×
238
        id: props.id,
×
239
        state: {
×
240
          visibility: ScenarioVisibility.FaceUp,
×
241
          colors: savedColors,
×
242
        },
×
243
      })
×
244
    );
×
245
  };
×
246

247
  return (
×
248
    <Box
×
249
      id={`card-root-${props.id}`}
×
250
      sx={{
×
251
        display: 'flex',
×
252
        flexDirection: 'row',
×
253
        color: theme.palette.divider,
×
254
        width: 'min-content',
×
255
      }}
×
256
    >
257
      <Box
×
258
        id='card-container'
×
259
        sx={{
×
260
          position: 'relative',
×
261
          zIndex: 0,
×
262
          flexGrow: 0,
×
263
          flexShrink: 0,
×
264
          width: '200px',
×
265
          boxSizing: 'border-box',
×
266
          marginX: '2px',
×
267
          marginY: theme.spacing(2),
×
268
          marginBottom: 0,
×
269
        }}
×
270
      >
271
        <Box
×
272
          id={`card-main-card-${props.id}`}
×
273
          sx={{
×
274
            position: 'relative',
×
275
            zIndex: 0,
×
276
            boxSizing: 'border-box',
×
277
            height: '244px',
×
278
            paddingTop: theme.spacing(2),
×
279
            paddingBottom: theme.spacing(2),
×
280
            border: `2px solid ${theme.palette.secondary.light}`,
×
281
            borderRadius: '3px',
×
282
            background: theme.palette.background.paper,
×
283
            color: theme.palette.secondary.main,
×
284
            cursor: 'pointer',
×
285

286
            '&:hover': {
×
287
              background: '#EEEEEEEE',
×
288
            },
×
289
          }}
×
290
          onClick={handleRestore}
×
291
        >
292
          <Box
×
293
            id={`card-front-${props.id}`}
×
294
            sx={{
×
295
              marginTop: '6px',
×
296
              marginBottom: '6px',
×
297
              height: '100%',
×
298
              display: 'flex',
×
299
              flexDirection: 'column',
×
300
            }}
×
301
          >
302
            <CardTitle
×
303
              color={theme.palette.secondary.light}
×
304
              label={
×
305
                i18n.exists(`scenario-names.${props.name}`, {ns: 'backend'})
×
306
                  ? tBackend(`scenario-names.${props.name}`)
×
307
                  : props.name
×
308
              }
309
            />
×
310
            <Box
×
311
              sx={{
×
312
                fontWeight: 'bolder',
×
313
                fontSize: '4rem',
×
314
                color: theme.palette.secondary.light,
×
315
                textAlign: 'center',
×
316
                flexGrow: 1,
×
317
                display: 'flex',
×
318
                justifyContent: 'center',
×
319
                alignItems: 'center',
×
320
              }}
×
321
              aria-label={'+'}
×
322
            >
×
323
              +
324
            </Box>
×
325
          </Box>
×
326
        </Box>
×
327
      </Box>
×
328
    </Box>
×
329
  );
330
}
×
331

332
function NewScenarioCard({
×
333
  models,
×
334
  npis,
×
335
  nodeLists,
×
336
  scenarioCreated,
×
337
}: {
×
338
  models: Models;
339
  npis: InterventionTemplates;
340
  nodeLists: NodeLists;
341
  scenarioCreated: (data: NewScenarioData) => void;
342
}): JSX.Element {
×
343
  const theme = useTheme();
×
344
  const {t} = useTranslation();
×
345
  const [newScenarioDialogOpen, setNewScenarioDialogOpen] = useState(false);
×
346

347
  return (
×
348
    <Box
×
349
      id={`new-scenario-card-root`}
×
350
      sx={{
×
351
        display: 'flex',
×
352
        flexDirection: 'row',
×
353
        color: theme.palette.divider,
×
354
        width: 'min-content',
×
355
      }}
×
356
    >
357
      <Box
×
358
        id='new-scenario-card-container'
×
359
        sx={{
×
360
          position: 'relative',
×
361
          zIndex: 0,
×
362
          flexGrow: 0,
×
363
          flexShrink: 0,
×
364
          width: '200px',
×
365
          boxSizing: 'border-box',
×
366
          marginX: '2px',
×
367
          marginY: theme.spacing(2),
×
368
          marginBottom: 0,
×
369
        }}
×
370
      >
371
        <Box
×
372
          id={`new-scenario-card-main-card`}
×
373
          sx={{
×
374
            position: 'relative',
×
375
            zIndex: 0,
×
376
            boxSizing: 'border-box',
×
377
            height: '244px',
×
378
            paddingTop: theme.spacing(2),
×
379
            paddingBottom: theme.spacing(2),
×
380
            border: `2px solid ${theme.palette.primary.main}`,
×
381
            borderRadius: '3px',
×
382
            background: theme.palette.background.paper,
×
383
            color: theme.palette.primary.main,
×
384
            cursor: 'pointer',
×
385

386
            '&:hover': {
×
387
              background: '#EEEEEEEE',
×
388
            },
×
389
          }}
×
390
          onClick={() => setNewScenarioDialogOpen(true)}
×
391
        >
392
          <Box
×
393
            id={`new-scenario-card-front`}
×
394
            sx={{
×
395
              marginTop: '6px',
×
396
              marginBottom: '6px',
×
397
              height: '100%',
×
398
              display: 'flex',
×
399
              flexDirection: 'column',
×
400
            }}
×
401
          >
402
            <CardTitle label={t('scenario-library.new-scenario')} />
×
403
            <Box
×
404
              sx={{
×
405
                fontWeight: 'bolder',
×
406
                fontSize: '3rem',
×
407
                color: theme.palette.primary.main,
×
408
                textAlign: 'center',
×
409
                flexGrow: 1,
×
410
                display: 'flex',
×
411
                justifyContent: 'center',
×
412
                alignItems: 'center',
×
413
              }}
×
414
              aria-label={t('scenario-library.new-scenario') as unknown as string}
×
415
            >
416
              <LibraryAddOutlined color='primary' fontSize='large' />
×
417
            </Box>
×
418
          </Box>
×
419
        </Box>
×
420
      </Box>
×
421
      <Dialog open={newScenarioDialogOpen} maxWidth='sm' fullWidth={true}>
×
422
        <NewScenarioDialog
×
423
          models={[models[0]?.name ?? 'SECIRVV']}
×
424
          npiOptions={npis.map((npi) => {
×
425
            return {
×
426
              id: npi.id,
×
427
              name: npi.name,
×
428
            };
×
429
          })}
×
430
          nodeOptions={[nodeLists[0]?.name ?? 'Districts of Germany']}
×
431
          colorOptions={theme.custom.scenarios.slice(1)}
×
432
          onSubmit={(data) => {
×
433
            if (data) {
×
434
              scenarioCreated(data);
×
435
            }
×
436
            setNewScenarioDialogOpen(false);
×
437
          }}
×
438
        />
×
439
      </Dialog>
×
440
    </Box>
×
441
  );
442
}
×
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