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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

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

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

75.48
/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 { 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
  getShowHelptips,
40
  getTenantCapabilities,
41
  getUserCapabilities,
42
  getUserSettings
43
} from '../../selectors';
44
import Tracking from '../../tracking';
45
import DeviceIdentityDisplay from '../common/deviceidentity';
46
import DocsLink from '../common/docslink';
47
import { MenderTooltipClickable } from '../common/mendertooltip';
48
import { RelativeTime } from '../common/time';
49
import DeviceConfiguration from './device-details/configuration';
50
import { TroubleshootTab } from './device-details/connection';
51
import Deployments from './device-details/deployments';
52
import DeviceInventory from './device-details/deviceinventory';
53
import DeviceSystem from './device-details/devicesystem';
54
import { IntegrationTab } from './device-details/devicetwin';
55
import { IdentityTab } from './device-details/identity';
56
import InstalledSoftware from './device-details/installedsoftware';
57
import MonitoringTab from './device-details/monitoring';
58
import DeviceNotifications from './device-details/notifications';
59
import DeviceQuickActions from './widgets/devicequickactions';
60

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

86
const refreshDeviceLength = TIMEOUTS.refreshDefault;
9✔
87

88
const GatewayConnectionNotification = ({ gatewayDevices, onClick }) => {
9✔
UNCOV
89
  const { classes } = useStyles();
×
90

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

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

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

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

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

191
export const ExpandedDevice = ({ actionCallbacks, deviceId, onClose, setDetailsTab, tabSelection }) => {
9✔
192
  const [socketClosed, setSocketClosed] = useState();
25✔
193
  const [troubleshootType, setTroubleshootType] = useState();
25✔
194
  const timer = useRef();
25✔
195
  const navigate = useNavigate();
25✔
196
  const { classes } = useStyles();
25✔
197
  const closeTimer = useRef();
25✔
198

199
  const { latest: latestAlerts = [] } = useSelector(state => state.monitor.alerts.byDeviceId[deviceId]) || {};
52✔
200
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
25!
201
  const { columnSelection = [] } = useSelector(getUserSettings);
25!
202
  const { defaultDeviceConfig: defaultConfig } = useSelector(getGlobalSettings);
25✔
203
  const { device, deviceConfigDeployment } = useSelector(state => getDeviceConfigDeployment(state, deviceId));
52✔
204
  const devicesById = useSelector(getDevicesById);
25✔
205
  const docsVersion = useSelector(getDocsVersion);
25✔
206
  const integrations = useSelector(getDeviceTwinIntegrations);
25✔
207
  const showHelptips = useSelector(getShowHelptips);
25✔
208
  const tenantCapabilities = useSelector(getTenantCapabilities);
25✔
209
  const userCapabilities = useSelector(getUserCapabilities);
25✔
210
  const dispatch = useDispatch();
25✔
211

212
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
25✔
213
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
25✔
214
  const isGateway = stringToBoolean(mender_is_gateway);
25✔
215

216
  const { hasAuditlogs } = tenantCapabilities;
25✔
217

218
  useEffect(() => {
25✔
219
    if (!deviceId) {
4✔
220
      return;
3✔
221
    }
222
    clearInterval(timer.current);
1✔
223
    timer.current = setInterval(() => dispatch(getDeviceInfo(deviceId)), refreshDeviceLength);
1✔
224
    dispatch(getDeviceInfo(deviceId));
1✔
225
    return () => {
1✔
226
      clearInterval(timer.current);
1✔
227
    };
228
  }, [deviceId, device.status, dispatch]);
229

230
  useEffect(() => {
25✔
231
    if (!(device.id && mender_gateway_system_id)) {
4!
232
      return;
4✔
233
    }
UNCOV
234
    dispatch(getGatewayDevices(device.id));
×
235
  }, [device.id, dispatch, mender_gateway_system_id]);
236

237
  useEffect(() => {
25✔
238
    if (!socketClosed) {
4!
239
      return;
4✔
240
    }
UNCOV
241
    clearTimeout(closeTimer.current);
×
UNCOV
242
    closeTimer.current = setTimeout(() => setSocketClosed(false), TIMEOUTS.fiveSeconds);
×
243
  }, [socketClosed]);
244

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

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

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

260
  const scrollToMonitor = () => setDetailsTab('monitor');
25✔
261

262
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
25✔
263

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

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

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

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

286
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
25✔
287
  const dispatchedSaveGlobalSettings = useCallback(settings => dispatch(saveGlobalSettings(settings)), [dispatch]);
25✔
288

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

351
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