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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

96.64
/frontend/src/js/components/devices/ExpandedDevice.tsx
1
// Copyright 2015 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useCallback, useEffect, useRef } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16
import { useNavigate } from 'react-router-dom';
2✔
17

2✔
18
import { Chip, Divider, Drawer, Tab, Tabs, chipClasses } from '@mui/material';
2✔
19
import { makeStyles } from 'tss-react/mui';
2✔
20

2✔
21
import DeviceIdentityDisplay from '@northern.tech/common-ui/DeviceIdentity';
2✔
22
import DocsLink from '@northern.tech/common-ui/DocsLink';
2✔
23
import { DrawerTitle } from '@northern.tech/common-ui/DrawerTitle';
2✔
24
import { RelativeTime } from '@northern.tech/common-ui/Time';
2✔
25
import { MenderTooltipClickable } from '@northern.tech/common-ui/helptips/MenderTooltip';
2✔
26
import storeActions from '@northern.tech/store/actions';
2✔
27
import { DEVICE_STATES, EXTERNAL_PROVIDER, TIMEOUTS, yes } from '@northern.tech/store/constants';
2✔
28
import {
2✔
29
  getDeviceConfigDeployment,
2✔
30
  getDeviceTwinIntegrations,
2✔
31
  getDevicesById,
2✔
32
  getGlobalSettings,
2✔
33
  getSelectedGroupInfo,
2✔
34
  getTenantCapabilities,
2✔
35
  getUserCapabilities,
2✔
36
  getUserSettings
2✔
37
} from '@northern.tech/store/selectors';
2✔
38
import { decommissionDevice, getDeviceInfo, getGatewayDevices, saveGlobalSettings } from '@northern.tech/store/thunks';
2✔
39
import { getDemoDeviceAddress, stringToBoolean } from '@northern.tech/utils/helpers';
2✔
40
import copy from 'copy-to-clipboard';
2✔
41

2✔
42
import GatewayConnectionIcon from '../../../assets/img/gateway-connection.svg';
2✔
43
import GatewayIcon from '../../../assets/img/gateway.svg';
2✔
44
import DeviceConfiguration from './device-details/Configuration';
2✔
45
import TroubleshootTab from './device-details/Connection';
2✔
46
import Deployments from './device-details/Deployments';
2✔
47
import DeviceInventory from './device-details/DeviceInventory';
2✔
48
import DeviceSystem from './device-details/DeviceSystem';
2✔
49
import { IntegrationTab } from './device-details/DeviceTwin';
2✔
50
import { IdentityTab } from './device-details/Identity';
2✔
51
import InstalledSoftware from './device-details/InstalledSoftware';
2✔
52
import MonitoringTab from './device-details/Monitoring';
2✔
53
import DeviceNotifications from './device-details/Notifications';
2✔
54
import DeviceQuickActions from './widgets/DeviceQuickActions';
2✔
55

2✔
56
const { setSnackbar } = storeActions;
10✔
57

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

2✔
83
const refreshDeviceLength = TIMEOUTS.refreshDefault;
10✔
84

