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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

75.95
/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 { connect } 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, 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 } from '../../constants/appConstants';
40
import { DEVICE_STATES, EXTERNAL_PROVIDER } from '../../constants/deviceConstants';
41
import { getDemoDeviceAddress, stringToBoolean } from '../../helpers';
42
import {
43
  getDeviceTwinIntegrations,
44
  getDocsVersion,
45
  getFeatures,
46
  getIdAttribute,
47
  getTenantCapabilities,
48
  getUserCapabilities,
49
  getUserSettings
50
} from '../../selectors';
51
import Tracking from '../../tracking';
52
import DeviceIdentityDisplay from '../common/deviceidentity';
53
import { MenderTooltipClickable } from '../common/mendertooltip';
54
import { RelativeTime } from '../common/time';
55
import DeviceConfiguration from './device-details/configuration';
56
import { TroubleshootTab } from './device-details/connection';
57
import Deployments from './device-details/deployments';
58
import DeviceInventory from './device-details/deviceinventory';
59
import DeviceSystem from './device-details/devicesystem';
60
import { IntegrationTab } from './device-details/devicetwin';
61
import { IdentityTab } from './device-details/identity';
62
import InstalledSoftware from './device-details/installedsoftware';
63
import MonitoringTab from './device-details/monitoring';
64
import DeviceNotifications from './device-details/notifications';
65
import DeviceQuickActions from './widgets/devicequickactions';
66

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

92
const refreshDeviceLength = TIMEOUTS.refreshDefault;
9✔
93

94
const GatewayConnectionNotification = ({ gatewayDevices, idAttribute, onClick }) => {
9✔
95
  const { classes } = useStyles();
×
96

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

103
  return (
×
104
    <MenderTooltipClickable
105
      placement="bottom"
106
      title={
107
        <div style={{ maxWidth: 350 }}>
108
          Connected to{' '}
109
          {gatewayDevices.length > 1 ? (
×
110
            'multiple devices'
111
          ) : (
112
            <DeviceIdentityDisplay device={gatewayDevices[0]} idAttribute={idAttribute} isEditable={false} hasAdornment={false} />
113
          )}
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, docsVersion, 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
          <a href={`https://docs.mender.io/${docsVersion}get-started/mender-gateway`} target="_blank" rel="noopener noreferrer">
132
            Mender Gateway documentation
133
          </a>
134
          . This device is reachable via <i>{ipAddress}</i>.
135
        </div>
136
      }
137
    >
138
      <Chip className={classes.gatewayChip} icon={<GatewayIcon />} label="Gateway" onClick={onClick} />
139
    </MenderTooltipClickable>
140
  );
141
};
142

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

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

