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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

77.17
/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, { useEffect, useState } from 'react';
15
import { Link } from 'react-router-dom';
16

17
import {
18
  Block as BlockIcon,
19
  CheckCircle as CheckCircleIcon,
20
  Edit as EditIcon,
21
  Error as ErrorIcon,
22
  Refresh as RefreshIcon,
23
  SaveAlt as SaveAltIcon
24
} from '@mui/icons-material';
25
import { Button, Checkbox, FormControlLabel, Typography } from '@mui/material';
26

27
import { DEPLOYMENT_ROUTES } from '../../../constants/deploymentConstants';
28
import { DEVICE_STATES } from '../../../constants/deviceConstants';
29
import { deepCompare, groupDeploymentDevicesStats, groupDeploymentStats, isEmpty, toggle } from '../../../helpers';
30
import Tracking from '../../../tracking';
31
import ConfigurationObject from '../../common/configurationobject';
32
import Confirm from '../../common/confirm';
33
import LogDialog from '../../common/dialogs/log';
34
import KeyValueEditor from '../../common/forms/keyvalueeditor';
35
import Loader from '../../common/loader';
36
import Time from '../../common/time';
37
import { ConfigureAddOnTip, ConfigureRaspberryLedTip, ConfigureTimezoneTip } from '../../helptips/helptooltips';
38
import ConfigImportDialog from './configimportdialog';
39
import DeviceDataCollapse from './devicedatacollapse';
40

41
const buttonStyle = { marginLeft: 30 };
10✔
42
const iconStyle = { margin: 12 };
10✔
43
const textStyle = { textTransform: 'capitalize', textAlign: 'left' };
10✔
44

45
const defaultReportTimeStamp = '0001-01-01T00:00:00Z';
10✔
46

47
const configHelpTipsMap = {
10✔
48
  'mender-demo-raspberrypi-led': {
49
    position: 'right',
50
    component: ConfigureRaspberryLedTip
51
  },
52
  timezone: {
53
    position: 'right',
54
    component: ConfigureTimezoneTip
55
  }
56
};
57

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

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

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

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

117
export const ConfigUpdateFailureActions = ({ hasLog, onSubmit, onCancel, setShowLog }) => (
10✔
118
  <>
4✔
119
    {hasLog && (
4!
120
      <Button color="secondary" onClick={setShowLog} style={buttonStyle}>
121
        View log
122
      </Button>
123
    )}
124
    <Button color="secondary" onClick={onSubmit} startIcon={<RefreshIcon fontSize="small" />} style={buttonStyle}>
125
      Retry
126
    </Button>
127
    <a className="margin-left-large" onClick={onCancel}>
128
      cancel changes
129
    </a>
130
  </>
131
);
132

