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

mendersoftware / gui / 914712491

pending completion
914712491

Pull #3798

gitlab-ci

mzedel
refac: refactored signup page to make better use of form capabilities

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3798: MEN-3530 - refactored forms

4359 of 6322 branches covered (68.95%)

92 of 99 new or added lines in 11 files covered. (92.93%)

1715 existing lines in 159 files now uncovered.

8203 of 9941 relevant lines covered (82.52%)

150.06 hits per line

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

72.0
/src/js/components/deployments/deployment-wizard/softwaredevices.js
1
// Copyright 2019 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 { Link } from 'react-router-dom';
16

17
import { ErrorOutline as ErrorOutlineIcon } from '@mui/icons-material';
18
import { Autocomplete, TextField, Tooltip } from '@mui/material';
19
import { makeStyles } from 'tss-react/mui';
20

21
import pluralize from 'pluralize';
22
import isUUID from 'validator/lib/isUUID';
23

24
import { DEPLOYMENT_TYPES } from '../../../constants/deploymentConstants';
25
import { ALL_DEVICES } from '../../../constants/deviceConstants';
26
import { stringToBoolean } from '../../../helpers';
27
import useWindowSize from '../../../utils/resizehook';
28
import AsyncAutocomplete from '../../common/asyncautocomplete';
29
import InfoHint from '../../common/info-hint';
30
import InfoText from '../../common/infotext';
31
import { getDeviceIdentityText } from '../../devices/base-devices';
32

33
const useStyles = makeStyles()(theme => ({
17✔
34
  infoStyle: {
35
    minWidth: 400,
36
    borderBottom: 'none'
37
  },
38
  selection: { minWidth: 'min-content', maxWidth: theme.spacing(50), minHeight: 96, marginBottom: theme.spacing(2) }
39
}));
40

41
const hardCodedStyle = {
17✔
42
  textField: {
43
    minWidth: 400
44
  }
45
};
46

47
export const getDevicesLink = ({ devices, group, hasFullFiltering, name }) => {
17✔
48
  let devicesLink = '/devices';
21✔
49
  if (devices.length && (!name || isUUID(name))) {
21!
UNCOV
50
    devicesLink = `${devicesLink}?id=${devices[0].id}`;
×
UNCOV
51
    if (hasFullFiltering) {
×
UNCOV
52
      devicesLink = `/devices?${devices.map(({ id }) => `id=${id}`).join('&')}`;
×
53
    }
UNCOV
54
    if (devices.length === 1) {
×
UNCOV
55
      const { systemDeviceIds = [] } = devices[0];
×
UNCOV
56
      devicesLink = `${devicesLink}${systemDeviceIds.map(id => `&id=${id}`).join('')}`;
×
57
    }
58
  } else if (group && group !== ALL_DEVICES) {
21✔
59
    devicesLink = `${devicesLink}?inventory=group:eq:${group}`;
1✔
60
  }
61
  return devicesLink;
21✔
62
};
63

64
export const getDeploymentTargetText = ({ deployment, devicesById, idAttribute }) => {
17✔
65
  const { devices = {}, group = '', name = '', type = DEPLOYMENT_TYPES.software } = deployment;
3,149✔
66
  let deviceList = Array.isArray(devices) ? devices : Object.values(devices);
3,149✔
67
  if (isUUID(name) && devicesById[name]) {
3,149!
UNCOV
68
    deviceList = [devicesById[name]];
×
69
  }
70
  if (type !== DEPLOYMENT_TYPES.configuration && (!deviceList.length || group || (deployment.name !== undefined && !isUUID(name)))) {
3,149!
71
    return (group || name) ?? '';
3,149!
72
  }
UNCOV
73
  return deviceList.map(device => getDeviceIdentityText({ device, idAttribute }) ?? device?.id).join(', ');
×
74
};
75

76
export const ReleasesWarning = ({ lacksReleases }) => (
17✔
77
  <div className="flexbox center-aligned">
67✔
78
    <ErrorOutlineIcon fontSize="small" style={{ marginRight: 4, top: 4, color: 'rgb(171, 16, 0)' }} />
79
    <InfoText>
80
      There are no {lacksReleases ? 'compatible ' : ''}artifacts available.{lacksReleases ? <br /> : ' '}
134✔
81
      <Link to="/releases">Upload one to the repository</Link> to get started.
82
    </InfoText>
83
  </div>
84
);
85

