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

mendersoftware / mender-server / 10425

11 Nov 2025 05:02PM UTC coverage: 78.221% (+3.8%) from 74.435%
10425

Pull #1022

gitlab-ci

mzedel
chore(gui): aligned snapshots w/ updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #1022: MEN-8452 - device filters + device page design adjustments

3865 of 5388 branches covered (71.73%)

Branch coverage included in aggregate %.

34 of 38 new or added lines in 13 files covered. (89.47%)

7 existing lines in 1 file now uncovered.

6845 of 8304 relevant lines covered (82.43%)

68.06 hits per line

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

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

18
// material ui
19
import { Delete as DeleteIcon, FilterList as FilterListIcon, Lock, SyncOutlined } from '@mui/icons-material';
20
import { Button, Typography } from '@mui/material';
21
import { makeStyles } from 'tss-react/mui';
22

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

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

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

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

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

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

88
const useStyles = makeStyles()(theme => ({
8✔
89
  filterCommon: {
90
    borderStyle: 'solid',
91
    borderWidth: 1,
92
    borderRadius: theme.shape.borderRadius,
93
    borderColor: theme.palette.divider,
94
    marginBottom: 10,
95
    [`.filter-list > .MuiChip-root > .MuiChip-label`]: {
96
      whiteSpace: 'normal'
97
    },
98
    ['&.filter-wrapper']: {
99
      padding: `${theme.spacing(3)} ${theme.spacing(2)}`
100
    }
101
  },
102
  selection: {
103
    fontSize: 13,
104
    marginLeft: theme.spacing(0.5),
105
    marginTop: 2
106
  }
107
}));
108

109
export const getHeaders = (columnSelection = [], currentStateHeaders, idAttribute, openSettingsDialog) => {
8✔
110
  const headers = columnSelection.length
82✔
111
    ? columnSelection.map(column => {
112
        let header = { ...column, attribute: { ...column }, textRender: defaultTextRender, sortable: true };
4✔
113
        header = Object.values(defaultHeaders).reduce(headersReducer, { column, header }).header;
4✔
114
        header = currentStateHeaders.reduce(headersReducer, { column, header }).header;
4✔
115
        return header;
4✔
116
      })
117
    : currentStateHeaders;
118
  const { attribute, scope } = idAttribute;
82✔
119
  return [
82✔
120
    {
121
      title: idAttributeTitleMap[attribute] ?? attribute,
97✔
122
      customize: openSettingsDialog,
123
      attribute: { name: attribute, scope },
124
      sortable: true,
125
      textRender: getDeviceIdentityText
126
    },
127
    ...headers,
128
    defaultHeaders.deviceStatus
129
  ];
130
};
131

132
const calculateColumnSelectionSize = (changedColumns, customColumnSizes) =>
8✔
133
  changedColumns.reduce(
1✔
134
    (accu, column) => {
135
      const size = customColumnSizes.find(({ attribute }) => attribute.name === column.key && attribute.scope === column.scope)?.size || minCellWidth;
4✔
136
      accu.columnSizes.push({ attribute: { name: column.key, scope: column.scope }, size });
4✔
137
      accu.selectedAttributes.push({ attribute: column.key, scope: column.scope });
4✔
138
      return accu;
4✔
139
    },
140
    { columnSizes: [], selectedAttributes: [] }
141
  );
142

143
const OnboardingComponent = ({ deviceListRef, onboardingState }) => {
8✔
144
  let onboardingComponent = null;
39✔
145
  const element = deviceListRef.current?.querySelector('body .deviceListItem > div');
39✔
146
  if (element) {
39✔
147
    const anchor = { left: 200, top: element.offsetTop + element.offsetHeight };
34✔
148
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEVICES_PENDING_ONBOARDING, onboardingState, { anchor }, onboardingComponent);
34✔
149
  } else if (deviceListRef.current) {
5✔
150
    const anchor = { top: deviceListRef.current.offsetTop + deviceListRef.current.offsetHeight / 3, left: deviceListRef.current.offsetWidth / 2 + 30 };
2✔
151
    onboardingComponent = getOnboardingComponentFor(
2✔
152
      onboardingSteps.DEVICES_PENDING_ONBOARDING_START,
153
      onboardingState,
154
      { anchor, place: 'top' },
155
      onboardingComponent
156
    );
157
  }
158
  return onboardingComponent;
39✔
159
};
160

