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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

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

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

80
  const { groupCount, selectedGroup, groupFilters = [] } = useSelector(getSelectedGroupInfo);
4!
81
  const filteringAttributes = useSelector(getSortedFilteringAttributes);
4✔
82
  const { canManageDevices } = useSelector(getUserCapabilities);
4✔
83
  const tenantCapabilities = useSelector(getTenantCapabilities);
4✔
84
  const { groupNames, ...groupsByType } = useSelector(getGroupsSelector);
4✔
85
  const groups = groupNames;
4✔
86
  const { total: acceptedCount = 0 } = useSelector(getAcceptedDevices);
4!
87
  const authRequestCount = useSelector(state => state.monitor.issueCounts.byType[DEVICE_ISSUE_OPTIONS.authRequests.key].total);
24✔
88
  const canPreview = useSelector(getIsPreview);
4✔
89
  const deviceLimit = useSelector(getDeviceLimit);
4✔
90
  const deviceListState = useSelector(state => state.devices.deviceList);
24✔
91
  const features = useSelector(getFeatures);
4✔
92
  const { hasReporting } = features;
4✔
93
  const filters = useSelector(getDeviceFilters);
4✔
94
  const limitMaxed = useSelector(getLimitMaxed);
4✔
95
  const { pending: pendingCount } = useSelector(getDeviceCountsByStatus);
4✔
96
  const showDeviceConnectionDialog = useSelector(state => state.users.showConnectDeviceDialog);
24✔
97
  const onboardingState = useSelector(getOnboardingState);
4✔
98
  const isEnterprise = useSelector(getIsEnterprise);
4✔
99
  const dispatch = useDispatch();
4✔
100
  const isInitialized = useRef(false);
4✔
101

102
  const [locationParams, setLocationParams] = useLocationParams('devices', {
4✔
103
    filteringAttributes,
104
    filters
105
  });
106

107
  const { refreshTrigger, selectedId, state: selectedState } = deviceListState;
4✔
108

109
  useEffect(() => {
4✔
110
    if (!isInitialized.current) {
2!
111
      return;
2✔
112
    }
UNCOV
113
    setLocationParams({ pageState: deviceListState, filters, selectedGroup });
×
114
    // eslint-disable-next-line react-hooks/exhaustive-deps
115
  }, [
116
    deviceListState.detailsTab,
117
    deviceListState.page,
118
    deviceListState.perPage,
119
    deviceListState.selectedIssues,
120
    // eslint-disable-next-line react-hooks/exhaustive-deps
121
    JSON.stringify(deviceListState.sort),
122
    selectedId,
123
    filters,
124
    selectedGroup,
125
    selectedState,
126
    setLocationParams
127
  ]);
128

129
  useEffect(() => {
4✔
130
    const { groupName, filters = [], id = [], ...remainder } = locationParams;
1!
131
    const { hasFullFiltering } = tenantCapabilities;
1✔
132
    if (groupName) {
1!
133
      if (groupName != selectedGroup) {
1!
UNCOV
134
        dispatch(selectGroup(groupName, filters));
×
135
      }
UNCOV
136
    } else if (filters.length) {
×
137
      // dispatch setDeviceFilters even when filters are empty, otherwise filter will not be reset
UNCOV
138
      dispatch(setDeviceFilters(filters));
×
139
    }
140
    // preset selectedIssues and selectedId with empty values, in case if remain properties are missing them
141
    let listState = { ...remainder };
1✔
142
    if (statusParam && Object.values(DEVICE_STATES).some(state => state === statusParam)) {
1!
UNCOV
143
      listState.state = statusParam;
×
144
    }
145

146
    if (id.length === 1 && Boolean(locationParams.open)) {
1!
UNCOV
147
      listState.selectedId = id[0];
×
148
    } else if (id.length && hasFullFiltering) {
1!
UNCOV
149
      dispatch(setDeviceFilters([...filters, { ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$in.key, value: id }]));
×
150
    }
151
    dispatch(setDeviceListState(listState)).then(() => {
1✔
152
      if (isInitialized.current) {
1!
UNCOV
153
        return;
×
154
      }
155
      isInitialized.current = true;
1✔
156
      dispatch(setDeviceListState({}, true, true));
1✔
157
      dispatch(setOfflineThreshold());
1✔
158
    });
159
    // eslint-disable-next-line react-hooks/exhaustive-deps
160
  }, [dispatch, JSON.stringify(tenantCapabilities), JSON.stringify(locationParams), statusParam]);
