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

mendersoftware / mender-server / 1539946168

13 Nov 2024 09:11AM UTC coverage: 72.77% (+0.03%) from 72.743%
1539946168

push

gitlab-ci

web-flow
Merge pull request #191 from mzedel/MEN-7570

MEN-7570 - SP RBAC + ensured linter runs on all TS files

4170 of 6064 branches covered (68.77%)

Branch coverage included in aggregate %.

182 of 199 new or added lines in 18 files covered. (91.46%)

2 existing lines in 1 file now uncovered.

42469 of 58027 relevant lines covered (73.19%)

16.79 hits per line

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

87.67
/frontend/src/js/components/settings/role-management/PermissionsItems.tsx
1
// Copyright 2024 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 { FunctionComponent, useCallback } from 'react';
15
import { Controller, FieldValues, UseFormSetValue, useFieldArray, useFormContext } from 'react-hook-form';
16

17
import { InfoOutlined as InfoOutlinedIcon, WarningAmber as WarningIcon } from '@mui/icons-material';
18
import { FormControl, InputLabel, MenuItem, Select, TextField, Tooltip } from '@mui/material';
19

20
import { PermissionsArea, UiPermission, uiPermissionsByArea } from '@northern.tech/store/constants';
21

22
import { PermissionsSelect, PermissionsSelectionBaseProps } from './PermissionsSelect';
23

24
export type ScopedUiPermissions = {
25
  item: string;
26
  uiPermissions: UiPermission[];
27
};
28

29
export type ItemSelectionType = ScopedUiPermissions & {
30
  disableEdit: boolean;
31
  notFound: boolean;
32
};
33

34
export type ItemScope = {
35
  title: string;
36
  notFound: boolean;
37
};
38

39
export const emptyItemSelection: ItemSelectionType = { item: '', uiPermissions: [], disableEdit: false, notFound: false };
5✔
40

41
const PermissionsAreaTitle: FunctionComponent<{ className?: string; explanation: string; title: string }> = ({ className = '', explanation, title }) => (
5✔
42
  <div className={`flexbox center-aligned margin-top ${className}`}>
288✔
43
    {title}
44
    <Tooltip arrow placement="bottom" title={explanation}>
45
      <InfoOutlinedIcon className="margin-left-small muted" fontSize="small" />
46
    </Tooltip>
47
  </div>
48
);
49

50
interface IPermissionsItem extends PermissionsSelectionBaseProps {
51
  area: PermissionsArea;
52
}
53

54
export const PermissionsItem: FunctionComponent<IPermissionsItem> = ({ area, disabled }) => (
5✔
55
  <div className="two-columns center-aligned margin-left-small" style={{ maxWidth: 500 }}>
144✔
56
    <PermissionsAreaTitle title={area.title} explanation={area.explanation} />
57
    <PermissionsSelect disabled={disabled} options={area.uiPermissions} permissionsArea={area} />
58
  </div>
59
);
60

61
const shouldExtendPermissionSelection = (changedSelection, currentItem, items) => {
5✔
62
  if (items.every(({ title }) => changedSelection.some(selectionItem => selectionItem.item === title))) {
24!
NEW
63
    return false;
×
64
  }
65
  if (changedSelection.every(selection => selection.item && selection.uiPermissions.length)) {
14!
NEW
66
    return true;
×
67
  }
68
  // the following is horrible, but I couldn't come up with a better solution that ensures only a single partly defined definition exists
69
  const filtered = changedSelection.filter(selection => {
8✔
70
    const isDifferentThanCurrent = selection !== currentItem;
16✔
71
    const isPartiallyDefined = selection.item || selection.uiPermissions.length;
16✔
72
    return isPartiallyDefined && isDifferentThanCurrent;
16✔
73
  });
74
  return filtered.length === 1;
8✔
75
};
76

77
interface IScopedPermissionSelect extends PermissionsSelectionBaseProps {
78
  index: number;
79
  permissionsArea: PermissionsArea;
80
  options: ItemScope[];
81
  itemSelection: ItemSelectionType;
82
  name: string;
83
  onChange: (index: number, change: { attribute: string; [change: string]: string }) => void;
84
}
85

