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

mendersoftware / gui / 947088195

pending completion
947088195

Pull #2661

gitlab-ci

mzedel
chore: improved device filter scrolling behaviour

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #2661: chore: added lint rules for hooks usage

4411 of 6415 branches covered (68.76%)

297 of 440 new or added lines in 62 files covered. (67.5%)

1617 existing lines in 163 files now uncovered.

8311 of 10087 relevant lines covered (82.39%)

192.12 hits per line

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

64.65
/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 { ExpandDevice } from '../helptips/helptooltips';
54
import { defaultHeaders, defaultTextRender, getDeviceIdentityText, routes as states } from './base-devices';
55
import DeviceList, { minCellWidth } from './devicelist';
56
import ColumnCustomizationDialog from './dialogs/custom-columns-dialog';
57
import ExpandedDevice from './expanded-device';
58
import DeviceQuickActions from './widgets/devicequickactions';
59
import Filters from './widgets/filters';
60
import DeviceIssuesSelection from './widgets/issueselection';
61
import ListOptions from './widgets/listoptions';
62

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

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

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

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

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

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

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

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

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

230
  // eslint-disable-next-line no-unused-vars
231
  const size = useWindowSize();
27✔
232

233
  const { classes } = useStyles();
27✔
234

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

247
  useEffect(() => {
27✔
248
    const columnHeaders = getHeaders(columnSelection, currentSelectedState.defaultHeaders, idAttribute, openSettingsDialog);
6✔
249
    setColumnHeaders(columnHeaders);
6✔
250
    // eslint-disable-next-line react-hooks/exhaustive-deps
251
  }, [columnSelection, idAttribute.attribute, currentSelectedState.defaultHeaders, openSettingsDialog]);
252

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

259
  const onDeviceStateSelectionChange = useCallback(
27✔
NEW
260
    newState => dispatch(setDeviceListState({ state: newState, page: 1, refreshTrigger: !refreshTrigger })),
×
261
    [dispatch, refreshTrigger]
262
  );
263

264
  useEffect(() => {
27✔
265
    if (onboardingState.complete) {
5!
UNCOV
266
      return;
×
267
    }
268
    if (pendingCount) {
5!
269
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING_START));
5✔
270
      return;
5✔
271
    }
UNCOV
272
    if (!acceptedCount) {
×
UNCOV
273
      return;
×
274
    }
UNCOV
275
    dispatch(advanceOnboarding(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING));
×
276

UNCOV
277
    if (acceptedCount < 2) {
×
UNCOV
278
      if (!window.sessionStorage.getItem('pendings-redirect')) {
×
UNCOV
279
        window.sessionStorage.setItem('pendings-redirect', true);
×
UNCOV
280
        onDeviceStateSelectionChange(DEVICE_STATES.accepted);
×
281
      }
UNCOV
282
      setTimeout(() => {
×
UNCOV
283
        const notification = getOnboardingComponentFor(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING_NOTIFICATION, onboardingState, {
×
284
          setSnackbar: dispatchedSetSnackbar
285
        });
UNCOV
286
        !!notification && dispatchedSetSnackbar('open', TIMEOUTS.refreshDefault, '', notification, () => {}, true);
×
287
      }, TIMEOUTS.debounceDefault);
288
    }
289
    // eslint-disable-next-line react-hooks/exhaustive-deps
290
  }, [acceptedCount, allCount, pendingCount, onboardingState.complete, dispatch, onDeviceStateSelectionChange, dispatchedSetSnackbar]);
291

292
  useEffect(() => {
27✔
293
    setShowFilters(false);
3✔
294
  }, [selectedGroup]);
295

296
  const refreshDevices = useCallback(() => {
27✔
UNCOV
297
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
×
UNCOV
298
    return dispatch(setDeviceListState({ refreshTrigger: !refreshTrigger })).catch(err =>
×
UNCOV
299
      setRetryTimer(err, 'devices', `Devices couldn't be loaded.`, refreshLength, dispatchedSetSnackbar)
×
300
    );
301
  }, [dispatch, dispatchedSetSnackbar, refreshTrigger, selectedState]);