2✔
85
const GatewayConnectionNotification = ({ gatewayDevices, onClick }) => {
10✔
86
  const { classes } = useStyles();
2✔
87

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

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

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

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

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

2✔
187
export const ExpandedDevice = ({ actionCallbacks, deviceId, onClose, setDetailsTab, tabSelection }) => {
10✔
188
  const timer = useRef();
42✔
189
  const navigate = useNavigate();
42✔
190
  const { classes } = useStyles();
42✔
191

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

2✔
203
  const { attributes = {}, isOffline, gatewayIds = [] } = device;
42✔
204
  const { mender_is_gateway, mender_gateway_system_id } = attributes;
42✔
205
  const isGateway = stringToBoolean(mender_is_gateway);
42✔
206

2✔
207
  useEffect(() => {
42✔
208
    clearInterval(timer.current);
6✔
209
    if (!deviceId) {
6✔
210
      return;
5✔
211
    }
2✔
212
    timer.current = setInterval(() => dispatch(getDeviceInfo(deviceId)), refreshDeviceLength);
3✔
213
    dispatch(getDeviceInfo(deviceId));
3✔
214
    return () => {
3✔
215
      clearInterval(timer.current);
3✔
216
    };
2✔
217
  }, [deviceId, device.status, dispatch]);
2✔
218

2✔
219
  useEffect(() => {
42✔
220
    if (!(device.id && mender_gateway_system_id)) {
6!
221
      return;
6✔
222
    }
2✔
223
    dispatch(getGatewayDevices(device.id));
2✔
224
  }, [device.id, dispatch, mender_gateway_system_id]);
2✔
225

2✔
226
  // close expanded device
2✔
227
  const onDecommissionDevice = deviceId => dispatch(decommissionDevice({ deviceId })).finally(onClose);
42✔
228

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

2✔
235
  const scrollToMonitor = () => setDetailsTab('monitor');
42✔
236

2✔
237
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
42✔
238

2✔
239
  const scrollToDeviceSystem = target => {
42✔
240
    if (target) {
2!
241
      return navigate(`/devices?${target}`);
2✔
242
    }
2✔
243
    return setDetailsTab('device-system');
2✔
244
  };
2✔
245

2✔
246
  const onCloseClick = useCallback(() => {
42✔
247
    if (deviceId) {
2!
248
      onClose();
2✔
249
    }
2✔
250
  }, [deviceId, onClose]);
2✔
251

2✔
252
  const availableTabs = tabs.reduce((accu, tab) => {
42✔
253
    if (tab.isApplicable({ device, integrations, tenantCapabilities, userCapabilities })) {
362✔
254
      accu.push(tab);
284✔
255
    }
2✔
256
    return accu;
362✔
257
  }, []);
2✔
258

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

2✔
261
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
42✔
262
  const dispatchedSaveGlobalSettings = useCallback(settings => dispatch(saveGlobalSettings(settings)), [dispatch]);
42✔
263

2✔
264
  const commonProps = {
42✔
265
    classes,
2✔
266
    columnSelection,
2✔
267
    defaultConfig,
2✔
268
    device,
2✔
269
    deviceConfigDeployment,
2✔
270
    integrations,
2✔
271
    latestAlerts,
2✔
272
    onDecommissionDevice,
2✔
273
    saveGlobalSettings: dispatchedSaveGlobalSettings,
2✔
274
    setDetailsTab,
2✔
275
    setSnackbar: dispatchedSetSnackbar,
2✔
276
    tenantCapabilities,
2✔
277
    userCapabilities
2✔
278
  };
2✔
279
  return (
42✔
280
    <Drawer anchor="right" className="expandedDevice" open={!!deviceId} onClose={onCloseClick} PaperProps={{ style: { minWidth: '67vw' } }}>
2✔
281
      <DrawerTitle
2✔
282
        title={<>Device information for {<DeviceIdentityDisplay device={device} isEditable={false} hasAdornment={false} style={{ marginLeft: 4 }} />}</>}
2✔
283
        onLinkCopy={copyLinkToClipboard}
2✔
284
        preCloser={
2✔
285
          <>
2✔
UNCOV
286
            {isGateway && <GatewayNotification device={device} onClick={() => scrollToDeviceSystem()} />}
2✔
287
            {!!gatewayIds.length && (
2!
288
              <GatewayConnectionNotification gatewayDevices={gatewayIds.map(gatewayId => devicesById[gatewayId])} onClick={scrollToDeviceSystem} />
2✔
289
            )}
2✔
290
            <div className={`${isOffline ? 'red' : 'muted'} margin-left margin-right flexbox`}>
2!
291
              <div className="margin-right-small">Latest activity:</div>
2✔
292
              <RelativeTime updateTime={device.check_in_time} />
2✔
293
            </div>
2✔
294
          </>
2✔
295
        }
2✔
296
        onClose={onCloseClick}
2✔
297
      />
2✔
298
      <DeviceNotifications alerts={latestAlerts} device={device} onClick={scrollToMonitor} />
2✔
299
      <Divider className={classes.dividerTop} />
2✔
UNCOV
300
      <Tabs value={selectedTab} onChange={(e, tab) => setDetailsTab(tab)} textColor="primary">
2✔
301
        {availableTabs.map(item => (
2✔
302
          <Tab key={item.value} label={item.title({ integrations })} value={item.value} />
284✔
303
        ))}
2✔
304
      </Tabs>
2✔
305
      <SelectedTab {...commonProps} />
2✔
306
      <DeviceQuickActions actionCallbacks={actionCallbacks} deviceId={device.id} selectedGroup={selectedStaticGroup} />
2✔
307
    </Drawer>
2✔
308
  );
2✔
309
};
2✔
310

2✔
311
export default ExpandedDevice;
2✔
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