204
export const ExpandedDevice = ({
9✔
205
  abortDeployment,
206
  actionCallbacks,
207
  applyDeviceConfig,
208
  columnSelection,
209
  decommissionDevice,
210
  defaultConfig,
211
  device,
212
  deviceConfigDeployment,
213
  deviceId,
214
  devicesById,
215
  docsVersion,
216
  features,
217
  getDeviceDeployments,
218
  getDeviceInfo,
219
  getDeviceLog,
220
  getDeviceTwin,
221
  getGatewayDevices,
222
  getSingleDeployment,
223
  groupFilters,
224
  idAttribute,
225
  integrations,
226
  latestAlerts,
227
  onClose,
228
  refreshDevices,
229
  resetDeviceDeployments,
230
  saveGlobalSettings,
231
  selectedGroup,
232
  setDetailsTab,
233
  setDeviceConfig,
234
  setDeviceTags,
235
  setDeviceTwin,
236
  setSnackbar,
237
  showHelptips,
238
  tabSelection,
239
  tenantCapabilities,
240
  userCapabilities
241
}) => {
242
  const [socketClosed, setSocketClosed] = useState(true);
11✔
243
  const [troubleshootType, setTroubleshootType] = useState();
11✔
244
  const timer = useRef();
11✔
245
  const navigate = useNavigate();
11✔
246
  const { classes } = useStyles();
11✔
247

248
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
11✔
249
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
11✔
250
  const isGateway = stringToBoolean(mender_is_gateway);
11✔
251

252
  const { hasAuditlogs } = tenantCapabilities;
11✔
253

254
  useEffect(() => {
11✔
255
    if (!deviceId) {
4✔
256
      return;
3✔
257
    }
258
    clearInterval(timer.current);
1✔
259
    timer.current = setInterval(() => getDeviceInfo(deviceId), refreshDeviceLength);
1✔
260
    getDeviceInfo(deviceId);
1✔
261
    return () => {
1✔
262
      clearInterval(timer.current);
1✔
263
    };
264
  }, [deviceId, device.status]);
265

266
  useEffect(() => {
11✔
267
    if (!(device.id && mender_gateway_system_id)) {
4!
268
      return;
4✔
269
    }
270
    getGatewayDevices(device.id);
×
271
  }, [device.id, mender_gateway_system_id]);
272

273
  const onDecommissionDevice = device_id => {
11✔
274
    // close dialog!
275
    // close expanded device
276
    // trigger reset of list!
277
    return decommissionDevice(device_id).finally(() => {
×
278
      refreshDevices();
×
279
      onClose();
×
280
    });
281
  };
282

283
  const launchTroubleshoot = type => {
11✔
284
    Tracking.event({ category: 'devices', action: 'open_terminal' });
×
285
    setSocketClosed(false);
×
286
    setTroubleshootType(type);
×
287
  };
288

289
  const copyLinkToClipboard = () => {
11✔
290
    const location = window.location.href.substring(0, window.location.href.indexOf('/devices') + '/devices'.length);
×
291
    copy(`${location}?id=${deviceId}`);
×
292
    setSnackbar('Link copied to clipboard');
×
293
  };
294

295
  const scrollToMonitor = () => setDetailsTab('monitor');
11✔
296

297
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
11✔
298

299
  const scrollToDeviceSystem = target => {
11✔
300
    if (target) {
×
301
      return navigate(`/devices?${target}`);
×
302
    }
303
    return setDetailsTab('device-system');
×
304
  };
305

306
  const onCloseClick = useCallback(() => {
11✔
307
    if (deviceId) {
×
308
      onClose();
×
309
    }
310
  }, [deviceId, onClose]);
311

312
  const availableTabs = tabs.reduce((accu, tab) => {
11✔
313
    if (tab.isApplicable({ device, integrations, tenantCapabilities, userCapabilities })) {
99✔
314
      accu.push(tab);
49✔
315
    }
316
    return accu;
99✔
317
  }, []);
318

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

321
  const commonProps = {
11✔
322
    abortDeployment,
323
    applyDeviceConfig,
324
    classes,
325
    columnSelection,
326
    defaultConfig,
327
    device,
328
    deviceConfigDeployment,
329
    docsVersion,
330
    getDeviceDeployments,
331
    getDeviceLog,
332
    getDeviceTwin,
333
    getSingleDeployment,
334
    integrations,
335
    latestAlerts,
336
    launchTroubleshoot,
337
    onDecommissionDevice,
338
    refreshDevices,
339
    resetDeviceDeployments,
340
    saveGlobalSettings,
341
    setDetailsTab,
342
    setDeviceConfig,
343
    setDeviceTags,
344
    setDeviceTwin,
345
    setSnackbar,
346
    setSocketClosed,
347
    setTroubleshootType,
348
    showHelptips,
349
    socketClosed,
350
    tenantCapabilities: { hasAuditlogs },
351
    troubleshootType,
352
    userCapabilities
353
  };
354
  return (
11✔
355
    <Drawer anchor="right" className="expandedDevice" open={!!deviceId} onClose={onCloseClick} PaperProps={{ style: { minWidth: '67vw' } }}>
356
      <div className="flexbox center-aligned space-between">
357
        <div className="flexbox center-aligned">
358
          <h3 className="flexbox">
359
            Device information for{' '}
360
            {<DeviceIdentityDisplay device={device} idAttribute={idAttribute} isEditable={false} hasAdornment={false} style={{ marginLeft: 4 }} />}
361
          </h3>
362
          <IconButton onClick={copyLinkToClipboard} size="large">
363
            <LinkIcon />
364
          </IconButton>
365
        </div>
366
        <div className="flexbox center-aligned">
367
          {isGateway && <GatewayNotification device={device} docsVersion={docsVersion} onClick={() => scrollToDeviceSystem()} />}
✔
368
          {!!gatewayIds.length && (
11!
369
            <GatewayConnectionNotification
370
              gatewayDevices={gatewayIds.map(gatewayId => devicesById[gatewayId])}
×
371
              idAttribute={idAttribute}
372
              onClick={scrollToDeviceSystem}
373
            />
374
          )}
375
          <div className={`${isOffline ? 'red' : 'muted'} margin-left margin-right flexbox`}>
11!
376
            <div className="margin-right-small">Last check-in:</div>
377
            <RelativeTime updateTime={device.updated_ts} />
378
          </div>
379
          <IconButton style={{ marginLeft: 'auto' }} onClick={onCloseClick} aria-label="close" size="large">
380
            <CloseIcon />
381
          </IconButton>
382
        </div>
383
      </div>
384
      <DeviceNotifications alerts={latestAlerts} device={device} isOffline={isOffline} onClick={scrollToMonitor} />
385
      <Divider className={classes.dividerTop} />
386
      <Tabs value={selectedTab} onChange={(e, tab) => setDetailsTab(tab)} textColor="primary">
×
387
        {availableTabs.map(item => (
388
          <Tab key={item.value} label={item.title({ integrations })} value={item.value} />
49✔
389
        ))}
390
      </Tabs>
391
      <SelectedTab {...commonProps} />
392
      <DeviceQuickActions
393
        actionCallbacks={actionCallbacks}
394
        devices={[device]}
395
        features={features}
396
        isSingleDevice
397
        selectedGroup={selectedStaticGroup}
398
        selectedRows={[0]}
399
        tenantCapabilities={tenantCapabilities}
400
        userCapabilities={userCapabilities}
401
      />
402
    </Drawer>
403
  );
404
};
405

