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

mendersoftware / gui / 1301920191

23 May 2024 07:13AM UTC coverage: 83.42% (-16.5%) from 99.964%
1301920191

Pull #4421

gitlab-ci

mzedel
fix: fixed an issue that sometimes prevented reopening paginated auditlog links

Ticket: None
Changelog: Title
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4421: MEN-7034 - device information in auditlog entries

4456 of 6367 branches covered (69.99%)

34 of 35 new or added lines in 7 files covered. (97.14%)

1668 existing lines in 162 files now uncovered.

8473 of 10157 relevant lines covered (83.42%)

140.52 hits per line

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

78.16
/src/js/components/devices/authorized-devices.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, useMemo, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16
import { useNavigate } from 'react-router-dom';
17

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

23
import { setSnackbar } from '../../actions/appActions';
24
import { deleteAuthset, setDeviceFilters, setDeviceListState, updateDevicesAuth } from '../../actions/deviceActions';
25
import { getIssueCountsByType } from '../../actions/monitorActions';
26
import { advanceOnboarding } from '../../actions/onboardingActions';
27
import { saveUserSettings, updateUserColumnSettings } from '../../actions/userActions';
28
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
29
import { ALL_DEVICES, DEVICE_ISSUE_OPTIONS, DEVICE_STATES, UNGROUPED_GROUP } from '../../constants/deviceConstants';
30
import { onboardingSteps } from '../../constants/onboardingConstants';
31
import { duplicateFilter, toggle } from '../../helpers';
32
import {
33
  getAvailableIssueOptionsByType,
34
  getDeviceCountsByStatus,
35
  getDeviceFilters,
36
  getFilterAttributes,
37
  getIdAttribute,
38
  getLimitMaxed,
39
  getMappedDevicesList,
40
  getOnboardingState,
41
  getSelectedGroupInfo,
42
  getTenantCapabilities,
43
  getUserCapabilities,
44
  getUserSettings
45
} from '../../selectors';
46
import { useDebounce } from '../../utils/debouncehook';
47
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
48
import useWindowSize from '../../utils/resizehook';
49
import { clearAllRetryTimers, setRetryTimer } from '../../utils/retrytimer';
50
import Loader from '../common/loader';
51
import { defaultHeaders, defaultTextRender, getDeviceIdentityText, routes as states } from './base-devices';
52
import DeviceList, { minCellWidth } from './devicelist';
53
import ColumnCustomizationDialog from './dialogs/custom-columns-dialog';
54
import ExpandedDevice from './expanded-device';
55
import DeviceQuickActions from './widgets/devicequickactions';
56
import Filters from './widgets/filters';
57
import DeviceIssuesSelection from './widgets/issueselection';
58
import ListOptions from './widgets/listoptions';
59

60
const deviceRefreshTimes = {
8✔
61
  [DEVICE_STATES.accepted]: TIMEOUTS.refreshLong,
62
  [DEVICE_STATES.pending]: TIMEOUTS.refreshDefault,
63
  [DEVICE_STATES.preauth]: TIMEOUTS.refreshLong,
64
  [DEVICE_STATES.rejected]: TIMEOUTS.refreshLong,
65
  default: TIMEOUTS.refreshDefault
66
};
67

68
const idAttributeTitleMap = {
8✔
69
  id: 'Device ID',
70
  name: 'Name'
71
};
72

73
const headersReducer = (accu, header) => {
8✔
74
  if (header.attribute.scope === accu.column.scope && (header.attribute.name === accu.column.name || header.attribute.alternative === accu.column.name)) {
40✔
75
    accu.header = { ...accu.header, ...header };
6✔
76
  }
77
  return accu;
40✔
78
};
79

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

119
export const getHeaders = (columnSelection = [], currentStateHeaders, idAttribute, openSettingsDialog) => {
8!
120
  const headers = columnSelection.length
36✔
121
    ? columnSelection.map(column => {
122
        let header = { ...column, attribute: { ...column }, textRender: defaultTextRender, sortable: true };
4✔
123
        header = Object.values(defaultHeaders).reduce(headersReducer, { column, header }).header;
4✔
124
        header = currentStateHeaders.reduce(headersReducer, { column, header }).header;
4✔
125
        return header;
4✔
126
      })
127
    : currentStateHeaders;
128
  return [
36✔
129
    {
130
      title: idAttributeTitleMap[idAttribute.attribute] ?? idAttribute.attribute,
44✔
131
      customize: openSettingsDialog,
132
      attribute: { name: idAttribute.attribute, scope: idAttribute.scope },
133
      sortable: true,
134
      textRender: getDeviceIdentityText
135
    },
136
    ...headers,
137
    defaultHeaders.deviceStatus
138
  ];
139
};
140

