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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

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

58.04
/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 { useLocation, 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 { SORTING_OPTIONS } from '../../constants/appConstants';
37
import { DEVICE_FILTERING_OPTIONS, DEVICE_ISSUE_OPTIONS, DEVICE_STATES, emptyFilter } from '../../constants/deviceConstants';
38
import * as DeviceConstants from '../../constants/deviceConstants.js';
39
import { onboardingSteps } from '../../constants/onboardingConstants';
40
import { toggle } from '../../helpers';
41
import {
42
  getAcceptedDevices,
43
  getDeviceCountsByStatus,
44
  getDeviceFilters,
45
  getDeviceLimit,
46
  getFeatures,
47
  getGroups as getGroupsSelector,
48
  getIsEnterprise,
49
  getIsPreview,
50
  getLimitMaxed,
51
  getOnboardingState,
52
  getSelectedGroupInfo,
53
  getSortedFilteringAttributes,
54
  getTenantCapabilities,
55
  getUserCapabilities
56
} from '../../selectors';
57
import { useLocationParams } from '../../utils/liststatehook';
58
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
59
import Global from '../settings/global';
60
import AuthorizedDevices from './authorized-devices';
61
import DeviceStatusNotification from './devicestatusnotification';
62
import MakeGatewayDialog from './dialogs/make-gateway-dialog';
63
import PreauthDialog, { DeviceLimitWarning } from './dialogs/preauth-dialog';
64
import CreateGroup from './group-management/create-group';
65
import CreateGroupExplainer from './group-management/create-group-explainer';
66
import RemoveGroup from './group-management/remove-group';
67
import Groups from './groups';
68
import DeviceAdditionWidget from './widgets/deviceadditionwidget';
69

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

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

105
  const [locationParams, setLocationParams] = useLocationParams('devices', {
2✔
106
    filteringAttributes,
107
    filters,
108
    defaults: { sort: { direction: SORTING_OPTIONS.desc } }
109
  });
110

111
  const { refreshTrigger, selectedId, state: selectedState } = deviceListState;
2✔
112

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

133
  useEffect(() => {
2✔
134
    // set isInitialized ref to false when location changes, otherwise when you go back setLocationParams will be set with a duplicate item
135
    isInitialized.current = false;
1✔
136
  }, [location]);
137

138
  useEffect(() => {
2✔
139
    const { groupName, filters = [], id = [], ...remainder } = locationParams;
1!
140
    const { hasFullFiltering } = tenantCapabilities;
1✔
141
    if (groupName) {
1!
142
      dispatch(selectGroup(groupName, filters));
1✔
143
    } else {
144
      // dispatch setDeviceFilters even when filters are empty, otherwise filter will not be reset
145
      dispatch(setDeviceFilters(filters));
×
146
      // if selected group exists in the state, but not set in locationParams then unset it
147
      selectedGroup && dispatch({ type: DeviceConstants.SELECT_GROUP, group: undefined });
×
148
    }
149
    // preset selectedIssues and selectedId with empty values, in case if remain properties are missing them
150
    let listState = { selectedIssues: [], selectedId: undefined, ...remainder };
1✔
151

152
    if (statusParam && Object.values(DEVICE_STATES).some(state => state === statusParam)) {
1!
153
      listState.state = statusParam;
×
154
    }
155

156
    if (id.length === 1 && Boolean(locationParams.open)) {
1!
157
      listState.selectedId = id[0];
×
158
    } else if (id.length && hasFullFiltering) {
1!
159
      dispatch(setDeviceFilters([...filters, { ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$in.key, value: id }]));
×
160
    }
161

162
    dispatch(setDeviceListState(listState)).then(() => {
1✔
163
      if (isInitialized.current) {
×
164
        return;
×
165
      }
166
      isInitialized.current = true;
×
167
      dispatch(setDeviceListState({}, true, true));
×
168
      dispatch(setOfflineThreshold());
×
169
    });
170
    // eslint-disable-next-line react-hooks/exhaustive-deps
171
  }, [dispatch, JSON.stringify(tenantCapabilities), JSON.stringify(locationParams), statusParam]);
172

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

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

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

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

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

217
  const openSettingsDialog = e => {
2✔
218
    e.preventDefault();
×
219
    setOpenIdDialog(toggle);
×
220
  };
221

222
  const onCreateGroupClose = () => {
2✔
223
    setModifyGroupDialog(false);
×
224
    setFromFilters(false);
×
225
    setTmpDevices([]);
×
226
  };
227

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

233
  const onShowDeviceStateClick = state => {
2✔
234
    dispatch(selectGroup());
×
235
    dispatch(setDeviceListState({ state }));
×
236
  };
237

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

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

248
  const toggleGroupRemoval = () => setRemoveGroup(toggle);
2✔
249

250
  const toggleMakeGatewayClick = () => setShowMakeGateway(toggle);
2✔
251

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

349
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