• 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

94.35
/frontend/src/js/components/devices/AuthorizedDevices.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, useState } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16
import { useNavigate } from 'react-router-dom';
2✔
17

2✔
18
// material ui
2✔
19
import { Autorenew as AutorenewIcon, Delete as DeleteIcon, FilterList as FilterListIcon, LockOutlined } from '@mui/icons-material';
2✔
20
import { Button } from '@mui/material';
2✔
21
import { makeStyles } from 'tss-react/mui';
2✔
22

2✔
23
import { defaultTextRender, getDeviceIdentityText } from '@northern.tech/common-ui/DeviceIdentity';
2✔
24
import Loader from '@northern.tech/common-ui/Loader';
2✔
25
import storeActions from '@northern.tech/store/actions';
2✔
26
import { ALL_DEVICES, DEVICE_ISSUE_OPTIONS, DEVICE_STATES, SORTING_OPTIONS, TIMEOUTS, UNGROUPED_GROUP, onboardingSteps } from '@northern.tech/store/constants';
2✔
27
import {
2✔
28
  getAvailableIssueOptionsByType,
2✔
29
  getDeviceCountsByStatus,
2✔
30
  getDeviceFilters,
2✔
31
  getFilterAttributes,
2✔
32
  getIdAttribute,
2✔
33
  getLimitMaxed,
2✔
34
  getMappedDevicesList,
2✔
35
  getOnboardingState,
2✔
36
  getSelectedGroupInfo,
2✔
37
  getUserCapabilities,
2✔
38
  getUserSettings,
2✔
39
  getUserSettingsInitialized
2✔
40
} from '@northern.tech/store/selectors';
2✔
41
import {
2✔
42
  advanceOnboarding,
2✔
43
  deleteAuthset,
2✔
44
  getIssueCountsByType,
2✔
45
  saveUserSettings,
2✔
46
  setDeviceListState,
2✔
47
  updateDevicesAuth,
2✔
48
  updateUserColumnSettings
2✔
49
} from '@northern.tech/store/thunks';
2✔
50
import { useDebounce } from '@northern.tech/utils/debouncehook';
2✔
51
import { toggle } from '@northern.tech/utils/helpers';
2✔
52
import { useWindowSize } from '@northern.tech/utils/resizehook';
2✔
53
import { clearAllRetryTimers, setRetryTimer } from '@northern.tech/utils/retrytimer';
2✔
54

2✔
55
import { getOnboardingComponentFor } from '../../utils/onboardingManager';
2✔
56
import { defaultHeaders, routes as states } from './BaseDevices';
2✔
57
import DeviceList, { minCellWidth } from './DeviceList';
2✔
58
import ExpandedDevice from './ExpandedDevice';
2✔
59
import ColumnCustomizationDialog from './dialogs/CustomColumnsDialog';
2✔
60
import DeviceQuickActions from './widgets/DeviceQuickActions';
2✔
61
import { DeviceStateSelection } from './widgets/DeviceStateSelection';
2✔
62
import Filters from './widgets/Filters';
2✔
63
import DeviceIssuesSelection from './widgets/IssueSelection';
2✔
64
import ListOptions from './widgets/ListOptions';
2✔
65

2✔
66
const { setDeviceFilters, setSnackbar } = storeActions;
10✔
67

2✔
68
const deviceRefreshTimes = {
10✔
69
  [DEVICE_STATES.accepted]: TIMEOUTS.refreshLong,
2✔
70
  [DEVICE_STATES.pending]: TIMEOUTS.refreshDefault,
2✔
71
  [DEVICE_STATES.preauth]: TIMEOUTS.refreshLong,
2✔
72
  [DEVICE_STATES.rejected]: TIMEOUTS.refreshLong,
2✔
73
  default: TIMEOUTS.refreshDefault
2✔
74
};
2✔
75

2✔
76
const idAttributeTitleMap = {
10✔
77
  id: 'Device ID',
2✔
78
  name: 'Name'
2✔
79
};
2✔
80

