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

mendersoftware / gui / 913068613

pending completion
913068613

Pull #3803

gitlab-ci

web-flow
Merge pull request #3801 from mzedel/men-6383

MEN-6383 - device check in time
Pull Request #3803: staging alignment

4418 of 6435 branches covered (68.66%)

178 of 246 new or added lines in 27 files covered. (72.36%)

8352 of 10138 relevant lines covered (82.38%)

160.95 hits per line

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

63.78
/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
  getFeatures,
38
  getFilterAttributes,
39
  getIdAttribute,
40
  getLimitMaxed,
41
  getMappedDevicesList,
42
  getOnboardingState,
43
  getSelectedGroupInfo,
44
  getShowHelptips,
45
  getTenantCapabilities,
46
  getUserCapabilities,
47
  getUserSettings
48
} from '../../selectors';
49
import { useDebounce } from '../../utils/debouncehook';
50
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
51
import useWindowSize from '../../utils/resizehook';
52
import { clearAllRetryTimers, setRetryTimer } from '../../utils/retrytimer';
53
import Loader from '../common/loader';
54
import { ExpandDevice } from '../helptips/helptooltips';
55
import { defaultHeaders, defaultTextRender, getDeviceIdentityText, routes as states } from './base-devices';
56
import DeviceList, { minCellWidth } from './devicelist';
57
import ColumnCustomizationDialog from './dialogs/custom-columns-dialog';
58
import ExpandedDevice from './expanded-device';
59
import DeviceQuickActions from './widgets/devicequickactions';
60
import Filters from './widgets/filters';
61
import DeviceIssuesSelection from './widgets/issueselection';
62
import ListOptions from './widgets/listoptions';
63

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

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

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

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

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

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

148
const OnboardingComponent = ({ authorizeRef, deviceListRef, onboardingState, selectedRows }) => {
9✔
149
  let onboardingComponent = null;
10✔
150
  if (deviceListRef.current) {
10✔
151
    const element = deviceListRef.current.querySelector('body .deviceListItem > div');
3✔
152
    const anchor = { left: 200, top: element ? element.offsetTop + element.offsetHeight : 170 };
3!
153
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING, onboardingState, { anchor }, onboardingComponent);
3✔
154
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED, onboardingState, { anchor }, onboardingComponent);
3✔
155
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEVICES_PENDING_ONBOARDING, onboardingState, { anchor }, onboardingComponent);
3✔
156
  }
157
  if (selectedRows && authorizeRef.current) {
10!
158
    const anchor = {
×
159
      left: authorizeRef.current.offsetLeft - authorizeRef.current.offsetWidth,
160
      top:
161
        authorizeRef.current.offsetTop +
162
        authorizeRef.current.offsetHeight -
163
        authorizeRef.current.lastElementChild.offsetHeight +
164
        authorizeRef.current.lastElementChild.firstElementChild.offsetHeight * 1.5
165
    };
166
    onboardingComponent = getOnboardingComponentFor(
×
167
      onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING,
168
      onboardingState,
169
      { place: 'left', anchor },
170
      onboardingComponent
171
    );
172
  }
173
  return onboardingComponent;
10✔
174
};
175

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

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

232
  // eslint-disable-next-line no-unused-vars
233
  const size = useWindowSize();
10✔
234

235
  const { classes } = useStyles();
10✔
236

237
  useEffect(() => {
10✔
238
    clearAllRetryTimers(dispatchedSetSnackbar);
3✔
239
    if (!filters.length && selectedGroup && groupFilters.length) {
3!
240
      dispatch(setDeviceFilters(groupFilters));
×
241
    }
242
    return () => {
3✔
243
      clearInterval(timer.current);
3✔
244
      clearAllRetryTimers(dispatchedSetSnackbar);
3✔
245
    };
246
  }, []);
247

248
  useEffect(() => {
10✔
249
    const columnHeaders = getHeaders(columnSelection, currentSelectedState.defaultHeaders, idAttribute, openSettingsDialog);
3✔
250
    setColumnHeaders(columnHeaders);
3✔
251
  }, [columnSelection, selectedState, idAttribute.attribute]);