141
const calculateColumnSelectionSize = (changedColumns, customColumnSizes) =>
8✔
142
  changedColumns.reduce(
1✔
143
    (accu, column) => {
144
      const size = customColumnSizes.find(({ attribute }) => attribute.name === column.key && attribute.scope === column.scope)?.size || minCellWidth;
4✔
145
      accu.columnSizes.push({ attribute: { name: column.key, scope: column.scope }, size });
4✔
146
      accu.selectedAttributes.push({ attribute: column.key, scope: column.scope });
4✔
147
      return accu;
4✔
148
    },
149
    { columnSizes: [], selectedAttributes: [] }
150
  );
151

152
const OnboardingComponent = ({ deviceListRef, onboardingState }) => {
8✔
153
  let onboardingComponent = null;
21✔
154
  const element = deviceListRef.current?.querySelector('body .deviceListItem > div');
21✔
155
  if (element) {
21✔
156
    const anchor = { left: 200, top: element.offsetTop + element.offsetHeight };
15✔
157
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEVICES_PENDING_ONBOARDING, onboardingState, { anchor }, onboardingComponent);
15✔
158
  } else if (deviceListRef.current) {
6✔
159
    const anchor = { top: deviceListRef.current.offsetTop + deviceListRef.current.offsetHeight / 3, left: deviceListRef.current.offsetWidth / 2 + 30 };
3✔
160
    onboardingComponent = getOnboardingComponentFor(
3✔
161
      onboardingSteps.DEVICES_PENDING_ONBOARDING_START,
162
      onboardingState,
163
      { anchor, place: 'top' },
164
      onboardingComponent
165
    );
166
  }
167
  return onboardingComponent;
21✔
168
};
169

170
export const Authorized = ({
8✔
171
  addDevicesToGroup,
172
  onGroupClick,
173
  onGroupRemoval,
174
  onMakeGatewayClick,
175
  onPreauthClick,
176
  openSettingsDialog,
177
  removeDevicesFromGroup,
178
  showsDialog
179
}) => {
180
  const limitMaxed = useSelector(getLimitMaxed);
23✔
181
  const devices = useSelector(state => getMappedDevicesList(state, 'deviceList'));
64✔
182
  const { selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
23!
183
  const { columnSelection = [] } = useSelector(getUserSettings);
23!
184
  const attributes = useSelector(getFilterAttributes);
23✔
185
  const { accepted: acceptedCount, pending: pendingCount, rejected: rejectedCount } = useSelector(getDeviceCountsByStatus);
23✔
186
  const allCount = acceptedCount + rejectedCount;
23✔
187
  const availableIssueOptions = useSelector(getAvailableIssueOptionsByType);
23✔
188
  const customColumnSizes = useSelector(state => state.users.customColumns);
64✔
189
  const deviceListState = useSelector(state => state.devices.deviceList);
64✔
190
  const { total: deviceCount } = deviceListState;
23✔
191
  const filters = useSelector(getDeviceFilters);
23✔
192
  const idAttribute = useSelector(getIdAttribute);
23✔
193
  const onboardingState = useSelector(getOnboardingState);
23✔
194
  const settingsInitialized = useSelector(state => state.users.settingsInitialized);
64✔
195
  const tenantCapabilities = useSelector(getTenantCapabilities);
23✔
196
  const userCapabilities = useSelector(getUserCapabilities);
23✔
197
  const dispatch = useDispatch();
23✔
198
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
23✔
199

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

223
  // eslint-disable-next-line no-unused-vars
224
  const size = useWindowSize();
23✔
225

226
  const { classes } = useStyles();
23✔
227

228
  useEffect(() => {
23✔
229
    clearAllRetryTimers(dispatchedSetSnackbar);
3✔
230
    if (!filters.length && selectedGroup && groupFilters.length) {
3!
UNCOV
231
      dispatch(setDeviceFilters(groupFilters));
×
232
    }
233
    return () => {
3✔
234
      clearInterval(timer.current);
3✔
235
      clearAllRetryTimers(dispatchedSetSnackbar);
3✔
236
    };
237
    // eslint-disable-next-line react-hooks/exhaustive-deps
238
  }, [dispatch, dispatchedSetSnackbar]);
239

240
  useEffect(() => {
23✔
241
    const columnHeaders = getHeaders(columnSelection, currentSelectedState.defaultHeaders, idAttribute, openSettingsDialog);
7✔
242
    setColumnHeaders(columnHeaders);
7✔
243
    // eslint-disable-next-line react-hooks/exhaustive-deps
244
  }, [columnSelection, idAttribute.attribute, currentSelectedState.defaultHeaders, openSettingsDialog]);
245

246
  useEffect(() => {
23✔
247
    // only set state after all devices id data retrieved
248
    setIsInitialized(isInitialized => isInitialized || (settingsInitialized && devicesInitialized && pageLoading === false));
10✔
249
    setDevicesInitialized(devicesInitialized => devicesInitialized || pageLoading === false);
10✔
250
  }, [settingsInitialized, devicesInitialized, pageLoading]);
251

252
  const onDeviceStateSelectionChange = useCallback(
23✔
UNCOV
253
    newState => dispatch(setDeviceListState({ state: newState, page: 1, refreshTrigger: !refreshTrigger }, true, false, false)),
×
254
    [dispatch, refreshTrigger]
255
  );
256

257
  useEffect(() => {
23✔
258
    if (onboardingState.complete) {
7!
UNCOV
259
      return;
×
260
    }
261
    if (pendingCount) {
7!
262
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING_START));
7✔
263
      return;
7✔
264
    }
UNCOV
265
    if (!acceptedCount) {
×
UNCOV
266
      return;
×
267
    }
UNCOV
268
    dispatch(advanceOnboarding(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING));
×
269

UNCOV
270
    if (acceptedCount < 2 && !window.sessionStorage.getItem('pendings-redirect')) {
×
UNCOV
271
      window.sessionStorage.setItem('pendings-redirect', true);
×
UNCOV
272
      onDeviceStateSelectionChange(DEVICE_STATES.accepted);
×
273
    }
274
    // eslint-disable-next-line react-hooks/exhaustive-deps
275
  }, [acceptedCount, allCount, pendingCount, onboardingState.complete, dispatch, onDeviceStateSelectionChange, dispatchedSetSnackbar]);
