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

mendersoftware / gui / 913068021

pending completion
913068021

Pull #3802

gitlab-ci

mzedel
chore: made tags retrieval optional to ease image startup without connectivity or in non-hosted environments

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3802: Offline container startup support

4408 of 6421 branches covered (68.65%)

8338 of 10131 relevant lines covered (82.3%)

145.0 hits per line

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

70.93
/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, 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
  getDeviceTwinIntegrations,
44
  getDevicesById,
45
  getDocsVersion,
46
  getFeatures,
47
  getGlobalSettings,
48
  getIdAttribute,
49
  getSelectedGroupInfo,
50
  getShowHelptips,
51
  getTenantCapabilities,
52
  getUserCapabilities,
53
  getUserSettings
54
} from '../../selectors';
55
import Tracking from '../../tracking';
56
import DeviceIdentityDisplay from '../common/deviceidentity';
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, idAttribute, 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 ? (
×
114
            'multiple devices'
115
          ) : (
116
            <DeviceIdentityDisplay device={gatewayDevices[0]} idAttribute={idAttribute} isEditable={false} hasAdornment={false} />
117
          )}
118
        </div>
119
      }
120
    >
121
      <Chip className={classes.gatewayChip} icon={<GatewayConnectionIcon className="connected" />} label="Connected to gateway" onClick={onGatewayClick} />
122
    </MenderTooltipClickable>
123
  );
124
};
125

126
const GatewayNotification = ({ device, docsVersion, onClick }) => {
9✔
127
  const ipAddress = getDemoDeviceAddress([device]);
1✔
128
  const { classes } = useStyles();
1✔
129
  return (
1✔
130
    <MenderTooltipClickable
131
      placement="bottom"
132
      title={
133
        <div style={{ maxWidth: 350 }}>
134
          For information about connecting other devices to this gateway, please refer to the{' '}
135
          <a href={`https://docs.mender.io/${docsVersion}get-started/mender-gateway`} target="_blank" rel="noopener noreferrer">
136
            Mender Gateway documentation
137
          </a>
138
          . This device is reachable via <i>{ipAddress}</i>.
139
        </div>
140
      }
141
    >
142
      <Chip className={classes.gatewayChip} icon={<GatewayIcon />} label="Gateway" onClick={onClick} />
143
    </MenderTooltipClickable>
144
  );
145
};
146

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

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

208
export const ExpandedDevice = ({ actionCallbacks, deviceId, onClose, refreshDevices, setDetailsTab, tabSelection }) => {
9✔
209
  const [socketClosed, setSocketClosed] = useState(true);
11✔
210
  const [troubleshootType, setTroubleshootType] = useState();
11✔
211
  const timer = useRef();
11✔
212
  const navigate = useNavigate();
11✔
213
  const { classes } = useStyles();
11✔
214

215
  const { latest: latestAlerts = [] } = useSelector(state => state.monitor.alerts.byDeviceId[deviceId]) || {};
11✔
216
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
11!
217
  const { columnSelection = [] } = useSelector(getUserSettings);
11!
218
  const { defaultDeviceConfig: defaultConfig } = useSelector(getGlobalSettings);
11✔
219
  const { device, deviceConfigDeployment } = useSelector(state => {
11✔
220
    const device = state.devices.byId[deviceId] || {};
11✔
221
    const { config = {} } = device;
11✔
222
    const { deployment_id: configDeploymentId } = config;
11✔
223
    const deviceConfigDeployment = state.deployments.byId[configDeploymentId] || {};
11✔
224
    return { device, deviceConfigDeployment };
11✔
225
  });
226
  const devicesById = useSelector(getDevicesById);
11✔
227
  const docsVersion = useSelector(getDocsVersion);
11✔
228
  const features = useSelector(getFeatures);
11✔
229
  const idAttribute = useSelector(getIdAttribute);
11✔
230
  const integrations = useSelector(getDeviceTwinIntegrations);
11✔
231
  const showHelptips = useSelector(getShowHelptips);
11✔
232
  const tenantCapabilities = useSelector(getTenantCapabilities);
11✔
233
  const userCapabilities = useSelector(getUserCapabilities);
11✔
234
  const dispatch = useDispatch();
11✔
235

236
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
11✔
237
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
11✔
238
  const isGateway = stringToBoolean(mender_is_gateway);
11✔
239

240
  const { hasAuditlogs } = tenantCapabilities;
11✔
241

242
  useEffect(() => {
11✔
243
    if (!deviceId) {
4✔
244
      return;
3✔
245
    }
246
    clearInterval(timer.current);
1✔
247
    timer.current = setInterval(() => dispatch(getDeviceInfo(deviceId)), refreshDeviceLength);
1✔
248
    dispatch(getDeviceInfo(deviceId));
1✔
249
    return () => {
1✔
250
      clearInterval(timer.current);
1✔
251
    };
252
  }, [deviceId, device.status]);
253

254
  useEffect(() => {
11✔
255
    if (!(device.id && mender_gateway_system_id)) {
4!
256
      return;
4✔
257
    }
258
    dispatch(getGatewayDevices(device.id));
×
259
  }, [device.id, mender_gateway_system_id]);
260

261
  const onDecommissionDevice = device_id => {
11✔
262
    // close dialog!
263
    // close expanded device
264
    // trigger reset of list!
265
    return dispatch(decommissionDevice(device_id)).finally(() => {
×
266
      refreshDevices();
×
267
      onClose();
×
268
    });
269
  };
270

271
  const launchTroubleshoot = type => {
11✔
272
    Tracking.event({ category: 'devices', action: 'open_terminal' });
×
273
    setSocketClosed(false);
×
274
    setTroubleshootType(type);
×
275
  };
276

277
  const copyLinkToClipboard = () => {
11✔
278
    const location = window.location.href.substring(0, window.location.href.indexOf('/devices') + '/devices'.length);
×
279
    copy(`${location}?id=${deviceId}`);
×
280
    setSnackbar('Link copied to clipboard');
×
281
  };
282

283
  const scrollToMonitor = () => setDetailsTab('monitor');
11✔
284

285
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
11✔
286

287
  const scrollToDeviceSystem = target => {
11✔
288
    if (target) {
×
289
      return navigate(`/devices?${target}`);
×
290
    }
291
    return setDetailsTab('device-system');
×
292
  };
293

294
  const onCloseClick = useCallback(() => {
11✔
295
    if (deviceId) {
×
296
      onClose();
×
297
    }
298
  }, [deviceId, onClose]);
299

300
  const availableTabs = tabs.reduce((accu, tab) => {
11✔
301
    if (tab.isApplicable({ device, integrations, tenantCapabilities, userCapabilities })) {
99✔
302
      accu.push(tab);
64✔
303
    }
304
    return accu;
99✔
305
  }, []);
306

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

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

394
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