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

mendersoftware / gui / 988636826

01 Sep 2023 04:04AM UTC coverage: 82.384% (-17.6%) from 99.964%
988636826

Pull #3969

gitlab-ci

web-flow
chore: Bump autoprefixer from 10.4.14 to 10.4.15

Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.4.14 to 10.4.15.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.14...10.4.15)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3969: chore: Bump autoprefixer from 10.4.14 to 10.4.15

4346 of 6321 branches covered (0.0%)

8259 of 10025 relevant lines covered (82.38%)

192.73 hits per line

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

72.57
/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 { useTheme } from '@mui/material/styles';
22
import { makeStyles } from 'tss-react/mui';
23

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

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

70
const idAttributeTitleMap = {
9✔
71
  id: 'Device ID',
72
  name: 'Name'
73
};
74

75
const headersReducer = (accu, header) => {
9✔
76
  if (header.attribute.scope === accu.column.scope && (header.attribute.name === accu.column.name || header.attribute.alternative === accu.column.name)) {
×
77
    accu.header = { ...accu.header, ...header };
×
78
  }
79
  return accu;
×
80
};
81

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

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

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

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

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

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

218
  // eslint-disable-next-line no-unused-vars
219
  const size = useWindowSize();
26✔
220

221
  const { classes } = useStyles();
26✔
222

223
  useEffect(() => {
26✔
224
    clearAllRetryTimers(dispatchedSetSnackbar);
3✔
225
    if (!filters.length && selectedGroup && groupFilters.length) {
3!
226
      dispatch(setDeviceFilters(groupFilters));
×
227
    }
228
    return () => {
3✔
229
      clearInterval(timer.current);
3✔
230
      clearAllRetryTimers(dispatchedSetSnackbar);
3✔
231
    };
232
    // eslint-disable-next-line react-hooks/exhaustive-deps
233
  }, [dispatch, dispatchedSetSnackbar]);
234

235
  useEffect(() => {
26✔
236
    const columnHeaders = getHeaders(columnSelection, currentSelectedState.defaultHeaders, idAttribute, openSettingsDialog);
6✔
237
    setColumnHeaders(columnHeaders);
6✔
238
    // eslint-disable-next-line react-hooks/exhaustive-deps
239
  }, [columnSelection, idAttribute.attribute, currentSelectedState.defaultHeaders, openSettingsDialog]);
240

241
  useEffect(() => {
26✔
242
    // only set state after all devices id data retrieved
243
    setIsInitialized(isInitialized => isInitialized || (settingsInitialized && devicesInitialized && pageLoading === false));
8✔
244
    setDevicesInitialized(devicesInitialized => devicesInitialized || pageLoading === false);
8✔
245
  }, [settingsInitialized, devicesInitialized, pageLoading]);
246

247
  const onDeviceStateSelectionChange = useCallback(
26✔
248
    newState => dispatch(setDeviceListState({ state: newState, page: 1, refreshTrigger: !refreshTrigger })),
×
249
    [dispatch, refreshTrigger]
250
  );
251

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

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

272
  useEffect(() => {
26✔
273
    setShowFilters(false);
3✔
274
  }, [selectedGroup]);
275

276
  const refreshDevices = useCallback(() => {
26✔
277
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
×
278
    return dispatch(setDeviceListState({}, true, true)).catch(err =>
×
279
      setRetryTimer(err, 'devices', `Devices couldn't be loaded.`, refreshLength, dispatchedSetSnackbar)
×
280
    );
281
  }, [dispatch, dispatchedSetSnackbar, selectedState]);
282

283
  useEffect(() => {
26✔
284
    if (!devicesInitialized) {
4✔
285
      return;
1✔
286
    }
287
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
3!
288
    clearInterval(timer.current);
3✔
289
    timer.current = setInterval(() => refreshDevices(), refreshLength);
3✔
290
  }, [devicesInitialized, refreshDevices, selectedState]);
291

292
  useEffect(() => {
26✔
293
    Object.keys(availableIssueOptions).map(key => dispatch(getIssueCountsByType(key, { filters, group: selectedGroup, state: selectedState })));
6✔
294
    availableIssueOptions[DEVICE_ISSUE_OPTIONS.authRequests.key]
5!
295
      ? dispatch(getIssueCountsByType(DEVICE_ISSUE_OPTIONS.authRequests.key, { filters: [] }))
296
      : undefined;
297
    // eslint-disable-next-line react-hooks/exhaustive-deps
298
  }, [selectedIssues.join(''), JSON.stringify(availableIssueOptions), selectedState, selectedGroup, dispatch, JSON.stringify(filters)]);
299

300
  /*
301
   * Devices
302
   */
303
  const devicesToIds = devices => devices.map(device => device.id);
26✔
304

305
  const onRemoveDevicesFromGroup = devices => {
26✔
306
    const deviceIds = devicesToIds(devices);
×
307
    removeDevicesFromGroup(deviceIds);
×
308
    // if devices.length = number on page but < deviceCount
309
    // move page back to pageNO 1
310
    if (devices.length === deviceIds.length) {
×
311
      handlePageChange(1);
×
312
    }
313
  };
