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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

70.06
/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, useState } 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 { abortDeployment, getDeviceDeployments, getDeviceLog, getSingleDeployment, resetDeviceDeployments } from '../../actions/deploymentActions';
28
import {
29
  applyDeviceConfig,
30
  decommissionDevice,
31
  getDeviceInfo,
32
  getDeviceTwin,
33
  getGatewayDevices,
34
  setDeviceConfig,
35
  setDeviceTags,
36
  setDeviceTwin
37
} from '../../actions/deviceActions';
38
import { saveGlobalSettings } from '../../actions/userActions';
39
import { TIMEOUTS, yes } from '../../constants/appConstants';
40
import { DEVICE_STATES, EXTERNAL_PROVIDER } from '../../constants/deviceConstants';
41
import { getDemoDeviceAddress, stringToBoolean } from '../../helpers';
42
import {
43
  getDeviceConfigDeployment,
44
  getDeviceTwinIntegrations,
45
  getDevicesById,
46
  getDocsVersion,
47
  getGlobalSettings,
48
  getSelectedGroupInfo,
49
  getShowHelptips,
50
  getTenantCapabilities,
51
  getUserCapabilities,
52
  getUserSettings
53
} from '../../selectors';
54
import Tracking from '../../tracking';
55
import DeviceIdentityDisplay from '../common/deviceidentity';
56
import DocsLink from '../common/docslink';
57
import { MenderTooltipClickable } from '../common/mendertooltip';
58
import { RelativeTime } from '../common/time';
59
import DeviceConfiguration from './device-details/configuration';
60
import { TroubleshootTab } from './device-details/connection';
61
import Deployments from './device-details/deployments';
62
import DeviceInventory from './device-details/deviceinventory';
63
import DeviceSystem from './device-details/devicesystem';
64
import { IntegrationTab } from './device-details/devicetwin';
65
import { IdentityTab } from './device-details/identity';
66
import InstalledSoftware from './device-details/installedsoftware';
67
import MonitoringTab from './device-details/monitoring';
68
import DeviceNotifications from './device-details/notifications';
69
import DeviceQuickActions from './widgets/devicequickactions';
70

71
const useStyles = makeStyles()(theme => ({
9✔
72
  gatewayChip: {
73
    backgroundColor: theme.palette.grey[400],
74
    color: theme.palette.grey[900],
75
    path: {
76
      fill: theme.palette.grey[900]
77
    },
78
    [`.${chipClasses.icon}`]: {
79
      marginLeft: 10,
80
      width: 20
81
    },
82
    [`.${chipClasses.icon}.connected`]: {
83
      transform: 'scale(1.3)',
84
      width: 15
85
    }
86
  },
87
  deviceConnection: {
88
    marginRight: theme.spacing(2)
89
  },
90
  dividerTop: {
91
    marginBottom: theme.spacing(3),
92
    marginTop: theme.spacing(2)
93
  }
94
}));
95

96
const refreshDeviceLength = TIMEOUTS.refreshDefault;
9✔
97

98
const GatewayConnectionNotification = ({ gatewayDevices, onClick }) => {
9✔
99
  const { classes } = useStyles();
×
100

101
  const onGatewayClick = () => {
×
102
    const query =
103
      gatewayDevices.length > 1 ? gatewayDevices.map(device => `id=${device.id}`).join('&') : `id=${gatewayDevices[0].id}&open=true&tab=device-system`;
×
104
    onClick(query);
×
105
  };
106

107
  return (
×
108
    <MenderTooltipClickable
109
      placement="bottom"
110
      title={
111
        <div style={{ maxWidth: 350 }}>
112
          Connected to{' '}
113
          {gatewayDevices.length > 1 ? 'multiple devices' : <DeviceIdentityDisplay device={gatewayDevices[0]} isEditable={false} hasAdornment={false} />}
×
114
        </div>
115
      }
116
    >
117
      <Chip className={classes.gatewayChip} icon={<GatewayConnectionIcon className="connected" />} label="Connected to gateway" onClick={onGatewayClick} />
118
    </MenderTooltipClickable>
119
  );
120
};
121

