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

mendersoftware / gui / 1213056175

14 Mar 2024 09:12AM UTC coverage: 83.598% (-16.4%) from 99.964%
1213056175

Pull #4355

gitlab-ci

mineralsfree
fix: Change minimal increment to 1 day, previous units updated automatically

Ticket: MEN-6831
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #4355: MEN-6831: Change minimal increment to 1 day, previous units updated automatically

4439 of 6330 branches covered (70.13%)

3 of 5 new or added lines in 2 files covered. (60.0%)

1633 existing lines in 162 files now uncovered.

8410 of 10060 relevant lines covered (83.6%)

140.64 hits per line

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

94.33
/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, WarningAmber as WarningIcon } from '@mui/icons-material';
18
import {
19
  Box,
20
  Button,
21
  Checkbox,
22
  Dialog,
23
  DialogActions,
24
  DialogContent,
25
  DialogTitle,
26
  Divider,
27
  Drawer,
28
  FormControl,
29
  FormHelperText,
30
  IconButton,
31
  InputLabel,
32
  MenuItem,
33
  Select,
34
  TextField,
35
  Tooltip
36
} from '@mui/material';
37
import { makeStyles } from 'tss-react/mui';
38

39
import validator from 'validator';
40

41
import { ALL_DEVICES } from '../../constants/deviceConstants';
42
import { ALL_RELEASES } from '../../constants/releaseConstants';
43
import { emptyRole, emptyUiPermissions, itemUiPermissionsReducer, rolesById, uiPermissionsByArea, uiPermissionsById } from '../../constants/userConstants';
44
import { deepCompare, isEmpty, toggle } from '../../helpers';
45

46
const menuProps = {
5✔
47
  anchorOrigin: {
48
    vertical: 'bottom',
49
    horizontal: 'left'
50
  },
51
  transformOrigin: {
52
    vertical: 'top',
53
    horizontal: 'left'
54
  }
55
};
56

57
const permissionEnabledDisabled = (uiPermission, values, permissionsArea, unscoped) => {
5✔
58
  const { permissionLevel, value: permissionValue, unscopedOnly = {} } = uiPermission;
135✔
59
  const disabled = values.some(permission => uiPermissionsById[permission].permissionLevel > permissionLevel);
135✔
60
  const enabled = values.some(permission => permission === permissionValue) || disabled;
135✔
61
  const skip = unscopedOnly[permissionsArea] && !unscoped;
135!
62
  return { enabled, disabled, skip };
135✔
63
};
64

65
const useStyles = makeStyles()(theme => ({
24✔
66
  buttons: { '&.flexbox.centered': { justifyContent: 'flex-end' } },
67
  roleDeletion: { marginRight: theme.spacing(2) },
68
  permissionSelect: { marginLeft: theme.spacing(-1.5) },
69
  permissionsTitle: { marginBottom: theme.spacing(-1), minHeight: theme.spacing(3) }
70
}));
71

72
const PermissionsSelect = ({ disabled, label, onChange, options, permissionsArea, unscoped, values }) => {
5✔
73
  const { classes } = useStyles();
139✔
74

75
  const onInputChange = ({ target: { value } }) => {
139✔
76
    if (value.includes('')) {
2!
UNCOV
77
      return onChange([]);
×
78
    }
79
    return onChange(value);
2✔
80
  };
81

82
  const { editablePermissions, selectedValues } = useMemo(
139✔
83
    () =>
84
      options.reduce(
45✔
85
        (accu, uiPermission) => {
86
          const { enabled, disabled, skip } = permissionEnabledDisabled(uiPermission, values, permissionsArea, unscoped);
135✔
87
          if (skip) {
135!
UNCOV
88
            return accu;
×
89
          }
90
          accu.editablePermissions.push({ enabled, disabled, ...uiPermission });
135✔
91
          if (enabled) {
135✔
92
            accu.selectedValues.push(uiPermission.value);
10✔
93
          }
94
          return accu;
135✔
95
        },
96
        { editablePermissions: [], selectedValues: [] }
97
      ),
98
    [options, permissionsArea, unscoped, values]
99
  );
100

101
  return (
139✔
102
    <FormControl>
103
      <InputLabel id="permission-selection-label">{label && !values.length ? label : ''}</InputLabel>
330✔
104
      <Select
105
        labelId="permission-selection-label"
106
        disabled={disabled}
107
        displayEmpty={!label}
108
        fullWidth
109
        MenuProps={menuProps}
110
        multiple
111
        onChange={onInputChange}
112
        renderValue={() => (selectedValues.length ? selectedValues.map(value => uiPermissionsById[value].title).join(', ') : 'None')}
130✔
113
        value={values}
114
      >
115
        {editablePermissions.map(uiPermission => (
116
          <MenuItem disabled={uiPermission.disabled} key={uiPermission.value} value={uiPermission.value}>
434✔
117
            <Checkbox className={classes.permissionSelect} checked={uiPermission.enabled} disabled={uiPermission.disabled} />
118
            <div className={uiPermission.disabled ? 'text-muted' : ''}>{uiPermission.title}</div>
434✔
119
          </MenuItem>
120
        ))}
121
        <MenuItem value="">None</MenuItem>
122
      </Select>
123
    </FormControl>
124
  );
125
};
126

