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

mendersoftware / gui / 1015445845

25 Sep 2023 09:43AM UTC coverage: 82.537% (-17.4%) from 99.964%
1015445845

Pull #4028

gitlab-ci

mzedel
chore: aligned release retrieval with v2 api models

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

4355 of 6315 branches covered (0.0%)

184 of 206 new or added lines in 19 files covered. (89.32%)

1724 existing lines in 164 files now uncovered.

8323 of 10084 relevant lines covered (82.54%)

208.49 hits per line

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

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

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

85
const refreshDeviceLength = TIMEOUTS.refreshDefault;
9✔
86

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

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

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

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

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

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

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

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

209
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
24✔
210
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
24✔
211
  const isGateway = stringToBoolean(mender_is_gateway);
24✔
212

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

225
  useEffect(() => {
24✔
226
    if (!(device.id && mender_gateway_system_id)) {
4!
227
      return;
4✔
228
    }
UNCOV
229
    dispatch(getGatewayDevices(device.id));
×
230
  }, [device.id, dispatch, mender_gateway_system_id]);
231

232
  useEffect(() => {
24✔
233
    if (!socketClosed) {
4!
234
      return;
4✔
235
    }
UNCOV
236
    clearTimeout(closeTimer.current);
×
UNCOV
237
    closeTimer.current = setTimeout(() => setSocketClosed(false), TIMEOUTS.fiveSeconds);
×
238
  }, [socketClosed]);
239

240
  // close expanded device
241
  const onDecommissionDevice = device_id => dispatch(decommissionDevice(device_id)).finally(onClose);
24✔
242

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

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

255
  const scrollToMonitor = () => setDetailsTab('monitor');
24✔
256

257
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
24✔
258

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

266
  const onCloseClick = useCallback(() => {
24✔
UNCOV
267
    if (deviceId) {
×
UNCOV
268
      onClose();
×
269
    }
270
  }, [deviceId, onClose]);
271

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

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

281
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
24✔
282
  const dispatchedSaveGlobalSettings = useCallback(settings => dispatch(saveGlobalSettings(settings)), [dispatch]);
24✔
283

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

345
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