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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

94.43
/frontend/src/js/components/settings/Global.tsx
1
// Copyright 2018 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16
import { Link } from 'react-router-dom';
2✔
17

2✔
18
import { AutoAwesomeOutlined as AutoAwesomeIcon, Edit as EditIcon } from '@mui/icons-material';
2✔
19
import {
2✔
20
  Button,
2✔
21
  Checkbox,
2✔
22
  FormControl,
2✔
23
  FormControlLabel,
2✔
24
  FormHelperText,
2✔
25
  InputLabel,
2✔
26
  MenuItem,
2✔
27
  Select,
2✔
28
  Switch,
2✔
29
  TextField,
2✔
30
  Typography,
2✔
31
  textFieldClasses
2✔
32
} from '@mui/material';
2✔
33
import { makeStyles } from 'tss-react/mui';
2✔
34

2✔
35
import DocsLink from '@northern.tech/common-ui/DocsLink';
2✔
36
import EnterpriseNotification from '@northern.tech/common-ui/EnterpriseNotification';
2✔
37
import { SupportLink } from '@northern.tech/common-ui/SupportLink';
2✔
38
import { BENEFITS, DEVICE_ONLINE_CUTOFF, TIMEOUTS, alertChannels, settingsKeys } from '@northern.tech/store/constants';
2✔
39
import {
2✔
40
  getDeviceIdentityAttributes,
2✔
41
  getFeatures,
2✔
42
  getGlobalSettings as getGlobalSettingsSelector,
2✔
43
  getIdAttribute,
2✔
44
  getIsPreview,
2✔
45
  getOfflineThresholdSettings,
2✔
46
  getOrganization,
2✔
47
  getTenantCapabilities,
2✔
48
  getUserCapabilities,
2✔
49
  getUserRoles
2✔
50
} from '@northern.tech/store/selectors';
2✔
51
import { changeNotificationSetting, getDeviceAttributes, getGlobalSettings, saveGlobalSettings } from '@northern.tech/store/thunks';
2✔
52
import { useDebounce } from '@northern.tech/utils/debouncehook';
2✔
53

2✔
54
import ArtifactGenerationSettings from './ArtifactGeneration';
2✔
55

2✔
56
const maxWidth = 750;
8✔
57

2✔
58
const useStyles = makeStyles()(theme => ({
8✔
59
  formWrapper: { display: 'flex', flexDirection: 'column', gap: theme.spacing(4) },
2✔
60
  threshold: {
2✔
61
    columnGap: theme.spacing(2),
2✔
62
    display: 'grid',
2✔
63
    gridTemplateColumns: '100px 100px',
2✔
64
    marginLeft: 0,
2✔
65
    [`.${textFieldClasses.root}`]: { minWidth: 'auto' }
2✔
66
  }
2✔
67
}));
2✔
68