133
export const DeviceConfiguration = ({
10✔
134
  abortDeployment,
135
  applyDeviceConfig,
136
  defaultConfig = {},
57✔
137
  device,
138
  deployment = {},
33✔
139
  getDeviceLog,
140
  getSingleDeployment,
141
  saveGlobalSettings,
142
  setDeviceConfig,
143
  setSnackbar,
144
  showHelptips
145
}) => {
146
  const { config = {}, status } = device;
57!
147
  const { configured, deployment_id, reported = {}, reported_ts, updated_ts } = config;
57!
148

149
  const [changedConfig, setChangedConfig] = useState();
57✔
150
  const [isEditDisabled, setIsEditDisabled] = useState(false);
57✔
151
  const [isAborting, setIsAborting] = useState(false);
57✔
152
  const [isEditingConfig, setIsEditingConfig] = useState(false);
57✔
153
  const [isSetAsDefault, setIsSetAsDefault] = useState(false);
57✔
154
  const [isUpdatingConfig, setIsUpdatingConfig] = useState(false);
57✔
155
  const [shouldUpdateEditor, setShouldUpdateEditor] = useState(false);
57✔
156
  const [showConfigImport, setShowConfigImport] = useState(false);
57✔
157
  const [showLog, setShowLog] = useState(false);
57✔
158
  const [updateFailed, setUpdateFailed] = useState();
57✔
159
  const [updateLog, setUpdateLog] = useState();
57✔
160

161
  useEffect(() => {
57✔
162
    setShouldUpdateEditor(toggle);
12✔
163
  }, [isEditingConfig, isUpdatingConfig]);
164

165
  useEffect(() => {
57✔
166
    if (device.config || changedConfig) {
7!
167
      setIsEditDisabled(isUpdatingConfig);
7✔
168
      setIsEditingConfig(isUpdatingConfig || updateFailed);
7✔
169
    }
170
  }, [isUpdatingConfig, updateFailed]);
171

172
  useEffect(() => {
57✔
173
    if (deployment.devices && deployment.devices[device.id]?.log) {
3!
174
      setUpdateLog(deployment.devices[device.id].log);
×
175
    }
176
  }, [deployment.devices]);
177

178
  useEffect(() => {
57✔
179
    if (deployment.status === 'finished') {
3✔
180
      // we have to rely on the device stats here as the state change might not have propagated to the deployment status
181
      // leaving all stats at 0 and giving a false impression of deployment success
182
      const stats = groupDeploymentStats(deployment);
1✔
183
      const deviceStats = groupDeploymentDevicesStats(deployment);
1✔
184
      setUpdateFailed(deployment.created > updated_ts && deployment.finished > reported_ts && (stats.failures || deviceStats.failures));
1!
185
      setIsUpdatingConfig(false);
1✔
186
    } else if (deployment.status) {
2!
187
      setChangedConfig(configured);
×
188
      // we can't rely on the deployment.status to be !== 'finished' since `deployment` is initialized as an empty object
189
      // and thus the undefined status would also point to an ongoing update
190
      setIsUpdatingConfig(true);
×
191
    }
192
  }, [deployment.status]);
193

194
  useEffect(() => {
57✔
195
    const { config = {} } = device;
3!
196
    if (!changedConfig && device.config && (!deployment_id || deployment.status)) {
3!
197
      const { configured = {}, reported = {}, reported_ts } = config;
2!
198
      let currentConfig = reported;
2✔
199
      const stats = groupDeploymentStats(deployment);
2✔
200
      if (deployment.status !== 'finished' || (deployment.finished > reported_ts && stats.failures)) {
2!
201
        currentConfig = configured;
2✔
202
      }
203
      setChangedConfig(currentConfig);
2✔
204
    }
205
    if (deployment.status !== 'finished' && deployment_id) {
3!
206
      getSingleDeployment(deployment_id);
×
207
    }
208
  }, [device.config, deployment.status]);
209

210
  const onConfigImport = ({ config, importType }) => {
57✔
211
    let updatedConfig = config;
×
212
    if (importType === 'default') {
×
213
      updatedConfig = defaultConfig.current;
×
214
    }
215
    setShouldUpdateEditor(toggle);
×
216
    setChangedConfig(updatedConfig);
×
217
    setShowConfigImport(false);
×
218
  };
219

220
  const onSetAsDefaultChange = () => setIsSetAsDefault(toggle);
57✔
221

222
  const onShowLog = () => {
57✔
223
    getDeviceLog(deployment_id, device.id).then(result => {
×
224
      setShowLog(true);
×
225
      setUpdateLog(result[1]);
×
226
    });
227
  };
228

229
  const onCancel = () => {
57✔
230
    setIsEditingConfig(isEmpty(reported));
1✔
231
    setChangedConfig(reported);
1✔
232
    let requests = [];
1✔
233
    if (deployment_id && deployment.status !== 'finished') {
1!
234
      requests.push(abortDeployment(deployment_id));
×
235
    }
236
    if (deepCompare(reported, changedConfig)) {
1!
237
      requests.push(Promise.resolve());
×
238
    } else {
239
      requests.push(setDeviceConfig(device.id, reported));
1✔
240
      if (isSetAsDefault) {
1!
241
        requests.push(saveGlobalSettings({ defaultDeviceConfig: { current: defaultConfig.previous } }));
1✔
242
      }
243
    }
244
    return Promise.all(requests).then(() => {
1✔
245
      setIsUpdatingConfig(false);
1✔
246
      setUpdateFailed(false);
1✔
247
      setIsAborting(false);
1✔
248
    });
249
  };
250

251
  const onSubmit = () => {
57✔
252
    Tracking.event({ category: 'devices', action: 'apply_configuration' });
2✔
253
    setIsUpdatingConfig(true);
2✔
254
    setUpdateFailed(false);
2✔
255
    return setDeviceConfig(device.id, changedConfig)
2✔
256
      .then(() => applyDeviceConfig(device.id, { retries: 0 }, isSetAsDefault, changedConfig))
2✔
257
      .then(() => {
258
        setUpdateFailed(false);
1✔
259
      })
260
      .catch(() => {
261
        setIsEditDisabled(false);
1✔
262
        setIsEditingConfig(true);
1✔
263
        setUpdateFailed(true);
1✔
264
        setIsUpdatingConfig(false);
1✔
265
      });
266
  };
267

268
  const onStartEdit = e => {
57✔
269
    e.stopPropagation();
2✔
270
    setChangedConfig(configured || reported);
2!
271
    setIsEditingConfig(true);
2✔
272
  };
273

274
  const onStartImportClick = e => {
57✔
275
    e.stopPropagation();
×
276
    setShowConfigImport(true);
×
277
  };
278

279
  const onAbortClick = () => setIsAborting(toggle);
57✔
280

281
  const hasDeviceConfig = !isEmpty(reported);
57✔
282
  let footer = hasDeviceConfig ? <ConfigUpToDateNote updated_ts={reported_ts} /> : <ConfigEmptyNote updated_ts={device.updated_ts} />;
57✔
283
  if (isEditingConfig) {
57✔
284
    footer = (
47✔
285
      <ConfigEditingActions
286
        hasDeviceConfig={hasDeviceConfig}
287
        isSetAsDefault={isSetAsDefault}
288
        onSetAsDefaultChange={onSetAsDefaultChange}
289
        onSubmit={onSubmit}
290
        onCancel={onCancel}
291
      />
292
    );
293
  }
294
  if (isUpdatingConfig || updateFailed) {
57✔
295
    const hasLog = deployment.devices && deployment.devices[device.id]?.log;
9✔
296
    footer = (
9✔
297
      <>
298
        <div className="flexbox">
299
          {isUpdatingConfig && <Loader show={true} style={{ marginRight: 15, marginTop: -15 }} />}
16✔
300
          {updateFailed && <ErrorIcon className="red" style={iconStyle} />}
12✔
301
          <ConfigUpdateNote isUpdatingConfig={isUpdatingConfig} isAccepted={status === DEVICE_STATES.accepted} />
302
        </div>
303
        {updateFailed ? (
9✔
304
          <ConfigUpdateFailureActions hasLog={hasLog} setShowLog={onShowLog} onSubmit={onSubmit} onCancel={onCancel} />
305
        ) : isAborting ? (
6!
306
          <Confirm cancel={onAbortClick} action={onCancel} type="abort" classes="margin-left-large" />
307
        ) : (
308
          <>
309
            <Button color="secondary" onClick={onAbortClick} startIcon={<BlockIcon fontSize="small" />} style={buttonStyle}>
310
              Abort update
311
            </Button>
312
            <Button
313
              color="secondary"
314
              component={Link}
315
              to={`/deployments/${deployment.status || DEPLOYMENT_ROUTES.active.key}?open=true&id=${deployment_id}`}
11✔
316
              style={buttonStyle}
317
            >
318
              View deployment
319
            </Button>
320
          </>
321
        )}
322
      </>
323
    );
324
  }
325

326
  const helpTipsMap = Object.entries(configHelpTipsMap).reduce((accu, [key, value]) => {
57✔
327
    accu[key] = {
114✔
328
      ...value,
329
      props: { deviceId: device.id }
330
    };
331
    return accu;
114✔
332
  }, {});
333

334
  return (
57✔
335
    <DeviceDataCollapse
336
      isAddOn
337
      title={
338
        <div className="two-columns">
339
          <div className="flexbox center-aligned">
340
            <h4 className="margin-right">Device configuration</h4>
341
            {!(isEditingConfig || isUpdatingConfig) && (
134✔
342
              <Button onClick={onStartEdit} startIcon={<EditIcon />} size="small">
343
                Edit
344
              </Button>
345
            )}
346
          </div>
347
          {isEditingConfig ? (
57✔
348
            <Button onClick={onStartImportClick} disabled={isUpdatingConfig} startIcon={<SaveAltIcon />} style={{ justifySelf: 'left' }}>
349
              Import configuration
350
            </Button>
351
          ) : null}
352
        </div>
353
      }
354
    >
355
      <div className="relative">
356
        {isEditingConfig ? (
57✔
357
          <KeyValueEditor
358
            disabled={isEditDisabled}
359
            errortext=""
360
            input={changedConfig}
361
            inputHelpTipsMap={helpTipsMap}
362
            onInputChange={setChangedConfig}
363
            reset={shouldUpdateEditor}
364
            showHelptips={showHelptips}
365
          />
366
        ) : (
367
          hasDeviceConfig && <ConfigurationObject config={reported} setSnackbar={setSnackbar} />
17✔
368
        )}
369
        {showHelptips && <ConfigureAddOnTip />}
57!
370
        <div className="flexbox center-aligned margin-bottom margin-top">{footer}</div>
371
        {showLog && <LogDialog logData={updateLog} onClose={() => setShowLog(false)} type="configUpdateLog" />}
×
372
        {showConfigImport && <ConfigImportDialog onCancel={() => setShowConfigImport(false)} onSubmit={onConfigImport} setSnackbar={setSnackbar} />}
×
373
      </div>
374
    </DeviceDataCollapse>
375
  );
376
};
377

378
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