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

mendersoftware / gui / 944676341

pending completion
944676341

Pull #3875

gitlab-ci

mzedel
chore: aligned snapshots with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3875: MEN-5414

4469 of 6446 branches covered (69.33%)

230 of 266 new or added lines in 43 files covered. (86.47%)

1712 existing lines in 161 files now uncovered.

8406 of 10170 relevant lines covered (82.65%)

196.7 hits per line

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

68.46
/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
  getFeatures,
48
  getGlobalSettings,
49
  getSelectedGroupInfo,
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✔
UNCOV
99
  const { classes } = useStyles();
×
100

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

UNCOV
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);
145✔
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',
24✔
165
    value: 'configuration',
166
    isApplicable: ({ userCapabilities: { canConfigure }, ...rest }) => canConfigure && deviceStatusCheck(rest, [DEVICE_STATES.accepted, DEVICE_STATES.preauth])
24✔
167
  },
168
  {
169
    component: MonitoringTab,
170
    title: () => 'Monitoring',
24✔
171
    value: 'monitor',
172
    isApplicable: deviceStatusCheck
173
  },
174
  {
175
    component: TroubleshootTab,
176
    title: () => 'Troubleshooting',
24✔
177
    value: 'troubleshoot',
178
    isApplicable: deviceStatusCheck
179
  },
180
  {
181
    component: IntegrationTab,
182
    title: ({ integrations }) => {
183
      if (integrations.length > 1) {
1!
UNCOV
184
        return 'Device Twin';
×
185
      }
186
      const { title, twinTitle } = EXTERNAL_PROVIDER[integrations[0].provider];
1✔
187
      return `${title} ${twinTitle}`;
1✔
188
    },
189
    value: 'device-twin',
190
    isApplicable: ({ integrations, ...rest }) => !!integrations.length && deviceStatusCheck(rest, [DEVICE_STATES.accepted, DEVICE_STATES.preauth])
24✔
191
  },
192
  {
193
    component: DeviceSystem,
194
    title: () => 'System',
1✔
195
    value: 'system',
196
    isApplicable: ({ device: { attributes = {} } }) => stringToBoolean(attributes?.mender_is_gateway ?? '')
24✔
197
  }
198
];
199

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

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

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

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

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

243
  // close expanded device
244
  const onDecommissionDevice = device_id => dispatch(decommissionDevice(device_id)).finally(onClose);
24✔
245

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

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

258
  const scrollToMonitor = () => setDetailsTab('monitor');
24✔
259

260
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
24✔
261

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

269
  const onCloseClick = useCallback(() => {
24✔
UNCOV
270
    if (deviceId) {
×
UNCOV
271
      onClose();
×
272
    }
273
  }, [deviceId, onClose]);
274

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

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

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

364
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