276

277
  useEffect(() => {
23✔
278
    setShowFilters(false);
3✔
279
  }, [selectedGroup]);
280
  const dispatchDeviceListState = useCallback(
23✔
281
    (options, shouldSelectDevices = true, forceRefresh = false, fetchAuth = false) => {
9✔
282
      return dispatch(setDeviceListState(options, shouldSelectDevices, forceRefresh, fetchAuth));
3✔
283
    },
284
    [dispatch]
285
  );
286

287
  const refreshDevices = useCallback(() => {
23✔
UNCOV
288
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
×
UNCOV
289
    return dispatchDeviceListState({}, true, true).catch(err =>
×
UNCOV
290
      setRetryTimer(err, 'devices', `Devices couldn't be loaded.`, refreshLength, dispatchedSetSnackbar)
×
291
    );
292
  }, [dispatchedSetSnackbar, selectedState, dispatchDeviceListState]);
293

294
  useEffect(() => {
23✔
295
    if (!devicesInitialized) {
4✔
296
      return;
1✔
297
    }
298
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
3!
299
    clearInterval(timer.current);
3✔
300
    timer.current = setInterval(() => refreshDevices(), refreshLength);
3✔
301
  }, [devicesInitialized, refreshDevices, selectedState]);
302

303
  useEffect(() => {
23✔
304
    Object.keys(availableIssueOptions).map(key => dispatch(getIssueCountsByType(key, { filters, group: selectedGroup, state: selectedState })));
6✔
305
    availableIssueOptions[DEVICE_ISSUE_OPTIONS.authRequests.key]
5!
306
      ? dispatch(getIssueCountsByType(DEVICE_ISSUE_OPTIONS.authRequests.key, { filters: [] }))
307
      : undefined;
308
    // eslint-disable-next-line react-hooks/exhaustive-deps
309
  }, [selectedIssues.join(''), JSON.stringify(availableIssueOptions), selectedState, selectedGroup, dispatch, JSON.stringify(filters)]);
310

311
  /*
312
   * Devices
313
   */
314
  const devicesToIds = devices => devices.map(device => device.id);
23✔
315

316
  const onRemoveDevicesFromGroup = devices => {
23✔
UNCOV
317
    const deviceIds = devicesToIds(devices);
×
UNCOV
318
    removeDevicesFromGroup(deviceIds);
×
319
    // if devices.length = number on page but < deviceCount
320
    // move page back to pageNO 1
UNCOV
321
    if (devices.length === deviceIds.length) {
×
UNCOV
322
      handlePageChange(1);
×
323
    }
324
  };
325

326
  const onAuthorizationChange = (devices, changedState) => {
23✔
UNCOV
327
    const deviceIds = devicesToIds(devices);
×
UNCOV
328
    return dispatchDeviceListState({ isLoading: true })
×
UNCOV
329
      .then(() => dispatch(updateDevicesAuth(deviceIds, changedState)))
×
UNCOV
330
      .then(() => onSelectionChange([]));
×
331
  };