302

303
  useEffect(() => {
27✔
304
    if (!devicesInitialized) {
5✔
305
      return;
1✔
306
    }
307
    const refreshLength = deviceRefreshTimes[selectedState] ?? deviceRefreshTimes.default;
4!
308
    clearInterval(timer.current);
4✔
309
    timer.current = setInterval(() => refreshDevices(), refreshLength);
4✔
310
  }, [devicesInitialized, refreshDevices, selectedState]);
311

312
  useEffect(() => {
27✔
313
    Object.keys(availableIssueOptions).map(key => dispatch(getIssueCountsByType(key, { filters, group: selectedGroup, state: selectedState })));
6✔
314
    availableIssueOptions[DEVICE_ISSUE_OPTIONS.authRequests.key]
5!
315
      ? dispatch(getIssueCountsByType(DEVICE_ISSUE_OPTIONS.authRequests.key, { filters: [] }))
316
      : undefined;
317
    // eslint-disable-next-line react-hooks/exhaustive-deps
318
  }, [selectedIssues.join(''), JSON.stringify(availableIssueOptions), selectedState, selectedGroup, dispatch, JSON.stringify(filters)]);
319

320
  /*
321
   * Devices
322
   */
323
  const devicesToIds = devices => devices.map(device => device.id);
27✔
324

325
  const onRemoveDevicesFromGroup = devices => {
27✔
UNCOV
326
    const deviceIds = devicesToIds(devices);
×
UNCOV
327
    removeDevicesFromGroup(deviceIds);
×
328
    // if devices.length = number on page but < deviceCount
329
    // move page back to pageNO 1
UNCOV
330
    if (devices.length === deviceIds.length) {
×
UNCOV
331
      handlePageChange(1);
×
332
    }
333
  };
334

335
  const onAuthorizationChange = (devices, changedState) => {
27✔
UNCOV
336
    const deviceIds = devicesToIds(devices);
×
UNCOV
337
    return dispatch(setDeviceListState({ isLoading: true }))
×
UNCOV
338
      .then(() => dispatch(updateDevicesAuth(deviceIds, changedState)))
×
UNCOV
339
      .then(() => onSelectionChange([]));
×
340
  };
341

342
  const onDeviceDismiss = devices =>
27✔
UNCOV
343
    dispatch(setDeviceListState({ isLoading: true }))
×
344
      .then(() => {
UNCOV
345
        const deleteRequests = devices.reduce((accu, device) => {
×
UNCOV
346
          if (device.auth_sets?.length) {
×
UNCOV
347
            accu.push(dispatch(deleteAuthset(device.id, device.auth_sets[0].id)));
×
348
          }
UNCOV
349
          return accu;
×
350
        }, []);
UNCOV
351
        return Promise.all(deleteRequests);
×
352
      })
UNCOV
353
      .then(() => onSelectionChange([]));
×
354

355
  const handlePageChange = page => dispatch(setDeviceListState({ selectedId: undefined, page, refreshTrigger: !refreshTrigger }));
27✔
356

357
  const onPageLengthChange = perPage => dispatch(setDeviceListState({ perPage, page: 1, refreshTrigger: !refreshTrigger }));
27✔
358

359
  const onSortChange = attribute => {
27✔
UNCOV
360
    let changedSortCol = attribute.name;
×
UNCOV
361
    let changedSortDown = sortDown === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
UNCOV
362
    if (changedSortCol !== sortCol) {
×
UNCOV
363
      changedSortDown = SORTING_OPTIONS.desc;
×
364
    }
UNCOV
365
    dispatch(
×
366
      setDeviceListState({
367
        sort: { direction: changedSortDown, key: changedSortCol, scope: attribute.scope },
368
        refreshTrigger: !refreshTrigger
369
      })
370
    );
371
  };
