• 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

72.73
/src/js/components/settings/global.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, { useCallback, useEffect, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16

17
import { Button, Checkbox, FormControl, FormControlLabel, FormHelperText, InputLabel, MenuItem, Select, Switch, TextField } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import { getDeviceAttributes } from '../../actions/deviceActions';
21
import { changeNotificationSetting } from '../../actions/monitorActions';
22
import { getGlobalSettings, saveGlobalSettings } from '../../actions/userActions';
23
import { TIMEOUTS } from '../../constants/appConstants';
24
import { offlineThresholds } from '../../constants/deviceConstants';
25
import { alertChannels } from '../../constants/monitorConstants';
26
import { settingsKeys } from '../../constants/userConstants';
27
import {
28
  getDeviceIdentityAttributes,
29
  getFeatures,
30
  getGlobalSettings as getGlobalSettingsSelector,
31
  getIdAttribute,
32
  getOfflineThresholdSettings,
33
  getTenantCapabilities,
34
  getUserCapabilities,
35
  getUserRoles
36
} from '../../selectors';
37
import { useDebounce } from '../../utils/debouncehook';
38
import DocsLink from '../common/docslink';
39
import { HELPTOOLTIPS, MenderHelpTooltip } from '../helptips/helptooltips';
40
import ArtifactGenerationSettings from './artifactgeneration';
41
import ReportingLimits from './reportinglimits';
42

43
const maxWidth = 750;
5✔
44

45
const useStyles = makeStyles()(theme => ({
5✔
46
  threshold: {
47
    display: 'grid',
48
    gridTemplateColumns: '100px 100px',
49
    columnGap: theme.spacing(2)
50
  },
51
  textInput: {
52
    marginTop: 0,
53
    minWidth: 'initial'
54
  }
55
}));
56

57
export const IdAttributeSelection = ({ attributes, dialog = false, onCloseClick, onSaveClick, selectedAttribute = '' }) => {
5!
58
  const [attributeSelection, setAttributeSelection] = useState('name');
6✔
59

60
  useEffect(() => {
6✔
61
    setAttributeSelection(selectedAttribute);
1✔
62
  }, [selectedAttribute]);
63

64
  const changed = selectedAttribute !== attributeSelection;
6✔
65

66
  const onChangeIdAttribute = ({ target: { value: attributeSelection } }) => {
6✔
67
    setAttributeSelection(attributeSelection);
×
68
    if (dialog) {
×
69
      return;
×
70
    }
71
    onSaveClick(undefined, { attribute: attributeSelection, scope: attributes.find(({ value }) => value === attributeSelection).scope });
×
72
  };
73

74
  const undoChanges = e => {
6✔
75
    setAttributeSelection(selectedAttribute);
×
76
    if (dialog) {
×
77
      onCloseClick(e);
×
78
    }
79
  };
80

81
  const saveSettings = e => onSaveClick(e, { attribute: attributeSelection, scope: attributes.find(({ value }) => value === attributeSelection).scope });
6✔
82

83
  return (
6✔
84
    <div className="flexbox space-between" style={{ alignItems: 'flex-start', maxWidth }}>
85
      <FormControl>
86
        <InputLabel shrink id="device-id">
87
          Device identity attribute
88
        </InputLabel>
89
        <Select value={attributeSelection} onChange={onChangeIdAttribute}>
90
          {attributes.map(item => (
91
            <MenuItem key={item.value} value={item.value}>
19✔
92
              {item.label}
93
            </MenuItem>
94
          ))}
95
        </Select>
96
        <FormHelperText className="info" component="div">
97
          <div className="margin-top-small margin-bottom-small">Choose a device identity attribute to use to identify your devices throughout the UI.</div>
98
          <div className="margin-top-small margin-bottom-small">
99
            <DocsLink path="client-installation/identity" title="Learn how to add custom identity attributes" /> to your devices.
100
          </div>
101
        </FormHelperText>
102
      </FormControl>
103
      {dialog && (
6!
104
        <div className="margin-left margin-top flexbox">
105
          <Button onClick={undoChanges} style={{ marginRight: 10 }}>
106
            Cancel
107
          </Button>
108
          <Button variant="contained" onClick={saveSettings} disabled={!changed} color="primary">
109
            Save
110
          </Button>
111
        </div>
112
      )}
113
    </div>
114
  );
115
};
116

117
export const GlobalSettingsDialog = ({
5✔
118
  attributes,
119
  hasReporting,
120
  isAdmin,
121
  notificationChannelSettings,
122
  offlineThresholdSettings,
123
  onChangeNotificationSetting,
124
  onCloseClick,
125
  onSaveClick,
126
  saveGlobalSettings,
127
  selectedAttribute,
128
  settings,
129
  tenantCapabilities,
130
  userCapabilities
131
}) => {
132
  const [channelSettings, setChannelSettings] = useState(notificationChannelSettings);
6✔
133
  const [currentInterval, setCurrentInterval] = useState(offlineThresholdSettings.interval);
6✔
134
  const [currentIntervalUnit, setCurrentIntervalUnit] = useState(offlineThresholdSettings.intervalUnit);
6✔
135
  const [intervalErrorText, setIntervalErrorText] = useState('');
6✔
136
  const debouncedOfflineThreshold = useDebounce({ interval: currentInterval, intervalUnit: currentIntervalUnit }, TIMEOUTS.threeSeconds);
6✔
137
  const timer = useRef(false);
6✔
138
  const { classes } = useStyles();
6✔
139
  const { needsDeploymentConfirmation = false } = settings;
6✔
140
  const { canDelta, hasMonitor } = tenantCapabilities;
6✔
141
  const { canManageReleases } = userCapabilities;
6✔
142

143
  useEffect(() => {
6✔
144
    setChannelSettings(notificationChannelSettings);
1✔
145
  }, [notificationChannelSettings]);
146

147
  useEffect(() => {
6✔
148
    setCurrentInterval(offlineThresholdSettings.interval);
1✔
149
    setCurrentIntervalUnit(offlineThresholdSettings.intervalUnit);
1✔
150
  }, [offlineThresholdSettings.interval, offlineThresholdSettings.intervalUnit]);
151

152
  useEffect(() => {
6✔
153
    if (!window.sessionStorage.getItem(settingsKeys.initialized) || !timer.current) {
1!
154
      return;
1✔
155
    }
156
    saveGlobalSettings({ offlineThreshold: debouncedOfflineThreshold }, false, true);
×
157
    // eslint-disable-next-line react-hooks/exhaustive-deps
158
  }, [JSON.stringify(debouncedOfflineThreshold), saveGlobalSettings]);
159

160
  useEffect(() => {
6✔
161
    const initTimer = setTimeout(() => (timer.current = true), TIMEOUTS.fiveSeconds);
1✔
162
    return () => {
1✔
163
      clearTimeout(initTimer);
1✔
164
    };
165
  }, []);
166

167
  const onNotificationSettingsClick = ({ target: { checked } }, channel) => {
6✔
168
    setChannelSettings({ ...channelSettings, channel: { enabled: !checked } });
×
169
    onChangeNotificationSetting(!checked, channel);
×
170
  };
171

172
  const onChangeOfflineIntervalUnit = ({ target: { value } }) => setCurrentIntervalUnit(value);
6✔
173
  const onChangeOfflineInterval = ({ target: { validity, value } }) => {
6✔
174
    if (validity.valid) {
×
175
      setCurrentInterval(value || 1);
×
176
      return setIntervalErrorText('');
×
177
    }
178
    setIntervalErrorText('Please enter a valid number between 1 and 1000.');
×
179
  };
180

181
  const toggleDeploymentConfirmation = () => {
6✔
182
    saveGlobalSettings({ needsDeploymentConfirmation: !needsDeploymentConfirmation });
×
183
  };
184

185
  return (
6✔
186
    <div style={{ maxWidth }} className="margin-top-small">
187
      <div className="flexbox center-aligned">
188
        <h2 className="margin-top-small margin-right-small">Global settings</h2>
189
        <MenderHelpTooltip id={HELPTOOLTIPS.globalSettings.id} placement="top" />
190
      </div>
191
      <IdAttributeSelection attributes={attributes} onCloseClick={onCloseClick} onSaveClick={onSaveClick} selectedAttribute={selectedAttribute} />
192
      {hasReporting && <ReportingLimits />}
12✔
193
      <InputLabel className="margin-top" shrink>
194
        Deployments
195
      </InputLabel>
196
      <div className="clickable flexbox center-aligned" onClick={toggleDeploymentConfirmation}>
197
        <p className="help-content">Require confirmation on deployment creation</p>
198
        <Switch checked={needsDeploymentConfirmation} />
199
      </div>
200
      {canManageReleases && canDelta && <ArtifactGenerationSettings />}
18✔
201
      {isAdmin &&
12!
202
        hasMonitor &&
203
        Object.keys(alertChannels).map(channel => (
204
          <FormControl key={channel}>
×
205
            <InputLabel className="capitalized-start" shrink id={`${channel}-notifications`}>
206
              {channel} notifications
207
            </InputLabel>
208
            <FormControlLabel
209
              control={<Checkbox checked={!channelSettings[channel].enabled} onChange={e => onNotificationSettingsClick(e, channel)} />}
×
210
              label={`Mute ${channel} notifications`}
211
            />
212
            <FormHelperText className="info" component="div">
213
              Mute {channel} notifications for deployment and monitoring issues for all users
214
            </FormHelperText>
215
          </FormControl>
216
        ))}
217

218
      <InputLabel className="margin-top" shrink id="offline-theshold">
219
        Offline threshold
220
      </InputLabel>
221
      <div className={classes.threshold}>
222
        <Select onChange={onChangeOfflineIntervalUnit} value={currentIntervalUnit}>
223
          {offlineThresholds.map(value => (
224
            <MenuItem key={value} value={value}>
18✔
225
              <div className="capitalized-start">{value}</div>
226
            </MenuItem>
227
          ))}
228
        </Select>
229
        <TextField
230
          className={classes.textInput}
231
          type="number"
232
          onChange={onChangeOfflineInterval}
233
          inputProps={{ min: '1', max: '1000' }}
234
          error={!!intervalErrorText}
235
          value={currentInterval}
236
        />
237
      </div>
238
      {!!intervalErrorText && (
6!
239
        <FormHelperText className="warning" component="div">
240
          {intervalErrorText}
241
        </FormHelperText>
242
      )}
243
      <FormHelperText className="info" component="div">
244
        Choose how long a device can go without reporting to the server before it is considered “offline”.
245
      </FormHelperText>
246
    </div>
247
  );
248
};
249

250
export const GlobalSettingsContainer = ({ closeDialog, dialog }) => {
5✔
251
  const dispatch = useDispatch();
3✔
252
  const attributes = useSelector(getDeviceIdentityAttributes);
3✔
253
  const { hasReporting } = useSelector(getFeatures);
3✔
254
  const { isAdmin } = useSelector(getUserRoles);
3✔
255
  const notificationChannelSettings = useSelector(state => state.monitor.settings.global.channels);
7✔
256
  const offlineThresholdSettings = useSelector(getOfflineThresholdSettings);
3✔
257
  const { attribute: selectedAttribute } = useSelector(getIdAttribute);
3✔
258
  const settings = useSelector(getGlobalSettingsSelector);
3✔
259
  const tenantCapabilities = useSelector(getTenantCapabilities);
3✔
260
  const userCapabilities = useSelector(getUserCapabilities);
3✔
261

262
  const [updatedSettings, setUpdatedSettings] = useState({ ...settings });
3✔
263

264
  useEffect(() => {
3✔
265
    if (!settings) {
1!
266
      dispatch(getGlobalSettings());
×
267
    }
268
    dispatch(getDeviceAttributes());
1✔
269
    // eslint-disable-next-line react-hooks/exhaustive-deps
270
  }, [dispatch, JSON.stringify(settings)]);
271

272
  useEffect(() => {
3✔
273
    setUpdatedSettings(current => ({ ...current, ...settings }));
1✔
274
    // eslint-disable-next-line react-hooks/exhaustive-deps
275
  }, [JSON.stringify(settings)]);
276

277
  const onCloseClick = e => {
3✔
278
    if (dialog) {
×
279
      return closeDialog(e);
×
280
    }
281
  };
282

283
  const saveAttributeSetting = (e, id_attribute) => {
3✔
284
    return dispatch(saveGlobalSettings({ ...updatedSettings, id_attribute }, false, true)).then(() => {
×
285
      if (dialog) {
×
286
        closeDialog(e);
×
287
      }
288
    });
289
  };
290

291
  const onChangeNotificationSetting = useCallback((...args) => dispatch(changeNotificationSetting(...args)), [dispatch]);
3✔
292
  const onSaveGlobalSettings = useCallback((...args) => dispatch(saveGlobalSettings(...args)), [dispatch]);
3✔
293

294
  if (dialog) {
3!
295
    return (
×
296
      <IdAttributeSelection
297
        attributes={attributes}
298
        dialog
299
        onCloseClick={onCloseClick}
300
        onSaveClick={saveAttributeSetting}
301
        selectedAttribute={selectedAttribute}
302
      />
303
    );
304
  }
305
  return (
3✔
306
    <GlobalSettingsDialog
307
      attributes={attributes}
308
      hasReporting={hasReporting}
309
      isAdmin={isAdmin}
310
      notificationChannelSettings={notificationChannelSettings}
311
      offlineThresholdSettings={offlineThresholdSettings}
312
      onChangeNotificationSetting={onChangeNotificationSetting}
313
      onCloseClick={onCloseClick}
314
      onSaveClick={saveAttributeSetting}
315
      saveGlobalSettings={onSaveGlobalSettings}
316
      settings={settings}
317
      selectedAttribute={selectedAttribute}
318
      tenantCapabilities={tenantCapabilities}
319
      userCapabilities={userCapabilities}
320
    />
321
  );
322
};
323
export default GlobalSettingsContainer;
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