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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

71.11
/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 { useDispatch } from 'react-redux';
16
import { Link } from 'react-router-dom';
17

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

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

25
import { getSystemDevices } from '../../../actions/deviceActions';
26
import { getReleases } from '../../../actions/releaseActions';
27
import { DEPLOYMENT_TYPES } from '../../../constants/deploymentConstants';
28
import { ALL_DEVICES } from '../../../constants/deviceConstants';
29
import { stringToBoolean } from '../../../helpers';
30
import { formatDeviceSearch } from '../../../utils/locationutils';
31
import useWindowSize from '../../../utils/resizehook';
32
import AsyncAutocomplete from '../../common/asyncautocomplete';
33
import InfoHint from '../../common/info-hint';
34
import InfoText from '../../common/infotext';
35
import { getDeviceIdentityText } from '../../devices/base-devices';
36

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

45
const hardCodedStyle = {
17✔
46
  textField: {
47
    minWidth: 400
48
  }
49
};
50

51
export const getDevicesLink = ({ devices, filter, group, hasFullFiltering, name }) => {
17✔
52
  let devicesLink = '/devices';
282✔
53
  if (devices.length && (!name || isUUID(name))) {
282!
UNCOV
54
    devicesLink = `${devicesLink}?id=${devices[0].id}`;
×
UNCOV
55
    if (hasFullFiltering) {
×
UNCOV
56
      devicesLink = `/devices?${devices.map(({ id }) => `id=${id}`).join('&')}`;
×
57
    }
UNCOV
58
    if (devices.length === 1) {
×
UNCOV
59
      const { systemDeviceIds = [] } = devices[0];
×
UNCOV
60
      devicesLink = `${devicesLink}${systemDeviceIds.map(id => `&id=${id}`).join('')}`;
×
61
    }
62
  } else if (group || filter) {
282✔
63
    devicesLink = `${devicesLink}?${formatDeviceSearch({ pageState: {}, filters: filter ? [filter] : [], selectedGroup: group })}`;
66!
64
  }
65
  return devicesLink;
282✔
66
};
67

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

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

90
export const Devices = ({
17✔
91
  deploymentObject,
92
  groupRef,
93
  groupNames,
94
  hasDevices,
95
  hasDynamicGroups,
96
  hasFullFiltering,
97
  hasPending,
98
  idAttribute,
99
  setDeploymentSettings
100
}) => {
101
  const { classes } = useStyles();
274✔
102
  // eslint-disable-next-line no-unused-vars
103
  const size = useWindowSize();
274✔
104
  const dispatch = useDispatch();
274✔
105

106
  const { deploymentDeviceCount = 0, devices = [], filter, group = null } = deploymentObject;
274✔
107
  // eslint-disable-next-line react-hooks/exhaustive-deps
108
  const device = useMemo(() => (devices.length === 1 ? devices[0] : {}), [devices.length]);
274!
109

110
  useEffect(() => {
274✔
111
    const { attributes = {} } = device;
6✔
112
    const { mender_is_gateway } = attributes;
6✔
113
    if (!device.id || !stringToBoolean(mender_is_gateway)) {
6!
114
      return;
6✔
115
    }
UNCOV
116
    dispatch(getSystemDevices(device.id, { perPage: 500 }));
×
117
    // eslint-disable-next-line react-hooks/exhaustive-deps
118
  }, [device.id, device.attributes?.mender_is_gateway, dispatch]);
119

120
  const deploymentSettingsUpdate = (e, value, reason) => {
274✔
121
    let update = { group: value };
2✔
122
    if (reason === 'clear') {
2!
UNCOV
123
      update = { ...update, deploymentDeviceCount: 0, devices: [] };
×
124
    }
125
    setDeploymentSettings(update);
2✔
126
  };
127

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

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

200
export const Software = ({ commonClasses, deploymentObject, releaseRef, releases, releasesById, setDeploymentSettings }) => {
17✔
201
  const [isLoadingReleases, setIsLoadingReleases] = useState(!releases.length);
274✔
202
  const dispatch = useDispatch();
274✔
203
  const { classes } = useStyles();
274✔
204
  const { devices = [], release: deploymentRelease = null, releaseSelectionLocked } = deploymentObject;
274✔
205
  const device = devices.length ? devices[0] : undefined;
274!
206

207
  useEffect(() => {
274✔
208
    setIsLoadingReleases(!releases.length);
9✔
209
  }, [releases.length]);
210

211
  const releaseItems = useMemo(() => {
274✔
212
    let releaseItems = releases.map(rel => releasesById[rel]);
302✔
213
    if (device && device.attributes) {
9!
214
      // If single device, don't show incompatible releases
UNCOV
215
      releaseItems = releaseItems.filter(rel => rel.device_types_compatible.some(type => device.attributes.device_type.includes(type)));
×
216
    }
217
    return releaseItems;
9✔
218
    // eslint-disable-next-line react-hooks/exhaustive-deps
219
  }, [device, releases]);
220

221
  const onReleaseSelectionChange = release => {
274✔
222
    if (release !== deploymentObject.release) {
90✔
223
      setDeploymentSettings({ release });
4✔
224
    }
225
  };
226

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

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

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