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

mendersoftware / mender-server / 1650027964

31 Jan 2025 10:31AM UTC coverage: 77.579% (+1.0%) from 76.606%
1650027964

Pull #400

gitlab-ci

mzedel
chore(gui): moved header styles out of less and into theme system

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #400: chore(gui): moved header styles out of less and into theme system

4330 of 6290 branches covered (68.84%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

4 existing lines in 1 file now uncovered.

42803 of 54465 relevant lines covered (78.59%)

21.57 hits per line

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

83.15
/frontend/src/js/components/settings/user-management/UserForm.tsx
1
// Copyright 2017 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
import { useWatch } from 'react-hook-form';
16

17
import { InfoOutlined } from '@mui/icons-material';
18
import {
19
  Checkbox,
20
  Collapse,
21
  Dialog,
22
  DialogActions,
23
  DialogContent,
24
  DialogTitle,
25
  FormControl,
26
  FormHelperText,
27
  InputLabel,
28
  ListItemText,
29
  MenuItem,
30
  Select,
31
  Tooltip
32
} from '@mui/material';
33
import { makeStyles } from 'tss-react/mui';
34

35
import EnterpriseNotification from '@northern.tech/common-ui/EnterpriseNotification';
36
import Form from '@northern.tech/common-ui/forms/Form';
37
import FormCheckbox from '@northern.tech/common-ui/forms/FormCheckbox';
38
import PasswordInput from '@northern.tech/common-ui/forms/PasswordInput';
39
import TextInput from '@northern.tech/common-ui/forms/TextInput';
40
import { BENEFITS, rolesById, rolesByName, uiPermissionsById } from '@northern.tech/store/constants';
41
import pluralize from 'pluralize';
42
import { isUUID } from 'validator';
43

44
const useStyles = makeStyles()(theme => ({
8✔
45
  formWrapper: { display: 'flex', flexDirection: 'column', gap: theme.spacing(2), paddingTop: theme.spacing(4) }
46
}));
47

48
export const UserRolesSelect = ({ currentUser, disabled, onSelect, roles, user }) => {
8✔
49
  const relevantRolesById = useMemo(
36✔
50
    () => roles.reduce((accu, role) => ({ ...accu, [role.value ?? role.name]: { ...role, value: role.value ?? role.name } }), {}),
18!
51
    [roles]
52
  );
53
  const [selectedRoleIds, setSelectedRoleIds] = useState(
36✔
54
    (user.roles || [rolesByName.admin]).reduce((accu, roleId) => {
43✔
55
      const foundRole = relevantRolesById[roleId];
36✔
56
      if (foundRole) {
36✔
57
        accu.push(roleId);
35✔
58
      }
59
      return accu;
36✔
60
    }, [])
61
  );
62

63
  const onInputChange = ({ target: { value } }) => {
36✔
64
    const { roles = [] } = user;
4!
65
    let newlySelectedRoles = value;
4✔
66
    if (value.includes('')) {
4!
67
      newlySelectedRoles = [];
×
68
    }
69
    const hadRoleChanges =
70
      roles.length !== newlySelectedRoles.length || roles.some(currentRoleId => !newlySelectedRoles.some(roleId => currentRoleId === roleId));
4✔
71
    setSelectedRoleIds(newlySelectedRoles);
4✔
72
    onSelect(newlySelectedRoles, hadRoleChanges);
4✔
73
  };
74

75
  const { editableRoles, showRoleUsageNotification } = useMemo(() => {
36✔
76
    const editableRoles = Object.entries(relevantRolesById).map(([value, role]) => {
8✔
77
      const enabled = selectedRoleIds.some(roleId => value === roleId);
42✔
78
      return { enabled, value, ...role };
42✔
79
    });
80
    const showRoleUsageNotification = selectedRoleIds.reduce((accu, roleId) => {
8✔
81
      const { permissions, uiPermissions } = relevantRolesById[roleId];
5✔
82
      const hasUiApiAccess = [rolesByName.ci].includes(roleId)
5✔
83
        ? false
84
        : roleId === rolesByName.admin ||
6✔
85
          permissions.some(permission => ![rolesByName.deploymentCreation.action].includes(permission.action)) ||
×
86
          uiPermissions.userManagement.includes(uiPermissionsById.read.value);
87
      if (hasUiApiAccess) {
5✔
88
        return false;
4✔
89
      }
90
      return typeof accu !== 'undefined' ? accu : true;
1!
91
    }, undefined);
92
    return { editableRoles, showRoleUsageNotification };
8✔
93
    // eslint-disable-next-line react-hooks/exhaustive-deps
94
  }, [JSON.stringify(relevantRolesById), selectedRoleIds]);
95

96
  return (
36✔
97
    <div className="flexbox margin-top-small" style={{ alignItems: 'flex-end' }}>
98
      <FormControl id="roles-form" style={{ maxWidth: 400 }}>
99
        <InputLabel id="roles-selection-label">Roles</InputLabel>
100
        <Select
101
          label="Roles"
102
          labelId="roles-selection-label"
103
          id={`roles-selector-${selectedRoleIds.length}`}
104
          disabled={disabled}
105
          multiple
106
          value={selectedRoleIds}
107
          required
108
          onChange={onInputChange}
109
          renderValue={selected => selected.map(role => relevantRolesById[role].name).join(', ')}
43✔
110
        >
111
          {editableRoles.map(role => (
112
            <MenuItem id={role.value} key={role.value} value={role.value}>
210✔
113
              <Checkbox id={`${role.value}-checkbox`} checked={role.enabled} />
114
              <ListItemText id={`${role.value}-text`} primary={role.name} />
115
            </MenuItem>
116
          ))}
117
        </Select>
118
        {showRoleUsageNotification && (
37✔
119
          <FormHelperText className="info">
120
            The selected {pluralize('role', selectedRoleIds.length)} may prevent {currentUser.email === user.email ? 'you' : <i>{user.email}</i>} from using the
1!
121
            Mender UI.
122
            <br />
123
            Consider adding the <i>{rolesById[rolesByName.readOnly].name}</i> role as well.
124
          </FormHelperText>
125
        )}
126
      </FormControl>
127
      <EnterpriseNotification className="margin-left-small" id={BENEFITS.rbac.id} />
128
    </div>
129
  );
130
};
131

132
export const PasswordLabel = () => (
8✔
133
  <div className="flexbox center-aligned">
250✔
134
    Optional
135
    <Tooltip
136
      title={
137
        <>
138
          <p>You can skip setting a password for now - you can opt to send the new user an email containing a password reset link by checking the box below.</p>
139
          <p>Organizations using Single Sign-On or other means of authorization may want to create users with no password.</p>
140
        </>
141
      }
142
    >
143
      <InfoOutlined fontSize="small" className="margin-left-small" />
144
    </Tooltip>
145
  </div>
146
);
147

148
const UserIdentifier = ({ onHasUserId }) => {
8✔
149
  const value = useWatch({ name: 'email', defaultValue: '' });
24✔
150

151
  useEffect(() => {
24✔
152
    onHasUserId(isUUID(value));
15✔
153
  }, [value, onHasUserId]);
154

155
  return <TextInput hint="Email" label="Email or User ID" id="email" validations="isLength:1,isUUID||isEmail,trim" required autocomplete="off" />;
24✔
156
};
157

158
export const UserForm = ({ closeDialog, currentUser, canManageUsers, isEnterprise, roles, submit }) => {
8✔
159
  const [hadRoleChanges, setHadRoleChanges] = useState(false);
3✔
160
  const [selectedRoles, setSelectedRoles] = useState();
3✔
161
  const [isAddingExistingUser, setIsAddingExistingUser] = useState(false);
3✔
162
  const { classes } = useStyles();
3✔
163

164
  const onSelect = (newlySelectedRoles, hadRoleChanges) => {
3✔
UNCOV
165
    setSelectedRoles(newlySelectedRoles);
×
UNCOV
166
    setHadRoleChanges(hadRoleChanges);
×
167
  };
168

169
  const onSubmit = data => {
3✔
170
    const { password, ...remainder } = data;
1✔
171
    const roleData = hadRoleChanges ? { roles: selectedRoles } : {};
1!
172
    if (isAddingExistingUser) {
1!
UNCOV
173
      const { email: userId } = data;
×
UNCOV
174
      return submit(userId, 'add');
×
175
    }
176
    return submit({ ...remainder, ...roleData, password }, 'create');
1✔
177
  };
178

179
  return (
3✔
180
    <Dialog open={true} fullWidth={true} maxWidth="sm">
181
      <DialogTitle>Add new user</DialogTitle>
182
      <DialogContent style={{ overflowY: 'initial' }}>
183
        <Form
184
          className={classes.formWrapper}
185
          onSubmit={onSubmit}
186
          handleCancel={closeDialog}
187
          submitLabel={`${isAddingExistingUser ? 'Add' : 'Create'} user`}
3!
188
          showButtons={true}
189
          autocomplete="off"
190
        >
191
          <UserIdentifier onHasUserId={setIsAddingExistingUser} />
192
          <Collapse in={!isAddingExistingUser}>
193
            <PasswordInput
194
              id="password"
195
              autocomplete="off"
196
              create
197
              edit={false}
198
              generate
199
              InputLabelProps={{ shrink: true }}
200
              label={<PasswordLabel />}
201
              placeholder="Password"
202
              validations="isLength:8"
203
            />
204
            <FormCheckbox id="shouldResetPassword" label="Send an email to the user containing a link to reset the password" />
205
            <UserRolesSelect currentUser={currentUser} disabled={!(canManageUsers && isEnterprise)} onSelect={onSelect} roles={roles} user={{}} />
5✔
206
          </Collapse>
207
        </Form>
208
      </DialogContent>
209
      <DialogActions />
210
    </Dialog>
211
  );
212
};
213

214
export default UserForm;
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