• 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

79.07
/src/js/components/devices/expanded-device.js
1
// Copyright 2015 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, useRef } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16
import { useNavigate } from 'react-router-dom';
17

18
import { Close as CloseIcon, Link as LinkIcon } from '@mui/icons-material';
19
import { Chip, Divider, Drawer, IconButton, Tab, Tabs, Tooltip, chipClasses } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import copy from 'copy-to-clipboard';
23

24
import GatewayConnectionIcon from '../../../assets/img/gateway-connection.svg';
25
import GatewayIcon from '../../../assets/img/gateway.svg';
26
import { setSnackbar } from '../../actions/appActions';
27
import { decommissionDevice, getDeviceInfo, getGatewayDevices } from '../../actions/deviceActions';
28
import { saveGlobalSettings } from '../../actions/userActions';
29
import { TIMEOUTS, yes } from '../../constants/appConstants';
30
import { DEVICE_STATES, EXTERNAL_PROVIDER } from '../../constants/deviceConstants';
31
import { getDemoDeviceAddress, stringToBoolean } from '../../helpers';
32
import {
33
  getDeviceConfigDeployment,
34
  getDeviceTwinIntegrations,
35
  getDevicesById,
36
  getDocsVersion,
37
  getGlobalSettings,
38
  getSelectedGroupInfo,
39
  getTenantCapabilities,
40
  getUserCapabilities,
41
  getUserSettings
42
} from '../../selectors';
43
import DeviceIdentityDisplay from '../common/deviceidentity';
44
import DocsLink from '../common/docslink';
45
import { MenderTooltipClickable } from '../common/mendertooltip';
46
import { RelativeTime } from '../common/time';
47
import DeviceConfiguration from './device-details/configuration';
48
import TroubleshootTab from './device-details/connection';
49
import Deployments from './device-details/deployments';
50
import DeviceInventory from './device-details/deviceinventory';
51
import DeviceSystem from './device-details/devicesystem';
52
import { IntegrationTab } from './device-details/devicetwin';
53
import { IdentityTab } from './device-details/identity';
54
import InstalledSoftware from './device-details/installedsoftware';
55
import MonitoringTab from './device-details/monitoring';
56
import DeviceNotifications from './device-details/notifications';
57
import DeviceQuickActions from './widgets/devicequickactions';
58

59
const useStyles = makeStyles()(theme => ({
8✔
60
  gatewayChip: {
61
    backgroundColor: theme.palette.grey[400],
62
    color: theme.palette.grey[900],
63
    path: {
64
      fill: theme.palette.grey[900]
65
    },
66
    [`.${chipClasses.icon}`]: {
67
      marginLeft: 10,
68
      width: 20
69
    },
70
    [`.${chipClasses.icon}.connected`]: {
71
      transform: 'scale(1.3)',
72
      width: 15
73
    }
74
  },
75
  deviceConnection: {
76
    marginRight: theme.spacing(2)
77
  },
78
  dividerTop: {
79
    marginBottom: theme.spacing(3),
80
    marginTop: theme.spacing(2)
81
  }
82
}));
83

84
const refreshDeviceLength = TIMEOUTS.refreshDefault;
8✔
85

86
const GatewayConnectionNotification = ({ gatewayDevices, onClick }) => {
8✔
87
  const { classes } = useStyles();
×
88

89
  const onGatewayClick = () => {
×
90
    const query =
91
      gatewayDevices.length > 1 ? gatewayDevices.map(device => `id=${device.id}`).join('&') : `id=${gatewayDevices[0].id}&open=true&tab=device-system`;
×
92
    onClick(query);
×
93
  };
94

95
  return (
×
96
    <MenderTooltipClickable
97
      placement="bottom"
98
      title={
99
        <div style={{ maxWidth: 350 }}>
100
          Connected to{' '}
101
          {gatewayDevices.length > 1 ? 'multiple devices' : <DeviceIdentityDisplay device={gatewayDevices[0]} isEditable={false} hasAdornment={false} />}
×
102
        </div>
103
      }
104
    >
105
      <Chip className={classes.gatewayChip} icon={<GatewayConnectionIcon className="connected" />} label="Connected to gateway" onClick={onGatewayClick} />
106
    </MenderTooltipClickable>
107
  );
108
};
109

110
const GatewayNotification = ({ device, onClick }) => {
8✔
111
  const ipAddress = getDemoDeviceAddress([device]);
1✔
112
  const { classes } = useStyles();
1✔
113
  return (
1✔
114
    <MenderTooltipClickable
115
      placement="bottom"
116
      title={
117
        <div style={{ maxWidth: 350 }}>
118
          For information about connecting other devices to this gateway, please refer to the{' '}
119
          <DocsLink path="get-started/mender-gateway" title="Mender Gateway documentation" />. This device is reachable via <i>{ipAddress}</i>.
120
        </div>
121
      }
122
    >
123
      <Chip className={classes.gatewayChip} icon={<GatewayIcon />} label="Gateway" onClick={onClick} />
124
    </MenderTooltipClickable>
125
  );
126
};
127

