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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

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

77.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, { useCallback, 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 InfoText from '../../common/infotext';
34
import { getDeviceIdentityText } from '../../devices/base-devices';
35
import { HELPTOOLTIPS, MenderHelpTooltip } from '../../helptips/helptooltips';
36

37
const useStyles = makeStyles()(theme => ({
16✔
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 = {
16✔
46
  textField: {
47
    minWidth: 400
48
  }
49
};
50

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

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

80
export const ReleasesWarning = ({ lacksReleases }) => (
16✔
81
  <div className="flexbox center-aligned">
100✔
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 /> : ' '}
200✔
85
      <Link to="/releases">Upload one to the repository</Link> to get started.
86
    </InfoText>
87
  </div>
88
);
89

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

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

110
  useEffect(() => {
242✔
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
    }
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) => {
242✔
121
    let update = { group: value };
2✔
122
    if (reason === 'clear') {
2!
123
      update = { ...update, deploymentDeviceCount: 0, devices: [] };
×
124
    }
125
    setDeploymentSettings(update);
2✔
126
  };
127

128
  const { deviceText, devicesLink, targetDeviceCount, targetDevicesText } = useMemo(() => {
242✔
129
    const devicesLink = getDevicesLink({ devices, group, hasFullFiltering, filters: filter?.filters });
242✔
130
    let deviceText = getDeploymentTargetText({ deployment: deploymentObject, idAttribute });
242✔
131
    let targetDeviceCount = deploymentDeviceCount;
242✔
132
    let targetDevicesText = `${deploymentDeviceCount} ${pluralize('devices', deploymentDeviceCount)}`;
242✔
133
    if (device?.id) {
242!
134
      const { attributes = {}, systemDeviceIds = [] } = device;
×
135
      const { mender_is_gateway } = attributes;
×
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
138
      targetDeviceCount = systemDeviceIds.length + 1;
×
139
    } else if (group) {
242✔
140
      deviceText = '';
60✔
141
      targetDevicesText = 'All devices';
60✔
142
      targetDeviceCount = 2;
60✔
143
      if (group !== ALL_DEVICES) {
60!
144
        targetDevicesText = `${targetDevicesText} in this group`;
×
145
        targetDeviceCount = deploymentDeviceCount;
×
146
      }
147
    }
148
    return { deviceText, devicesLink, targetDeviceCount, targetDevicesText };
242✔
149
  }, [devices, filter, group, hasFullFiltering, deploymentObject, idAttribute, deploymentDeviceCount, device]);
150

151
  return (
242✔
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 ? (
242!
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)}
244✔
166
              options={groupNames}
167
              onChange={deploymentSettingsUpdate}
168
              renderInput={params => (
169
                <TextField {...params} placeholder="Select a device group" InputProps={{ ...params.InputProps }} className={classes.textField} />
251✔
170
              )}
171
              value={group}
172
            />
173
            {!(hasDevices || hasDynamicGroups) && (
486!
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 && (
302✔
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 }) => {
16✔
201
  const [isLoadingReleases, setIsLoadingReleases] = useState(!releases.length);
242✔
202
  const dispatch = useDispatch();
242✔
203
  const { classes } = useStyles();
242✔
204
  const { devices = [], release: deploymentRelease = null, releaseSelectionLocked } = deploymentObject;
242✔
205
  const device = devices.length ? devices[0] : undefined;
242!
206

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

211
  const releaseItems = useMemo(() => {
242✔
212
    let releaseItems = releases.map(rel => releasesById[rel]);
302✔
213
    if (device && device.attributes) {
9!
214
      // If single device, don't show incompatible releases
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 = useCallback(
242✔
222
    release => {
223
      if (release !== deploymentObject.release) {
4✔
224
        setDeploymentSettings({ release });
2✔
225
      }
226
    },
227
    [deploymentObject.release, setDeploymentSettings]
228
  );
229

230
  const onReleaseInputChange = useCallback(
242✔
231
    inputValue => {
232
      setIsLoadingReleases(!releases.length);
×
233
      return dispatch(getReleases({ page: 1, perPage: 100, searchTerm: inputValue, searchOnly: true })).finally(() => setIsLoadingReleases(false));
×
234
    },
235
    [dispatch, releases.length]
236
  );
237

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

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