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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

63.56
/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 { connect } 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 { getDocsVersion, getIdAttribute, getOfflineThresholdSettings, getTenantCapabilities, getUserCapabilities, getUserRoles } from '../../selectors';
28
import { useDebounce } from '../../utils/debouncehook';
29
import InfoHint from '../common/info-hint';
30
import ArtifactGenerationSettings from './artifactgeneration';
31
import ReportingLimits from './reportinglimits';
32

33
const maxWidth = 750;
6✔
34

35
const useStyles = makeStyles()(theme => ({
6✔
36
  threshold: {
37
    display: 'grid',
38
    gridTemplateColumns: '100px 100px',
39
    columnGap: theme.spacing(2)
40
  },
41
  textInput: {
42
    marginTop: 0,
43
    minWidth: 'initial'
44
  }
45
}));
46

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

50
  useEffect(() => {
2✔
51
    setAttributeSelection(selectedAttribute);
1✔
52
  }, [selectedAttribute]);
53

54
  const changed = selectedAttribute !== attributeSelection;
2✔
55

56
  const onChangeIdAttribute = ({ target: { value: attributeSelection } }) => {
2✔
57
    setAttributeSelection(attributeSelection);
×
58
    if (dialog) {
×
59
      return;
×
60
    }
61
    onSaveClick(undefined, { attribute: attributeSelection, scope: attributes.find(({ value }) => value === attributeSelection).scope });
×
62
  };
63

64
  const undoChanges = e => {
2✔
65
    setAttributeSelection(selectedAttribute);
×
66
    if (dialog) {
×
67
      onCloseClick(e);
×
68
    }
69
  };
70

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

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

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

138
  useEffect(() => {
2✔
139
    setChannelSettings(notificationChannelSettings);
1✔
140
  }, [notificationChannelSettings]);
141

142
  useEffect(() => {
2✔
143
    setCurrentInterval(offlineThresholdSettings.interval);
1✔
144
    setCurrentIntervalUnit(offlineThresholdSettings.intervalUnit);
1✔
145
  }, [offlineThresholdSettings.interval, offlineThresholdSettings.intervalUnit]);
146

147
  useEffect(() => {
2✔
148
    if (!window.sessionStorage.getItem(settingsKeys.initialized) || !timer.current) {
1!
149
      return;
1✔
150
    }
151
    saveGlobalSettings({ offlineThreshold: { interval: debouncedInterval, intervalUnit: debouncedIntervalUnit } }, false, true);
×
152
  }, [debouncedInterval, debouncedIntervalUnit]);
153

154
  useEffect(() => {
2✔
155
    const initTimer = setTimeout(() => (timer.current = true), TIMEOUTS.threeSeconds);
1✔
156
    return () => {
1✔
157
      clearTimeout(initTimer);
1✔
158
    };
159
  }, []);
160

161
  const onNotificationSettingsClick = ({ target: { checked } }, channel) => {
2✔
162
    setChannelSettings({ ...channelSettings, channel: { enabled: !checked } });
×
163
    onChangeNotificationSetting(!checked, channel);
×
164
  };
165

166
  const onChangeOfflineIntervalUnit = ({ target: { value } }) => setCurrentIntervalUnit(value);
2✔
167
  const onChangeOfflineInterval = ({ target: { validity, value } }) => {
2✔
168
    if (validity.valid) {
×
169
      setCurrentInterval(value || 1);
×
170
      return setIntervalErrorText('');
×
171
    }
172
    setIntervalErrorText('Please enter a valid number between 1 and 1000.');
×
173
  };
174

175
  const toggleDeploymentConfirmation = () => {
2✔
176
    saveGlobalSettings({ needsDeploymentConfirmation: !needsDeploymentConfirmation });
×
177
  };
178

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

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

248
export const GlobalSettingsContainer = ({
6✔
249
  attributes,
250
  changeNotificationSetting,
251
  closeDialog,
252
  dialog,
253
  docsVersion,
254
  getDeviceAttributes,
255
  getGlobalSettings,
256
  hasReporting,
257
  isAdmin,
258
  notificationChannelSettings,
259
  offlineThresholdSettings,
260
  saveGlobalSettings,
261
  selectedAttribute,
262
  settings,
263
  tenantCapabilities,
264
  userCapabilities
265
}) => {
266
  const [updatedSettings, setUpdatedSettings] = useState({ ...settings });
2✔
267

268
  useEffect(() => {
2✔
269
    if (!settings) {
1!
270
      getGlobalSettings();
×
271
    }
272
    getDeviceAttributes();
1✔
273
  }, []);
274

275
  useEffect(() => {
2✔
276
    setUpdatedSettings({ ...updatedSettings, ...settings });
1✔
277
  }, [JSON.stringify(settings)]);
278

279
  const onCloseClick = e => {
2✔
280
    if (dialog) {
×
281
      return closeDialog(e);
×
282
    }
283
  };
284

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

293
  if (dialog) {
2!
294
    return (
×
295
      <IdAttributeSelection
296
        attributes={attributes}
297
        dialog
298
        docsVersion={docsVersion}
299
        onCloseClick={onCloseClick}
300
        onSaveClick={saveAttributeSetting}
301
        selectedAttribute={selectedAttribute}
302
      />
303
    );
304
  }
305
  return (
2✔
306
    <GlobalSettingsDialog
307
      attributes={attributes}
308
      docsVersion={docsVersion}
309
      hasReporting={hasReporting}
310
      isAdmin={isAdmin}
311
      notificationChannelSettings={notificationChannelSettings}
312
      offlineThresholdSettings={offlineThresholdSettings}
313
      onChangeNotificationSetting={changeNotificationSetting}
314
      onCloseClick={onCloseClick}
315
      onSaveClick={saveAttributeSetting}
316
      saveGlobalSettings={saveGlobalSettings}
317
      settings={settings}
318
      selectedAttribute={selectedAttribute}
319
      tenantCapabilities={tenantCapabilities}
320
      userCapabilities={userCapabilities}
321
    />
322
  );
323
};
324

325
const actionCreators = { changeNotificationSetting, getDeviceAttributes, getGlobalSettings, saveGlobalSettings };
6✔
326

327
const mapStateToProps = state => {
6✔
328
  const attributes = state.devices.filteringAttributes.identityAttributes.slice(0, state.devices.filteringAttributesLimit);
1✔
329
  const id_attributes = attributes.reduce(
1✔
330
    (accu, value) => {
331
      accu.push({ value, label: value, scope: 'identity' });
1✔
332
      return accu;
1✔
333
    },
334
    [
335
      { value: 'name', label: 'Name', scope: 'tags' },
336
      { value: 'id', label: 'Device ID', scope: 'identity' }
337
    ]
338
  );
339
  return {
1✔
340
    // limit the selection of the available attribute to AVAILABLE_ATTRIBUTE_LIMIT
341
    attributes: id_attributes,
342
    hasReporting: state.app.features.hasReporting,
343
    isAdmin: getUserRoles(state).isAdmin,
344
    devicesCount: Object.keys(state.devices.byId).length,
345
    docsVersion: getDocsVersion(state),
346
    notificationChannelSettings: state.monitor.settings.global.channels,
347
    offlineThresholdSettings: getOfflineThresholdSettings(state),
348
    selectedAttribute: getIdAttribute(state).attribute,
349
    settings: state.users.globalSettings,
350
    tenantCapabilities: getTenantCapabilities(state),
351
    userCapabilities: getUserCapabilities(state)
352
  };
353
};
354

355
export default connect(mapStateToProps, actionCreators)(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