406
const actionCreators = {
9✔
407
  abortDeployment,
408
  applyDeviceConfig,
409
  decommissionDevice,
410
  getDeviceDeployments,
411
  getDeviceLog,
412
  getDeviceInfo,
413
  getDeviceTwin,
414
  getGatewayDevices,
415
  getSingleDeployment,
416
  resetDeviceDeployments,
417
  saveGlobalSettings,
418
  setDeviceConfig,
419
  setDeviceTags,
420
  setDeviceTwin,
421
  setSnackbar
422
};
423

424
const mapStateToProps = (state, ownProps) => {
9✔
425
  const device = state.devices.byId[ownProps.deviceId] || {};
11✔
426
  const { config = {} } = device;
11✔
427
  const { deployment_id: configDeploymentId } = config;
11✔
428
  const { latest = [] } = state.monitor.alerts.byDeviceId[ownProps.deviceId] || {};
11✔
429
  let selectedGroup;
430
  let groupFilters = [];
11✔
431
  if (state.devices.groups.selectedGroup && state.devices.groups.byId[state.devices.groups.selectedGroup]) {
11✔
432
    selectedGroup = state.devices.groups.selectedGroup;
3✔
433
    groupFilters = state.devices.groups.byId[selectedGroup].filters || [];
3!
434
  }
435
  const { columnSelection = [] } = getUserSettings(state);
11!
436
  return {
11✔
437
    alertListState: state.monitor.alerts.alertList,
438
    columnSelection,
439
    defaultConfig: state.users.globalSettings.defaultDeviceConfig,
440
    device,
441
    deviceConfigDeployment: state.deployments.byId[configDeploymentId] || {},
22✔
442
    devicesById: state.devices.byId,
443
    docsVersion: getDocsVersion(state),
444
    features: getFeatures(state),
445
    groupFilters,
446
    idAttribute: getIdAttribute(state),
447
    integrations: getDeviceTwinIntegrations(state),
448
    latestAlerts: latest,
449
    onboardingComplete: state.onboarding.complete,
450
    selectedGroup,
451
    showHelptips: state.users.showHelptips,
452
    tenantCapabilities: getTenantCapabilities(state),
453
    userCapabilities: getUserCapabilities(state)
454
  };
455
};
456

457
export default connect(mapStateToProps, actionCreators)(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