2✔
81
const headersReducer = (accu, header) => {
10✔
82
  if (header.attribute.scope === accu.column.scope && header.attribute.name === accu.column.name) {
42✔
83
    accu.header = { ...accu.header, ...header };
8✔
84
  }
2✔
85
  return accu;
42✔
86
};
2✔
87

2✔
88
const useStyles = makeStyles()(theme => ({
10✔
89
  filterCommon: {
2✔
90
    borderStyle: 'solid',
2✔
91
    borderWidth: 1,
2✔
92
    borderRadius: 5,
2✔
93
    borderColor: theme.palette.grey[100],
2✔
94
    background: theme.palette.background.default,
2✔
95
    [`.filter-list > .MuiChip-root`]: {
2✔
96
      marginBottom: theme.spacing()
2✔
97
    },
2✔
98
    [`.filter-list > .MuiChip-root > .MuiChip-label`]: {
2✔
99
      whiteSpace: 'normal'
2✔
100
    },
2✔
101
    ['&.filter-header']: {
2✔
102
      overflow: 'hidden',
2✔
103
      zIndex: 2
2✔
104
    },
2✔
105
    ['&.filter-toggle']: {
2✔
106
      background: 'transparent',
2✔
107
      borderBottomRightRadius: 0,
2✔
108
      borderBottomLeftRadius: 0,
2✔
109
      borderBottomColor: theme.palette.background.default,
2✔
110
      marginBottom: -1
2✔
111
    },
2✔
112
    ['&.filter-wrapper']: {
2✔
113
      padding: 20,
2✔
114
      borderTopLeftRadius: 0
2✔
115
    }
2✔
116
  },
2✔
117
  selection: {
2✔
118
    fontSize: 13,
2✔
119
    marginLeft: theme.spacing(0.5),
2✔
120
    marginTop: 2
2✔
121
  }
2✔
122
}));
2✔
123

2✔
124
export const getHeaders = (columnSelection = [], currentStateHeaders, idAttribute, openSettingsDialog) => {
10✔
125
  const headers = columnSelection.length
84✔
126
    ? columnSelection.map(column => {
2✔
127
        let header = { ...column, attribute: { ...column }, textRender: defaultTextRender, sortable: true };
6✔
128
        header = Object.values(defaultHeaders).reduce(headersReducer, { column, header }).header;
6✔
129
        header = currentStateHeaders.reduce(headersReducer, { column, header }).header;
6✔
130
        return header;
6✔
131
      })
2✔
132
    : currentStateHeaders;
2✔
133
  const { attribute, scope } = idAttribute;
84✔
134
  return [
84✔
135
    {
2✔
136
      title: idAttributeTitleMap[attribute] ?? attribute,
2✔
137
      customize: openSettingsDialog,
2✔
138
      attribute: { name: attribute, scope },
2✔
139
      sortable: true,
2✔
140
      textRender: getDeviceIdentityText
2✔
141
    },
2✔
142
    ...headers,
2✔
143
    defaultHeaders.deviceStatus
2✔
144
  ];
2✔
145
};
2✔
146

2✔
147
const calculateColumnSelectionSize = (changedColumns, customColumnSizes) =>
10✔
148
  changedColumns.reduce(
3✔
149
    (accu, column) => {
2✔
150
      const size = customColumnSizes.find(({ attribute }) => attribute.name === column.key && attribute.scope === column.scope)?.size || minCellWidth;
6✔
151
      accu.columnSizes.push({ attribute: { name: column.key, scope: column.scope }, size });
6✔
152
      accu.selectedAttributes.push({ attribute: column.key, scope: column.scope });
6✔
153
      return accu;
6✔
154
    },
2✔
155
    { columnSizes: [], selectedAttributes: [] }
2✔
156
  );
2✔
157

