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

mendersoftware / gui / 1493849842

13 Oct 2024 07:39AM UTC coverage: 83.457% (-16.5%) from 99.965%
1493849842

Pull #4531

gitlab-ci

web-flow
chore: Bump send and express in /tests/e2e_tests

Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4531: chore: Bump send and express in /tests/e2e_tests

4486 of 6422 branches covered (69.85%)

8551 of 10246 relevant lines covered (83.46%)

151.3 hits per line

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

61.65
/src/js/components/devices/device-details/configuration.js
1
// Copyright 2021 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
import { Link } from 'react-router-dom';
17

18
import { Block as BlockIcon, CheckCircle as CheckCircleIcon, Error as ErrorIcon, Refresh as RefreshIcon, SaveAlt as SaveAltIcon } from '@mui/icons-material';
19
import { Button, Checkbox, FormControlLabel, Typography } from '@mui/material';
20

21
import { setSnackbar } from '../../../actions/appActions';
22
import { abortDeployment, getDeviceLog, getSingleDeployment } from '../../../actions/deploymentActions';
23
import { applyDeviceConfig, setDeviceConfig } from '../../../actions/deviceActions';
24
import { saveGlobalSettings } from '../../../actions/userActions';
25
import { BENEFITS, TIMEOUTS } from '../../../constants/appConstants';
26
import { DEPLOYMENT_ROUTES, DEPLOYMENT_STATES } from '../../../constants/deploymentConstants';
27
import { DEVICE_STATES } from '../../../constants/deviceConstants';
28
import { deepCompare, groupDeploymentDevicesStats, groupDeploymentStats, isEmpty, toggle } from '../../../helpers';
29
import { getDeviceConfigDeployment, getTenantCapabilities, getUserCapabilities } from '../../../selectors';
30
import Tracking from '../../../tracking';
31
import ConfigurationObject from '../../common/configurationobject';
32
import Confirm, { EditButton } from '../../common/confirm';
33
import LogDialog from '../../common/dialogs/log';
34
import { DOCSTIPS, DocsTooltip } from '../../common/docslink';
35
import EnterpriseNotification from '../../common/enterpriseNotification';
36
import KeyValueEditor from '../../common/forms/keyvalueeditor';
37
import { InfoHintContainer } from '../../common/info-hint';
38
import Loader from '../../common/loader';
39
import Time from '../../common/time';
40
import { HELPTOOLTIPS, MenderHelpTooltip } from '../../helptips/helptooltips';
41
import ConfigImportDialog from './configimportdialog';
42
import DeviceDataCollapse from './devicedatacollapse';
43

44
const buttonStyle = { marginLeft: 30 };
9✔
45
const iconStyle = { margin: 12 };
9✔
46
const textStyle = { textTransform: 'capitalize', textAlign: 'left' };
9✔
47

48
const defaultReportTimeStamp = '0001-01-01T00:00:00Z';
9✔
49

50
const configHelpTipsMap = {
9✔
51
  'mender-demo-raspberrypi-led': {
52
    position: 'right',
53
    component: ({ anchor, ...props }) => <MenderHelpTooltip style={anchor} id={HELPTOOLTIPS.configureRaspberryLedTip.id} contentProps={props} />
×
54
  },
55
  timezone: {
56
    position: 'right',
57
    component: ({ anchor, ...props }) => <MenderHelpTooltip style={anchor} id={HELPTOOLTIPS.configureTimezoneTip.id} contentProps={props} />
×
58
  }
59
};
60

61
export const ConfigUpToDateNote = ({ updated_ts = defaultReportTimeStamp }) => (
9!
62
  <div className="flexbox margin-small">
2✔
63
    <CheckCircleIcon className="green" style={iconStyle} />
64
    <div>
65
      <Typography variant="subtitle2" style={textStyle}>
66
        Configuration up-to-date on the device
67
      </Typography>
68
      <Typography variant="caption" className="muted" style={textStyle}>
69
        Updated: {<Time value={updated_ts} />}
70
      </Typography>
71
    </div>
72
  </div>
73
);
74