332

333
  const onDeviceDismiss = devices =>
23✔
UNCOV
334
    dispatchDeviceListState({ isLoading: true })
×
335
      .then(() => {
UNCOV
336
        const deleteRequests = devices.reduce((accu, device) => {
×
UNCOV
337
          if (device.auth_sets?.length) {
×
UNCOV
338
            accu.push(dispatch(deleteAuthset(device.id, device.auth_sets[0].id)));
×
339
          }
UNCOV
340
          return accu;
×
341
        }, []);
UNCOV
342
        return Promise.all(deleteRequests);
×
343
      })
UNCOV
344
      .then(() => onSelectionChange([]));
×
345

346
  const handlePageChange = useCallback(page => dispatchDeviceListState({ selectedId: undefined, page }), [dispatchDeviceListState]);
23✔
347

348
  const onPageLengthChange = perPage => dispatchDeviceListState({ perPage, page: 1, refreshTrigger: !refreshTrigger });
23✔
349

350
  const onSortChange = attribute => {
23✔
UNCOV
351
    let changedSortCol = attribute.name;
×
UNCOV
352
    let changedSortDown = sortDown === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
UNCOV
353
    if (changedSortCol !== sortCol) {
×
UNCOV
354
      changedSortDown = SORTING_OPTIONS.desc;
×
355
    }
UNCOV
356
    dispatchDeviceListState({
×
357
      sort: { direction: changedSortDown, key: changedSortCol, scope: attribute.scope },
358
      refreshTrigger: !refreshTrigger
359
    });
360
  };
361

362
  const setDetailsTab = detailsTab => dispatchDeviceListState({ detailsTab, setOnly: true });
23✔
363

364
  const onDeviceIssuesSelectionChange = ({ target: { value: selectedIssues } }) =>
23✔
365
    dispatchDeviceListState({ selectedIssues, page: 1, refreshTrigger: !refreshTrigger });
1✔
366

367
  const onSelectionChange = (selection = []) => {
23!
368
    if (!onboardingState.complete && selection.length) {
1!
369
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING));
1✔
370
    }
371
    dispatchDeviceListState({ selection, setOnly: true });
1✔
372
  };
373

374
  const onToggleCustomizationClick = () => setShowCustomization(toggle);
23✔
375

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

388
  const onExpandClick = (device = {}) => {
23!
UNCOV
389
    dispatchedSetSnackbar('');
×
UNCOV
390
    const { id } = device;
×
UNCOV
391
    dispatchDeviceListState({ selectedId: deviceListState.selectedId === id ? undefined : id, detailsTab: deviceListState.detailsTab || 'identity' });
×
UNCOV
392
    if (!onboardingState.complete) {
×
UNCOV
393
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING));
×
394
    }
395
  };
396

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

399
  const onCloseExpandedDevice = useCallback(() => dispatchDeviceListState({ selectedId: undefined, detailsTab: '' }), [dispatchDeviceListState]);
23✔
400

401
  const onResizeColumns = useCallback(columns => dispatch(updateUserColumnSettings(columns)), [dispatch]);
23✔
402

403
  const actionCallbacks = {
23✔
404
    onAddDevicesToGroup: addDevicesToGroup,
405
    onAuthorizationChange,
406
    onCreateDeployment: onCreateDeploymentClick,
407
    onDeviceDismiss,
408
    onPromoteGateway: onMakeGatewayClick,
409
    onRemoveDevicesFromGroup
410
  };
411

412
  const listOptionHandlers = [{ key: 'customize', title: 'Customize', onClick: onToggleCustomizationClick }];
23✔
413
  const EmptyState = currentSelectedState.emptyState;
23✔
414

415
  const groupLabel = selectedGroup ? decodeURIComponent(selectedGroup) : ALL_DEVICES;
23✔
416
  const isUngroupedGroup = selectedGroup && selectedGroup === UNGROUPED_GROUP.id;
23✔
417
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
23✔
418

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

514
export default Authorized;
515

516
export const DeviceStateSelection = ({ onStateChange, selectedState = '', states }) => {
8!
517
  const { classes } = useStyles();
23✔
518
  const availableStates = useMemo(() => Object.values(states).filter(duplicateFilter), [states]);
23✔
519

520
  return (
23✔
521
    <div className="flexbox centered">
522
      Status:
523
      <Select className={classes.selection} disableUnderline onChange={e => onStateChange(e.target.value)} value={selectedState}>
1✔
524
        {availableStates.map(state => (
525
          <MenuItem key={state.key} value={state.key}>
117✔
526
            {state.title()}
527
          </MenuItem>
528
        ))}
529
      </Select>
530
    </div>
531
  );
532
};
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