127
const PermissionsAreaTitle = ({ className = '', explanation, title }) => (
5✔
128
  <div className={`flexbox center-aligned margin-top ${className}`}>
114✔
129
    {title}
130
    <Tooltip arrow placement="bottom" title={explanation}>
131
      <InfoOutlinedIcon className="margin-left-small muted" fontSize="small" />
132
    </Tooltip>
133
  </div>
134
);
135

136
const PermissionsItem = ({ area, ...remainder }) => (
5✔
137
  <>
87✔
138
    <PermissionsAreaTitle title={area.title} explanation={area.explanation} />
139
    <PermissionsSelect options={area.uiPermissions} {...remainder} />
140
  </>
141
);
142

143
const groupsFilter = stateGroups =>
5✔
144
  Object.entries(stateGroups).reduce(
9✔
145
    (accu, [name, groupInfo]) => {
146
      if (!groupInfo.filters.length) {
18✔
147
        accu.push(name);
9✔
148
      }
149
      return accu;
18✔
150
    },
151
    [ALL_DEVICES]
152
  );
153

154
const releasesFilter = stateReleaseTags => [ALL_RELEASES, ...Object.keys(stateReleaseTags)];
9✔
155

156
const scopedPermissionAreas = {
5✔
157
  groups: {
158
    key: 'groups',
159
    filter: groupsFilter,
160
    placeholder: 'Search groups',
161
    excessiveAccessConfig: {
162
      selector: ALL_DEVICES,
163
      warning: `For 'All devices', users with the Manage permission may also create, edit and delete devices groups.`
164
    }
165
  },
166
  releases: {
167
    key: 'releases',
168
    filter: releasesFilter,
169
    placeholder: 'Search release tags',
170
    excessiveAccessConfig: {
171
      selector: ALL_RELEASES,
172
      warning: `For 'All releases', users with the Manage permission may also upload and delete releases.`
173
    }
174
  }
175
};
176

177
const maybeExtendPermissionSelection = (changedGroupSelection, currentGroup, items) => {
5✔
178
  if (items.every(item => changedGroupSelection.some(selectionItem => selectionItem.item === item))) {
8!
UNCOV
179
    return changedGroupSelection;
×
180
  }
181
  if (changedGroupSelection.every(selection => selection.item && selection.uiPermissions.length)) {
4✔
182
    changedGroupSelection.push(emptyItemSelection);
1✔
183
    return changedGroupSelection;
1✔
184
  }
185
  // the following is horrible, but I couldn't come up with a better solution that ensures only a single partly defined definition exists
186
  const filtered = changedGroupSelection.filter(selection => !((!selection.item || !selection.uiPermissions.length) && selection !== currentGroup));
4✔
187
  if (!filtered.some(selection => !selection.item || !selection.uiPermissions.length)) {
2✔
188
    filtered.push(emptyItemSelection);
1✔
189
  }
190
  return filtered;
2✔
191
};
192