2✔
158
const OnboardingComponent = ({ deviceListRef, onboardingState }) => {
10✔
159
  let onboardingComponent = null;
41✔
160
  const element = deviceListRef.current?.querySelector('body .deviceListItem > div');
41✔
161
  if (element) {
41✔
162
    const anchor = { left: 200, top: element.offsetTop + element.offsetHeight };
36✔
163
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEVICES_PENDING_ONBOARDING, onboardingState, { anchor }, onboardingComponent);
36✔
164
  } else if (deviceListRef.current) {
7✔
165
    const anchor = { top: deviceListRef.current.offsetTop + deviceListRef.current.offsetHeight / 3, left: deviceListRef.current.offsetWidth / 2 + 30 };
4✔
166
    onboardingComponent = getOnboardingComponentFor(
4✔
167
      onboardingSteps.DEVICES_PENDING_ONBOARDING_START,
2✔
168
      onboardingState,
2✔
169
      { anchor, place: 'top' },
2✔
170
      onboardingComponent
2✔
171
    );
2✔
172
  }
2✔
173
  return onboardingComponent;
41✔
174
};
2✔
175

2✔
176
export const Authorized = ({
10✔
177
  addDevicesToGroup,
2✔
178
  onGroupClick,
2✔
179
  onGroupRemoval,
2✔
180
  onMakeGatewayClick,
2✔
181
  onPreauthClick,
2✔
182
  openSettingsDialog,
2✔
183
  removeDevicesFromGroup,
2✔
184
  showsDialog,
2✔
185
  changeLocation
2✔
186
}) => {
2✔
187
  const limitMaxed = useSelector(getLimitMaxed);
43✔
188
  const devices = useSelector(state => getMappedDevicesList(state, 'deviceList'));
84✔
189
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
43✔
190
  const { columnSelection = [] } = useSelector(getUserSettings);
43✔
191
  const attributes = useSelector(getFilterAttributes);
43✔
192
  const { accepted: acceptedCount, pending: pendingCount, rejected: rejectedCount } = useSelector(getDeviceCountsByStatus);
43✔
193
  const allCount = acceptedCount + rejectedCount;
43✔
194
  const availableIssueOptions = useSelector(getAvailableIssueOptionsByType);
43✔
195
  const customColumnSizes = useSelector(state => state.users.customColumns);
84✔
196
  const deviceListState = useSelector(state => state.devices.deviceList);
84✔
197
  const { total: deviceCount } = deviceListState;
43✔
198
  const filters = useSelector(getDeviceFilters);
43✔
199
  const idAttribute = useSelector(getIdAttribute);
43✔
200
  const onboardingState = useSelector(getOnboardingState);
43✔
201
  const settingsInitialized = useSelector(getUserSettingsInitialized);
43✔
202
  const userCapabilities = useSelector(getUserCapabilities);
43✔
203
  const dispatch = useDispatch();
43✔
204
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
43✔
205

2✔
206
  const {
2✔
207
    refreshTrigger,
2✔
208
    selectedId,
2✔
209
    selectedIssues = [],
2✔
210
    isLoading: pageLoading,
2✔
211
    selection: selectedRows,
2✔
212
    sort = {},
2✔
213
    state: selectedState,
2✔
214
    detailsTab: tabSelection
2✔
215
  } = deviceListState;
43✔
216
  const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol } = sort;
43✔
217
  const { canManageDevices, canManageUsers } = userCapabilities;
43✔
218
  const currentSelectedState = states[selectedState] ?? states.devices;
43!
219
  const [columnHeaders, setColumnHeaders] = useState([]);
43✔
220
  const [isInitialized, setIsInitialized] = useState(false);
43✔
221
  const [devicesInitialized, setDevicesInitialized] = useState(!!devices.length);
43✔
222
  const [showFilters, setShowFilters] = useState(false);
43✔
223
  const [showCustomization, setShowCustomization] = useState(false);
43✔
224
  const deviceListRef = useRef();
43✔
225
  const timer = useRef();
43✔
226
  const navigate = useNavigate();
43✔
227

2✔
228
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2✔
229
  const size = useWindowSize();
43✔
230

2✔
231
  const { classes } = useStyles();
43✔
232