75
export const ConfigEmptyNote = ({ updated_ts = '' }) => (
9!
76
  <div className="flexbox column margin-small">
2✔
77
    <Typography variant="subtitle2">The device appears to either have an empty configuration or not to have reported a configuration yet.</Typography>
78
    <Typography variant="caption" className="muted" style={textStyle}>
79
      Updated: {<Time value={updated_ts} />}
80
    </Typography>
81
  </div>
82
);
83

84
export const ConfigEditingActions = ({ canSetDefault, hasDeviceConfig, isSetAsDefault, onSetAsDefaultChange, onSubmit, onCancel }) => (
9✔
85
  <>
19✔
86
    {canSetDefault && (
38✔
87
      <div style={{ maxWidth: 275 }}>
88
        <FormControlLabel
89
          control={<Checkbox color="primary" checked={isSetAsDefault} onChange={onSetAsDefaultChange} size="small" />}
90
          label="Save as default configuration"
91
          style={{ marginTop: 0 }}
92
        />
93
        <div className="muted">You can import these key value pairs when configuring other devices</div>
94
      </div>
95
    )}
96
    <Button variant="contained" color="primary" onClick={onSubmit} style={buttonStyle}>
97
      Save and apply to device
98
    </Button>
99
    {hasDeviceConfig && (
19!
100
      <Button onClick={onCancel} style={buttonStyle}>
101
        Cancel changes
102
      </Button>
103
    )}
104
  </>
105
);
106

107
export const ConfigUpdateNote = ({ isUpdatingConfig, isAccepted }) => (
9✔
108
  <div>
17✔
109
    <Typography variant="subtitle2" style={textStyle}>
110
      {!isAccepted
17!
111
        ? 'Configuration will be applied once the device is connected'
112
        : isUpdatingConfig
17✔
113
          ? 'Updating configuration on device...'
114
          : 'Configuration could not be updated on device'}
115
    </Typography>
116
    <Typography variant="caption" className="muted" style={textStyle}>
117
      Status: {isUpdatingConfig || !isAccepted ? 'pending' : 'failed'}
47✔
118
    </Typography>
119
  </div>
120
);
121

122
export const ConfigUpdateFailureActions = ({ hasLog, onSubmit, onCancel, setShowLog }) => (
9✔
123
  <>
14✔
124
    {hasLog && (
14!
125
      <Button color="secondary" onClick={setShowLog} style={buttonStyle}>
126
        View log
127
      </Button>
128
    )}
129
    <Button color="secondary" onClick={onSubmit} startIcon={<RefreshIcon fontSize="small" />} style={buttonStyle}>
130
      Retry
131
    </Button>
132
    <a className="margin-left-large" onClick={onCancel}>
133
      cancel changes
134
    </a>
135
  </>
136
);
137