128
const deviceStatusCheck = ({ device: { status = DEVICE_STATES.accepted } }, states = [DEVICE_STATES.accepted]) => states.includes(status);
145✔
129

130
const tabs = [
8✔
131
  { component: IdentityTab, title: () => 'Identity', value: 'identity', isApplicable: yes },
24✔
132
  {
133
    component: DeviceInventory,
134
    title: () => 'Inventory',
24✔
135
    value: 'inventory',
136
    isApplicable: deviceStatusCheck
137
  },
138
  {
139
    component: InstalledSoftware,
140
    title: () => 'Software',
24✔
141
    value: 'software',
142
    isApplicable: deviceStatusCheck
143
  },
144
  {
145
    component: Deployments,
146
    title: () => 'Deployments',
24✔
147
    value: 'deployments',
148
    isApplicable: deviceStatusCheck
149
  },
150
  {
151
    component: DeviceConfiguration,
152
    title: () => 'Configuration',
24✔
153
    value: 'configuration',
154
    isApplicable: ({ userCapabilities: { canConfigure }, ...rest }) => canConfigure && deviceStatusCheck(rest, [DEVICE_STATES.accepted, DEVICE_STATES.preauth])
24✔
155
  },
156
  {
157
    component: MonitoringTab,
158
    title: () => 'Monitoring',
24✔
159
    value: 'monitor',
160
    isApplicable: deviceStatusCheck
161
  },
162
  {
163
    component: TroubleshootTab,
164
    title: () => 'Troubleshooting',
24✔
165
    value: 'troubleshoot',
166
    isApplicable: deviceStatusCheck
167
  },
168
  {
169
    component: IntegrationTab,
170
    title: ({ integrations }) => {
171
      if (integrations.length > 1) {
1!
172
        return 'Device Twin';
×
173
      }
174
      const { title, twinTitle } = EXTERNAL_PROVIDER[integrations[0].provider];
1✔
175
      return `${title} ${twinTitle}`;
1✔
176
    },
177
    value: 'device-twin',
178
    isApplicable: ({ integrations, ...rest }) => !!integrations.length && deviceStatusCheck(rest, [DEVICE_STATES.accepted, DEVICE_STATES.preauth])
24✔
179
  },
180
  {
181
    component: DeviceSystem,
182
    title: () => 'System',
1✔
183
    value: 'system',
184
    isApplicable: ({ device: { attributes = {} } }) => stringToBoolean(attributes?.mender_is_gateway ?? '')
24✔
185
  }
186
];
187