193
const ItemSelection = ({
5✔
194
  disableEdit,
195
  items,
196
  itemsSelection,
197
  setter,
198
  permissionsArea,
199
  placeholder,
200
  excessiveAccessConfig: { selector: excessiveAccessSelector, warning: excessiveAccessWarning }
201
}) => {
202
  const onItemSelect = (index, { target: { value } }, currentSelection) => {
27✔
203
    let changedSelection = [...currentSelection];
1✔
204
    changedSelection[index] = { ...changedSelection[index], item: value };
1✔
205
    changedSelection = maybeExtendPermissionSelection(changedSelection, changedSelection[index], items);
1✔
206
    setter(changedSelection);
1✔
207
  };
208

209
  // find missing items in selected items
210
  itemsSelection.forEach((selection, key) => {
27✔
211
    if (selection.item && !items.includes(selection.item)) {
52✔
212
      items.push(selection.item);
1✔
213
      itemsSelection[key].notFound = true;
1✔
214
    }
215
  });
216

217
  const onItemPermissionSelect = (index, selectedPermissions, currentSelection) => {
27✔
218
    let changedSelection = [...currentSelection];
2✔
219
    changedSelection[index] = { ...changedSelection[index], uiPermissions: selectedPermissions };
2✔
220
    changedSelection = maybeExtendPermissionSelection(changedSelection, changedSelection[index], items);
2✔
221
    setter(changedSelection);
2✔
222
  };
223

224
  const isItemNotFound = item => !!itemsSelection.filter(selection => selection.item === item && selection.notFound === true).length;
702✔
225

226
  const { title, uiPermissions, explanation } = uiPermissionsByArea[permissionsArea];
27✔
227
  return (
27✔
228
    <>
229
      <PermissionsAreaTitle className="margin-left-small" explanation={explanation} title={title} />
230
      {itemsSelection.map((itemSelection, index) => {
231
        const disabled = disableEdit || itemSelection.disableEdit;
52✔
232
        return (
52✔
233
          <div className="flexbox center-aligned margin-left" key={`${itemSelection.item}-${index}`}>
234
            <div className="two-columns center-aligned" style={{ maxWidth: 500 }}>
235
              {disabled ? (
52!
236
                // empty label as a shortcut to align the layout with the select path
237
                <TextField disabled defaultValue={itemSelection.item} label=" " />
238
              ) : (
239
                <FormControl>
240
                  <InputLabel id="permissions-group-selection-label">{!itemSelection.item ? placeholder : ''}</InputLabel>
52✔
241
                  <Select labelId="permissions-group-selection-label" onChange={e => onItemSelect(index, e, itemsSelection)} value={itemSelection.item}>
1✔
242
                    {items.map(item => (
243
                      <MenuItem disabled={isItemNotFound(item)} key={item} value={item}>
110✔
244
                        <Box title={isItemNotFound(item) ? 'This item was removed' : ''} className="flexbox center-aligned">
110✔
245
                          {isItemNotFound(item) && <WarningIcon style={{ marginRight: 4 }} />}
116✔
246
                          {item}
247
                        </Box>
248
                      </MenuItem>
249
                    ))}
250
                  </Select>
251
                </FormControl>
252
              )}
253
              <PermissionsSelect
254
                disabled={disabled}
255
                label="Select"
256
                onChange={selectedPermissions => onItemPermissionSelect(index, selectedPermissions, itemsSelection)}
2✔
257
                options={uiPermissions}
258
                permissionsArea={permissionsArea}
259
                unscoped={itemSelection.item === excessiveAccessSelector}
260
                values={itemSelection.uiPermissions}
261
              />
262
            </div>
263
            {itemSelection.item === excessiveAccessSelector && (
59✔
264
              <div className="margin-left text-muted" style={{ alignSelf: 'flex-end' }}>
265
                {excessiveAccessWarning}
266
              </div>
267
            )}
268
          </div>
269
        );
270
      })}
271
    </>
272
  );
273
};
274

275
const emptyItemSelection = { item: '', uiPermissions: [], disableEdit: false };
5✔
276

277
const permissionMapper = uiPermission => uiPermissionsById[uiPermission].value;
7✔
278

279
const uiPermissionCompare = (existingPermissions, changedPermissions) => deepCompare(existingPermissions, changedPermissions);
5✔
280

