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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

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

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

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

2✔
19
import { ErrorOutline as ErrorOutlineIcon } from '@mui/icons-material';
2✔
20
import { Alert, Divider, Drawer, formControlLabelClasses } from '@mui/material';
2✔
21
import { makeStyles } from 'tss-react/mui';
2✔
22

2✔
23
import { DrawerTitle } from '@northern.tech/common-ui/DrawerTitle';
2✔
24
import InfoHint from '@northern.tech/common-ui/InfoHint';
2✔
25
import Form from '@northern.tech/common-ui/forms/Form';
2✔
26
import FormCheckbox from '@northern.tech/common-ui/forms/FormCheckbox';
2✔
27
import PasswordInput from '@northern.tech/common-ui/forms/PasswordInput';
2✔
28
import TextInput from '@northern.tech/common-ui/forms/TextInput';
2✔
29
import { TIMEOUTS, rolesByName } from '@northern.tech/store/constants';
2✔
30
import { getOrganization, getSsoConfig } from '@northern.tech/store/selectors';
2✔
31
import { useAppDispatch } from '@northern.tech/store/store';
2✔
32
import { addTenant, checkEmailExists, getSsoConfigs } from '@northern.tech/store/thunks';
2✔
33
import { useDebounce } from '@northern.tech/utils/debouncehook';
2✔
34

2✔
35
import { HELPTOOLTIPS } from '../helptips/HelpTooltips';
2✔
36
import { MenderHelpTooltip } from '../helptips/MenderTooltip';
2✔
37
import { PasswordLabel } from '../settings/user-management/UserForm';
2✔
38

2✔
39
interface TenantCreateFormProps {
2✔
40
  onCloseClick: () => void;
2✔
41
  open: boolean;
2✔
42
}
2✔
43

2✔
44
const useStyles = makeStyles()(theme => ({
6✔
45
  buttonWrapper: {
2✔
46
    '&.button-wrapper': {
2✔
47
      justifyContent: 'start'
2✔
48
    }
2✔
49
  },
2✔
50
  formWrapper: {
2✔
51
    display: 'flex',
2✔
52
    flexDirection: 'column',
2✔
53
    gap: theme.spacing(2),
2✔
54
    [`.${formControlLabelClasses.root}`]: { marginTop: 0 },
2✔
55
    '.required .relative': { marginLeft: theme.spacing(10) }
2✔
56
  },
2✔
57
  devLimitInput: { marginTop: 10, maxWidth: 150, minWidth: 130 }
2✔
58
}));
2✔
59

2✔
60
interface UserInputsProps {
2✔
61
  adminExists: boolean;
2✔
62
  checkEmailExists: (email: string) => Promise<void>;
2✔
63
}
2✔
64

2✔
65
const userExistsInfo =
2✔
66
  'This user already has a Mender account, and will be assigned as admin to the new tenant. If you want to create a brand new user, try a different email address.';
6✔
67
const newUserInfo = 'This will create a new user as admin of the new tenant.';
6✔
68

2✔
69
const UserInputs = (props: UserInputsProps) => {
6✔
70
  const { checkEmailExists, adminExists } = props;
108✔
71
  const [emailInfoText, setEmailInfoText] = useState<string>('');
108✔
72

2✔
73
  const { watch, getFieldState, setValue } = useFormContext();
108✔
74

2✔
75
  const enteredEmail = watch('email');
108✔
76
  const debouncedEmail = useDebounce(enteredEmail, TIMEOUTS.debounceDefault);
108✔
77

2✔
78
  useEffect(() => {
108✔
79
    const { invalid: isInvalidEmail } = getFieldState('email');
6✔
80
    if (!debouncedEmail || isInvalidEmail) {
6✔
81
      return;
5✔
82
    }
2✔
83
    checkEmailExists(debouncedEmail);
3✔
84
  }, [debouncedEmail, getFieldState, checkEmailExists]);
2✔
85

2✔
86
  useEffect(() => {
108✔
87
    const { invalid: isInvalidEmail } = getFieldState('email');
7✔
88
    if (!debouncedEmail || isInvalidEmail) {
7✔
89
      return;
5✔
90
    }
2✔
91
    if (adminExists) {
4✔
92
      setEmailInfoText(userExistsInfo);
3✔
93
      setValue('password', '');
3✔
94
      return;
3✔
95
    }
2✔
96
    setEmailInfoText(newUserInfo);
3✔
97
  }, [debouncedEmail, getFieldState, setValue, adminExists]);
2✔
98

2✔
99
  return (
108✔
100
    <>
2✔
101
      <div className="flexbox center-aligned">
2✔
102
        <TextInput validations="isEmail,trim" required id="email" label="Admin user" />
2✔
103
        <MenderHelpTooltip className="required" id={HELPTOOLTIPS.tenantAdmin.id} />
2✔
104
      </div>
2✔
105
      {!adminExists && (
2✔
106
        <>
2✔
107
          <PasswordInput
2✔
108
            className="margin-bottom-small"
2✔
109
            label={<PasswordLabel />}
2✔
110
            id="password"
2✔
111
            InputLabelProps={{ shrink: true }}
2✔
112
            validations={`isLength:8,isNot:${enteredEmail}`}
2✔
113
            placeholder="Password"
2✔
114
            create
2✔
115
            generate
2✔
116
          />
2✔
117
          <FormCheckbox id="send_reset_password" label="Send an email to the user containing a link to reset the password" />
2✔
118
        </>
2✔
119
      )}
2✔
120
      {emailInfoText ? <InfoHint content={emailInfoText} /> : <div />}
2✔
121
    </>
2✔
122
  );
2✔
123
};
2✔
124