252

253
  useEffect(() => {
10✔
254
    // only set state after all devices id data retrieved
255
    setIsInitialized(isInitialized => isInitialized || (settingsInitialized && devicesInitialized && pageLoading === false));
4✔
256
    setDevicesInitialized(devicesInitialized => devicesInitialized || pageLoading === false);
4✔
257
  }, [settingsInitialized, devicesInitialized, pageLoading]);
258

259
  useEffect(() => {
10✔
260
    if (onboardingState.complete) {
3!
261
      return;
×
262
    }
263
    if (pendingCount) {
3!
264
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING_START));
3✔
265
      return;
3✔
266
    }
267
    if (!acceptedCount) {
×
268
      return;
×
269
    }
270
    dispatch(advanceOnboarding(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING));
×
271

272
    if (acceptedCount < 2) {
×
273
      if (!window.sessionStorage.getItem('pendings-redirect')) {
×
274
        window.sessionStorage.setItem('pendings-redirect', true);
×
275
        onDeviceStateSelectionChange(DEVICE_STATES.accepted);
×
276
      }
277
      setTimeout(() => {
×
278
        const notification = getOnboardingComponentFor(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING_NOTIFICATION, onboardingState, {
×
279
          setSnackbar: dispatchedSetSnackbar
280
        });
281
        !!notification && dispatchedSetSnackbar('open', TIMEOUTS.refreshDefault, '', notification, () => {}, true);
×
282
      }, TIMEOUTS.debounceDefault);
283
    }
284
  }, [acceptedCount, allCount, pendingCount, onboardingState.complete]);
285

286
  useEffect(() => {
10✔
287
    setShowFilters(false);
3✔
288
  }, [selectedGroup]);
289

290
  const refreshDevices = useCallback(() => {
10✔
NEW
291
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
×
NEW
292
    return dispatch(setDeviceListState({ refreshTrigger: !refreshTrigger })).catch(err =>
×
NEW
293
      setRetryTimer(err, 'devices', `Devices couldn't be loaded.`, refreshLength, dispatchedSetSnackbar)
×
294
    );
295
  }, [refreshTrigger, selectedState]);
296

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

306
  useEffect(() => {
10✔
307
    Object.keys(availableIssueOptions).map(key => dispatch(getIssueCountsByType(key, { filters, group: selectedGroup, state: selectedState })));
3✔
308
    availableIssueOptions[DEVICE_ISSUE_OPTIONS.authRequests.key]
3!
309
      ? dispatch(getIssueCountsByType(DEVICE_ISSUE_OPTIONS.authRequests.key, { filters: [] }))
310
      : undefined;
311
  }, [selectedIssues, availableIssueOptions, selectedState, selectedGroup]);
312

313
  /*
314
   * Devices
315
   */
316
  const devicesToIds = devices => devices.map(device => device.id);
10✔
317

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

328
  const onAuthorizationChange = (devices, changedState) => {
10✔
329
    const deviceIds = devicesToIds(devices);
×
330
    return dispatch(setDeviceListState({ isLoading: true }))
×
331
      .then(() => dispatch(updateDevicesAuth(deviceIds, changedState)))
×
332
      .then(() => onSelectionChange([]));
×
333
  };
334

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

348
  const handlePageChange = page => dispatch(setDeviceListState({ selectedId: undefined, page, refreshTrigger: !refreshTrigger }));
10✔
349

350
  const onPageLengthChange = perPage => dispatch(setDeviceListState({ perPage, page: 1, refreshTrigger: !refreshTrigger }));
10✔
351

352
  const onSortChange = attribute => {
10✔
353
    let changedSortCol = attribute.name;
×
354
    let changedSortDown = sortDown === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
355
    if (changedSortCol !== sortCol) {
×
356
      changedSortDown = SORTING_OPTIONS.desc;
×
357
    }
358
    dispatch(
×
359
      setDeviceListState({
360
        sort: { direction: changedSortDown, key: changedSortCol, scope: attribute.scope },
361
        refreshTrigger: !refreshTrigger
362
      })
363
    );
364
  };