138
export const DeviceConfiguration = ({ defaultConfig = {}, device: { id: deviceId } }) => {
9✔
139
  const { device, deviceConfigDeployment: deployment } = useSelector(state => getDeviceConfigDeployment(state, deviceId));
40✔
140
  const { hasDeviceConfig } = useSelector(getTenantCapabilities);
36✔
141
  const { canManageUsers } = useSelector(getUserCapabilities);
36✔
142
  const { config = {}, status } = device;
36!
143
  const { configured = {}, deployment_id, reported = {}, reported_ts, updated_ts } = config;
36!
144
  const isRelevantDeployment = deployment.created > updated_ts && (!reported_ts || deployment.finished > reported_ts);
36!
145
  const [changedConfig, setChangedConfig] = useState();
36✔
146
  const [editableConfig, setEditableConfig] = useState();
36✔
147
  const [isAborting, setIsAborting] = useState(false);
36✔
148
  const [isEditingConfig, setIsEditingConfig] = useState(false);
36✔
149
  const [isSetAsDefault, setIsSetAsDefault] = useState(false);
36✔
150
  const [isUpdatingConfig, setIsUpdatingConfig] = useState(false);
36✔
151
  const [showConfigImport, setShowConfigImport] = useState(false);
36✔
152
  const [showLog, setShowLog] = useState(false);
36✔
153
  const [updateFailed, setUpdateFailed] = useState();
36✔
154
  const [updateLog, setUpdateLog] = useState();
36✔
155
  const dispatch = useDispatch();
36✔
156
  const deploymentTimer = useRef();
36✔
157

158
  useEffect(() => {
36✔
159
    if (!isEmpty(config) && !isEmpty(changedConfig) && !isEditingConfig) {
27!
160
      setIsEditingConfig(isUpdatingConfig || updateFailed);
×
161
    }
162
    // eslint-disable-next-line react-hooks/exhaustive-deps
163
  }, [JSON.stringify(config), JSON.stringify(changedConfig), isEditingConfig, isUpdatingConfig, updateFailed]);
164

165
  useEffect(() => {
36✔
166
    if (deployment.devices && deployment.devices[device.id]?.log) {
2!
167
      setUpdateLog(deployment.devices[device.id].log);
×
168
    }
169

170
    // eslint-disable-next-line react-hooks/exhaustive-deps
171
  }, [JSON.stringify(deployment.devices), device.id]);
172

173
  useEffect(() => {
36✔
174
    clearInterval(deploymentTimer.current);
2✔
175
    if (isRelevantDeployment && deployment.status !== DEPLOYMENT_STATES.finished) {
2!
176
      deploymentTimer.current = setInterval(() => dispatch(getSingleDeployment(deployment_id)), TIMEOUTS.refreshDefault);
×
177
    } else if (deployment_id && !isRelevantDeployment) {
2!
178
      dispatch(getSingleDeployment(deployment_id));
×
179
    }
180
    return () => {
2✔
181
      clearInterval(deploymentTimer.current);
2✔
182
    };
183
  }, [deployment.status, deployment_id, dispatch, isRelevantDeployment]);
184

185
  useEffect(() => {
36✔
186
    if (!isRelevantDeployment) {
2!
187
      return;
2✔
188
    }
189
    if (deployment.status === DEPLOYMENT_STATES.finished) {
×
190
      // we have to rely on the device stats here as the state change might not have propagated to the deployment status
191
      // leaving all stats at 0 and giving a false impression of deployment success
192
      const stats = groupDeploymentStats(deployment);
×
193
      const deviceStats = groupDeploymentDevicesStats(deployment);
×
194
      const updateFailed = !!((stats.failures || deviceStats.failures) && deployment.device_count);
×
195
      setUpdateFailed(updateFailed);
×
196
      setIsEditingConfig(updateFailed);
×
197
      setIsUpdatingConfig(false);
×
198
    } else if (deployment.status) {
×
199
      setChangedConfig(configured);
×
200
      setEditableConfig(configured);
×
201
      // we can't rely on the deployment.status to be !== 'finished' since `deployment` is initialized as an empty object
202
      // and thus the undefined status would also point to an ongoing update
203
      setIsUpdatingConfig(true);
×
204
    }
205
    // eslint-disable-next-line react-hooks/exhaustive-deps
206
  }, [JSON.stringify(configured), JSON.stringify(deployment.stats), deployment.created, deployment.status, deployment.finished, isRelevantDeployment]);
207

208
  useEffect(() => {
36✔
209
    if (!isRelevantDeployment) {
3!
210
      return;
3✔
211
    }
212
    if (!changedConfig && !isEmpty(config) && (!deployment_id || deployment.status)) {
×
213
      // let currentConfig = reported;
214
      const stats = groupDeploymentStats(deployment);
×
215
      if (deployment.status !== DEPLOYMENT_STATES.finished || stats.failures) {
×
216
        setEditableConfig(configured);
×
217
        setChangedConfig(configured);
×
218
      }
219
    }
220
    // eslint-disable-next-line react-hooks/exhaustive-deps
221
  }, [dispatch, JSON.stringify(config), deployment.status, !changedConfig, JSON.stringify(deployment), isRelevantDeployment]);
222

223
  const onConfigImport = ({ config, importType }) => {
36✔
224
    let updatedConfig = config;
×
225
    if (importType === 'default') {
×
226
      updatedConfig = defaultConfig.current;
×
227
    }
228
    setChangedConfig(updatedConfig);
×
229
    setEditableConfig(updatedConfig);
×
230
    setShowConfigImport(false);
×
231
  };
232

233
  const onSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
36✔
234

235
  const onSetAsDefaultChange = () => setIsSetAsDefault(toggle);
36✔
236

237
  const onShowLog = () =>
36✔
238
    dispatch(getDeviceLog(deployment_id, device.id)).then(result => {
×
239
      setShowLog(true);
×
240
      setUpdateLog(result[1]);
×
241
    });
242

243
  const onCancel = () => {
36✔
244
    if (!isEmpty(reported)) {
×
245
      setEditableConfig(reported);
×
246
      setChangedConfig(reported);
×
247
    }
248
    let requests = [];
×
249
    if (deployment_id && deployment.status !== DEPLOYMENT_STATES.finished) {
×
250
      requests.push(dispatch(abortDeployment(deployment_id)));
×
251
    }
252
    if (deepCompare(reported, changedConfig)) {
×
253
      requests.push(Promise.resolve());
×
254
    } else {
255
      requests.push(dispatch(setDeviceConfig(device.id, reported)));
×
256
      if (isSetAsDefault && canManageUsers) {
×
257
        requests.push(dispatch(saveGlobalSettings({ defaultDeviceConfig: { current: defaultConfig.previous } })));
×
258
      }
259
    }
260
    return Promise.all(requests).then(() => {
×
261
      setIsUpdatingConfig(false);
×
262
      setUpdateFailed(false);
×
263
      setIsAborting(false);
×
264
    });
265
  };
266

267
  const onSubmit = () => {
36✔
268
    Tracking.event({ category: 'devices', action: 'apply_configuration' });
3✔
269
    setIsUpdatingConfig(true);
3✔
270
    setUpdateFailed(false);
3✔
271
    return dispatch(setDeviceConfig(device.id, changedConfig))
3✔
272
      .then(() => dispatch(applyDeviceConfig(device.id, { retries: 0 }, isSetAsDefault, changedConfig)))
×
273
      .catch(() => {
274
        setIsEditingConfig(true);
2✔
275
        setUpdateFailed(true);
2✔
276
        setIsUpdatingConfig(false);
2✔
277
      });
278
  };
279

280
  const onStartEdit = e => {
36✔
281
    e.stopPropagation();
1✔
282
    const nextEditableConfig = { ...configured, ...reported };
1✔
283
    setChangedConfig(nextEditableConfig);
1✔
284
    setEditableConfig(nextEditableConfig);
1✔
285
    setIsEditingConfig(true);
1✔
286
  };
287

288
  const onStartImportClick = e => {
36✔
289
    e.stopPropagation();
×
290
    setShowConfigImport(true);
×
291
  };
292

293
  const onAbortClick = () => setIsAborting(toggle);
36✔
294

295
  const hasDeviceConfiguration = !isEmpty(reported);
36✔
296
  let footer = hasDeviceConfiguration ? <ConfigUpToDateNote updated_ts={reported_ts} /> : <ConfigEmptyNote updated_ts={updated_ts} />;
36✔
297
  if (isEditingConfig) {
36✔
298
    footer = (
34✔
299
      <ConfigEditingActions
300
        canSetDefault={canManageUsers}
301
        hasDeviceConfig={hasDeviceConfiguration}
302
        isSetAsDefault={isSetAsDefault}
303
        onSetAsDefaultChange={onSetAsDefaultChange}
304
        onSubmit={onSubmit}
305
        onCancel={onCancel}
306
      />
307
    );
308
  }
309
  if (isUpdatingConfig || updateFailed) {
36✔
310
    const hasLog = deployment.devices && deployment.devices[device.id]?.log;
16!
311
    footer = (
16✔
312
      <>
313
        <div className="flexbox">
314
          {isUpdatingConfig && <Loader show={true} style={{ marginRight: 15, marginTop: -15 }} />}
19✔
315
          {updateFailed && <ErrorIcon className="red" style={iconStyle} />}
29✔
316
          <ConfigUpdateNote isUpdatingConfig={isUpdatingConfig} isAccepted={status === DEVICE_STATES.accepted} />
317
        </div>
318
        {updateFailed ? (
16✔
319
          <ConfigUpdateFailureActions hasLog={hasLog} setShowLog={onShowLog} onSubmit={onSubmit} onCancel={onCancel} />
320
        ) : isAborting ? (
3!
321
          <Confirm cancel={onAbortClick} action={onCancel} type="abort" classes="margin-left-large" />
322
        ) : (
323
          <>
324
            <Button color="secondary" onClick={onAbortClick} startIcon={<BlockIcon fontSize="small" />} style={buttonStyle}>
325
              Abort update
326
            </Button>
327
            <Button
328
              color="secondary"
329
              component={Link}
330
              to={`/deployments/${deployment.status || DEPLOYMENT_ROUTES.active.key}?open=true&id=${deployment_id}`}
6✔
331
              style={buttonStyle}
332
            >
333
              View deployment
334
            </Button>
335
          </>
336
        )}
337
      </>
338
    );
339
  }
340

341
  const helpTipsMap = Object.entries(configHelpTipsMap).reduce((accu, [key, value]) => {
36✔
342
    accu[key] = {
72✔
343
      ...value,
344
      props: { deviceId: device.id }
345
    };
346
    return accu;
72✔
347
  }, {});
348

349
  return (
36✔
350
    <DeviceDataCollapse
351
      isAddOn
352
      title={
353
        <div className="two-columns">
354
          <div className="flexbox center-aligned">
355
            <h4 className="margin-right">Device configuration</h4>
356
            {hasDeviceConfig && !(isEditingConfig || isUpdatingConfig) && <EditButton onClick={onStartEdit} />}
112✔
357
          </div>
358
          <div className="flexbox center-aligned">
359
            {isEditingConfig ? (
36✔
360
              <Button onClick={onStartImportClick} disabled={isUpdatingConfig} startIcon={<SaveAltIcon />} style={{ justifySelf: 'left' }}>
361
                Import configuration
362
              </Button>
363
            ) : null}
364
            <InfoHintContainer>
365
              <EnterpriseNotification id={BENEFITS.deviceConfiguration.id} />
366
              <MenderHelpTooltip id={HELPTOOLTIPS.configureAddOnTip.id} style={{ marginTop: 5 }} />
367
              <DocsTooltip id={DOCSTIPS.deviceConfig.id} />
368
            </InfoHintContainer>
369
          </div>
370
        </div>
371
      }
372
    >
373
      <div className="relative">
374
        {isEditingConfig ? (
36✔
375
          <KeyValueEditor
376
            disabled={isUpdatingConfig}
377
            errortext=""
378
            initialInput={editableConfig}
379
            inputHelpTipsMap={helpTipsMap}
380
            onInputChange={setChangedConfig}
381
          />
382
        ) : (
383
          hasDeviceConfig && <ConfigurationObject config={reported} setSnackbar={onSetSnackbar} />
4✔
384
        )}
385
        {hasDeviceConfig && <div className="flexbox center-aligned margin-bottom margin-top">{footer}</div>}
72✔
386
        {showLog && <LogDialog logData={updateLog} onClose={() => setShowLog(false)} type="configUpdateLog" />}
×
387
        {showConfigImport && <ConfigImportDialog onCancel={() => setShowConfigImport(false)} onSubmit={onConfigImport} />}
×
388
      </div>
389
    </DeviceDataCollapse>
390
  );
391
};
392

393
export default DeviceConfiguration;
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