2✔
125
const tenantAdminDefaults = { email: '', name: '', password: '', sso: false, binary_delta: false, device_limit: undefined, send_reset_password: false };
6✔
126
export const TenantCreateForm = (props: TenantCreateFormProps) => {
6✔
127
  const { onCloseClick, open } = props;
8✔
128
  const { device_count: spDeviceUtilization = 0, device_limit: spDeviceLimit = 0 } = useSelector(getOrganization);
8✔
129
  const ssoConfig = useSelector(getSsoConfig);
8✔
130
  const dispatch = useAppDispatch();
8✔
131

2✔
132
  const { classes } = useStyles();
8✔
133
  const [adminExists, setAdminExists] = useState<boolean>(false);
8✔
134
  const [hasError, setHasError] = useState<boolean>(false);
8✔
135

2✔
136
  const quota = spDeviceLimit - spDeviceUtilization || 0;
8✔
137
  const numericValidation = {
8✔
138
    min: { value: 1, message: 'The limit must be 1 or more' },
2✔
139
    max: { value: quota, message: `The device limit must be ${quota} or fewer` }
2✔
140
  };
2✔
141

2✔
142
  useEffect(() => {
8✔
143
    dispatch(getSsoConfigs());
4✔
144
  }, [dispatch]);
2✔
145

2✔
146
  const onCheckEmailExists = useCallback(
8✔
147
    async (email: string) => {
2✔
148
      const exists = await dispatch(checkEmailExists(email)).unwrap();
3✔
149
      setAdminExists(exists);
3✔
150
    },
2✔
151
    [dispatch]
2✔
152
  );
2✔
153

2✔
154
  const submitNewTenant = useCallback(
8✔
155
    async data => {
2✔
156
      const { email, password, device_limit, send_reset_password, sso, ...remainder } = data;
4✔
157
      let selectionState = { device_limit: Number(device_limit), restrict_sso_to_parent: sso, sso, ...remainder };
4✔
158
      if (adminExists) {
4✔
159
        selectionState = { users: [{ role: rolesByName.admin, email }], ...selectionState };
3✔
160
      } else {
2✔
161
        selectionState = { admin: { password, email, send_reset_password }, ...selectionState };
3✔
162
      }
2✔
163
      try {
4✔
164
        await dispatch(addTenant(selectionState)).unwrap(); // only awaiting the thunk resolution to not get rejected
4✔
165
        onCloseClick();
3✔
166
      } catch {
2✔
167
        setHasError(true);
3✔
168
      }
2✔
169
    },
2✔
170
    [adminExists, dispatch, onCloseClick]
2✔
171
  );
2✔
172

2✔
173
  return (
8✔
174
    <Drawer open={open} onClose={onCloseClick} anchor="right" PaperProps={{ style: { minWidth: '67vw' } }}>
2✔
175
      <DrawerTitle title="Add a tenant" onClose={onCloseClick} />
2✔
176
      <Divider className="margin-bottom-large" />
2✔
177
      <Form
2✔
178
        initialValues={tenantAdminDefaults}
2✔
179
        classes={classes}
2✔
180
        className={classes.formWrapper}
2✔
UNCOV
181
        handleCancel={() => onCloseClick()}
2✔
182
        showButtons
2✔
183
        onSubmit={submitNewTenant}
2✔
184
        submitLabel="Create tenant"
2✔
185
        autocomplete="off"
2✔
186
      >
2✔
187
        {hasError && (
2✔
188
          <Alert icon={<ErrorOutlineIcon />} severity="error">
2✔
189
            There was an error while creating the tenant. Please try again, or contact support.
2✔
190
          </Alert>
2✔
191
        )}
2✔
192
        <TextInput required validations="isLength:3,trim" id="name" hint="Name" label="Name" />
2✔
193
        <UserInputs adminExists={adminExists} checkEmailExists={onCheckEmailExists} />
2✔
194
        <div className="flexbox center-aligned">
2✔
195
          <TextInput
2✔
196
            required
2✔
197
            id="device_limit"
2✔
198
            hint={`${quota}`}
2✔
199
            type="number"
2✔
200
            label="Set device limit"
2✔
201
            className={classes.devLimitInput}
2✔
202
            InputProps={{ inputProps: { min: 1, max: quota } }}
2✔
203
            numericValidations={numericValidation}
2✔
204
          />
2✔
205
          <MenderHelpTooltip className="required" id={HELPTOOLTIPS.subTenantDeviceLimit.id} />
2✔
206
        </div>
2✔
207
        <div className="flexbox center-aligned">
2✔
208
          <FormCheckbox id="binary_delta" label="Enable Delta Artifact generation" />
2✔
209
          <MenderHelpTooltip id={HELPTOOLTIPS.subTenantDeltaArtifactGeneration.id} />
2✔
210
        </div>
2✔
211
        {!!ssoConfig && (
2✔
212
          <>
2✔
213
            <div className="flexbox center-aligned">
2✔
214
              <FormCheckbox id="sso" label="Restrict to Service Provider’s Single Sign-On settings" />
2✔
215
              <MenderHelpTooltip className="flexbox center-aligned" id={HELPTOOLTIPS.subTenantSSO.id} />
2✔
216
            </div>
2✔
217
            <div className="margin-top-x-small margin-bottom">
2✔
218
              <Link to="/settings/organization">View Single Sign-On settings</Link>
2✔
219
            </div>
2✔
220
          </>
2✔
221
        )}
2✔
222
      </Form>
2✔
223
    </Drawer>
2✔
224
  );
2✔
225
};
2✔
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