281
const deriveItemsAndPermissions = (stateItems, roleItems, options = {}) => {
5!
282
  const { adding, disableEdit, filter } = options;
18✔
283
  let filteredStateItems = filter(stateItems);
18✔
284
  let itemSelections = Object.entries(roleItems).map(([group, permissions]) => ({
18✔
285
    ...emptyItemSelection,
286
    item: group,
287
    uiPermissions: permissions.map(permissionMapper)
288
  }));
289
  if (!adding && !itemSelections.length && filteredStateItems.length !== Object.keys(stateItems).length && !isEmpty(roleItems)) {
18!
UNCOV
290
    filteredStateItems = Object.keys(roleItems);
×
UNCOV
291
    itemSelections = Object.keys(roleItems).map(group => ({
×
292
      item: group,
293
      uiPermissions: [uiPermissionsById.read.value, uiPermissionsById.manage.value],
294
      disableEdit: true
295
    }));
296
  } else if (!disableEdit) {
18✔
297
    itemSelections.push(emptyItemSelection);
16✔
298
  }
299
  return { filtered: filteredStateItems, selections: itemSelections };
18✔
300
};
301

302
const permissionCompatibilityReducer = (accu, permission) => ({ [ALL_RELEASES]: [...accu[ALL_RELEASES], permission] });
5✔
303

304
const DeleteRoleDialog = ({ dismiss, open, submit, name }) => (
5✔
305
  <Dialog open={open}>
29✔
306
    <DialogTitle>Delete role?</DialogTitle>
307
    <DialogContent style={{ overflow: 'hidden' }}>
308
      Are you sure you want to delete the role{' '}
309
      <b>
310
        <i>{name}</i>
311
      </b>
312
      ?
313
    </DialogContent>
314
    <DialogActions>
315
      <Button style={{ marginRight: 10 }} onClick={dismiss}>
316
        Cancel
317
      </Button>
318
      <Button variant="contained" color="primary" onClick={submit}>
319
        Delete role
320
      </Button>
321
    </DialogActions>
322
  </Dialog>
323
);
324