188
export const ExpandedDevice = ({ actionCallbacks, deviceId, onClose, setDetailsTab, tabSelection }) => {
8✔
189
  const timer = useRef();
24✔
190
  const navigate = useNavigate();
24✔
191
  const { classes } = useStyles();
24✔
192

193
  const { latest: latestAlerts = [] } = useSelector(state => state.monitor.alerts.byDeviceId[deviceId]) || {};
50✔
194
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
24!
195
  const { columnSelection = [] } = useSelector(getUserSettings);
24!
196
  const { defaultDeviceConfig: defaultConfig } = useSelector(getGlobalSettings);
24✔
197
  const { device, deviceConfigDeployment } = useSelector(state => getDeviceConfigDeployment(state, deviceId));
50✔
198
  const devicesById = useSelector(getDevicesById);
24✔
199
  const docsVersion = useSelector(getDocsVersion);
24✔
200
  const integrations = useSelector(getDeviceTwinIntegrations);
24✔
201
  const tenantCapabilities = useSelector(getTenantCapabilities);
24✔
202
  const userCapabilities = useSelector(getUserCapabilities);
24✔
203
  const dispatch = useDispatch();
24✔
204

205
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
24✔
206
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
24✔
207
  const isGateway = stringToBoolean(mender_is_gateway);
24✔
208

209
  useEffect(() => {
24✔
210
    clearInterval(timer.current);
4✔
211
    if (!deviceId) {
4✔
212
      return;
3✔
213
    }
214
    timer.current = setInterval(() => dispatch(getDeviceInfo(deviceId)), refreshDeviceLength);
1✔
215
    dispatch(getDeviceInfo(deviceId));
1✔
216
    return () => {
1✔
217
      clearInterval(timer.current);
1✔
218
    };
219
  }, [deviceId, device.status, dispatch]);
220

221
  useEffect(() => {
24✔
222
    if (!(device.id && mender_gateway_system_id)) {
4!
223
      return;
4✔
224
    }
225
    dispatch(getGatewayDevices(device.id));
×
226
  }, [device.id, dispatch, mender_gateway_system_id]);
227

228
  // close expanded device
229
  const onDecommissionDevice = device_id => dispatch(decommissionDevice(device_id)).finally(onClose);
24✔
230

231
  const copyLinkToClipboard = () => {
24✔
232
    const location = window.location.href.substring(0, window.location.href.indexOf('/devices') + '/devices'.length);
×
233
    copy(`${location}?id=${deviceId}`);
×
234
    setSnackbar('Link copied to clipboard');
×
235
  };
236

237
  const scrollToMonitor = () => setDetailsTab('monitor');
24✔
238

239
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
24✔
240

241
  const scrollToDeviceSystem = target => {
24✔
242
    if (target) {
×
243
      return navigate(`/devices?${target}`);
×
244
    }
245
    return setDetailsTab('device-system');
×
246
  };
247

248
  const onCloseClick = useCallback(() => {
24✔
249
    if (deviceId) {
×
250
      onClose();
×
251
    }
252
  }, [deviceId, onClose]);
253

254
  const availableTabs = tabs.reduce((accu, tab) => {
24✔
255
    if (tab.isApplicable({ device, integrations, tenantCapabilities, userCapabilities })) {
216✔
256
      accu.push(tab);
170✔
257
    }
258
    return accu;
216✔
259
  }, []);
260

261
  const { component: SelectedTab, value: selectedTab } = availableTabs.find(tab => tab.value === tabSelection) ?? tabs[0];
170✔
262

263
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
24✔
264
  const dispatchedSaveGlobalSettings = useCallback(settings => dispatch(saveGlobalSettings(settings)), [dispatch]);
24✔
265

266
  const commonProps = {
24✔
267
    classes,
268
    columnSelection,
269
    defaultConfig,
270
    device,
271
    deviceConfigDeployment,
272
    docsVersion,
273
    integrations,
274
    latestAlerts,
275
    onDecommissionDevice,
276
    saveGlobalSettings: dispatchedSaveGlobalSettings,
277
    setDetailsTab,
278
    setSnackbar: dispatchedSetSnackbar,
279
    tenantCapabilities,
280
    userCapabilities
281
  };
282
  return (
24✔
283
    <Drawer anchor="right" className="expandedDevice" open={!!deviceId} onClose={onCloseClick} PaperProps={{ style: { minWidth: '67vw' } }}>
284
      <div className="flexbox center-aligned space-between">
285
        <div className="flexbox center-aligned">
286
          <h3 className="flexbox">
287
            Device information for {<DeviceIdentityDisplay device={device} isEditable={false} hasAdornment={false} style={{ marginLeft: 4 }} />}
288
          </h3>
289
          <IconButton onClick={copyLinkToClipboard} size="large">
290
            <LinkIcon />
291
          </IconButton>
292
        </div>
293
        <div className="flexbox center-aligned">
294
          {isGateway && <GatewayNotification device={device} onClick={() => scrollToDeviceSystem()} />}
✔
295
          {!!gatewayIds.length && (
24!
296
            <GatewayConnectionNotification gatewayDevices={gatewayIds.map(gatewayId => devicesById[gatewayId])} onClick={scrollToDeviceSystem} />
×
297
          )}
298
          <div className={`${isOffline ? 'red' : 'muted'} margin-left margin-right flexbox`}>
24!
299
            <Tooltip title="The last time the device communicated with the Mender server" placement="bottom">
300
              <div className="margin-right-small">Last check-in:</div>
301
            </Tooltip>
302
            <RelativeTime updateTime={device.check_in_time} />
303
          </div>
304
          <IconButton style={{ marginLeft: 'auto' }} onClick={onCloseClick} aria-label="close" size="large">
305
            <CloseIcon />
306
          </IconButton>
307
        </div>
308
      </div>
309
      <DeviceNotifications alerts={latestAlerts} device={device} onClick={scrollToMonitor} />
310
      <Divider className={classes.dividerTop} />
311
      <Tabs value={selectedTab} onChange={(e, tab) => setDetailsTab(tab)} textColor="primary">
×
312
        {availableTabs.map(item => (
313
          <Tab key={item.value} label={item.title({ integrations })} value={item.value} />
170✔
314
        ))}
315
      </Tabs>
316
      <SelectedTab {...commonProps} />
317
      <DeviceQuickActions actionCallbacks={actionCallbacks} deviceId={device.id} selectedGroup={selectedStaticGroup} />
318
    </Drawer>
319
  );
320
};
321

322
export default ExpandedDevice;
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