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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 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';
251✔
53
  if (devices.length && (!name || isUUID(name))) {
251!
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) {
251✔
63
    devicesLink = `${devicesLink}?${formatDeviceSearch({ pageState: {}, filters, selectedGroup: group })}`;
62✔
64
  }
65
  return devicesLink;
251✔
66
};
67

68
export const getDeploymentTargetText = ({ deployment, devicesById, idAttribute }) => {
16✔
69
  const { devices = {}, group = '', name = '', type = DEPLOYMENT_TYPES.software } = deployment;
3,761✔
70
  let deviceList = Array.isArray(devices) ? devices : Object.values(devices);
3,761✔
71
  if (isUUID(name) && devicesById[name]) {
3,761!
72
    deviceList = [devicesById[name]];
×
73
  }
74
  if (type !== DEPLOYMENT_TYPES.configuration && (!deviceList.length || group || (deployment.name !== undefined && !isUUID(name)))) {
3,761!
75
    return (group || name) ?? '';
3,761!
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