2✔
233
  useEffect(() => {
43✔
234
    clearAllRetryTimers(dispatchedSetSnackbar);
5✔
235
    if (!filters.length && selectedGroup && groupFilters.length) {
5!
236
      dispatch(setDeviceFilters(groupFilters));
2✔
237
    }
2✔
238
    return () => {
5✔
239
      clearInterval(timer.current);
5✔
240
      clearAllRetryTimers(dispatchedSetSnackbar);
5✔
241
    };
2✔
242
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
243
  }, [dispatch, dispatchedSetSnackbar]);
2✔
244

2✔
245
  useEffect(() => {
43✔
246
    const columnHeaders = getHeaders(columnSelection, currentSelectedState.defaultHeaders, idAttribute, canManageUsers ? openSettingsDialog : undefined);
17!
247
    setColumnHeaders(columnHeaders);
17✔
248
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
249
  }, [canManageUsers, columnSelection, idAttribute.attribute, currentSelectedState.defaultHeaders, openSettingsDialog]);
2✔
250

2✔
251
  useEffect(() => {
43✔
252
    // only set state after all devices id data retrieved
2✔
253
    setIsInitialized(isInitialized => isInitialized || (settingsInitialized && devicesInitialized && pageLoading === false));
14✔
254
    setDevicesInitialized(devicesInitialized => devicesInitialized || pageLoading === false);
14✔
255
  }, [settingsInitialized, devicesInitialized, pageLoading]);
2✔
256

2✔
257
  const onDeviceStateSelectionChange = useCallback(
43✔
258
    newState => {
2✔
259
      changeLocation(newState);
2✔
260
      dispatch(
2✔
261
        setDeviceListState({ state: newState, page: 1, refreshTrigger: !refreshTrigger, shouldSelectDevices: true, forceRefresh: false, fetchAuth: false })
2✔
262
      );
2✔
263
    },
2✔
264
    [dispatch, changeLocation, refreshTrigger]
2✔
265
  );
2✔
266

2✔
267
  useEffect(() => {
43✔
268
    if (onboardingState.complete) {
12!
269
      return;
2✔
270
    }
2✔
271
    if (pendingCount) {
12!
272
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING_START));
12✔
273
      return;
12✔
274
    }
2✔
275
    if (!acceptedCount) {
2!
276
      return;
2✔
277
    }
2✔
278

2✔
UNCOV
279
    if (acceptedCount < 2 && !window.sessionStorage.getItem('pendings-redirect')) {
2!
280
      window.sessionStorage.setItem('pendings-redirect', true);
2✔
281
      onDeviceStateSelectionChange(DEVICE_STATES.accepted);
2✔
282
    }
2✔
283
  }, [acceptedCount, allCount, pendingCount, onboardingState.complete, dispatch, onDeviceStateSelectionChange, dispatchedSetSnackbar]);
2✔
284

2✔
285
  useEffect(() => {
43✔
286
    setShowFilters(false);
5✔
287
  }, [selectedGroup]);
2✔
288
  const dispatchDeviceListState = useCallback(
43✔
289
    (options, shouldSelectDevices = true, forceRefresh = false, fetchAuth = false) =>
2✔
290
      dispatch(setDeviceListState({ ...options, shouldSelectDevices, forceRefresh, fetchAuth })),
5✔
291
    [dispatch]
2✔
292
  );
2✔
293

2✔
294
  const refreshDevices = useCallback(() => {
43✔
295
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
2!
296
    return dispatchDeviceListState({}, true, true).catch(err =>
2✔
297
      setRetryTimer(err, 'devices', `Devices couldn't be loaded.`, refreshLength, dispatchedSetSnackbar)
2✔
298
    );
2✔
299
  }, [dispatchedSetSnackbar, selectedState, dispatchDeviceListState]);
2✔
300

2✔
301
  useEffect(() => {
43✔
302
    if (!devicesInitialized) {
6✔
303
      return;
3✔
304
    }
2✔
305
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
5!
306
    clearInterval(timer.current);
6✔
307
    timer.current = setInterval(() => refreshDevices(), refreshLength);
6✔
308
  }, [devicesInitialized, refreshDevices, selectedState]);
2✔
309