325
export const RoleDefinition = ({
5✔
326
  adding,
327
  editing,
328
  features,
329
  stateGroups,
330
  stateReleaseTags,
331
  onCancel,
332
  onSubmit,
333
  removeRole,
334
  selectedRole = { ...emptyRole }
×
335
}) => {
336
  const [description, setDescription] = useState(selectedRole.description);
33✔
337
  const [groups, setGroups] = useState([]);
33✔
338
  const [releases, setReleases] = useState([]);
33✔
339
  const [name, setName] = useState(selectedRole.name);
33✔
340
  const [nameError, setNameError] = useState(false);
33✔
341
  const [auditlogPermissions, setAuditlogPermissions] = useState([]);
33✔
342
  const [groupSelections, setGroupSelections] = useState([]);
33✔
343
  const [releasesPermissions, setReleasesPermissions] = useState([]);
33✔
344
  const [releaseTagSelections, setReleaseTagSelections] = useState([]);
33✔
345
  const [removeDialog, setRemoveDialog] = useState(false);
33✔
346
  const [userManagementPermissions, setUserManagementPermissions] = useState([]);
33✔
347
  const { classes } = useStyles();
33✔
348
  const { hasReleaseTags } = features;
33✔
349

350
  useEffect(() => {
33✔
351
    const { name: roleName = '', description: roleDescription = '' } = selectedRole;
9!
352
    const { auditlog, groups: roleGroups = {}, releases: roleReleases = {}, userManagement } = { ...emptyUiPermissions, ...selectedRole.uiPermissions };
9!
353
    const disableEdit = editing && Boolean(rolesById[roleName] || !selectedRole.editable);
9✔
354
    setName(roleName);
9✔
355
    setDescription(roleDescription);
9✔
356
    setUserManagementPermissions(userManagement.map(permissionMapper));
9✔
357
    setAuditlogPermissions(auditlog.map(permissionMapper));
9✔
358
    const { filtered: filteredStateGroups, selections: groupSelections } = deriveItemsAndPermissions(stateGroups, roleGroups, {
9✔
359
      adding,
360
      disableEdit,
361
      filter: scopedPermissionAreas.groups.filter
362
    });
363
    setGroups(filteredStateGroups);
9✔
364
    setGroupSelections(groupSelections);
9✔
365
    const { filtered: filteredReleases, selections: releaseTagSelections } = deriveItemsAndPermissions(stateReleaseTags, roleReleases, {
9✔
366
      adding,
367
      disableEdit,
368
      filter: scopedPermissionAreas.releases.filter
369
    });
370
    setReleases(filteredReleases);
9✔
371
    setReleaseTagSelections(releaseTagSelections);
9✔
372
    setReleasesPermissions(
9✔
373
      releaseTagSelections.reduce((accu, { item, uiPermissions }) => {
374
        if (item === ALL_RELEASES) {
8!
UNCOV
375
          return [...accu, ...uiPermissions];
×
376
        }
377
        return accu;
8✔
378
      }, [])
379
    );
380
    // eslint-disable-next-line react-hooks/exhaustive-deps
381
  }, [adding, editing, JSON.stringify(selectedRole), JSON.stringify(stateGroups), JSON.stringify(stateReleaseTags)]);
382

383
  const validateNameChange = ({ target: { value } }) => {
33✔
UNCOV
384
    setNameError(!(value && validator.isWhitelisted(value, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-')));
×
UNCOV
385
    setName(value);
×
386
  };
387

388
  const onSubmitClick = () => {
33✔
389
    const allowUserManagement = userManagementPermissions.includes(uiPermissionsById.manage.value);
1✔
390
    const role = {
1✔
391
      source: selectedRole,
392
      allowUserManagement,
393
      description,
394
      name,
395
      uiPermissions: {
396
        auditlog: auditlogPermissions,
397
        groups: groupSelections,
398
        releases: hasReleaseTags ? releaseTagSelections : [{ item: ALL_RELEASES, uiPermissions: releasesPermissions }],
1!
399
        userManagement: userManagementPermissions
400
      }
401
    };
402
    onSubmit(role);
1✔
403
  };
404

405
  const onRemoveRole = () => {
33✔
406
    setRemoveDialog(false);
1✔
407
    removeRole(name);
1✔
408
    onCancel();
1✔
409
  };
410

411
  const onToggleRemoveDialog = () => setRemoveDialog(toggle);
33✔
412

413
  const disableEdit = editing && Boolean(rolesById[selectedRole.id] || !selectedRole.editable);
33✔
414
  const isSubmitDisabled = useMemo(() => {
33✔
415
    const changedPermissions = {
26✔
416
      ...emptyUiPermissions,
417
      auditlog: auditlogPermissions,
418
      userManagement: userManagementPermissions,
419
      groups: groupSelections.reduce(itemUiPermissionsReducer, {}),
420
      releases: hasReleaseTags
26!
421
        ? releaseTagSelections.reduce(itemUiPermissionsReducer, {})
422
        : releasesPermissions.reduce(permissionCompatibilityReducer, { [ALL_RELEASES]: [] })
423
    };
424
    const { hasPartiallyDefinedAreas, hasAreaPermissions } = [...groupSelections, ...releaseTagSelections].reduce(
26✔
425
      (accu, { item, uiPermissions }) => {
426
        accu.hasPartiallyDefinedAreas = accu.hasPartiallyDefinedAreas || (item && !uiPermissions.length) || (!item && uiPermissions.length);
60✔
427
        accu.hasAreaPermissions = accu.hasAreaPermissions || !!(item && uiPermissions.length);
60✔
428
        return accu;
60✔
429
      },
430
      { hasPartiallyDefinedAreas: false, hasAreaPermissions: false }
431
    );
432
    return Boolean(
26✔
433
      disableEdit ||
116✔
434
        !name ||
435
        nameError ||
436
        hasPartiallyDefinedAreas ||
437
        !(auditlogPermissions.length || hasAreaPermissions || releasesPermissions.length || userManagementPermissions.length) ||
30!
438
        (Object.entries({ description, name }).every(([key, value]) => selectedRole[key] === value) &&
19✔
439
          uiPermissionCompare(selectedRole.uiPermissions, changedPermissions))
440
    );
441
  }, [
442
    auditlogPermissions,
443
    userManagementPermissions,
444
    groupSelections,
445
    hasReleaseTags,
446
    releaseTagSelections,
447
    releasesPermissions,
448
    disableEdit,
449
    name,
450
    nameError,
451
    description,
452
    selectedRole
453
  ]);
454

455
  return (
33✔
456
    <Drawer anchor="right" open={adding || editing} PaperProps={{ style: { minWidth: 600, width: '50vw' } }}>
60✔
457
      <div className="flexbox margin-bottom-small space-between">
458
        <h3>{adding ? 'Add a' : 'Edit'} role</h3>
33✔
459
        <div className="flexbox center-aligned">
460
          {editing && !rolesById[selectedRole.id] && (
81✔
461
            <Button
462
              className={`flexbox center-aligned ${classes.roleDeletion}`}
463
              color="secondary"
464
              disabled={!!rolesById[selectedRole.id]}
465
              onClick={onToggleRemoveDialog}
466
            >
467
              delete role
468
            </Button>
469
          )}
470
          <IconButton onClick={onCancel} aria-label="close">
471
            <CloseIcon />
472
          </IconButton>
473
        </div>
474
      </div>
475
      <Divider />
476
      <div className="flexbox column" style={{ width: 500 }}>
477
        <FormControl>
478
          <TextField label="Name" id="role-name" value={name} disabled={disableEdit || editing} onChange={validateNameChange} />
64✔
479
          {nameError && <FormHelperText className="warning">Invalid character in role name. Valid characters are a-z, A-Z, 0-9, _ and -</FormHelperText>}
33!
480
        </FormControl>
481
        <TextField
482
          disabled={disableEdit}
483
          label="Description"
484
          id="role-description"
485
          multiline
486
          placeholder="-"
487
          onChange={e => setDescription(e.target.value)}
9✔
488
          value={description}
489
        />
490
      </div>
491

492
      <InputLabel className={`margin-top ${classes.permissionsTitle}`} shrink>
493
        Permissions
494
      </InputLabel>
495
      <div className="two-columns center-aligned margin-left-small" style={{ maxWidth: 500 }}>
496
        <PermissionsItem
497
          area={uiPermissionsByArea.userManagement}
498
          disabled={disableEdit}
499
          onChange={setUserManagementPermissions}
500
          values={userManagementPermissions}
501
        />
502
        <PermissionsItem disabled={disableEdit} area={uiPermissionsByArea.auditlog} onChange={setAuditlogPermissions} values={auditlogPermissions} />
503
        {!hasReleaseTags && (
66✔
504
          <PermissionsItem disabled={disableEdit} area={uiPermissionsByArea.releases} onChange={setReleasesPermissions} values={releasesPermissions} />
505
        )}
506
      </div>
507
      {(!disableEdit || !!releaseTagSelections.length) && hasReleaseTags && (
66!
508
        <ItemSelection
509
          disableEdit={disableEdit}
510
          excessiveAccessConfig={scopedPermissionAreas.releases.excessiveAccessConfig}
511
          items={releases}
512
          itemsSelection={releaseTagSelections}
513
          permissionsArea={scopedPermissionAreas.releases.key}
514
          placeholder={scopedPermissionAreas.releases.placeholder}
515
          setter={setReleaseTagSelections}
516
        />
517
      )}
518
      {(!disableEdit || !!groupSelections.length) && (
66✔
519
        <ItemSelection
520
          disableEdit={disableEdit}
521
          excessiveAccessConfig={scopedPermissionAreas.groups.excessiveAccessConfig}
522
          items={groups}
523
          itemsSelection={groupSelections}
524
          permissionsArea={scopedPermissionAreas.groups.key}
525
          placeholder={scopedPermissionAreas.groups.placeholder}
526
          setter={setGroupSelections}
527
        />
528
      )}
529
      <Divider className="margin-top-large" light />
530
      <div className={`flexbox centered margin-top ${classes.buttons}`}>
531
        <Button className="margin-right" onClick={onCancel}>
532
          Cancel
533
        </Button>
534
        <Button color="secondary" variant="contained" target="_blank" disabled={isSubmitDisabled} onClick={onSubmitClick}>
535
          Submit
536
        </Button>
537
      </div>
538
      <DeleteRoleDialog dismiss={onToggleRemoveDialog} open={removeDialog} submit={onRemoveRole} name={name} />
539
    </Drawer>
540
  );
541
};
542

543
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