86
export const Devices = ({
17✔
87
  deploymentObject,
88
  getSystemDevices,
89
  groupRef,
90
  groups,
91
  hasDevices,
92
  hasDynamicGroups,
93
  hasFullFiltering,
94
  hasPending,
95
  idAttribute,
96
  setDeploymentSettings
97
}) => {
98
  const { classes } = useStyles();
249✔
99
  // eslint-disable-next-line no-unused-vars
100
  const size = useWindowSize();
249✔
101

102
  const { deploymentDeviceCount = 0, devices = [], group = null } = deploymentObject;
249✔
103
  const device = devices.length === 1 ? devices[0] : {};
249!
104

105
  useEffect(() => {
249✔
106
    const { attributes = {} } = device;
6✔
107
    const { mender_is_gateway } = attributes;
6✔
108
    if (!device.id || !stringToBoolean(mender_is_gateway)) {
6!
109
      return;
6✔
110
    }
UNCOV
111
    getSystemDevices(device.id, { perPage: 500 });
×
112
  }, [device.id, device.attributes?.mender_is_gateway]);
113

114
  const deploymentSettingsUpdate = (e, value, reason) => {
249✔
115
    let update = { group: value };
2✔
116
    if (reason === 'clear') {
2!
UNCOV
117
      update = { ...update, deploymentDeviceCount: 0, devices: [] };
×
118
    }
119
    setDeploymentSettings(update);
2✔
120
  };
121

122
  const groupItems = [ALL_DEVICES, ...Object.keys(groups)];
249✔
123
  const { deviceText, devicesLink, targetDeviceCount, targetDevicesText } = useMemo(() => {
249✔
124
    const devicesLink = getDevicesLink({ devices, group, hasFullFiltering });
16✔
125
    let deviceText = getDeploymentTargetText({ deployment: deploymentObject, idAttribute });
16✔
126
    let targetDeviceCount = deploymentDeviceCount;
16✔
127
    let targetDevicesText = `${deploymentDeviceCount} ${pluralize('devices', deploymentDeviceCount)}`;
16✔
128
    if (device?.id) {
16!
UNCOV
129
      const { attributes = {}, systemDeviceIds = [] } = device;
×
UNCOV
130
      const { mender_is_gateway } = attributes;
×
UNCOV
131
      deviceText = `${getDeviceIdentityText({ device, idAttribute })}${stringToBoolean(mender_is_gateway) ? ' (System)' : ''}`;
×
132
      // here we hope the number of systemDeviceIds doesn't exceed the queried 500 and add the gateway device
UNCOV
133
      targetDeviceCount = systemDeviceIds.length + 1;
×
134
    } else if (group) {
16✔
135
      deviceText = '';
4✔
136
      targetDevicesText = 'All devices';
4✔
137
      targetDeviceCount = 2;
4✔
138
      if (group !== ALL_DEVICES) {
4!
UNCOV
139
        targetDevicesText = `${targetDevicesText} in this group`;
×
UNCOV
140
        targetDeviceCount = deploymentDeviceCount;
×
141
      }
142
    }
143
    return { deviceText, devicesLink, targetDeviceCount, targetDevicesText };
16✔
144
  }, [devices, group, idAttribute, deploymentDeviceCount]);
145

146
  return (
249✔
147
    <>
148
      <h4 className="margin-bottom-none margin-top-none">Select a device group to target</h4>
149
      <div ref={groupRef} className={classes.selection}>
150
        {deviceText ? (
249!
151
          <TextField value={deviceText} label={pluralize('device', devices.length)} disabled={true} className={classes.infoStyle} />
152
        ) : (
153
          <div>
154
            <Autocomplete
155
              id="deployment-device-group-selection"
156
              autoSelect
157
              autoHighlight
158
              filterSelectedOptions
159
              handleHomeEndKeys
160
              disabled={!(hasDevices || hasDynamicGroups)}
251✔
161
              options={groupItems}
162
              onChange={deploymentSettingsUpdate}
163
              renderInput={params => (
164
                <TextField {...params} placeholder="Select a device group" InputProps={{ ...params.InputProps }} className={classes.textField} />
258✔
165
              )}
166
              value={group}
167
            />
168
            {!(hasDevices || hasDynamicGroups) && (
500!
169
              <InfoText style={{ marginTop: '10px' }}>
170
                <ErrorOutlineIcon style={{ marginRight: '4px', fontSize: '18px', top: '4px', color: 'rgb(171, 16, 0)', position: 'relative' }} />
171
                There are no connected devices.{' '}
172
                {hasPending ? (
×
173
                  <span>
174
                    <Link to="/devices/pending">Accept pending devices</Link> to get started.
175
                  </span>
176
                ) : (
177
                  <span>
178
                    <Link to="/help/get-started">Read the help pages</Link> for help with connecting devices.
179
                  </span>
180
                )}
181
              </InfoText>
182
            )}
183
          </div>
184
        )}
185
        {!!targetDeviceCount && (
323✔
186
          <InfoText>
187
            {targetDevicesText} will be targeted. <Link to={devicesLink}>View the {pluralize('devices', targetDeviceCount)}</Link>
188
          </InfoText>
189
        )}
190
      </div>
191
    </>
192
  );
193
};
194