86
const ScopeSelect: FunctionComponent<IScopedPermissionSelect> = ({ disabled, permissionsArea, index, options, itemSelection, name = '', onChange }) => {
5!
87
  const { control } = useFormContext();
266✔
88
  const { key, placeholder } = permissionsArea;
266✔
89
  return disabled ? (
266!
90
    // empty label as a shortcut to align the layout with the select path
91
    <TextField disabled defaultValue={itemSelection.item} label=" " />
92
  ) : (
93
    <FormControl>
94
      <InputLabel id={`${key}-scope-selection-select-label`}>{!itemSelection.item ? placeholder : ''}</InputLabel>
266✔
95
      <Controller
96
        name={name || `${key}.${index}.item`}
266!
97
        control={control}
98
        render={({ field }) => (
99
          <Select labelId={`${key}-scope-selection-select-label`} disabled={disabled} {...field} onChange={({ target: { value } }) => onChange(value)}>
266✔
100
            {options.map(option => (
101
              <MenuItem disabled={option.notFound} key={option.title} value={option.title}>
653✔
102
                <div title={option.notFound ? 'This item was removed' : ''} className="flexbox center-aligned">
653✔
103
                  {option.notFound && <WarningIcon style={{ marginRight: 4 }} />}
662✔
104
                  {option.title}
105
                </div>
106
              </MenuItem>
107
            ))}
108
          </Select>
109
        )}
110
      />
111
    </FormControl>
112
  );
113
};
114

115
const ScopedPermissionsItem: FunctionComponent<Omit<IScopedPermissionSelect, 'name'>> = ({
5✔
116
  permissionsArea,
117
  disabled: disableEdit,
118
  index,
119
  itemSelection,
120
  options,
121
  onChange
122
}) => {
123
  const { excessiveAccessConfig, key } = permissionsArea;
266✔
124
  const { selector: excessiveAccessSelector, warning: excessiveAccessWarning } = excessiveAccessConfig;
266✔
125
  const { uiPermissions } = uiPermissionsByArea[key];
266✔
126
  const { item } = itemSelection;
266✔
127

128
  const disabled = disableEdit || itemSelection.disableEdit;
266✔
129
  return (
266✔
130
    <div className="flexbox center-aligned margin-left">
131
      <div className="two-columns center-aligned" style={{ maxWidth: 500 }}>
132
        <ScopeSelect
133
          disabled={disabled}
134
          permissionsArea={permissionsArea}
135
          index={index}
136
          options={options}
137
          itemSelection={itemSelection}
138
          onChange={item => onChange(index, { item, attribute: 'item' })}
4✔
139
          name={`${key}.${index}.item`}
140
        />
141
        <PermissionsSelect
142
          disabled={disabled}
143
          name={`${key}.${index}.uiPermissions`}
144
          label="Select"
145
          onChange={uiPermissions => onChange(index, { uiPermissions, attribute: 'uiPermissions' })}
4✔
146
          options={uiPermissions}
147
          permissionsArea={permissionsArea}
148
          unscoped={item === excessiveAccessSelector}
149
        />
150
      </div>
151
      {item === excessiveAccessSelector && (
310✔
152
        <div className="margin-left text-muted" style={{ alignSelf: 'flex-end' }}>
153
          {excessiveAccessWarning}
154
        </div>
155
      )}
156
    </div>
157
  );
158
};
159

160
interface IItemSelection extends PermissionsSelectionBaseProps {
161
  options: ItemScope[];
162
  permissionsArea: PermissionsArea;
163
  setValue: UseFormSetValue<FieldValues>;
164
}
165

166
export const ItemSelection: FunctionComponent<IItemSelection> = ({ disabled, options, permissionsArea, setValue }) => {
5✔
167
  const { control, watch } = useFormContext();
144✔
168
  const { key } = permissionsArea;
144✔
169
  const { title, explanation } = uiPermissionsByArea[key];
144✔
170
  const { fields, append } = useFieldArray({ control, name: permissionsArea.key });
144✔
171
  const watchFieldArray = watch(permissionsArea.key);
144✔
172
  const controlledFields = fields.map((field, index) => ({ ...field, ...watchFieldArray[index] }));
266✔
173

174
  const onItemPermissionSelectChange = useCallback(
144✔
175
    (index, { attribute, ...change }) => {
176
      let changedSelection = [...controlledFields];
8✔
177
      changedSelection[index] = { ...changedSelection[index], ...change };
8✔
178
      if (shouldExtendPermissionSelection(changedSelection, changedSelection[index], options)) {
8!
NEW
179
        append(emptyItemSelection);
×
180
      }
181
      setValue(`${key}.${index}.${attribute}`, change[attribute]);
8✔
182
    },
183
    [append, setValue, key, controlledFields, options]
184
  );
185

186
  return (
144✔
187
    <>
188
      <PermissionsAreaTitle className="margin-left-small" explanation={explanation} title={title} />
189
      {controlledFields.map((field, index) => (
190
        <ScopedPermissionsItem
266✔
191
          key={field.id}
192
          disabled={disabled}
193
          permissionsArea={permissionsArea}
194
          itemSelection={field}
195
          index={index}
196
          options={options}
197
          onChange={onItemPermissionSelectChange}
198
        />
199
      ))}
200
    </>
201
  );
202
};
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