2✔
69
export const IdAttributeSelection = ({ attributes, dialog = false, onCloseClick, onSaveClick, selectedAttribute = '' }) => {
8✔
70
  const [attributeSelection, setAttributeSelection] = useState('name');
4✔
71

2✔
72
  useEffect(() => {
4✔
73
    setAttributeSelection(selectedAttribute);
3✔
74
  }, [selectedAttribute]);
2✔
75

2✔
76
  const changed = selectedAttribute !== attributeSelection;
4✔
77

2✔
78
  const onChangeIdAttribute = ({ target: { value: attributeSelection } }) => {
4✔
79
    setAttributeSelection(attributeSelection);
2✔
80
    if (dialog) {
2!
81
      return;
2✔
82
    }
2✔
83
    onSaveClick(undefined, { attribute: attributeSelection, scope: attributes.find(({ value }) => value === attributeSelection).scope });
2✔
84
  };
2✔
85

2✔
86
  const undoChanges = e => {
4✔
87
    setAttributeSelection(selectedAttribute);
2✔
88
    if (dialog) {
2!
89
      onCloseClick(e);
2✔
90
    }
2✔
91
  };
2✔
92

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

2✔
95
  return (
4✔
96
    <div className="flexbox space-between" style={{ alignItems: 'start', maxWidth }}>
2✔
97
      <div className="flexbox column">
2✔
98
        <FormControl className="margin-top-none">
2✔
99
          <InputLabel id="device-id">Device identity attribute</InputLabel>
2✔
100
          <Select label="Device identity attribute" labelId="device-id" value={attributeSelection} onChange={onChangeIdAttribute}>
2✔
101
            {attributes.map(item => (
2✔
102
              <MenuItem key={item.value} value={item.value}>
8✔
103
                {item.label}
2✔
104
              </MenuItem>
2✔
105
            ))}
2✔
106
          </Select>
2✔
107
          <FormHelperText className="info margin-left-none" component="div">
2✔
108
            <div>Choose a device identity attribute to use to identify your devices throughout the UI.</div>
2✔
109
            <div className={`margin-top-x-small ${dialog ? 'margin-bottom-small' : ''}`}>
2!
110
              <DocsLink path="client-installation/identity" title="Learn how to add custom identity attributes" /> to your devices.
2✔
111
            </div>
2✔
112
          </FormHelperText>
2✔
113
        </FormControl>
2✔
114
      </div>
2✔
115
      {dialog && (
2!
116
        <div className="margin-left margin-top flexbox">
2✔
117
          <Button onClick={undoChanges} style={{ marginRight: 10 }}>
2✔
118
            Cancel
2✔
119
          </Button>
2✔
120
          <Button variant="contained" onClick={saveSettings} disabled={!changed} color="primary">
2✔
121
            Save
2✔
122
          </Button>
2✔
123
        </div>
2✔
124
      )}
2✔
125
    </div>
2✔
126
  );
2✔
127
};
2✔
128

2✔
129
const ToggleSetting = ({
8✔
130
  description,
2✔
131
  disabled = false,
2✔
132
  title,
2✔
133
  onClick,
2✔
134
  value
2✔
135
}: {
2✔
136
  description?: string;
2✔
137
  disabled?: boolean;
2✔
138
  onClick: () => void;
2✔
139
  title: string | ReactNode;
2✔
140
  value: boolean;
2✔
141
}) => (
2✔
142
  <div className="flexbox column">
6✔
143
    <FormControl variant="standard">
2✔
144
      <FormControlLabel
2✔
145
        disabled={disabled}
2✔
146
        classes={{ label: 'capitalized-start' }}
2✔
147
        className="align-self-start margin-left-none margin-top-none"
2✔
148
        control={<Switch className="margin-left-small" checked={value} onClick={onClick} />}
2✔
149
        label={title}
2✔
150
        labelPlacement="start"
2✔
151
      />
2✔
152
    </FormControl>
2✔
153
    {!!description && (
2✔
154
      <Typography className="margin-top-x-small" variant="body2">
2✔
155
        {description}
2✔
156
      </Typography>
2✔
157
    )}
2✔
158
  </div>
2✔
159
);
2✔
160

