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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

88.52
/src/js/components/settings/roledefinition.js
1
// Copyright 2020 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import React, { useEffect, useMemo, useState } from 'react';
15

16
// material ui
17
import { Close as CloseIcon, InfoOutlined as InfoOutlinedIcon } from '@mui/icons-material';
18
import { Button, Checkbox, Divider, Drawer, FormControl, FormHelperText, IconButton, InputLabel, MenuItem, Select, TextField, Tooltip } from '@mui/material';
19
import { makeStyles } from 'tss-react/mui';
20

21
import validator from 'validator';
22

23
import { ALL_DEVICES } from '../../constants/deviceConstants';
24
import { ALL_RELEASES } from '../../constants/releaseConstants';
25
import { emptyRole, emptyUiPermissions, itemUiPermissionsReducer, rolesById, uiPermissionsByArea, uiPermissionsById } from '../../constants/userConstants';
26
import { deepCompare, isEmpty } from '../../helpers';
27

28
const menuProps = {
6✔
29
  anchorOrigin: {
30
    vertical: 'bottom',
31
    horizontal: 'left'
32
  },
33
  transformOrigin: {
34
    vertical: 'top',
35
    horizontal: 'left'
36
  }
37
};
38

39
const permissionEnabledDisabled = (uiPermission, values, permissionsArea, unscoped) => {
6✔
40
  const { permissionLevel, value: permissionValue, unscopedOnly = {} } = uiPermission;
75✔
41
  const disabled = values.some(permission => uiPermissionsById[permission].permissionLevel > permissionLevel);
75✔
42
  const enabled = values.some(permission => permission === permissionValue) || disabled;
75✔
43
  const skip = unscopedOnly[permissionsArea] && !unscoped;
75!
44
  return { enabled, disabled, skip };
75✔
45
};
46

47
const useStyles = makeStyles()(theme => ({
14✔
48
  buttons: { '&.flexbox.centered': { justifyContent: 'flex-end' } },
49
  roleDeletion: { marginRight: theme.spacing(2) },
50
  permissionSelect: { marginLeft: theme.spacing(-1.5) },
51
  permissionsTitle: { marginBottom: theme.spacing(-1), minHeight: theme.spacing(3) }
52
}));
53

54
const PermissionsSelect = ({ disabled, label, onChange, options, permissionsArea, unscoped, values }) => {
6✔
55
  const { classes } = useStyles();
107✔
56

57
  const onInputChange = ({ target: { value } }) => {
107✔
58
    if (value.includes('')) {
2!
59
      return onChange([]);
×
60
    }
61
    return onChange(value);
2✔
62
  };
63

64
  const { editablePermissions, selectedValues } = useMemo(
107✔
65
    () =>
66
      options.reduce(
24✔
67
        (accu, uiPermission) => {
68
          const { enabled, disabled, skip } = permissionEnabledDisabled(uiPermission, values, permissionsArea, unscoped);
75✔
69
          if (skip) {
75!
70
            return accu;
×
71
          }
72
          accu.editablePermissions.push({ enabled, disabled, ...uiPermission });
75✔
73
          if (enabled) {
75✔
74
            accu.selectedValues.push(uiPermission.value);
7✔
75
          }
76
          return accu;
75✔
77
        },
78
        { editablePermissions: [], selectedValues: [] }
79
      ),
80
    [options, permissionsArea, unscoped, values]
81
  );
82

83
  return (
107✔
84
    <FormControl>
85
      <InputLabel id="permission-selection-label">{label && !values.length ? label : ''}</InputLabel>
255✔
86
      <Select
87
        labelId="permission-selection-label"
88
        disabled={disabled}
89
        displayEmpty={!label}
90
        fullWidth
91
        MenuProps={menuProps}
92
        multiple
93
        onChange={onInputChange}
94
        renderValue={() => (selectedValues.length ? selectedValues.map(value => uiPermissionsById[value].title).join(', ') : 'None')}
97✔
95
        value={values}
96
      >
97
        {editablePermissions.map(uiPermission => (
98
          <MenuItem disabled={uiPermission.disabled} key={uiPermission.value} value={uiPermission.value}>
337✔
99
            <Checkbox className={classes.permissionSelect} checked={uiPermission.enabled} disabled={uiPermission.disabled} />
100
            <div className={uiPermission.disabled ? 'text-muted' : ''}>{uiPermission.title}</div>
337✔
101
          </MenuItem>
102
        ))}
103
        <MenuItem value="">None</MenuItem>
104
      </Select>
105
    </FormControl>
106
  );
107
};
108

