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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

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

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

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

84
const refreshDeviceLength = TIMEOUTS.refreshDefault;
8✔
85

86
const GatewayConnectionNotification = ({ gatewayDevices, onClick }) => {
8✔
87
  const { classes } = useStyles();
×
88

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

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

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

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

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

188
export const ExpandedDevice = ({ actionCallbacks, deviceId, onClose, setDetailsTab, tabSelection }) => {
8✔
189
  const timer = useRef();
24✔
190
  const navigate = useNavigate();
24✔
191
  const { classes } = useStyles();
24✔
192

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

205
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
24✔
206
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
24✔
207
  const isGateway = stringToBoolean(mender_is_gateway);
24✔
208

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

221
  useEffect(() => {
24✔
222
    if (!(device.id && mender_gateway_system_id)) {
4!
223
      return;
4✔
224
    }
225
    dispatch(getGatewayDevices(device.id));
×
226
  }, [device.id, dispatch, mender_gateway_system_id]);
227

228
  // close expanded device
229
  const onDecommissionDevice = device_id => dispatch(decommissionDevice(device_id)).finally(onClose);
24✔
230

231
  const copyLinkToClipboard = () => {
24✔
232
    const location = window.location.href.substring(0, window.location.href.indexOf('/devices') + '/devices'.length);
×
233
    copy(`${location}?id=${deviceId}`);
×
234
    setSnackbar('Link copied to clipboard');
×
235
  };
236

237
  const scrollToMonitor = () => setDetailsTab('monitor');
24✔
238

239
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
24✔
240

241
  const scrollToDeviceSystem = target => {
24✔
242
    if (target) {
×
243
      return navigate(`/devices?${target}`);
×
244
    }
245
    return setDetailsTab('device-system');
×
246
  };
247

248
  const onCloseClick = useCallback(() => {
24✔
249
    if (deviceId) {
×
250
      onClose();
×
251
    }
252
  }, [deviceId, onClose]);
253

254
  const availableTabs = tabs.reduce((accu, tab) => {
24✔
255
    if (tab.isApplicable({ device, integrations, tenantCapabilities, userCapabilities })) {
216✔
256
      accu.push(tab);
170✔
257
    }
258
    return accu;
216✔
259
  }, []);
260

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

263
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
24✔
264
  const dispatchedSaveGlobalSettings = useCallback(settings => dispatch(saveGlobalSettings(settings)), [dispatch]);
24✔
265

266
  const commonProps = {
24✔
267
    classes,
268
    columnSelection,
269
    defaultConfig,
270
    device,
271
    deviceConfigDeployment,
272
    docsVersion,
273
    integrations,
274
    latestAlerts,
275
    onDecommissionDevice,
276
    saveGlobalSettings: dispatchedSaveGlobalSettings,
277
    setDetailsTab,
278
    setSnackbar: dispatchedSetSnackbar,
279
    tenantCapabilities,
280
    userCapabilities
281
  };
282
  return (
24✔
283
    <Drawer anchor="right" className="expandedDevice" open={!!deviceId} onClose={onCloseClick} PaperProps={{ style: { minWidth: '67vw' } }}>
284
      <div className="flexbox center-aligned space-between">
285
        <div className="flexbox center-aligned">
286
          <h3 className="flexbox">
287
            Device information for {<DeviceIdentityDisplay device={device} isEditable={false} hasAdornment={false} style={{ marginLeft: 4 }} />}
288
          </h3>
289
          <IconButton onClick={copyLinkToClipboard} size="large">
290
            <LinkIcon />
291
          </IconButton>
292
        </div>
293
        <div className="flexbox center-aligned">
294
          {isGateway && <GatewayNotification device={device} onClick={() => scrollToDeviceSystem()} />}
✔
295
          {!!gatewayIds.length && (
24!
296
            <GatewayConnectionNotification gatewayDevices={gatewayIds.map(gatewayId => devicesById[gatewayId])} onClick={scrollToDeviceSystem} />
×
297
          )}
298
          <div className={`${isOffline ? 'red' : 'muted'} margin-left margin-right flexbox`}>
24!
299
            <Tooltip title="The last time the device communicated with the Mender server" placement="bottom">
300
              <div className="margin-right-small">Last check-in:</div>
301
            </Tooltip>
302
            <RelativeTime updateTime={device.check_in_time} />
303
          </div>
304
          <IconButton style={{ marginLeft: 'auto' }} onClick={onCloseClick} aria-label="close" size="large">
305
            <CloseIcon />
306
          </IconButton>
307
        </div>
308
      </div>
309
      <DeviceNotifications alerts={latestAlerts} device={device} onClick={scrollToMonitor} />
310
      <Divider className={classes.dividerTop} />
311
      <Tabs value={selectedTab} onChange={(e, tab) => setDetailsTab(tab)} textColor="primary">
×
312
        {availableTabs.map(item => (
313
          <Tab key={item.value} label={item.title({ integrations })} value={item.value} />
170✔
314
        ))}
315
      </Tabs>
316
      <SelectedTab {...commonProps} />
317
      <DeviceQuickActions actionCallbacks={actionCallbacks} deviceId={device.id} selectedGroup={selectedStaticGroup} />
318
    </Drawer>
319
  );
320
};
321

322
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