195
export const Software = ({
17✔
196
  commonClasses,
197
  deploymentObject,
198
  getReleases,
199
  releaseRef,
200
  releases,
201
  releasesById,
202
  releaseSelectionLocked,
203
  setDeploymentSettings
204
}) => {
205
  const [isLoadingReleases, setIsLoadingReleases] = useState(!releases.length);
249✔
206
  const { classes } = useStyles();
249✔
207
  const { devices = [], release: deploymentRelease = null } = deploymentObject;
249✔
208
  const device = devices.length ? devices[0] : undefined;
249!
209

210
  useEffect(() => {
249✔
211
    setIsLoadingReleases(!releases.length);
9✔
212
  }, [releases.length]);
213

214
  const releaseItems = useMemo(() => {
249✔
215
    let releaseItems = releases.map(rel => releasesById[rel]);
302✔
216
    if (device && device.attributes) {
9!
217
      // If single device, don't show incompatible releases
UNCOV
218
      releaseItems = releaseItems.filter(rel => rel.device_types_compatible.some(type => device.attributes.device_type.includes(type)));
×
219
    }
220
    return releaseItems;
9✔
221
  }, [releases, device]);
222

223
  const onReleaseSelectionChange = release => {
249✔
224
    if (release !== deploymentObject.release) {
2!
225
      setDeploymentSettings({ release });
2✔
226
    }
227
  };
228

229
  const onReleaseInputChange = inputValue => {
249✔
UNCOV
230
    setIsLoadingReleases(!releases.length);
×
UNCOV
231
    return getReleases({ page: 1, perPage: 100, searchTerm: inputValue, searchOnly: true }).finally(() => setIsLoadingReleases(false));
×
232
  };
233

234
  const releaseDeviceTypes = (deploymentRelease && deploymentRelease.device_types_compatible) ?? [];
249✔
235
  const devicetypesInfo = (
236
    <Tooltip title={<p>{releaseDeviceTypes.join(', ')}</p>} placement="bottom">
249✔
237
      <span className="link">
238
        {releaseDeviceTypes.length} device {pluralize('types', releaseDeviceTypes.length)}
239
      </span>
240
    </Tooltip>
241
  );
242

243
  return (
249✔
244
    <>
245
      <h4 className="margin-bottom-none margin-top-none">Select a Release to deploy</h4>
246
      <div className={commonClasses.columns}>
247
        <div ref={releaseRef} className={classes.selection}>
248
          {releaseSelectionLocked ? (
249✔
249
            <TextField value={deploymentRelease?.Name} label="Release" disabled={true} className={classes.infoStyle} />
250
          ) : (
251
            <AsyncAutocomplete
252
              id="deployment-release-selection"
253
              initialValue={deploymentRelease?.Name}
254
              labelAttribute="Name"
255
              placeholder="Select a Release"
256
              selectionAttribute="Name"
257
              options={releaseItems}
258
              onChange={onReleaseInputChange}
259
              onChangeSelection={onReleaseSelectionChange}
260
              isLoading={isLoadingReleases}
261
              styles={hardCodedStyle}
262
            />
263
          )}
264
          {!releaseItems.length ? (
249✔
265
            <ReleasesWarning lacksReleases />
266
          ) : (
267
            !!releaseDeviceTypes.length && <InfoText style={{ marginBottom: 0 }}>This Release is compatible with {devicetypesInfo}.</InfoText>
255✔
268
          )}
269
        </div>
270
        <InfoHint content="The deployment will skip any devices in the group that are already on the target Release version, or that have an incompatible device type." />
271
      </div>
272
    </>
273
  );
274
};
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