161
export const Authorized = ({
8✔
162
  addDevicesToGroup,
163
  onGroupClick,
164
  onGroupRemoval,
165
  onMakeGatewayClick,
166
  onPreauthClick,
167
  openSettingsDialog,
168
  removeDevicesFromGroup,
169
  showsDialog,
170
  changeLocation
171
}) => {
172
  const limitMaxed = useSelector(getLimitMaxed);
41✔
173
  const devices = useSelector(state => getMappedDevicesList(state, 'deviceList'));
82✔
174
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
41✔
175
  const { columnSelection = [] } = useSelector(getUserSettings);
41✔
176
  const attributes = useSelector(getFilterAttributes);
41✔
177
  const { accepted: acceptedCount, pending: pendingCount, rejected: rejectedCount } = useSelector(getDeviceCountsByStatus);
41✔
178
  const allCount = acceptedCount + rejectedCount;
41✔
179
  const availableIssueOptions = useSelector(getAvailableIssueOptionsByType);
41✔
180
  const customColumnSizes = useSelector(state => state.users.customColumns);
82✔
181
  const deviceListState = useSelector(state => state.devices.deviceList);
82✔
182
  const { total: deviceCount } = deviceListState;
41✔
183
  const filters = useSelector(getDeviceFilters);
41✔
184
  const idAttribute = useSelector(getIdAttribute);
41✔
185
  const onboardingState = useSelector(getOnboardingState);
41✔
186
  const settingsInitialized = useSelector(getUserSettingsInitialized);
41✔
187
  const userCapabilities = useSelector(getUserCapabilities);
41✔
188
  const dispatch = useDispatch();
41✔
189
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
41✔
190

191
  const {
192
    refreshTrigger,
193
    selectedId,
194
    selectedIssues = [],
41✔
195
    isLoading: pageLoading,
196
    selection: selectedRows,
197
    sort = {},
41✔
198
    state: selectedState,
199
    detailsTab: tabSelection
200
  } = deviceListState;
41✔
201
  const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol } = sort;
41✔
202
  const { canManageDevices, canManageUsers } = userCapabilities;
41✔
203
  const currentSelectedState = states[selectedState] ?? states.devices;
41!
204
  const [columnHeaders, setColumnHeaders] = useState([]);
41✔
205
  const [isInitialized, setIsInitialized] = useState(false);
41✔
206
  const [devicesInitialized, setDevicesInitialized] = useState(!!devices.length);
41✔
207
  const [showFilters, setShowFilters] = useState(false);
41✔
208
  const [showCustomization, setShowCustomization] = useState(false);
41✔
209
  const deviceListRef = useRef();
41✔
210
  const timer = useRef();
41✔
211
  const navigate = useNavigate();
41✔
212

213
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
214
  const size = useWindowSize();
41✔
215

216
  const { classes } = useStyles();
41✔
217

218
  useEffect(() => {
41✔
219
    clearAllRetryTimers(dispatchedSetSnackbar);
3✔
220
    if (!filters.length && selectedGroup && groupFilters.length) {
3!
221
      dispatch(setDeviceFilters(groupFilters));
×
222
    }
223
    return () => {
3✔
224
      clearInterval(timer.current);
3✔
225
      clearAllRetryTimers(dispatchedSetSnackbar);
3✔
226
    };
227
    // eslint-disable-next-line react-hooks/exhaustive-deps
228
  }, [dispatch, dispatchedSetSnackbar]);
229

230
  useEffect(() => {
41✔
231
    const columnHeaders = getHeaders(columnSelection, currentSelectedState.defaultHeaders, idAttribute, canManageUsers ? openSettingsDialog : undefined);
15!
232
    setColumnHeaders(columnHeaders);
15✔
233
    // eslint-disable-next-line react-hooks/exhaustive-deps
234
  }, [canManageUsers, columnSelection, idAttribute.attribute, currentSelectedState.defaultHeaders, openSettingsDialog]);
235

236
  useEffect(() => {
41✔
237
    // only set state after all devices id data retrieved
238
    setIsInitialized(isInitialized => isInitialized || (settingsInitialized && devicesInitialized && pageLoading === false));
12✔
239
    setDevicesInitialized(devicesInitialized => devicesInitialized || pageLoading === false);
12✔
240
  }, [settingsInitialized, devicesInitialized, pageLoading]);
241