109
const PermissionsAreaTitle = ({ className = '', explanation, title }) => (
6✔
110
  <div className={`flexbox center-aligned margin-top ${className}`}>
87✔
111
    {title}
112
    <Tooltip arrow placement="bottom" title={explanation}>
113
      <InfoOutlinedIcon className="margin-left-small muted" fontSize="small" />
114
    </Tooltip>
115
  </div>
116
);
117

118
const PermissionsItem = ({ area, ...remainder }) => (
6✔
119
  <>
66✔
120
    <PermissionsAreaTitle title={area.title} explanation={area.explanation} />
121
    <PermissionsSelect options={area.uiPermissions} {...remainder} />
122
  </>
123
);
124

125
const groupsFilter = stateGroups =>
6✔
126
  Object.entries(stateGroups).reduce(
7✔
127
    (accu, [name, groupInfo]) => {
128
      if (!groupInfo.filters.length) {
14✔
129
        accu.push(name);
7✔
130
      }
131
      return accu;
14✔
132
    },
133
    [ALL_DEVICES]
134
  );
135

136
const releasesFilter = stateReleaseTags => [ALL_RELEASES, ...Object.keys(stateReleaseTags)];
7✔
137

138
const scopedPermissionAreas = {
6✔
139
  groups: {
140
    key: 'groups',
141
    filter: groupsFilter,
142
    placeholder: 'Search groups',
143
    excessiveAccessConfig: {
144
      selector: ALL_DEVICES,
145
      warning: `For 'All devices', users with the Manage permission may also create, edit and delete devices groups.`
146
    }
147
  },
148
  releases: {
149
    key: 'releases',
150
    filter: releasesFilter,
151
    placeholder: 'Search release tags',
152
    excessiveAccessConfig: {
153
      selector: ALL_RELEASES,
154
      warning: `For 'All releases', users with the Manage permission may also upload and delete releases.`
155
    }
156
  }
157
};
158

159
const maybeExtendPermissionSelection = (changedGroupSelection, currentGroup, items) => {
6✔
160
  if (items.every(item => changedGroupSelection.some(selectionItem => selectionItem.item === item))) {
8!
161
    return changedGroupSelection;
×
162
  }
163
  if (changedGroupSelection.every(selection => selection.item && selection.uiPermissions.length)) {
4✔
164
    changedGroupSelection.push(emptyItemSelection);
1✔
165
    return changedGroupSelection;
1✔
166
  }
167
  // the following is horrible, but I couldn't come up with a better solution that ensures only a single partly defined definition exists
168
  const filtered = changedGroupSelection.filter(selection => !((!selection.item || !selection.uiPermissions.length) && selection !== currentGroup));
4✔
169
  if (!filtered.some(selection => !selection.item || !selection.uiPermissions.length)) {
2✔
170
    filtered.push(emptyItemSelection);
1✔
171
  }
172
  return filtered;
2✔
173
};
174