2✔
310
  useEffect(() => {
43✔
311
    Object.keys(availableIssueOptions).forEach(key => dispatch(getIssueCountsByType({ type: key, filters, group: selectedGroup, state: selectedState })));
8✔
312
    if (availableIssueOptions[DEVICE_ISSUE_OPTIONS.authRequests.key]) {
7!
313
      dispatch(getIssueCountsByType({ type: DEVICE_ISSUE_OPTIONS.authRequests.key, options: { filters: [] } }));
2✔
314
    }
2✔
315
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
316
  }, [selectedIssues.join(''), JSON.stringify(availableIssueOptions), selectedState, selectedGroup, dispatch, JSON.stringify(filters)]);
2✔
317

2✔
318
  /*
2✔
319
   * Devices
2✔
320
   */
2✔
321
  const devicesToIds = devices => devices.map(device => device.id);
43✔
322

2✔
323
  const onRemoveDevicesFromGroup = devices => {
43✔
324
    const deviceIds = devicesToIds(devices);
2✔
325
    removeDevicesFromGroup(deviceIds);
2✔
326
    // if devices.length = number on page but < deviceCount
2✔
327
    // move page back to pageNO 1
2✔
328
    if (devices.length === deviceIds.length) {
2!
329
      handlePageChange(1);
2✔
330
    }
2✔
331
  };
2✔
332

2✔
333
  const onAuthorizationChange = (devices, changedState) => {
43✔
334
    const deviceIds = devicesToIds(devices);
2✔
335
    return dispatchDeviceListState({ isLoading: true })
2✔
336
      .then(() => dispatch(updateDevicesAuth({ deviceIds, status: changedState })))
2✔
337
      .then(() => onSelectionChange([]));
2✔
338
  };
2✔
339

2✔
340
  const onDeviceDismiss = devices =>
43✔
341
    dispatchDeviceListState({ isLoading: true })
2✔
342
      .then(() => {
2✔
343
        const deleteRequests = devices.reduce((accu, device) => {
2✔
344
          if (device.auth_sets?.length) {
2!
345
            accu.push(dispatch(deleteAuthset({ deviceId: device.id, authId: device.auth_sets[0].id })));
2✔
346
          }
2✔
347
          return accu;
2✔
348
        }, []);
2✔
349
        return Promise.all(deleteRequests);
2✔
350
      })
2✔
351
      .then(() => onSelectionChange([]));
2✔
352

2✔
353
  const handlePageChange = useCallback(page => dispatchDeviceListState({ selectedId: undefined, page }), [dispatchDeviceListState]);
43✔
354

2✔
355
  const onPageLengthChange = perPage => dispatchDeviceListState({ perPage, page: 1, refreshTrigger: !refreshTrigger });
43✔
356

2✔
357
  const onSortChange = attribute => {
43✔
358
    const changedSortCol = attribute.name;
2✔
359
    let changedSortDown = sortDown === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
2!
360
    if (changedSortCol !== sortCol) {
2!
361
      changedSortDown = SORTING_OPTIONS.desc;
2✔
362
    }
2✔
363
    dispatchDeviceListState({
2✔
364
      sort: { direction: changedSortDown, key: changedSortCol, scope: attribute.scope },
2✔
365
      refreshTrigger: !refreshTrigger
2✔
366
    });
2✔
367
  };
2✔
368

2✔
369
  const setDetailsTab = detailsTab => dispatchDeviceListState({ detailsTab, setOnly: true });
43✔
370

2✔
371
  const onDeviceIssuesSelectionChange = ({ target: { value: selectedIssues } }) =>
43✔
372
    dispatchDeviceListState({ selectedIssues, page: 1, refreshTrigger: !refreshTrigger });
3✔
373

2✔
374
  const onSelectionChange = (selection = []) => {
43✔
375
    if (!onboardingState.complete && selection.length) {
3!
376
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING));
3✔
377
    }
2✔
378
    dispatchDeviceListState({ selection, setOnly: true });
3✔
379
  };
2✔
380

2✔
381
  const onToggleCustomizationClick = () => setShowCustomization(toggle);
43✔
382

