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

mendersoftware / gui / 908425489

pending completion
908425489

Pull #3799

gitlab-ci

mzedel
chore: aligned loader usage in devices list with deployment devices list

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3799: MEN-6553

4406 of 6423 branches covered (68.6%)

18 of 19 new or added lines in 3 files covered. (94.74%)

1777 existing lines in 167 files now uncovered.

8329 of 10123 relevant lines covered (82.28%)

144.7 hits per line

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

56.54
/src/js/components/devices/device-groups.js
1
// Copyright 2018 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, { useEffect, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16
import { useParams } from 'react-router-dom';
17

18
import { AddCircle as AddIcon } from '@mui/icons-material';
19
import { Dialog, DialogContent, DialogTitle } from '@mui/material';
20

21
import pluralize from 'pluralize';
22

23
import { setOfflineThreshold, setSnackbar } from '../../actions/appActions';
24
import {
25
  addDynamicGroup,
26
  addStaticGroup,
27
  getAllDeviceCounts,
28
  preauthDevice,
29
  removeDevicesFromGroup,
30
  removeDynamicGroup,
31
  removeStaticGroup,
32
  selectGroup,
33
  setDeviceFilters,
34
  setDeviceListState,
35
  updateDynamicGroup
36
} from '../../actions/deviceActions';
37
import { setShowConnectingDialog } from '../../actions/userActions';
38
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
39
import { DEVICE_FILTERING_OPTIONS, DEVICE_ISSUE_OPTIONS, DEVICE_STATES, emptyFilter } from '../../constants/deviceConstants';
40
import { toggle } from '../../helpers';
41
import {
42
  getAcceptedDevices,
43
  getDeviceCountsByStatus,
44
  getDeviceFilters,
45
  getDeviceLimit,
46
  getDocsVersion,
47
  getFeatures,
48
  getGroups as getGroupsSelector,
49
  getIsEnterprise,
50
  getIsPreview,
51
  getLimitMaxed,
52
  getSelectedGroupInfo,
53
  getShowHelptips,
54
  getTenantCapabilities,
55
  getUserCapabilities
56
} from '../../selectors';
57
import { useLocationParams } from '../../utils/liststatehook';
58
import Global from '../settings/global';
59
import AuthorizedDevices from './authorized-devices';
60
import DeviceStatusNotification from './devicestatusnotification';
61
import MakeGatewayDialog from './dialogs/make-gateway-dialog';
62
import PreauthDialog, { DeviceLimitWarning } from './dialogs/preauth-dialog';
63
import CreateGroup from './group-management/create-group';
64
import CreateGroupExplainer from './group-management/create-group-explainer';
65
import RemoveGroup from './group-management/remove-group';
66
import Groups from './groups';
67
import DeviceAdditionWidget from './widgets/deviceadditionwidget';
68

69
const refreshLength = TIMEOUTS.refreshDefault;
4✔
70