175
const ItemSelection = ({
6✔
176
  disableEdit,
177
  items,
178
  itemsSelection,
179
  setter,
180
  permissionsArea,
181
  placeholder,
182
  excessiveAccessConfig: { selector: excessiveAccessSelector, warning: excessiveAccessWarning }
183
}) => {
184
  const onItemSelect = (index, { target: { value } }, currentSelection) => {
21✔
185
    let changedSelection = [...currentSelection];
1✔
186
    changedSelection[index] = { ...changedSelection[index], item: value };
1✔
187
    changedSelection = maybeExtendPermissionSelection(changedSelection, changedSelection[index], items);
1✔
188
    setter(changedSelection);
1✔
189
  };
190

191
  const onItemPermissionSelect = (index, selectedPermissions, currentSelection) => {
21✔
192
    let changedSelection = [...currentSelection];
2✔
193
    changedSelection[index] = { ...changedSelection[index], uiPermissions: selectedPermissions };
2✔
194
    changedSelection = maybeExtendPermissionSelection(changedSelection, changedSelection[index], items);
2✔
195
    setter(changedSelection);
2✔
196
  };
197

198
  const { title, uiPermissions, explanation } = uiPermissionsByArea[permissionsArea];
21✔
199
  return (
21✔
200
    <>
201
      <PermissionsAreaTitle className="margin-left-small" explanation={explanation} title={title} />
202
      {itemsSelection.map((itemSelection, index) => {
203
        const disabled = disableEdit || itemSelection.disableEdit;
41✔
204
        return (
41✔
205
          <div className="flexbox center-aligned margin-left" key={`${itemSelection.item}-${index}`}>
206
            <div className="two-columns center-aligned" style={{ maxWidth: 500 }}>
207
              {disabled ? (
41!
208
                // empty label as a shortcut to align the layout with the select path
209
                <TextField disabled defaultValue={itemSelection.item} label=" " />
210
              ) : (
211
                <FormControl>
212
                  <InputLabel id="permissions-group-selection-label">{!itemSelection.item ? placeholder : ''}</InputLabel>
41✔
213
                  <Select labelId="permissions-group-selection-label" onChange={e => onItemSelect(index, e, itemsSelection)} value={itemSelection.item}>
1✔
214
                    {items.map(item => (
215
                      <MenuItem key={item} value={item}>
82✔
216
                        {item}
217
                      </MenuItem>
218
                    ))}
219
                  </Select>
220
                </FormControl>
221
              )}
222
              <PermissionsSelect
223
                disabled={disabled}
224
                label="Select"
225
                onChange={selectedPermissions => onItemPermissionSelect(index, selectedPermissions, itemsSelection)}
2✔
226
                options={uiPermissions}
227
                permissionsArea={permissionsArea}
228
                unscoped={itemSelection.item === excessiveAccessSelector}
229
                values={itemSelection.uiPermissions}
230
              />
231
            </div>
232
            {itemSelection.item === excessiveAccessSelector && (
47✔
233
              <div className="margin-left text-muted" style={{ alignSelf: 'flex-end' }}>
234
                {excessiveAccessWarning}
235
              </div>
236
            )}
237
          </div>
238
        );
239
      })}
240
    </>
241
  );
242
};
243

244
const emptyItemSelection = { item: '', uiPermissions: [], disableEdit: false };
6✔
245

246
const permissionMapper = uiPermission => uiPermissionsById[uiPermission].value;
6✔
247

248
const uiPermissionCompare = (existingPermissions, changedPermissions) => deepCompare(existingPermissions, changedPermissions);
6✔
249

250
const deriveItemsAndPermissions = (stateItems, roleItems, options = {}) => {
6!
251
  const { adding, disableEdit, filter } = options;
14✔
252
  let filteredStateItems = filter(stateItems);
14✔
253
  let itemSelections = Object.entries(roleItems).map(([group, permissions]) => ({
14✔
254
    ...emptyItemSelection,
255
    item: group,
256
    uiPermissions: permissions.map(permissionMapper)
257
  }));
258
  if (!adding && !itemSelections.length && filteredStateItems.length !== Object.keys(stateItems).length && !isEmpty(roleItems)) {
14!
259
    filteredStateItems = Object.keys(roleItems);
×
260
    itemSelections = Object.keys(roleItems).map(group => ({
×
261
      item: group,
262
      uiPermissions: [uiPermissionsById.read.value, uiPermissionsById.manage.value],
263
      disableEdit: true
264
    }));
265
  } else if (!disableEdit) {
14✔
266
    itemSelections.push(emptyItemSelection);
12✔
267
  }
268
  return { filtered: filteredStateItems, selections: itemSelections };
14✔
269
};
270

271
const permissionCompatibilityReducer = (accu, permission) => ({ [ALL_RELEASES]: [...accu[ALL_RELEASES], permission] });
6✔
272