122
const GatewayNotification = ({ device, onClick }) => {
9✔
123
  const ipAddress = getDemoDeviceAddress([device]);
1✔
124
  const { classes } = useStyles();
1✔
125
  return (
1✔
126
    <MenderTooltipClickable
127
      placement="bottom"
128
      title={
129
        <div style={{ maxWidth: 350 }}>
130
          For information about connecting other devices to this gateway, please refer to the{' '}
131
          <DocsLink path="get-started/mender-gateway" title="Mender Gateway documentation" />. This device is reachable via <i>{ipAddress}</i>.
132
        </div>
133
      }
134
    >
135
      <Chip className={classes.gatewayChip} icon={<GatewayIcon />} label="Gateway" onClick={onClick} />
136
    </MenderTooltipClickable>
137
  );
138
};
139

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

142
const tabs = [
9✔
143
  { component: IdentityTab, title: () => 'Identity', value: 'identity', isApplicable: yes },
24✔
144
  {
145
    component: DeviceInventory,
146
    title: () => 'Inventory',
24✔
147
    value: 'inventory',
148
    isApplicable: deviceStatusCheck
149
  },
150
  {
151
    component: InstalledSoftware,
152
    title: () => 'Software',
24✔
153
    value: 'software',
154
    isApplicable: deviceStatusCheck
155
  },
156
  {
157
    component: Deployments,
158
    title: () => 'Deployments',
24✔
159
    value: 'deployments',
160
    isApplicable: deviceStatusCheck
161
  },
162
  {
163
    component: DeviceConfiguration,
164
    title: () => 'Configuration',
19✔
165
    value: 'configuration',
166
    isApplicable: ({ tenantCapabilities: { hasDeviceConfig }, userCapabilities: { canConfigure }, ...rest }) =>
167
      hasDeviceConfig && canConfigure && deviceStatusCheck(rest, [DEVICE_STATES.accepted, DEVICE_STATES.preauth])
24✔
168
  },
169
  {
170
    component: MonitoringTab,
171
    title: () => 'Monitoring',
19✔
172
    value: 'monitor',
173
    isApplicable: ({ tenantCapabilities: { hasMonitor }, ...rest }) => deviceStatusCheck(rest) && hasMonitor
24✔
174
  },
175
  {
176
    component: TroubleshootTab,
177
    title: () => 'Troubleshooting',
19✔
178
    value: 'troubleshoot',
179
    isApplicable: ({ tenantCapabilities: { hasDeviceConnect }, ...rest }) => deviceStatusCheck(rest) && hasDeviceConnect
24✔
180
  },
181
  {
182
    component: IntegrationTab,
183
    title: ({ integrations }) => {
184
      if (integrations.length > 1) {
1!
185
        return 'Device Twin';
×
186
      }
187
      const { title, twinTitle } = EXTERNAL_PROVIDER[integrations[0].provider];
1✔
188
      return `${title} ${twinTitle}`;
1✔
189
    },
190
    value: 'device-twin',
191
    isApplicable: ({ integrations, ...rest }) => !!integrations.length && deviceStatusCheck(rest, [DEVICE_STATES.accepted, DEVICE_STATES.preauth])
24✔
192
  },
193
  {
194
    component: DeviceSystem,
195
    title: () => 'System',
1✔
196
    value: 'system',
197
    isApplicable: ({ device: { attributes = {} } }) => stringToBoolean(attributes?.mender_is_gateway ?? '')
24✔
198
  }
199
];
200

