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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

56.88
/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 { SORTING_OPTIONS } from '../../constants/appConstants';
37
import { DEVICE_FILTERING_OPTIONS, DEVICE_ISSUE_OPTIONS, DEVICE_STATES, emptyFilter } from '../../constants/deviceConstants';
38
import { onboardingSteps } from '../../constants/onboardingConstants';
39
import { toggle } from '../../helpers';
40
import {
41
  getAcceptedDevices,
42
  getDeviceCountsByStatus,
43
  getDeviceFilters,
44
  getDeviceLimit,
45
  getFeatures,
46
  getGroups as getGroupsSelector,
47
  getIsEnterprise,
48
  getIsPreview,
49
  getLimitMaxed,
50
  getOnboardingState,
51
  getSelectedGroupInfo,
52
  getSortedFilteringAttributes,
53
  getTenantCapabilities,
54
  getUserCapabilities
55
} from '../../selectors';
56
import { useLocationParams } from '../../utils/liststatehook';
57
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
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
export const DeviceGroups = () => {
3✔
70
  const [createGroupExplanation, setCreateGroupExplanation] = useState(false);
2✔
71
  const [fromFilters, setFromFilters] = useState(false);
2✔
72
  const [modifyGroupDialog, setModifyGroupDialog] = useState(false);
2✔
73
  const [openIdDialog, setOpenIdDialog] = useState(false);
2✔
74
  const [openPreauth, setOpenPreauth] = useState(false);
2✔
75
  const [showMakeGateway, setShowMakeGateway] = useState(false);
2✔
76
  const [removeGroup, setRemoveGroup] = useState(false);
2✔
77
  const [tmpDevices, setTmpDevices] = useState([]);
2✔
78
  const deviceConnectionRef = useRef();
2✔
79
  const { status: statusParam } = useParams();
2✔
80

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

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

109
  const { refreshTrigger, selectedId, state: selectedState } = deviceListState;
2✔
110

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

132
  useEffect(() => {
2✔
133
    const { groupName, filters = [], id = [], ...remainder } = locationParams;
1!
134
    const { hasFullFiltering } = tenantCapabilities;
1✔
135
    if (groupName) {
1!
136
      dispatch(selectGroup(groupName, filters));
×
137
    } else if (filters.length) {
1!
138
      dispatch(setDeviceFilters(filters));
×
139
    }
140
    let listState = { ...remainder };
1✔
141
    if (statusParam && Object.values(DEVICE_STATES).some(state => state === statusParam)) {
1!
142
      listState.state = statusParam;
×
143
    }
144
    if (id.length === 1 && Boolean(locationParams.open)) {
1!
145
      listState.selectedId = id[0];
×
146
    } else if (id.length && hasFullFiltering) {
1!
147
      dispatch(setDeviceFilters([...filters, { ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$in.key, value: id }]));
×
148
    }
149
    dispatch(setDeviceListState(listState)).then(() => {
1✔
150
      if (isInitialized.current) {
×
151
        return;
×
152
      }
153
      isInitialized.current = true;
×
154
      dispatch(setDeviceListState({}, true, true));
×
155
      dispatch(setOfflineThreshold());
×
156
    });
157
    // eslint-disable-next-line react-hooks/exhaustive-deps
158
  }, [dispatch, JSON.stringify(tenantCapabilities), JSON.stringify(locationParams), statusParam]);
159

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

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

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

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

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

204
  const openSettingsDialog = e => {
2✔
205
    e.preventDefault();
×
206
    setOpenIdDialog(toggle);
×
207
  };
208

209
  const onCreateGroupClose = () => {
2✔
210
    setModifyGroupDialog(false);
×
211
    setFromFilters(false);
×
212
    setTmpDevices([]);
×
213
  };
214

215
  const onPreauthSaved = addMore => {
2✔
216
    setOpenPreauth(!addMore);
×
217
    dispatch(setDeviceListState({ page: 1, refreshTrigger: !refreshTrigger }));
×
218
  };
219

220
  const onShowDeviceStateClick = state => {
2✔
221
    dispatch(selectGroup());
×
222
    dispatch(setDeviceListState({ state }));
×
223
  };
224

225
  const onGroupSelect = groupName => {
2✔
226
    dispatch(selectGroup(groupName));
×
227
    dispatch(setDeviceListState({ page: 1, refreshTrigger: !refreshTrigger, selection: [] }));
×
228
  };
229

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

235
  const toggleGroupRemoval = () => setRemoveGroup(toggle);
2✔
236

237
  const toggleMakeGatewayClick = () => setShowMakeGateway(toggle);
2✔
238

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

336
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