2✔
161
export const GlobalSettingsDialog = ({
8✔
162
  attributes,
2✔
163
  isAdmin,
2✔
164
  notificationChannelSettings,
2✔
165
  offlineThresholdSettings,
2✔
166
  onChangeNotificationSetting,
2✔
167
  onCloseClick,
2✔
168
  onSaveClick,
2✔
169
  saveGlobalSettings,
2✔
170
  selectedAttribute,
2✔
171
  settings,
2✔
172
  tenantCapabilities,
2✔
173
  userCapabilities
2✔
174
}) => {
2✔
175
  const [channelSettings, setChannelSettings] = useState(notificationChannelSettings);
4✔
176
  const [currentInterval, setCurrentInterval] = useState(offlineThresholdSettings.interval);
4✔
177
  const [intervalErrorText, setIntervalErrorText] = useState('');
4✔
178
  const [showDeltaConfig, setShowDeltaConfig] = useState(false);
4✔
179
  const debouncedOfflineThreshold = useDebounce(currentInterval, TIMEOUTS.threeSeconds);
4✔
180
  const timer = useRef(false);
4✔
181
  const { classes } = useStyles();
4✔
182
  const { aiFeatures = {}, needsDeploymentConfirmation = false } = settings;
4✔
183
  const { enabled: isAiEnabled, trainingEnabled: isAiTrainingEnabled } = aiFeatures;
4✔
184
  const { hasMonitor, isEnterprise } = tenantCapabilities;
4✔
185
  const { canManageReleases, canManageUsers } = userCapabilities;
4✔
186
  const { trial: isTrial = true } = useSelector(getOrganization);
4✔
187
  const { hasDelta: hasDeltaArtifactGeneration } = useSelector(state => state.deployments.config) ?? {};
5!
188
  const { hasAiEnabled } = useSelector(getFeatures);
4✔
189
  const isPreview = useSelector(getIsPreview);
4✔
190

2✔
191
  useEffect(() => {
4✔
192
    setChannelSettings(notificationChannelSettings);
3✔
193
  }, [notificationChannelSettings]);
2✔
194

2✔
195
  useEffect(() => {
4✔
196
    setCurrentInterval(offlineThresholdSettings.interval);
3✔
197
  }, [offlineThresholdSettings.interval]);
2✔
198

2✔
199
  useEffect(() => {
4✔
200
    if (!window.sessionStorage.getItem(settingsKeys.initialized) || !timer.current || !canManageUsers) {
3!
201
      return;
3✔
202
    }
2✔
203
    saveGlobalSettings({ offlineThreshold: { interval: debouncedOfflineThreshold, intervalUnit: DEVICE_ONLINE_CUTOFF.intervalName }, notify: true });
2✔
204
  }, [canManageUsers, debouncedOfflineThreshold, saveGlobalSettings]);
2✔
205

2✔
206
  useEffect(() => {
4✔
207
    const initTimer = setTimeout(() => (timer.current = true), TIMEOUTS.fiveSeconds);
3✔
208
    return () => {
3✔
209
      clearTimeout(initTimer);
3✔
210
    };
2✔
211
  }, []);
2✔
212

2✔
213
  const onNotificationSettingsClick = useCallback(
4✔
214
    channel => {
2✔
215
      const checked = channelSettings[channel].enabled;
2✔
216
      setChannelSettings({ ...channelSettings, [channel]: { enabled: !checked } });
2✔
217
      onChangeNotificationSetting({ enabled: !checked, channel });
2✔
218
    },
2✔
219
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
220
    [JSON.stringify(channelSettings)]
2✔
221
  );
2✔
222

2✔
223
  const onChangeOfflineInterval = ({ target: { validity, value } }) => {
4✔
224
    if (validity.valid) {
2!
225
      setCurrentInterval(value || 1);
2!
226
      return setIntervalErrorText('');
2✔
227
    }
2✔
228
    setIntervalErrorText('Please enter a valid number between 1 and 1000.');
2✔
229
  };
2✔
230

2✔
231
  const toggleDeploymentConfirmation = () => saveGlobalSettings({ needsDeploymentConfirmation: !needsDeploymentConfirmation });
4✔
232

2✔
233
  const onEditDeltaClick = () => setShowDeltaConfig(true);
4✔
234

2✔
235
  const onToggleAiClick = useCallback(current => saveGlobalSettings({ aiFeatures: { ...aiFeatures, enabled: !current } }), [aiFeatures, saveGlobalSettings]);
4✔
236

2✔
237
  const onToggleAiTrainingClick = useCallback(
4✔
UNCOV
238
    ({ target: { checked } }) => saveGlobalSettings({ aiFeatures: { ...aiFeatures, trainingEnabled: checked } }),
2✔
239
    [aiFeatures, saveGlobalSettings]
2✔
240
  );
2✔
241

2✔
242
  return (
4✔
243
    <div style={{ maxWidth }} className="margin-top-small">
2✔
244
      <Typography variant="h6">Global settings</Typography>
2✔
245
      <Typography className="margin-top-x-small margin-bottom-large" variant="body2">
2✔
246
        Global settings are applied organization-wide. Modifying these settings will affect all users.
2✔
247
      </Typography>
2✔
248
      <div className={classes.formWrapper}>
2✔
249
        <IdAttributeSelection attributes={attributes} onCloseClick={onCloseClick} onSaveClick={onSaveClick} selectedAttribute={selectedAttribute} />
2✔
250
        {canManageUsers && (
2✔
251
          <ToggleSetting
2✔
252
            title="Deployments confirmation"
2✔
253
            description="Always require confirmation on deployment creation"
2✔
254
            onClick={toggleDeploymentConfirmation}
2✔
255
            value={needsDeploymentConfirmation}
2✔
256
          />
2✔
257
        )}
2✔
258
        {canManageReleases && (
2✔
259
          <div>
2✔
260
            <div className="flexbox center-aligned">
2✔
261
              <Typography variant="subtitle1">Delta Artifacts generation</Typography>
2✔
262
              <EnterpriseNotification className="margin-left-small" id={BENEFITS.deltaGeneration.id} />
2✔
263
            </div>
2✔
264
            <Button
2✔
265
              className="margin-top-x-small"
2✔
266
              disabled={!(isEnterprise && hasDeltaArtifactGeneration)}
2✔
267
              onClick={onEditDeltaClick}
2✔
268
              variant="text"
2✔
269
              endIcon={<EditIcon />}
2✔
270
            >
2✔
271
              Edit configuration
2✔
272
            </Button>
2✔
273
            {!isEnterprise && (
2!
274
              <Typography className="margin-top-small" variant="body2">
2✔
275
                Automatic delta artifacts generation is not enabled in your account. If you want to start using this feature, <SupportLink variant="ourTeam" />{' '}
2✔
276
                or <Link to="/subscription">upgrade</Link>
2✔
277
                {isTrial ? '' : ' to Mender Enterprise'}.
2!
278
              </Typography>
2✔
279
            )}
2✔
280
          </div>
2✔
281
        )}
2✔
282
        {isAdmin &&
2!
283
          hasMonitor &&
2✔
284
          Object.keys(alertChannels).map(channel => (
2✔
285
            <ToggleSetting
2✔
286
              key={channel}
2✔
287
              value={channelSettings[channel].enabled}
2✔
288
              onClick={() => onNotificationSettingsClick(channel)}
2✔
289
              title={`${channel} notifications`}
2✔
290
              description={`${channel} notifications for deployment and monitoring issues for all users`}
2✔
291
            />
2✔
292
          ))}
2✔
293
        <div>
2✔
294
          <Typography className="margin-bottom-small" variant="subtitle1">
2✔
295
            Offline threshold
2✔
296
          </Typography>
2✔
297
          <FormControl variant="standard">
2✔
298
            <FormControlLabel
2✔
299
              className={classes.threshold}
2✔
300
              control={
2✔
301
                <TextField
2✔
302
                  type="number"
2✔
303
                  onChange={onChangeOfflineInterval}
2✔
304
                  slotProps={{ htmlInput: { min: '1', max: '1000' } }}
2✔
305
                  error={!!intervalErrorText}
2✔
306
                  value={currentInterval}
2✔
307
                  variant="outlined"
2✔
308
                />
2✔
309
              }
2✔
310
              label={<div className="capitalized-start">{DEVICE_ONLINE_CUTOFF.intervalName}</div>}
2✔
311
            />
2✔
312
            {!!intervalErrorText && <FormHelperText className="warning">{intervalErrorText}</FormHelperText>}
2!
313
            <FormHelperText>Choose how long a device can go without reporting to the server before it is considered “offline”.</FormHelperText>
2✔
314
          </FormControl>
2✔
315
        </div>
2!
316
        {(isPreview || hasAiEnabled) && (
2✔
317
          <div>
2✔
318
            <ToggleSetting
2✔
319
              value={isAiEnabled}
2✔
UNCOV
320
              onClick={() => onToggleAiClick(isAiEnabled)}
2✔
321
              title={
2✔
322
                <div className="flexbox center-aligned">
2✔
323
                  <AutoAwesomeIcon className="margin-right-x-small" fontSize="small" color={isAiEnabled ? 'secondary' : 'inherit'} />
2!
324
                  <Typography variant="subtitle1">AI features (experimental)</Typography>
2✔
325
                </div>
2✔
326
              }
2✔
327
              description="Enable AI features for all users. We'll try to remove any sensitive details, such as URLs and timestamps, before sending your data for AI analysis. AI features are rate limited to 50 requests per day. "
2✔
328
            />
2✔
329
            <FormControlLabel
2✔
330
              control={<Checkbox disabled={!isAiEnabled} checked={isAiTrainingEnabled} onChange={onToggleAiTrainingClick} />}
2✔
331
              label="Allow us to use data for training"
2✔
332
            />
2✔
333
            <Typography variant="body2">This allows us to enhance the responses you get, collect your feedback, and refine the AI model.</Typography>
2✔
334
          </div>
2✔
335
        )}
2✔
336
      </div>
2✔
UNCOV
337
      <ArtifactGenerationSettings open={showDeltaConfig} onClose={() => setShowDeltaConfig(false)} />
2✔
338
    </div>
2✔
339
  );
2✔
340
};
2✔
341

