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

mendersoftware / gui / 919001084

pending completion
919001084

Pull #3839

gitlab-ci

mzedel
revert: "chore: bump node from 20.2.0-alpine to 20.3.1-alpine"

This reverts commit cbfcd7663.

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3839: Combined PRs

4399 of 6397 branches covered (68.77%)

8302 of 10074 relevant lines covered (82.41%)

162.96 hits per line

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

64.84
/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, { 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
  getDocsVersion,
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 InfoHint from '../common/info-hint';
39
import ArtifactGenerationSettings from './artifactgeneration';
40
import ReportingLimits from './reportinglimits';
41

42
const maxWidth = 750;
6✔
43

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

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

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

63
  const changed = selectedAttribute !== attributeSelection;
2✔
64

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

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

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

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

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

147
  useEffect(() => {
2✔
148
    setChannelSettings(notificationChannelSettings);
1✔
149
  }, [notificationChannelSettings]);
150

151
  useEffect(() => {
2✔
152
    setCurrentInterval(offlineThresholdSettings.interval);
1✔
153
    setCurrentIntervalUnit(offlineThresholdSettings.intervalUnit);
1✔
154
  }, [offlineThresholdSettings.interval, offlineThresholdSettings.intervalUnit]);
155

156
  useEffect(() => {
2✔
157
    if (!window.sessionStorage.getItem(settingsKeys.initialized) || !timer.current) {
1!
158
      return;
1✔
159
    }
160
    saveGlobalSettings({ offlineThreshold: { interval: debouncedInterval, intervalUnit: debouncedIntervalUnit } }, false, true);
×
161
  }, [debouncedInterval, debouncedIntervalUnit]);
162

163
  useEffect(() => {
2✔
164
    const initTimer = setTimeout(() => (timer.current = true), TIMEOUTS.threeSeconds);
1✔
165
    return () => {
1✔
166
      clearTimeout(initTimer);
1✔
167
    };
168
  }, []);
169

170
  const onNotificationSettingsClick = ({ target: { checked } }, channel) => {
2✔
171
    setChannelSettings({ ...channelSettings, channel: { enabled: !checked } });
×
172
    onChangeNotificationSetting(!checked, channel);
×
173
  };
174

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

184
  const toggleDeploymentConfirmation = () => {
2✔
185
    saveGlobalSettings({ needsDeploymentConfirmation: !needsDeploymentConfirmation });
×
186
  };
187

188
  return (
2✔
189
    <div style={{ maxWidth }} className="margin-top-small">
190
      <h2 className="margin-top-small">Global settings</h2>
191
      <InfoHint content="These settings apply to all users, so changes made here may affect other users' experience." style={{ marginBottom: 30 }} />
192
      <IdAttributeSelection
193
        attributes={attributes}
194
        docsVersion={docsVersion}
195
        onCloseClick={onCloseClick}
196
        onSaveClick={onSaveClick}
197
        selectedAttribute={selectedAttribute}
198
      />
199
      {hasReporting && <ReportingLimits />}
4✔
200
      <InputLabel className="margin-top" shrink>
201
        Deployments
202
      </InputLabel>
203
      <div className="clickable flexbox center-aligned" onClick={toggleDeploymentConfirmation}>
204
        <p className="help-content">Require confirmation on deployment creation</p>
205
        <Switch checked={needsDeploymentConfirmation} />
206
      </div>
207
      {canManageReleases && canDelta && <ArtifactGenerationSettings />}
6✔
208
      {isAdmin &&
4!
209
        hasMonitor &&
210
        Object.keys(alertChannels).map(channel => (
211
          <FormControl key={channel}>
×
212
            <InputLabel className="capitalized-start" shrink id={`${channel}-notifications`}>
213
              {channel} notifications
214
            </InputLabel>
215
            <FormControlLabel
216
              control={<Checkbox checked={!channelSettings[channel].enabled} onChange={e => onNotificationSettingsClick(e, channel)} />}
×
217
              label={`Mute ${channel} notifications`}
218
            />
219
            <FormHelperText className="info" component="div">
220
              Mute {channel} notifications for deployment and monitoring issues for all users
221
            </FormHelperText>
222
          </FormControl>
223
        ))}
224

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

257
export const GlobalSettingsContainer = ({ closeDialog, dialog }) => {
6✔
258
  const dispatch = useDispatch();
2✔
259
  const attributes = useSelector(state => {
2✔
260
    // limit the selection of the available attribute to AVAILABLE_ATTRIBUTE_LIMIT
261
    const attributes = state.devices.filteringAttributes.identityAttributes.slice(0, state.devices.filteringAttributesLimit);
2✔
262
    return attributes.reduce(
2✔
263
      (accu, value) => {
264
        accu.push({ value, label: value, scope: 'identity' });
2✔
265
        return accu;
2✔
266
      },
267
      [
268
        { value: 'name', label: 'Name', scope: 'tags' },
269
        { value: 'id', label: 'Device ID', scope: 'identity' }
270
      ]
271
    );
272
  });
273
  const { hasReporting } = useSelector(getFeatures);
2✔
274
  const { isAdmin } = useSelector(getUserRoles);
2✔
275
  const docsVersion = useSelector(getDocsVersion);
2✔
276
  const notificationChannelSettings = useSelector(state => state.monitor.settings.global.channels);
2✔
277
  const offlineThresholdSettings = useSelector(getOfflineThresholdSettings);
2✔
278
  const { attribute: selectedAttribute } = useSelector(getIdAttribute);
2✔
279
  const settings = useSelector(getGlobalSettingsSelector);
2✔
280
  const tenantCapabilities = useSelector(getTenantCapabilities);
2✔
281
  const userCapabilities = useSelector(getUserCapabilities);
2✔
282

283
  const [updatedSettings, setUpdatedSettings] = useState({ ...settings });
2✔
284

285
  useEffect(() => {
2✔
286
    if (!settings) {
1!
287
      dispatch(getGlobalSettings());
×
288
    }
289
    dispatch(getDeviceAttributes());
1✔
290
  }, []);
291

292
  useEffect(() => {
2✔
293
    setUpdatedSettings({ ...updatedSettings, ...settings });
1✔
294
  }, [JSON.stringify(settings)]);
295

296
  const onCloseClick = e => {
2✔
297
    if (dialog) {
×
298
      return closeDialog(e);
×
299
    }
300
  };
301

302
  const saveAttributeSetting = (e, id_attribute) => {
2✔
303
    return dispatch(saveGlobalSettings({ ...updatedSettings, id_attribute }, false, true)).then(() => {
×
304
      if (dialog) {
×
305
        closeDialog(e);
×
306
      }
307
    });
308
  };
309

310
  if (dialog) {
2!
311
    return (
×
312
      <IdAttributeSelection
313
        attributes={attributes}
314
        dialog
315
        docsVersion={docsVersion}
316
        onCloseClick={onCloseClick}
317
        onSaveClick={saveAttributeSetting}
318
        selectedAttribute={selectedAttribute}
319
      />
320
    );
321
  }
322
  return (
2✔
323
    <GlobalSettingsDialog
324
      attributes={attributes}
325
      docsVersion={docsVersion}
326
      hasReporting={hasReporting}
327
      isAdmin={isAdmin}
328
      notificationChannelSettings={notificationChannelSettings}
329
      offlineThresholdSettings={offlineThresholdSettings}
330
      onChangeNotificationSetting={(...args) => dispatch(changeNotificationSetting(...args))}
×
331
      onCloseClick={onCloseClick}
332
      onSaveClick={saveAttributeSetting}
333
      saveGlobalSettings={(...args) => dispatch(saveGlobalSettings(...args))}
×
334
      settings={settings}
335
      selectedAttribute={selectedAttribute}
336
      tenantCapabilities={tenantCapabilities}
337
      userCapabilities={userCapabilities}
338
    />
339
  );
340
};
341
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