314

315
  const onAuthorizationChange = (devices, changedState) => {
26✔
316
    const deviceIds = devicesToIds(devices);
×
317
    return dispatch(setDeviceListState({ isLoading: true }))
×
318
      .then(() => dispatch(updateDevicesAuth(deviceIds, changedState)))
×
319
      .then(() => onSelectionChange([]));
×
320
  };
321

322
  const onDeviceDismiss = devices =>
26✔
323
    dispatch(setDeviceListState({ isLoading: true }))
×
324
      .then(() => {
325
        const deleteRequests = devices.reduce((accu, device) => {
×
326
          if (device.auth_sets?.length) {
×
327
            accu.push(dispatch(deleteAuthset(device.id, device.auth_sets[0].id)));
×
328
          }
329
          return accu;
×
330
        }, []);
331
        return Promise.all(deleteRequests);
×
332
      })
333
      .then(() => onSelectionChange([]));
×
334

335
  const handlePageChange = useCallback(page => dispatch(setDeviceListState({ selectedId: undefined, page })), [dispatch]);
26✔
336

337
  const onPageLengthChange = perPage => dispatch(setDeviceListState({ perPage, page: 1, refreshTrigger: !refreshTrigger }));
26✔
338

339
  const onSortChange = attribute => {
26✔
340
    let changedSortCol = attribute.name;
×
341
    let changedSortDown = sortDown === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
342
    if (changedSortCol !== sortCol) {
×
343
      changedSortDown = SORTING_OPTIONS.desc;
×
344
    }
345
    dispatch(
×
346
      setDeviceListState({
347
        sort: { direction: changedSortDown, key: changedSortCol, scope: attribute.scope },
348
        refreshTrigger: !refreshTrigger
349
      })
350
    );
351
  };
352

353
  const setDetailsTab = detailsTab => dispatch(setDeviceListState({ detailsTab, setOnly: true }));
26✔
354

355
  const onDeviceIssuesSelectionChange = ({ target: { value: selectedIssues } }) =>
26✔
356
    dispatch(setDeviceListState({ selectedIssues, page: 1, refreshTrigger: !refreshTrigger }));
1✔
357

358
  const onSelectionChange = (selection = []) => {
26!
359
    if (!onboardingState.complete && selection.length) {
1!
360
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING));
1✔
361
    }
362
    dispatch(setDeviceListState({ selection, setOnly: true }));
1✔
363
  };
364

365
  const onToggleCustomizationClick = () => setShowCustomization(toggle);
26✔
366

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

379
  const onExpandClick = (device = {}) => {
26!
380
    dispatchedSetSnackbar('');
×
381
    const { id } = device;
×
382
    dispatch(setDeviceListState({ selectedId: deviceListState.selectedId === id ? undefined : id, detailsTab: deviceListState.detailsTab || 'identity' }));
×
383
    if (!onboardingState.complete) {
×
384
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING));
×
385
    }
386
  };
387

388
  const onCreateDeploymentClick = devices => {
26✔
389
    const devicesLink = !onboardingState.complete ? '' : `&${devices.map(({ id }) => `deviceId=${id}`).join('&')}`;
×
390
    return navigate(`/deployments?open=true${devicesLink}`);
×
391
  };
392

393
  const onCloseExpandedDevice = useCallback(() => dispatch(setDeviceListState({ selectedId: undefined, detailsTab: '' })), [dispatch]);
26✔
394

395
  const actionCallbacks = {
26✔
396
    onAddDevicesToGroup: addDevicesToGroup,
397
    onAuthorizationChange,
398
    onCreateDeployment: onCreateDeploymentClick,
399
    onDeviceDismiss,
400
    onPromoteGateway: onMakeGatewayClick,
401
    onRemoveDevicesFromGroup
402
  };
403

404
  const listOptionHandlers = [{ key: 'customize', title: 'Customize', onClick: onToggleCustomizationClick }];
26✔
405
  const EmptyState = currentSelectedState.emptyState;
26✔
406

407
  const groupLabel = selectedGroup ? decodeURIComponent(selectedGroup) : ALL_DEVICES;
26✔
408
  const isUngroupedGroup = selectedGroup && selectedGroup === UNGROUPED_GROUP.id;
26✔
409
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
26✔
410

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

508
export default Authorized;
509

510
export const DeviceStateSelection = ({ onStateChange, selectedState = '', states }) => {
9!
511
  const theme = useTheme();
25✔
512
  const availableStates = useMemo(() => Object.values(states).filter(duplicateFilter), [states]);
25✔
513

514
  return (
25✔
515
    <div className="flexbox centered">
516
      Status:
517
      <Select
518
        disableUnderline
519
        onChange={e => onStateChange(e.target.value)}
1✔
520
        value={selectedState}
521
        style={{ fontSize: 13, marginLeft: theme.spacing(), marginTop: 2 }}
522
      >
523
        {availableStates.map(state => (
524
          <MenuItem key={state.key} value={state.key}>
127✔
525
            {state.title()}
526
          </MenuItem>
527
        ))}
528
      </Select>
529
    </div>
530
  );
531
};
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