2✔
342
export const GlobalSettingsContainer = ({ closeDialog, dialog }) => {
8✔
343
  const dispatch = useDispatch();
4✔
344
  const attributes = useSelector(getDeviceIdentityAttributes);
4✔
345
  const { isAdmin } = useSelector(getUserRoles);
4✔
346
  const notificationChannelSettings = useSelector(state => state.monitor.settings.global.channels);
5✔
347
  const offlineThresholdSettings = useSelector(getOfflineThresholdSettings);
4✔
348
  const { attribute: selectedAttribute } = useSelector(getIdAttribute);
4✔
349
  const settings = useSelector(getGlobalSettingsSelector);
4✔
350
  const tenantCapabilities = useSelector(getTenantCapabilities);
4✔
351
  const userCapabilities = useSelector(getUserCapabilities);
4✔
352

2✔
353
  const [updatedSettings, setUpdatedSettings] = useState({ ...settings });
4✔
354

2✔
355
  useEffect(() => {
4✔
356
    if (!settings) {
3!
357
      dispatch(getGlobalSettings());
2✔
358
    }
2✔
359
    dispatch(getDeviceAttributes());
3✔
360
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
361
  }, [dispatch, JSON.stringify(settings)]);
2✔
362

2✔
363
  useEffect(() => {
4✔
364
    setUpdatedSettings(current => ({ ...current, ...settings }));
3✔
365
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
366
  }, [JSON.stringify(settings)]);
2✔
367

2✔
368
  const onCloseClick = e => {
4✔
369
    if (dialog) {
2!
370
      return closeDialog(e);
2✔
371
    }
2✔
372
  };
2✔
373

2✔
374
  const onChangeNotificationSetting = useCallback((...args) => dispatch(changeNotificationSetting(...args)), [dispatch]);
4✔
375
  const onSaveGlobalSettings = useCallback((...args) => dispatch(saveGlobalSettings(...args)), [dispatch]);
4✔
376

2✔
377
  const saveAttributeSetting = (e, id_attribute) =>
4✔
378
    onSaveGlobalSettings({ ...updatedSettings, id_attribute, notify: true }).then(() => {
2✔
379
      if (dialog) {
2!
380
        closeDialog(e);
2✔
381
      }
2✔
382
    });
2✔
383

2✔
384
  if (dialog) {
4!
385
    return (
2✔
386
      <IdAttributeSelection
2✔
387
        attributes={attributes}
2✔
388
        dialog
2✔
389
        onCloseClick={onCloseClick}
2✔
390
        onSaveClick={saveAttributeSetting}
2✔
391
        selectedAttribute={selectedAttribute}
2✔
392
      />
2✔
393
    );
2✔
394
  }
2✔
395
  return (
4✔
396
    <GlobalSettingsDialog
2✔
397
      attributes={attributes}
2✔
398
      isAdmin={isAdmin}
2✔
399
      notificationChannelSettings={notificationChannelSettings}
2✔
400
      offlineThresholdSettings={offlineThresholdSettings}
2✔
401
      onChangeNotificationSetting={onChangeNotificationSetting}
2✔
402
      onCloseClick={onCloseClick}
2✔
403
      onSaveClick={saveAttributeSetting}
2✔
404
      saveGlobalSettings={onSaveGlobalSettings}
2✔
405
      settings={settings}
2✔
406
      selectedAttribute={selectedAttribute}
2✔
407
      tenantCapabilities={tenantCapabilities}
2✔
408
      userCapabilities={userCapabilities}
2✔
409
    />
2✔
410
  );
2✔
411
};
2✔
412
export default GlobalSettingsContainer;
2✔
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