161

162
  /*
163
   * Groups
164
   */
165
  const removeCurrentGroup = () => {
4✔
UNCOV
166
    const request = groupFilters.length ? dispatch(removeDynamicGroup(selectedGroup)) : dispatch(removeStaticGroup(selectedGroup));
×
UNCOV
167
    return request.then(toggleGroupRemoval).catch(console.log);
×
168
  };
169

170
  // Edit groups from device selection
171
  const addDevicesToGroup = tmpDevices => {
4✔
172
    // (save selected devices in state, open dialog)
UNCOV
173
    setTmpDevices(tmpDevices);
×
UNCOV
174
    setModifyGroupDialog(toggle);
×
175
  };
176

177
  const createGroupFromDialog = (devices, group) => {
4✔
UNCOV
178
    let request = fromFilters ? dispatch(addDynamicGroup(group, filters)) : dispatch(addStaticGroup(group, devices));
×
UNCOV
179
    return request.then(() => {
×
180
      // reached end of list
UNCOV
181
      setCreateGroupExplanation(false);
×
UNCOV
182
      setModifyGroupDialog(false);
×
UNCOV
183
      setFromFilters(false);
×
184
    });
185
  };
186

187
  const onGroupClick = () => {
4✔
UNCOV
188
    if (selectedGroup && groupFilters.length) {
×
UNCOV
189
      return dispatch(updateDynamicGroup(selectedGroup, filters));
×
190
    }
UNCOV
191
    setModifyGroupDialog(true);
×
UNCOV
192
    setFromFilters(true);
×
193
  };
194

195
  const onRemoveDevicesFromGroup = devices => {
4✔
UNCOV
196
    const isGroupRemoval = devices.length >= groupCount;
×
197
    let request;
UNCOV
198
    if (isGroupRemoval) {
×
UNCOV
199
      request = dispatch(removeStaticGroup(selectedGroup));
×
200
    } else {
UNCOV
201
      request = dispatch(removeDevicesFromGroup(selectedGroup, devices));
×
202
    }
UNCOV
203
    return request.catch(console.log);
×
204
  };
205

206
  const openSettingsDialog = e => {
4✔
UNCOV
207
    e.preventDefault();
×
UNCOV
208
    setOpenIdDialog(toggle);
×
209
  };
210

211
  const onCreateGroupClose = () => {
4✔
UNCOV
212
    setModifyGroupDialog(false);
×
UNCOV
213
    setFromFilters(false);
×
UNCOV
214
    setTmpDevices([]);
×
215
  };
216

217
  const onPreauthSaved = addMore => {
4✔
UNCOV
218
    setOpenPreauth(!addMore);
×
UNCOV
219
    dispatch(setDeviceListState({ page: 1, refreshTrigger: !refreshTrigger }));
×
220
  };
221

222
  const onShowDeviceStateClick = state => {
4✔
UNCOV
223
    dispatch(selectGroup());
×
UNCOV
224
    dispatch(setDeviceListState({ state }));
×
225
  };
226

227
  const onGroupSelect = groupName => {
4✔
UNCOV
228
    dispatch(selectGroup(groupName));
×
UNCOV
229
    dispatch(setDeviceListState({ page: 1, refreshTrigger: !refreshTrigger, selection: [] }));
×
230
  };
231

232
  const onShowAuthRequestDevicesClick = () => {
4✔
UNCOV
233
    dispatch(setDeviceFilters([]));
×
UNCOV
234
    dispatch(setDeviceListState({ selectedIssues: [DEVICE_ISSUE_OPTIONS.authRequests.key], page: 1 }));
×
235
  };
236

237
  const toggleGroupRemoval = () => setRemoveGroup(toggle);
4✔
238

239
  const toggleMakeGatewayClick = () => setShowMakeGateway(toggle);
4✔
240

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

338
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