242
  const onDeviceStateSelectionChange = useCallback(
41✔
243
    newState => {
244
      changeLocation(newState);
×
245
      dispatch(
×
246
        setDeviceListState({ state: newState, page: 1, refreshTrigger: !refreshTrigger, shouldSelectDevices: true, forceRefresh: false, fetchAuth: false })
247
      );
248
    },
249
    [dispatch, changeLocation, refreshTrigger]
250
  );
251

252
  useEffect(() => {
41✔
253
    if (onboardingState.complete) {
10!
254
      return;
×
255
    }
256
    if (pendingCount) {
10!
257
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING_START));
10✔
258
      return;
10✔
259
    }
260
    if (!acceptedCount) {
×
261
      return;
×
262
    }
263

264
    if (acceptedCount < 2 && !window.sessionStorage.getItem('pendings-redirect')) {
×
265
      window.sessionStorage.setItem('pendings-redirect', true);
×
266
      onDeviceStateSelectionChange(DEVICE_STATES.accepted);
×
267
    }
268
  }, [acceptedCount, allCount, pendingCount, onboardingState.complete, dispatch, onDeviceStateSelectionChange, dispatchedSetSnackbar]);
269

270
  useEffect(() => {
41✔
271
    if (selectedGroup) {
3✔
272
      setShowFilters(false);
1✔
273
    }
274
  }, [selectedGroup]);
275

276
  const dispatchDeviceListState = useCallback(
41✔
277
    (options, shouldSelectDevices = true, forceRefresh = false, fetchAuth = false) =>
9✔
278
      dispatch(setDeviceListState({ ...options, shouldSelectDevices, forceRefresh, fetchAuth })),
3✔
279
    [dispatch]
280
  );
281

282
  const refreshDevices = useCallback(() => {
41✔
283
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
×
284
    return dispatchDeviceListState({}, true, true).catch(err =>
×
285
      setRetryTimer(err, 'devices', `Devices couldn't be loaded.`, refreshLength, dispatchedSetSnackbar)
286
    );
287
  }, [dispatchedSetSnackbar, selectedState, dispatchDeviceListState]);
288

289
  useEffect(() => {
41✔
290
    if (!devicesInitialized) {
4✔
291
      return;
1✔
292
    }
293
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
3!
294
    clearInterval(timer.current);
4✔
295
    timer.current = setInterval(() => refreshDevices(), refreshLength);
4✔
296
  }, [devicesInitialized, refreshDevices, selectedState]);
297

298
  useEffect(() => {
41✔
299
    Object.keys(availableIssueOptions).forEach(key => dispatch(getIssueCountsByType({ type: key, filters, group: selectedGroup, state: selectedState })));
6✔
300
    if (availableIssueOptions[DEVICE_ISSUE_OPTIONS.authRequests.key]) {
5!
301
      dispatch(getIssueCountsByType({ type: DEVICE_ISSUE_OPTIONS.authRequests.key, options: { filters: [] } }));
×
302
    }
303
    // eslint-disable-next-line react-hooks/exhaustive-deps
304
  }, [selectedIssues.join(''), JSON.stringify(availableIssueOptions), selectedState, selectedGroup, dispatch, JSON.stringify(filters)]);
305

306
  /*
307
   * Devices
308
   */
309
  const devicesToIds = devices => devices.map(device => device.id);
41✔
310

311
  const onRemoveDevicesFromGroup = devices => {
41✔
312
    const deviceIds = devicesToIds(devices);
×
313
    removeDevicesFromGroup(deviceIds);
×
314
    // if devices.length = number on page but < deviceCount
315
    // move page back to pageNO 1
316
    if (devices.length === deviceIds.length) {
×
317
      handlePageChange(1);
×
318
    }
319
  };
320

321
  const onAuthorizationChange = (devices, changedState) => {
41✔
322
    const deviceIds = devicesToIds(devices);
×
323
    return dispatchDeviceListState({ isLoading: true })
×
324
      .then(() => dispatch(updateDevicesAuth({ deviceIds, status: changedState })))
×
325
      .then(() => onSelectionChange([]));
×
326
  };
327

328
  const onDeviceDismiss = devices =>
41✔
329
    dispatchDeviceListState({ isLoading: true })
×
330
      .then(() => {
331
        const deleteRequests = devices.reduce((accu, device) => {
×
332
          if (device.auth_sets?.length) {
×
333
            accu.push(dispatch(deleteAuthset({ deviceId: device.id, authId: device.auth_sets[0].id })));
×
334
          }
335
          return accu;
×
336
        }, []);
337
        return Promise.all(deleteRequests);
×
338
      })