372

373
  const onFilterChange = () => handlePageChange(1);
27✔
374

375
  const setDetailsTab = detailsTab => dispatch(setDeviceListState({ detailsTab, setOnly: true }));
27✔
376

377
  const onDeviceIssuesSelectionChange = ({ target: { value: selectedIssues } }) =>
27✔
378
    dispatch(setDeviceListState({ selectedIssues, page: 1, refreshTrigger: !refreshTrigger }));
1✔
379

380
  const onSelectionChange = (selection = []) => {
27!
381
    if (!onboardingState.complete && selection.length) {
1!
382
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ACCEPTING_ONBOARDING));
1✔
383
    }
384
    dispatch(setDeviceListState({ selection, setOnly: true }));
1✔
385
  };
386

387
  const onToggleCustomizationClick = () => setShowCustomization(toggle);
27✔
388

389
  const onChangeColumns = useCallback(
27✔
390
    (changedColumns, customColumnSizes) => {
391
      const { columnSizes, selectedAttributes } = calculateColumnSelectionSize(changedColumns, customColumnSizes);
1✔
392
      dispatch(updateUserColumnSettings(columnSizes));
1✔
393
      dispatch(saveUserSettings({ columnSelection: changedColumns }));
1✔
394
      // we don't need an explicit refresh trigger here, since the selectedAttributes will be different anyway & otherwise the shown list should still be valid
395
      dispatch(setDeviceListState({ selectedAttributes }));
1✔
396
      setShowCustomization(false);
1✔
397
    },
398
    [dispatch]
399
  );
400

401
  const onExpandClick = (device = {}) => {
27!
UNCOV
402
    dispatchedSetSnackbar('');
×
UNCOV
403
    const { attributes = {}, id, status } = device;
×
NEW
404
    dispatch(setDeviceListState({ selectedId: deviceListState.selectedId === id ? undefined : id, detailsTab: deviceListState.detailsTab || 'identity' }));
×
UNCOV
405
    if (!onboardingState.complete) {
×
UNCOV
406
      dispatch(advanceOnboarding(onboardingSteps.DEVICES_PENDING_ONBOARDING));
×
UNCOV
407
      if (status === DEVICE_STATES.accepted && Object.values(attributes).some(value => value)) {
×
UNCOV
408
        dispatch(advanceOnboarding(onboardingSteps.DEVICES_ACCEPTED_ONBOARDING_NOTIFICATION));
×
409
      }
410
    }
411
  };
412

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

415
  const onCloseExpandedDevice = useCallback(() => dispatch(setDeviceListState({ selectedId: undefined, detailsTab: '' })), [dispatch]);
27✔
416

417
  const actionCallbacks = {
27✔
418
    onAddDevicesToGroup: addDevicesToGroup,
419
    onAuthorizationChange,
420
    onCreateDeployment: onCreateDeploymentClick,
421
    onDeviceDismiss,
422
    onPromoteGateway: onMakeGatewayClick,
423
    onRemoveDevicesFromGroup
424
  };
425

426
  const listOptionHandlers = [{ key: 'customize', title: 'Customize', onClick: onToggleCustomizationClick }];
27✔
427
  const devicePendingTip = getOnboardingComponentFor(onboardingSteps.DEVICES_PENDING_ONBOARDING_START, onboardingState);
27✔
428

429
  const EmptyState = currentSelectedState.emptyState;
27✔
430

431
  const groupLabel = selectedGroup ? decodeURIComponent(selectedGroup) : ALL_DEVICES;
27✔
432
  const isUngroupedGroup = selectedGroup && selectedGroup === UNGROUPED_GROUP.id;
27✔
433
  const selectedStaticGroup = selectedGroup && !groupFilters.length ? selectedGroup : undefined;
27✔
434

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

551
export default Authorized;
552

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

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