365

366
  const onFilterChange = () => handlePageChange(1);
10✔
367

368
  const onDeviceStateSelectionChange = newState => dispatch(setDeviceListState({ state: newState, page: 1, refreshTrigger: !refreshTrigger }));
10✔
369

370
  const setDetailsTab = detailsTab => dispatch(setDeviceListState({ detailsTab, setOnly: true }));
10✔
371

372
  const onDeviceIssuesSelectionChange = ({ target: { value: selectedIssues } }) =>
10✔
373
    dispatch(setDeviceListState({ selectedIssues, page: 1, refreshTrigger: !refreshTrigger }));
1✔
374

375
  const onSelectionChange = (selection = []) => {
10!
376
    if (!onboardingState.complete && selection.length) {
1!
377
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING));
1✔
378
    }
379
    dispatch(setDeviceListState({ selection, setOnly: true }));
1✔
380
  };
381

382
  const onToggleCustomizationClick = () => setShowCustomization(toggle);
10✔
383

384
  const onChangeColumns = (changedColumns, customColumnSizes) => {
10✔
385
    const { columnSizes, selectedAttributes } = calculateColumnSelectionSize(changedColumns, customColumnSizes);
1✔
386
    dispatch(updateUserColumnSettings(columnSizes));
1✔
387
    dispatch(saveUserSettings({ columnSelection: changedColumns }));
1✔
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
389
    dispatch(setDeviceListState({ selectedAttributes }));
1✔
390
    setShowCustomization(false);
1✔
391
  };
392

393
  const onExpandClick = (device = {}) => {
10!
394
    dispatchedSetSnackbar('');
×
395
    const { attributes = {}, id, status } = device;
×
396
    dispatch(setDeviceListState({ selectedId: deviceListState.selectedId === id ? undefined : id }));
×
397
    if (!onboardingState.complete) {
×
398
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING));
×
399
      if (status === DEVICE_STATES.accepted && Object.values(attributes).some(value => value)) {
×
400
        dispatch(advanceOnboarding(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING_NOTIFICATION));
×
401
      }
402
    }
403
  };
404

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

407
  const actionCallbacks = {
10✔
408
    onAddDevicesToGroup: addDevicesToGroup,
409
    onAuthorizationChange,
410
    onCreateDeployment: onCreateDeploymentClick,
411
    onDeviceDismiss,
412
    onPromoteGateway: onMakeGatewayClick,
413
    onRemoveDevicesFromGroup
414
  };
415

416
  const listOptionHandlers = [{ key: 'customize', title: 'Customize', onClick: onToggleCustomizationClick }];
10✔
417
  const devicePendingTip = getOnboardingComponentFor(onboardingSteps.DEVICES_PENDING_ONBOARDING_START, onboardingState);
10✔
418

419
  const EmptyState = currentSelectedState.emptyState;
10✔
420

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

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

550
export default Authorized;
551

552
export const DeviceStateSelection = ({ onStateChange, selectedState = '', states }) => {
9!
553
  const theme = useTheme();
12✔
554
  const availableStates = useMemo(() => Object.values(states).filter(duplicateFilter), [states]);
12✔
555

556
  return (
12✔
557
    <div className="flexbox centered">
558
      Status:
559
      <Select
560
        disableUnderline
561
        onChange={e => onStateChange(e.target.value)}
1✔
562
        value={selectedState}
563
        style={{ fontSize: 13, marginLeft: theme.spacing(), marginTop: 2 }}
564
      >
565
        {availableStates.map(state => (
566
          <MenuItem key={state.key} value={state.key}>
62✔
567
            {state.title()}
568
          </MenuItem>
569
        ))}
570
      </Select>
571
    </div>
572
  );
573
};
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