273
export const RoleDefinition = ({
6✔
274
  adding,
275
  editing,
276
  features,
277
  stateGroups,
278
  stateReleaseTags,
279
  onCancel,
280
  onSubmit,
281
  removeRole,
282
  selectedRole = { ...emptyRole }
×
283
}) => {
284
  const [description, setDescription] = useState(selectedRole.description);
28✔
285
  const [groups, setGroups] = useState([]);
28✔
286
  const [releases, setReleases] = useState([]);
28✔
287
  const [name, setName] = useState(selectedRole.name);
28✔
288
  const [nameError, setNameError] = useState(false);
28✔
289
  const [auditlogPermissions, setAuditlogPermissions] = useState([]);
28✔
290
  const [groupSelections, setGroupSelections] = useState([]);
28✔
291
  const [releasesPermissions, setReleasesPermissions] = useState([]);
28✔
292
  const [releaseTagSelections, setReleaseTagSelections] = useState([]);
28✔
293
  const [userManagementPermissions, setUserManagementPermissions] = useState([]);
28✔
294
  const { classes } = useStyles();
28✔
295
  const { hasReleaseTags } = features;
28✔
296

297
  useEffect(() => {
28✔
298
    const { name: roleName = '', description: roleDescription = '' } = selectedRole;
7!
299
    const { auditlog, groups: roleGroups = {}, releases: roleReleases = {}, userManagement } = { ...emptyUiPermissions, ...selectedRole.uiPermissions };
7!
300
    const disableEdit = editing && Boolean(rolesById[roleName] || !selectedRole.editable);
7✔
301
    setName(roleName);
7✔
302
    setDescription(roleDescription);
7✔
303
    setUserManagementPermissions(userManagement.map(permissionMapper));
7✔
304
    setAuditlogPermissions(auditlog.map(permissionMapper));
7✔
305
    const { filtered: filteredStateGroups, selections: groupSelections } = deriveItemsAndPermissions(stateGroups, roleGroups, {
7✔
306
      adding,
307
      disableEdit,
308
      filter: scopedPermissionAreas.groups.filter
309
    });
310
    setGroups(filteredStateGroups);
7✔
311
    setGroupSelections(groupSelections);
7✔
312
    const { filtered: filteredReleases, selections: releaseTagSelections } = deriveItemsAndPermissions(stateReleaseTags, roleReleases, {
7✔
313
      adding,
314
      disableEdit,
315
      filter: scopedPermissionAreas.releases.filter
316
    });
317
    setReleases(filteredReleases);
7✔
318
    setReleaseTagSelections(releaseTagSelections);
7✔
319
    setReleasesPermissions(
7✔
320
      releaseTagSelections.reduce((accu, { item, uiPermissions }) => {
321
        if (item === ALL_RELEASES) {
6!
322
          return [...accu, ...uiPermissions];
×
323
        }
324
        return accu;
6✔
325
      }, [])
326
    );
327
  }, [adding, editing, selectedRole, stateGroups, stateReleaseTags]);
328

329
  const validateNameChange = ({ target: { value } }) => {
28✔
330
    setNameError(!(value && validator.isWhitelisted(value, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-')));
×
331
    setName(value);
×
332
  };
333

334
  const onSubmitClick = () => {
28✔
335
    const allowUserManagement = userManagementPermissions.includes(uiPermissionsById.manage.value);
1✔
336
    const role = {
1✔
337
      source: selectedRole,
338
      allowUserManagement,
339
      description,
340
      name,
341
      uiPermissions: {
342
        auditlog: auditlogPermissions,
343
        groups: groupSelections,
344
        releases: hasReleaseTags ? releaseTagSelections : [{ item: ALL_RELEASES, uiPermissions: releasesPermissions }],
1!
345
        userManagement: userManagementPermissions
346
      }
347
    };
348
    onSubmit(role);
1✔
349
  };
350

351
  const onRemoveRole = () => {
28✔
352
    removeRole(name);
1✔
353
    onCancel();
1✔
354
  };
355

356
  const disableEdit = editing && Boolean(rolesById[selectedRole.id] || !selectedRole.editable);
28✔
357
  const isSubmitDisabled = useMemo(() => {
28✔
358
    const changedPermissions = {
22✔
359
      ...emptyUiPermissions,
360
      auditlog: auditlogPermissions,
361
      userManagement: userManagementPermissions,
362
      groups: groupSelections.reduce(itemUiPermissionsReducer, {}),
363
      releases: hasReleaseTags
22!
364
        ? releaseTagSelections.reduce(itemUiPermissionsReducer, {})
365
        : releasesPermissions.reduce(permissionCompatibilityReducer, { [ALL_RELEASES]: [] })
366
    };
367
    const { hasPartiallyDefinedAreas, hasAreaPermissions } = [...groupSelections, ...releaseTagSelections].reduce(
22✔
368
      (accu, { item, uiPermissions }) => {
369
        accu.hasPartiallyDefinedAreas = accu.hasPartiallyDefinedAreas || (item && !uiPermissions.length) || (!item && uiPermissions.length);
51✔
370
        accu.hasAreaPermissions = accu.hasAreaPermissions || !!(item && uiPermissions.length);
51✔
371
        return accu;
51✔
372
      },
373
      { hasPartiallyDefinedAreas: false, hasAreaPermissions: false }
374
    );
375
    return Boolean(
22✔
376
      disableEdit ||
108✔
377
        !name ||
378
        nameError ||
379
        hasPartiallyDefinedAreas ||
380
        !(auditlogPermissions.length || hasAreaPermissions || releasesPermissions.length || userManagementPermissions.length) ||
30!
381
        (Object.entries({ description, name }).every(([key, value]) => selectedRole[key] === value) &&
19✔
382
          uiPermissionCompare(selectedRole.uiPermissions, changedPermissions))
383
    );
384
  }, [auditlogPermissions, description, disableEdit, groupSelections, name, nameError, releasesPermissions, releaseTagSelections, userManagementPermissions]);
385

386
  return (
28✔
387
    <Drawer anchor="right" open={adding || editing} PaperProps={{ style: { minWidth: 600, width: '50vw' } }}>
54✔
388
      <div className="flexbox margin-bottom-small space-between">
389
        <h3>{adding ? 'Add a' : 'Edit'} role</h3>
28✔
390
        <div className="flexbox center-aligned">
391
          {editing && !rolesById[selectedRole.id] && (
68✔
392
            <Button
393
              className={`flexbox center-aligned ${classes.roleDeletion}`}
394
              color="secondary"
395
              disabled={!!rolesById[selectedRole.id]}
396
              onClick={onRemoveRole}
397
            >
398
              delete role
399
            </Button>
400
          )}
401
          <IconButton onClick={onCancel} aria-label="close">
402
            <CloseIcon />
403
          </IconButton>
404
        </div>
405
      </div>
406
      <Divider />
407
      <div className="flexbox column" style={{ width: 500 }}>
408
        <FormControl>
409
          <TextField label="Name" id="role-name" value={name} disabled={disableEdit || editing} onChange={validateNameChange} />
54✔
410
          {nameError && <FormHelperText className="warning">Invalid character in role name. Valid characters are a-z, A-Z, 0-9, _ and -</FormHelperText>}
28!
411
        </FormControl>
412
        <TextField
413
          disabled={disableEdit}
414
          label="Description"
415
          id="role-description"
416
          multiline
417
          placeholder="-"
418
          onChange={e => setDescription(e.target.value)}
9✔
419
          value={description}
420
        />
421
      </div>
422

423
      <InputLabel className={`margin-top ${classes.permissionsTitle}`} shrink>
424
        Permissions
425
      </InputLabel>
426
      <div className="two-columns center-aligned margin-left-small" style={{ maxWidth: 500 }}>
427
        <PermissionsItem
428
          area={uiPermissionsByArea.userManagement}
429
          disabled={disableEdit}
430
          onChange={setUserManagementPermissions}
431
          values={userManagementPermissions}
432
        />
433
        <PermissionsItem disabled={disableEdit} area={uiPermissionsByArea.auditlog} onChange={setAuditlogPermissions} values={auditlogPermissions} />
434
        {!hasReleaseTags && (
56✔
435
          <PermissionsItem disabled={disableEdit} area={uiPermissionsByArea.releases} onChange={setReleasesPermissions} values={releasesPermissions} />
436
        )}
437
      </div>
438
      {(!disableEdit || !!releaseTagSelections.length) && hasReleaseTags && (
56!
439
        <ItemSelection
440
          disableEdit={disableEdit}
441
          excessiveAccessConfig={scopedPermissionAreas.releases.excessiveAccessConfig}
442
          items={releases}
443
          itemsSelection={releaseTagSelections}
444
          permissionsArea={scopedPermissionAreas.releases.key}
445
          placeholder={scopedPermissionAreas.releases.placeholder}
446
          setter={setReleaseTagSelections}
447
        />
448
      )}
449
      {(!disableEdit || !!groupSelections.length) && (
56✔
450
        <ItemSelection
451
          disableEdit={disableEdit}
452
          excessiveAccessConfig={scopedPermissionAreas.groups.excessiveAccessConfig}
453
          items={groups}
454
          itemsSelection={groupSelections}
455
          permissionsArea={scopedPermissionAreas.groups.key}
456
          placeholder={scopedPermissionAreas.groups.placeholder}
457
          setter={setGroupSelections}
458
        />
459
      )}
460
      <Divider className="margin-top-large" light />
461
      <div className={`flexbox centered margin-top ${classes.buttons}`}>
462
        <Button className="margin-right" onClick={onCancel}>
463
          Cancel
464
        </Button>
465
        <Button color="secondary" variant="contained" target="_blank" disabled={isSubmitDisabled} onClick={onSubmitClick}>
466
          Submit
467
        </Button>
468
      </div>
469
    </Drawer>
470
  );
471
};
472

473
export default RoleDefinition;
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

© 2025 Coveralls, Inc