201
export const ExpandedDevice = ({ actionCallbacks, deviceId, onClose, setDetailsTab, tabSelection }) => {
9✔
202
  const [socketClosed, setSocketClosed] = useState(true);
24✔
203
  const [troubleshootType, setTroubleshootType] = useState();
24✔
204
  const timer = useRef();
24✔
205
  const navigate = useNavigate();
24✔
206
  const { classes } = useStyles();
24✔
207

208
  const { latest: latestAlerts = [] } = useSelector(state => state.monitor.alerts.byDeviceId[deviceId]) || {};
53✔
209
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
24!
210
  const { columnSelection = [] } = useSelector(getUserSettings);
24!
211
  const { defaultDeviceConfig: defaultConfig } = useSelector(getGlobalSettings);
24✔
212
  const { device, deviceConfigDeployment } = useSelector(state => getDeviceConfigDeployment(state, deviceId));
53✔
213
  const devicesById = useSelector(getDevicesById);
24✔
214
  const docsVersion = useSelector(getDocsVersion);
24✔
215
  const integrations = useSelector(getDeviceTwinIntegrations);
24✔
216
  const showHelptips = useSelector(getShowHelptips);
24✔
217
  const tenantCapabilities = useSelector(getTenantCapabilities);
24✔
218
  const userCapabilities = useSelector(getUserCapabilities);
24✔
219
  const dispatch = useDispatch();
24✔
220

221
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
24✔
222
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
24✔
223
  const isGateway = stringToBoolean(mender_is_gateway);
24✔
224

225
  const { hasAuditlogs } = tenantCapabilities;
24✔
226

227
  useEffect(() => {
24✔
228
    if (!deviceId) {
4✔
229
      return;
3✔
230
    }
231
    clearInterval(timer.current);
1✔
232
    timer.current = setInterval(() => dispatch(getDeviceInfo(deviceId)), refreshDeviceLength);
1✔
233
    dispatch(getDeviceInfo(deviceId));
1✔
234
    return () => {
1✔
235
      clearInterval(timer.current);
1✔
236
    };
237
  }, [deviceId, device.status]);
238

239
  useEffect(() => {
24✔
240
    if (!(device.id && mender_gateway_system_id)) {
4!
241
      return;
4✔
242
    }
243
    dispatch(getGatewayDevices(device.id));
×
244
  }, [device.id, mender_gateway_system_id]);
245

246
  // close expanded device
247
  const onDecommissionDevice = device_id => dispatch(decommissionDevice(device_id)).finally(onClose);
24✔
248

249
  const launchTroubleshoot = type => {
24✔
250
    Tracking.event({ category: 'devices', action: 'open_terminal' });
×
251
    setSocketClosed(false);
×
252
    setTroubleshootType(type);
×
253
  };
254

255
  const copyLinkToClipboard = () => {
24✔
256
    const location = window.location.href.substring(0, window.location.href.indexOf('/devices') + '/devices'.length);
×
257
    copy(`${location}?id=${deviceId}`);
×
258
    setSnackbar('Link copied to clipboard');
×
259
  };
260

261
  const scrollToMonitor = () => setDetailsTab('monitor');
24✔
262

263
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
24✔
264

265
  const scrollToDeviceSystem = target => {
24✔
266
    if (target) {
×
267
      return navigate(`/devices?${target}`);
×
268
    }
269
    return setDetailsTab('device-system');
×
270
  };
271

272
  const onCloseClick = useCallback(() => {
24✔
273
    if (deviceId) {
×
274
      onClose();
×
275
    }
276
  }, [deviceId, onClose]);
277

278
  const availableTabs = tabs.reduce((accu, tab) => {
24✔
279
    if (tab.isApplicable({ device, integrations, tenantCapabilities, userCapabilities })) {
216✔
280
      accu.push(tab);
155✔
281
    }
282
    return accu;
216✔
283
  }, []);
284

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

287
  const commonProps = {
24✔
288
    abortDeployment: id => dispatch(abortDeployment(id)),
×
289
    applyDeviceConfig: (...args) => dispatch(applyDeviceConfig(...args)),
×
290
    classes,
291
    columnSelection,
292
    defaultConfig,
293
    device,
294
    deviceConfigDeployment,
295
    docsVersion,
296
    getDeviceDeployments: (...args) => dispatch(getDeviceDeployments(...args)),
×
297
    getDeviceLog: (...args) => dispatch(getDeviceLog(...args)),
×
298
    getDeviceTwin: (...args) => dispatch(getDeviceTwin(...args)),
×
299
    getSingleDeployment: id => dispatch(getSingleDeployment(id)),
×
300
    integrations,
301
    latestAlerts,
302
    launchTroubleshoot,
303
    onDecommissionDevice,
304
    resetDeviceDeployments: id => dispatch(resetDeviceDeployments(id)),
×
305
    saveGlobalSettings: settings => dispatch(saveGlobalSettings(settings)),
×
306
    setDetailsTab,
307
    setDeviceConfig: (...args) => dispatch(setDeviceConfig(...args)),
×
308
    setDeviceTags: (...args) => dispatch(setDeviceTags(...args)),
×
309
    setDeviceTwin: (...args) => dispatch(setDeviceTwin(...args)),
×
310
    setSnackbar: (...args) => dispatch(setSnackbar(...args)),
×
311
    setSocketClosed,
312
    setTroubleshootType,
313
    showHelptips,
314
    socketClosed,
315
    tenantCapabilities: { hasAuditlogs },
316
    troubleshootType,
317
    userCapabilities
318
  };
319
  return (
24✔
320
    <Drawer anchor="right" className="expandedDevice" open={!!deviceId} onClose={onCloseClick} PaperProps={{ style: { minWidth: '67vw' } }}>
321
      <div className="flexbox center-aligned space-between">
322
        <div className="flexbox center-aligned">
323
          <h3 className="flexbox">
324
            Device information for {<DeviceIdentityDisplay device={device} isEditable={false} hasAdornment={false} style={{ marginLeft: 4 }} />}
325
          </h3>
326
          <IconButton onClick={copyLinkToClipboard} size="large">
327
            <LinkIcon />
328
          </IconButton>
329
        </div>
330
        <div className="flexbox center-aligned">
331
          {isGateway && <GatewayNotification device={device} onClick={() => scrollToDeviceSystem()} />}
✔
332
          {!!gatewayIds.length && (
24!
333
            <GatewayConnectionNotification gatewayDevices={gatewayIds.map(gatewayId => devicesById[gatewayId])} onClick={scrollToDeviceSystem} />
×
334
          )}
335
          <div className={`${isOffline ? 'red' : 'muted'} margin-left margin-right flexbox`}>
24!
336
            <Tooltip title="The last time the device communicated with the Mender server" placement="bottom">
337
              <div className="margin-right-small">Last check-in:</div>
338
            </Tooltip>
339
            <RelativeTime updateTime={device.updated_ts} />
340
          </div>
341
          <IconButton style={{ marginLeft: 'auto' }} onClick={onCloseClick} aria-label="close" size="large">
342
            <CloseIcon />
343
          </IconButton>
344
        </div>
345
      </div>
346
      <DeviceNotifications alerts={latestAlerts} device={device} isOffline={isOffline} onClick={scrollToMonitor} />
347
      <Divider className={classes.dividerTop} />
348
      <Tabs value={selectedTab} onChange={(e, tab) => setDetailsTab(tab)} textColor="primary">
×
349
        {availableTabs.map(item => (
350
          <Tab key={item.value} label={item.title({ integrations })} value={item.value} />
155✔
351
        ))}
352
      </Tabs>
353
      <SelectedTab {...commonProps} />
354
      <DeviceQuickActions actionCallbacks={actionCallbacks} deviceId={device.id} selectedGroup={selectedStaticGroup} />
355
    </Drawer>
356
  );
357
};
358

359
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