2✔
383
  const onChangeColumns = useCallback(
43✔
384
    (changedColumns, customColumnSizes) => {
2✔
385
      const { columnSizes, selectedAttributes } = calculateColumnSelectionSize(changedColumns, customColumnSizes);
3✔
386
      dispatch(updateUserColumnSettings({ columns: columnSizes }));
3✔
387
      dispatch(saveUserSettings({ columnSelection: changedColumns }));
3✔
388
      // we don't need an explicit refresh trigger here, since the selectedAttributes will be different anyway & otherwise the shown list should still be valid
2✔
389
      dispatchDeviceListState({ selectedAttributes });
3✔
390
      setShowCustomization(false);
3✔
391
    },
2✔
392
    [dispatch, dispatchDeviceListState]
2✔
393
  );
2✔
394

2✔
395
  const onExpandClick = (device = {}) => {
43!
396
    dispatchedSetSnackbar('');
2✔
397
    const { id } = device;
2✔
398
    dispatchDeviceListState({ selectedId: deviceListState.selectedId === id ? undefined : id, detailsTab: deviceListState.detailsTab || 'identity' });
2!
399
    if (!onboardingState.complete) {
2!
400
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING));
2✔
401
    }
2✔
402
  };
2✔
403

2✔
404
  const onCreateDeploymentClick = devices => navigate(`/deployments?open=true&${devices.map(({ id }) => `deviceId=${id}`).join('&')}`);
43✔
405

2✔
406
  const onCloseExpandedDevice = useCallback(() => dispatchDeviceListState({ selectedId: undefined, detailsTab: '' }), [dispatchDeviceListState]);
43✔
407

2✔
408
  const onResizeColumns = useCallback(columns => dispatch(updateUserColumnSettings({ columns })), [dispatch]);
43✔
409

2✔
410
  const actionCallbacks = {
43✔
411
    onAddDevicesToGroup: addDevicesToGroup,
2✔
412
    onAuthorizationChange,
2✔
413
    onCreateDeployment: onCreateDeploymentClick,
2✔
414
    onDeviceDismiss,
2✔
415
    onPromoteGateway: onMakeGatewayClick,
2✔
416
    onRemoveDevicesFromGroup
2✔
417
  };
2✔
418

2✔
419
  const listOptionHandlers = [{ key: 'customize', title: 'Customize', onClick: onToggleCustomizationClick }];
43✔
420
  const EmptyState = currentSelectedState.emptyState;
43✔
421

2✔
422
  const groupLabel = selectedGroup ? decodeURIComponent(selectedGroup) : ALL_DEVICES;
43✔
423
  const isUngroupedGroup = selectedGroup && selectedGroup === UNGROUPED_GROUP.id;
43✔
424
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
43✔
425

2✔
426
  const openedDevice = useDebounce(selectedId, TIMEOUTS.debounceShort);
43✔
427
  const issueOptions = Object.values(availableIssueOptions);