339
      .then(() => onSelectionChange([]));
×
340

341
  const handlePageChange = useCallback(page => dispatchDeviceListState({ selectedId: undefined, page }), [dispatchDeviceListState]);
41✔
342

343
  const onPageLengthChange = perPage => dispatchDeviceListState({ perPage, page: 1, refreshTrigger: !refreshTrigger });
41✔
344

345
  const onSortChange = attribute => {
41✔
346
    const changedSortCol = attribute.name;
×
347
    let changedSortDown = sortDown === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
348
    if (changedSortCol !== sortCol) {
×
349
      changedSortDown = SORTING_OPTIONS.desc;
×
350
    }
351
    dispatchDeviceListState({
×
352
      sort: { direction: changedSortDown, key: changedSortCol, scope: attribute.scope },
353
      refreshTrigger: !refreshTrigger
354
    });
355
  };
356

357
  const setDetailsTab = detailsTab => dispatchDeviceListState({ detailsTab, setOnly: true });
41✔
358

359
  const onDeviceIssuesSelectionChange = ({ target: { value: selectedIssues } }) =>
41✔
360
    dispatchDeviceListState({ selectedIssues, page: 1, refreshTrigger: !refreshTrigger });
1✔
361

362
  const onSelectionChange = (selection = []) => {
41✔
363
    if (!onboardingState.complete && selection.length) {
1!
364
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING));
1✔
365
    }
366
    dispatchDeviceListState({ selection, setOnly: true });
1✔
367
  };
368

369
  const onToggleCustomizationClick = () => setShowCustomization(toggle);
41✔
370

371
  const onChangeColumns = useCallback(
41✔
372
    (changedColumns, customColumnSizes) => {
373
      const { columnSizes, selectedAttributes } = calculateColumnSelectionSize(changedColumns, customColumnSizes);
1✔
374
      dispatch(updateUserColumnSettings({ columns: columnSizes }));
1✔
375
      dispatch(saveUserSettings({ columnSelection: changedColumns }));
1✔
376
      // we don't need an explicit refresh trigger here, since the selectedAttributes will be different anyway & otherwise the shown list should still be valid
377
      dispatchDeviceListState({ selectedAttributes });
1✔
378
      setShowCustomization(false);
1✔
379
    },
380
    [dispatch, dispatchDeviceListState]
381
  );
382

383
  const onExpandClick = (device = {}) => {
41!
384
    dispatchedSetSnackbar('');
×
385
    const { id } = device;
×
386
    dispatchDeviceListState({ selectedId: deviceListState.selectedId === id ? undefined : id, detailsTab: deviceListState.detailsTab || 'identity' });
×
387
    if (!onboardingState.complete) {
×
388
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING));
×
389
    }
390
  };
391

392
  const onCreateDeploymentClick = devices => navigate(`/deployments?open=true&${devices.map(({ id }) => `deviceId=${id}`).join('&')}`);
41✔
393

394
  const onCloseExpandedDevice = useCallback(() => dispatchDeviceListState({ selectedId: undefined, detailsTab: '' }), [dispatchDeviceListState]);
41✔
395

396
  const onResizeColumns = useCallback(columns => dispatch(updateUserColumnSettings({ columns })), [dispatch]);
41✔
397

398
  const actionCallbacks = {
41✔
399
    onAddDevicesToGroup: addDevicesToGroup,
400
    onAuthorizationChange,
401
    onCreateDeployment: onCreateDeploymentClick,
402
    onDeviceDismiss,
403
    onPromoteGateway: onMakeGatewayClick,
404
    onRemoveDevicesFromGroup
405
  };
406

407
  const listOptionHandlers = [{ key: 'customize', title: 'Customize', onClick: onToggleCustomizationClick }];
41✔
408
  const EmptyState = currentSelectedState.emptyState;
41✔
409

410
  const groupLabel = selectedGroup ? decodeURIComponent(selectedGroup) : ALL_DEVICES;
41✔
411
  const isUngroupedGroup = selectedGroup && selectedGroup === UNGROUPED_GROUP.id;
41✔
412
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
41✔
413

414
  const openedDevice = useDebounce(selectedId, TIMEOUTS.debounceShort);
41✔
415
  const issueOptions = Object.values(availableIssueOptions);
