• 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

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