71
export const DeviceGroups = () => {
4✔
72
  const [createGroupExplanation, setCreateGroupExplanation] = useState(false);
1✔
73
  const [fromFilters, setFromFilters] = useState(false);
1✔
74
  const [modifyGroupDialog, setModifyGroupDialog] = useState(false);
1✔
75
  const [openIdDialog, setOpenIdDialog] = useState(false);
1✔
76
  const [openPreauth, setOpenPreauth] = useState(false);
1✔
77
  const [showMakeGateway, setShowMakeGateway] = useState(false);
1✔
78
  const [removeGroup, setRemoveGroup] = useState(false);
1✔
79
  const [tmpDevices, setTmpDevices] = useState([]);
1✔
80
  const deviceTimer = useRef();
1✔
81
  const { status: statusParam } = useParams();
1✔
82

83
  const { groupCount, selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
1!
84
  const filteringAttributes = useSelector(state => ({
1✔
85
    ...state.devices.filteringAttributes,
86
    identityAttributes: [...state.devices.filteringAttributes.identityAttributes, 'id']
87
  }));
88
  const { canManageDevices } = useSelector(getUserCapabilities);
1✔
89
  const tenantCapabilities = useSelector(getTenantCapabilities);
1✔
90
  const { groupNames, ...groupsByType } = useSelector(getGroupsSelector);
1✔
91
  const groups = groupNames;
1✔
92
  const { total: acceptedCount = 0 } = useSelector(getAcceptedDevices);
1!
93
  const authRequestCount = useSelector(state => state.monitor.issueCounts.byType[DEVICE_ISSUE_OPTIONS.authRequests.key].total);
1✔
94
  const canPreview = useSelector(getIsPreview);
1✔
95
  const deviceLimit = useSelector(getDeviceLimit);
1✔
96
  const deviceListState = useSelector(state => state.devices.deviceList);
1✔
97
  const docsVersion = useSelector(getDocsVersion);
1✔
98
  const features = useSelector(getFeatures);
1✔
99
  const { hasReporting } = features;
1✔
100
  const filters = useSelector(getDeviceFilters);
1✔
101
  const limitMaxed = useSelector(getLimitMaxed);
1✔
102
  const { pending: pendingCount } = useSelector(getDeviceCountsByStatus);
1✔
103
  const showDeviceConnectionDialog = useSelector(state => state.users.showConnectDeviceDialog);
1✔
104
  const showHelptips = useSelector(getShowHelptips);
1✔
105
  const isEnterprise = useSelector(getIsEnterprise);
1✔
106
  const dispatch = useDispatch();
1✔
107

108
  const [locationParams, setLocationParams] = useLocationParams('devices', {
1✔
109
    filteringAttributes,
110
    filters,
111
    defaults: { sort: { direction: SORTING_OPTIONS.desc } }
112
  });
113

114
  const { refreshTrigger, selectedId, state: selectedState } = deviceListState;
1✔
115

116
  useEffect(() => {
1✔
117
    if (!deviceTimer.current) {
1!
118
      return;
1✔
119
    }
UNCOV
120
    setLocationParams({ pageState: deviceListState, filters, selectedGroup });
×
121
  }, [
122
    deviceListState.detailsTab,
123
    deviceListState.page,
124
    deviceListState.perPage,
125
    deviceListState.selectedIssues,
126
    JSON.stringify(deviceListState.sort),
127
    selectedId,
128
    filters,
129
    selectedGroup,
130
    selectedState
131
  ]);
132

133
  useEffect(() => {
1✔
134
    if (locationParams.groupName) {
1!
UNCOV
135
      dispatch(selectGroup(locationParams.groupName));
×
136
    }
137
    let listState = { setOnly: true };
1✔
138
    if (locationParams.open && locationParams.id.length) {
1!
UNCOV
139
      listState = { ...listState, selectedId: locationParams.id[0], detailsTab: locationParams.detailsTab };
×
140
    }
141
    if (!locationParams.id?.length && selectedId) {
1!
UNCOV
142
      listState = { ...listState, detailsTab: 'identity' };
×
143
    }
144
    dispatch(setDeviceListState(listState));
1✔
145
  }, [locationParams.detailsTab, locationParams.groupName, JSON.stringify(locationParams.id), locationParams.open]);
146

147
  useEffect(() => {
1✔
148
    const { groupName, filters = [], id = [], ...remainder } = locationParams;
1!
149
    const { hasFullFiltering } = tenantCapabilities;
1✔
150
    if (groupName) {
1!
UNCOV
151
      dispatch(selectGroup(groupName, filters));
×
152
    } else if (filters.length) {
1!
UNCOV
153
      dispatch(setDeviceFilters(filters));
×
154
    }
155
    const state = statusParam && Object.values(DEVICE_STATES).some(state => state === statusParam) ? statusParam : selectedState;
1!
156
    let listState = { ...remainder, state, refreshTrigger: !refreshTrigger };
1✔
157
    if (id.length === 1 && Boolean(locationParams.open)) {
1!
UNCOV
158
      listState.selectedId = id[0];
×
159
    } else if (id.length && hasFullFiltering) {
1!
UNCOV
160
      dispatch(setDeviceFilters([...filters, { ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$in.key, value: id }]));
×
161
    }
162
    dispatch(setDeviceListState(listState));
1✔
163
    clearInterval(deviceTimer.current);
1✔
164
    deviceTimer.current = setInterval(() => dispatch(getAllDeviceCounts()), refreshLength);
1✔
165
    dispatch(setOfflineThreshold());
1✔
166
    return () => {
1✔
167
      clearInterval(deviceTimer.current);
1✔
168
    };
169
  }, []);
170

171
  /*
172
   * Groups
173
   */
174
  const removeCurrentGroup = () => {
1✔
UNCOV
175
    const request = groupFilters.length ? dispatch(removeDynamicGroup(selectedGroup)) : dispatch(removeStaticGroup(selectedGroup));
×
UNCOV
176
    return request.then(toggleGroupRemoval).catch(console.log);
×
177
  };
178

179
  // Edit groups from device selection
180
  const addDevicesToGroup = tmpDevices => {
1✔
181
    // (save selected devices in state, open dialog)
UNCOV
182
    setTmpDevices(tmpDevices);
×
UNCOV
183
    setModifyGroupDialog(toggle);
×
184
  };
185

186
  const createGroupFromDialog = (devices, group) => {
1✔
UNCOV
187
    let request = fromFilters ? dispatch(addDynamicGroup(group, filters)) : dispatch(addStaticGroup(group, devices));
×
UNCOV
188
    return request.then(() => {
×
189
      // reached end of list
UNCOV
190
      setCreateGroupExplanation(false);
×
UNCOV
191
      setModifyGroupDialog(false);
×
UNCOV
192
      setFromFilters(false);
×
193
    });
194
  };
195

196
  const onGroupClick = () => {
1✔
UNCOV
197
    if (selectedGroup && groupFilters.length) {
×
UNCOV
198
      return dispatch(updateDynamicGroup(selectedGroup, filters));
×
199
    }
UNCOV
200
    setModifyGroupDialog(true);
×
UNCOV
201
    setFromFilters(true);
×
202
  };
203

204
  const onRemoveDevicesFromGroup = devices => {
1✔
UNCOV
205
    const isGroupRemoval = devices.length >= groupCount;
×
206
    let request;
UNCOV
207
    if (isGroupRemoval) {
×
UNCOV
208
      request = dispatch(removeStaticGroup(selectedGroup));
×
209
    } else {
UNCOV
210
      request = dispatch(removeDevicesFromGroup(selectedGroup, devices));
×
211
    }
UNCOV
212
    return request.catch(console.log);
×
213
  };
214

215
  const openSettingsDialog = e => {
1✔
UNCOV
216
    e.preventDefault();
×
UNCOV
217
    setOpenIdDialog(toggle);
×
218
  };
219

220
  const onCreateGroupClose = () => {
1✔
UNCOV
221
    setModifyGroupDialog(false);
×
UNCOV
222
    setFromFilters(false);
×
UNCOV
223
    setTmpDevices([]);
×
224
  };
225

226
  const onPreauthSaved = addMore => {
1✔
UNCOV
227
    setOpenPreauth(!addMore);
×
UNCOV
228
    dispatch(setDeviceListState({ page: 1, refreshTrigger: !refreshTrigger }));
×
229
  };
230

231
  const onShowDeviceStateClick = state => {
1✔
UNCOV
232
    dispatch(selectGroup());
×
UNCOV
233
    dispatch(setDeviceListState({ state }));
×
234
  };
235

236
  const onGroupSelect = groupName => {
1✔
UNCOV
237
    dispatch(selectGroup(groupName));
×
UNCOV
238
    dispatch(setDeviceListState({ page: 1, refreshTrigger: !refreshTrigger, selection: [] }));
×
239
  };
240

241
  const onShowAuthRequestDevicesClick = () => {
1✔
UNCOV
242
    dispatch(setDeviceFilters([]));
×
UNCOV
243
    dispatch(setDeviceListState({ selectedIssues: [DEVICE_ISSUE_OPTIONS.authRequests.key], page: 1 }));
×
244
  };
245

246
  const toggleGroupRemoval = () => setRemoveGroup(toggle);
1✔
247

248
  const toggleMakeGatewayClick = () => setShowMakeGateway(toggle);
1✔
249

250
  return (
1✔
251
    <>
252
      <div className="tab-container with-sub-panels margin-bottom-small" style={{ padding: 0, minHeight: 'initial' }}>
253
        <h3 style={{ marginBottom: 0 }}>Devices</h3>
254
        <div className="flexbox space-between margin-left-large margin-right center-aligned padding-bottom padding-top-small">
255
          {hasReporting && !!authRequestCount && (
1!
256
            <a className="flexbox center-aligned margin-right-large" onClick={onShowAuthRequestDevicesClick}>
257
              <AddIcon fontSize="small" style={{ marginRight: 6 }} />
258
              {authRequestCount} new device authentication {pluralize('request', authRequestCount)}
259
            </a>
260
          )}
261
          {!!pendingCount && !selectedGroup && selectedState !== DEVICE_STATES.pending ? (
3!
262
            <DeviceStatusNotification deviceCount={pendingCount} state={DEVICE_STATES.pending} onClick={onShowDeviceStateClick} />
263
          ) : (
264
            <div />
265
          )}
266
          {canManageDevices && (
2✔
267
            <DeviceAdditionWidget
268
              docsVersion={docsVersion}
269
              features={features}
UNCOV
270
              onConnectClick={() => dispatch(setShowConnectingDialog(true))}
×
271
              onMakeGatewayClick={toggleMakeGatewayClick}
272
              onPreauthClick={setOpenPreauth}
273
              tenantCapabilities={tenantCapabilities}
274
            />
275
          )}
276
        </div>
277
      </div>
278
      <div className="tab-container with-sub-panels" style={{ padding: 0, height: '100%' }}>
279
        <Groups
280
          className="leftFixed"
281
          acceptedCount={acceptedCount}
282
          changeGroup={onGroupSelect}
283
          groups={groupsByType}
284
          openGroupDialog={setCreateGroupExplanation}
285
          selectedGroup={selectedGroup}
286
          showHelptips={showHelptips}
287
        />
288
        <div className="rightFluid relative" style={{ paddingTop: 0 }}>
289
          {limitMaxed && <DeviceLimitWarning acceptedDevices={acceptedCount} deviceLimit={deviceLimit} />}
1!
290
          <AuthorizedDevices
291
            addDevicesToGroup={addDevicesToGroup}
292
            onGroupClick={onGroupClick}
293
            onGroupRemoval={toggleGroupRemoval}
294
            onMakeGatewayClick={toggleMakeGatewayClick}
295
            onPreauthClick={setOpenPreauth}
296
            openSettingsDialog={openSettingsDialog}
297
            removeDevicesFromGroup={onRemoveDevicesFromGroup}
298
            showsDialog={showDeviceConnectionDialog || removeGroup || modifyGroupDialog || createGroupExplanation || openIdDialog || openPreauth}
6✔
299
          />
300
        </div>
301
        {removeGroup && <RemoveGroup onClose={toggleGroupRemoval} onRemove={removeCurrentGroup} />}
1!
302
        {modifyGroupDialog && (
1!
303
          <CreateGroup
304
            addListOfDevices={createGroupFromDialog}
305
            fromFilters={fromFilters}
306
            isCreation={fromFilters || !groups.length}
×
307
            selectedDevices={tmpDevices}
308
            onClose={onCreateGroupClose}
309
          />
310
        )}
UNCOV
311
        {createGroupExplanation && <CreateGroupExplainer isEnterprise={isEnterprise} onClose={() => setCreateGroupExplanation(false)} />}
×
312
        {openIdDialog && (
1!
313
          <Dialog open>
314
            <DialogTitle>Default device identity attribute</DialogTitle>
315
            <DialogContent style={{ overflow: 'hidden' }}>
316
              <Global dialog closeDialog={openSettingsDialog} />
317
            </DialogContent>
318
          </Dialog>
319
        )}
320
        {openPreauth && (
1!
321
          <PreauthDialog
322
            acceptedDevices={acceptedCount}
323
            deviceLimit={deviceLimit}
324
            limitMaxed={limitMaxed}
UNCOV
325
            preauthDevice={authset => dispatch(preauthDevice(authset))}
×
326
            onSubmit={onPreauthSaved}
UNCOV
327
            onCancel={() => setOpenPreauth(false)}
×
UNCOV
328
            setSnackbar={message => dispatch(setSnackbar(message))}
×
329
          />
330
        )}
331
        {showMakeGateway && <MakeGatewayDialog docsVersion={docsVersion} isPreRelease={canPreview} onCancel={toggleMakeGatewayClick} />}
1!
332
      </div>
333
    </>
334
  );
335
};
336

337
export default DeviceGroups;
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