41✔
416
  return (
41✔
417
    <>
418
      <div className="margin-left-small">
419
        <div className="flexbox center-aligned">
420
          <Typography className="margin-right" variant="h6">
421
            {isUngroupedGroup ? UNGROUPED_GROUP.name : groupLabel}
41!
422
          </Typography>
423
          <div className="flexbox space-between center-aligned" style={{ flexGrow: 1 }}>
424
            <div className="flexbox center-aligned">
425
              <DeviceStateSelection className={classes.selection} onStateChange={onDeviceStateSelectionChange} selectedState={selectedState} states={states} />
426
              {!!issueOptions.length && (
63✔
427
                <DeviceIssuesSelection
428
                  className={classes.selection}
429
                  onChange={onDeviceIssuesSelectionChange}
430
                  options={issueOptions}
431
                  selection={selectedIssues}
432
                />
433
              )}
434
              {selectedGroup && !isUngroupedGroup && (
73✔
435
                <Typography variant="subtitle1" className="margin-left flexbox center-aligned">
436
                  {!groupFilters.length ? (
16!
437
                    <>
438
                      Static <Lock className="margin-left-x-small" fontSize="small" />
439
                    </>
440
                  ) : (
441
                    <>
442
                      Dynamic <SyncOutlined className="margin-left-x-small" fontSize="small" />
443
                    </>
444
                  )}
445
                </Typography>
446
              )}
447
            </div>
448
            {canManageDevices && selectedGroup && !isUngroupedGroup && (
114✔
449
              <Button onClick={onGroupRemoval} startIcon={<DeleteIcon />}>
450
                Remove group
451
              </Button>
452
            )}
453
          </div>
454
        </div>
455
        <div className="flexbox center-aligned space-between margin-top margin-bottom-x-small">
456
          {!isUngroupedGroup && (
82✔
457
            <Button
458
              className="flexbox"
459
              color={showFilters ? 'neutral' : ''}
41!
NEW
460
              onClick={() => setShowFilters(toggle)}
×
461
              startIcon={<FilterListIcon />}
462
              variant={showFilters ? 'contained' : 'text'}
41!
463
            >
464
              Filters
465
            </Button>
466
          )}
467
          <ListOptions options={listOptionHandlers} title="Table options" />
468
        </div>
469
        <Filters className={classes.filterCommon} onGroupClick={onGroupClick} open={showFilters} />
470
      </div>
471
      <Loader show={!isInitialized} />
472
      <div className="padding-bottom" ref={deviceListRef}>
473
        {devices.length > 0 ? (
41✔
474
          <DeviceList
475
            columnHeaders={columnHeaders}
476
            customColumnSizes={customColumnSizes}
477
            devices={devices}
478
            deviceListState={deviceListState}
479
            idAttribute={idAttribute}
480
            onChangeRowsPerPage={onPageLengthChange}
481
            onExpandClick={onExpandClick}
482
            onPageChange={handlePageChange}
483
            onResizeColumns={onResizeColumns}
484
            onSelect={onSelectionChange}
485
            onSort={onSortChange}
486
            pageLoading={pageLoading}
487
            pageTotal={deviceCount}
488
            PaginationProps={{ classes: { spacer: 'flexbox no-basis', toolbar: 'padding-left-none' } }}
489
          />
490
        ) : (
491
          <EmptyState allCount={allCount} canManageDevices={canManageDevices} filters={filters} limitMaxed={limitMaxed} onClick={onPreauthClick} />
492
        )}
493
      </div>
494
      <ExpandedDevice
495
        actionCallbacks={actionCallbacks}
496
        deviceId={openedDevice}
497
        onClose={onCloseExpandedDevice}
498
        setDetailsTab={setDetailsTab}
499
        tabSelection={tabSelection}
500
      />
501
      {!selectedId && !showsDialog && <OnboardingComponent deviceListRef={deviceListRef} onboardingState={onboardingState} />}
123✔
502
      {canManageDevices && !!selectedRows.length && (
102✔
503
        <DeviceQuickActions actionCallbacks={actionCallbacks} deviceId={openedDevice} selectedGroup={selectedStaticGroup} />
504
      )}
505
      <ColumnCustomizationDialog
506
        attributes={attributes}
507
        columnHeaders={columnHeaders}
508
        customColumnSizes={customColumnSizes}
509
        idAttribute={idAttribute}
510
        open={showCustomization}
511
        onCancel={onToggleCustomizationClick}
512
        onSubmit={onChangeColumns}
513
      />
514
    </>
515
  );
516
};
517

518
export default Authorized;
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