43✔
428
  return (
43✔
429
    <>
2✔
430
      <div className="margin-left-small">
2✔
431
        <div className="flexbox">
2✔
432
          <h3 className="margin-right">{isUngroupedGroup ? UNGROUPED_GROUP.name : groupLabel}</h3>
2!
433
          <div className="flexbox space-between center-aligned" style={{ flexGrow: 1 }}>
2✔
434
            <div className="flexbox">
2✔
435
              <DeviceStateSelection className={classes.selection} onStateChange={onDeviceStateSelectionChange} selectedState={selectedState} states={states} />
2✔
436
              {!!issueOptions.length && (
2✔
437
                <DeviceIssuesSelection
2✔
438
                  className={classes.selection}
2✔
439
                  onChange={onDeviceIssuesSelectionChange}
2✔
440
                  options={issueOptions}
2✔
441
                  selection={selectedIssues}
2✔
442
                />
2✔
443
              )}
2✔
444
              {selectedGroup && !isUngroupedGroup && (
2✔
445
                <div className="margin-left muted flexbox centered">
2✔
446
                  {!groupFilters.length ? <LockOutlined fontSize="small" /> : <AutorenewIcon fontSize="small" />}
2!
447
                  <span>{!groupFilters.length ? 'Static' : 'Dynamic'}</span>
2!
448
                </div>
2✔
449
              )}
2✔
450
            </div>
2✔
451
            {canManageDevices && selectedGroup && !isUngroupedGroup && (
2✔
452
              <Button onClick={onGroupRemoval} startIcon={<DeleteIcon />}>
2✔
453
                Remove group
2✔
454
              </Button>
2✔
455
            )}
2✔
456
          </div>
2✔
457
        </div>
2✔
458
        <div className="flexbox space-between">
2✔
459
          {!isUngroupedGroup && (
2✔
460
            <div className={`flexbox centered filter-header ${showFilters ? `${classes.filterCommon} filter-toggle` : ''}`}>
2!
461
              <Button
2✔
462
                color="secondary"
2✔
463
                disableRipple
2✔
UNCOV
464
                onClick={() => setShowFilters(toggle)}
2✔
465
                startIcon={<FilterListIcon />}
2✔
466
                style={{ backgroundColor: 'transparent' }}
2✔
467
              >
2✔
468
                {filters.length > 0 ? `Filters (${filters.length})` : 'Filters'}
2!
469
              </Button>
2✔
470
            </div>
2✔
471
          )}
2✔
472
          <ListOptions options={listOptionHandlers} title="Table options" />
2✔
473
        </div>
2✔
474
        <Filters className={classes.filterCommon} onGroupClick={onGroupClick} open={showFilters} />
2✔
475
      </div>
2✔
476
      <Loader show={!isInitialized} />
2✔
477
      <div className="padding-bottom" ref={deviceListRef}>
2✔
478
        {devices.length > 0 ? (
2✔
479
          <DeviceList
2✔
480
            columnHeaders={columnHeaders}
2✔
481
            customColumnSizes={customColumnSizes}
2✔
482
            devices={devices}
2✔
483
            deviceListState={deviceListState}
2✔
484
            idAttribute={idAttribute}
2✔
485
            onChangeRowsPerPage={onPageLengthChange}
2✔
486
            onExpandClick={onExpandClick}
2✔
487
            onPageChange={handlePageChange}
2✔
488
            onResizeColumns={onResizeColumns}
2✔
489
            onSelect={onSelectionChange}
2✔
490
            onSort={onSortChange}
2✔
491
            pageLoading={pageLoading}
2✔
492
            pageTotal={deviceCount}
2✔
493
          />
2✔
494
        ) : (
2✔
495
          <EmptyState allCount={allCount} canManageDevices={canManageDevices} filters={filters} limitMaxed={limitMaxed} onClick={onPreauthClick} />
2✔
496
        )}
2✔
497
      </div>
2✔
498
      <ExpandedDevice
2✔
499
        actionCallbacks={actionCallbacks}
2✔
500
        deviceId={openedDevice}
2✔
501
        onClose={onCloseExpandedDevice}
2✔
502
        setDetailsTab={setDetailsTab}
2✔
503
        tabSelection={tabSelection}
2✔
504
      />
2✔
505
      {!selectedId && !showsDialog && <OnboardingComponent deviceListRef={deviceListRef} onboardingState={onboardingState} />}
2✔
506
      {canManageDevices && !!selectedRows.length && (
2✔
507
        <DeviceQuickActions actionCallbacks={actionCallbacks} deviceId={openedDevice} selectedGroup={selectedStaticGroup} />
2✔
508
      )}
2✔
509
      <ColumnCustomizationDialog
2✔
510
        attributes={attributes}
2✔
511
        columnHeaders={columnHeaders}
2✔
512
        customColumnSizes={customColumnSizes}
2✔
513
        idAttribute={idAttribute}
2✔
514
        open={showCustomization}
2✔
515
        onCancel={onToggleCustomizationClick}
2✔
516
        onSubmit={onChangeColumns}
2✔
517
      />
2✔
518
    </>
2✔
519
  );
2✔
520
};
2✔
521

2✔
522
export default Authorized;
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