• 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

62.7
/src/js/components/devices/device-details/devicetwin.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, useRef, useState } from 'react';
15
import { Link } from 'react-router-dom';
16

17
import { CheckCircleOutlined, CloudUploadOutlined as CloudUpload, Refresh as RefreshIcon } from '@mui/icons-material';
18
import { Button } from '@mui/material';
19
import { makeStyles } from 'tss-react/mui';
20

21
import Editor, { DiffEditor, loader } from '@monaco-editor/react';
22
import pluralize from 'pluralize';
23

24
import { TIMEOUTS } from '../../../constants/appConstants';
25
import { EXTERNAL_PROVIDER } from '../../../constants/deviceConstants';
26
import { deepCompare, isEmpty } from '../../../helpers';
27
import InfoHint from '../../common/info-hint';
28
import Loader from '../../common/loader';
29
import Time from '../../common/time';
30
import DeviceDataCollapse from './devicedatacollapse';
31

32
loader.config({ paths: { vs: '/ui/vs' } });
11✔
33

34
const useStyles = makeStyles()(theme => ({
11✔
35
  buttonSpacer: { marginLeft: theme.spacing(2) },
36
  title: { alignItems: 'baseline' },
37
  titleContainer: { width: '100%' },
38
  diffStatus: {
39
    minHeight: 75,
40
    display: 'grid',
41
    gridTemplateColumns: 'min-content 300px max-content',
42
    gridColumnGap: theme.spacing(2),
43
    alignItems: 'center',
44
    background: theme.palette.grey[100],
45
    width: 'min-content'
46
  }
47
}));
48

49
export const LastSyncNote = ({ updateTime }) => (
11✔
50
  <div className="muted slightly-smaller" style={{ marginTop: 2 }}>
5✔
51
    Last synced: <Time value={updateTime} />
52
  </div>
53
);
54

55
const NoDiffStatus = () => {
11✔
56
  const { classes } = useStyles();
1✔
57
  return (
1✔
58
    <div className={['padding', classes.diffStatus]}>
59
      <CheckCircleOutlined className="green" />
60
      <div>No difference between desired and reported configuration</div>
61
    </div>
62
  );
63
};
64

65
export const TwinError = ({ providerTitle, twinError }) => (
11✔
66
  <InfoHint
2✔
67
    content={
68
      <>
69
        {twinError}
70
        <br />
71
        Please check your connection string in the <Link to="/settings/integrations">Integration settings</Link>, and check that the device exists in your{' '}
72
        {providerTitle}
73
      </>
74
    }
75
  />
76
);
77

78
export const TwinSyncStatus = ({ diffCount, providerTitle, twinError, updateTime }) => {
11✔
79
  const classes = useStyles();
3✔
80
  if (twinError) {
3✔
81
    return <TwinError providerTitle={providerTitle} twinError={twinError} />;
1✔
82
  }
83
  return !diffCount ? (
2✔
84
    <NoDiffStatus />
85
  ) : (
86
    <div className={['padding', classes.diffStatus]}>
87
      <CloudUpload />
88
      <div>
89
        <b>
90
          Found {diffCount} {pluralize('difference', diffCount)}
91
        </b>{' '}
92
        between desired and reported configuration
93
      </div>
94
      <LastSyncNote updateTime={updateTime} />
95
    </div>
96
  );
97
};
98

99
export const Title = ({ providerTitle, twinTitle, updateTime }) => {
11✔
100
  const { classes } = useStyles();
3✔
101
  return (
3✔
102
    <div className={`flexbox center-aligned space-between ${classes.titleContainer}`}>
103
      <div className={`flexbox ${classes.title}`}>
104
        <h4 className="margin-right">
105
          {providerTitle} {twinTitle}
106
        </h4>
107
        <LastSyncNote updateTime={updateTime} />
108
      </div>
109
      <Link to="/settings/integrations">Integration settings</Link>
110
    </div>
111
  );
112
};
113

114
const editorProps = {
11✔
115
  height: 500,
116
  defaultLanguage: 'json',
117
  language: 'json',
118
  loading: <Loader show />,
119
  options: {
120
    autoClosingOvertype: 'auto',
121
    codeLens: false,
122
    contextmenu: false,
123
    enableSplitViewResizing: false,
124
    formatOnPaste: true,
125
    lightbulb: { enabled: false },
126
    minimap: { enabled: false },
127
    lineNumbersMinChars: 3,
128
    quickSuggestions: false,
129
    renderOverviewRuler: false,
130
    scrollBeyondLastLine: false,
131
    readOnly: true
132
  }
133
};
134
const maxWidth = 800;
11✔
135

136
const indentation = 4; // number of spaces, tab based indentation won't show in the editor, but be converted to 4 spaces
11✔
137

138
const stringifyTwin = twin => JSON.stringify(twin, undefined, indentation) ?? '';
11!
139

140
export const DeviceTwin = ({ device, getDeviceTwin, integration, setDeviceTwin }) => {
11✔
141
  const [configured, setConfigured] = useState('');
3✔
142
  const [diffCount, setDiffCount] = useState(0);
3✔
143
  const [isEditing, setIsEditing] = useState(false);
3✔
144
  const [isRefreshing, setIsRefreshing] = useState(false);
3✔
145
  const [errorMessage, setErrorMessage] = useState('');
3✔
146
  const [initialized, setInitialized] = useState(false);
3✔
147
  const [reported, setReported] = useState('');
3✔
148
  const [updated, setUpdated] = useState('');
3✔
149
  const [isSync, setIsSync] = useState(true);
3✔
150
  const editorRef = useRef(null);
3✔
151
  const { classes } = useStyles();
3✔
152

153
  const externalProvider = EXTERNAL_PROVIDER[integration.provider];
3✔
154
  const { [integration.id]: deviceTwin = {} } = device.twinsByIntegration ?? {};
3!
155
  const { desired: configuredTwin = {}, reported: reportedTwin = {}, twinError, updated_ts: updateTime = device.created_ts } = deviceTwin;
3✔
156

157
  useEffect(() => {
3✔
158
    const textContent = stringifyTwin(configuredTwin);
1✔
159
    setConfigured(textContent);
1✔
160
    setUpdated(textContent);
1✔
161
    setReported(stringifyTwin(reportedTwin));
1✔
162
  }, [open]);
163

164
  useEffect(() => {
3✔
165
    setReported(stringifyTwin(reportedTwin));
2✔
166
    if (isEditing) {
2!
167
      return;
×
168
    }
169
    const textContent = stringifyTwin(configuredTwin);
2✔
170
    setConfigured(textContent);
2✔
171
    setUpdated(textContent);
2✔
172
  }, [configuredTwin, reportedTwin, isEditing]);
173

174
  useEffect(() => {
3✔
175
    setIsSync(deepCompare(reported, configured));
2✔
176
  }, [configured, reported]);
177

178
  const handleEditorDidMount = (editor, monaco) => {
3✔
179
    editorRef.current = { editor, monaco, modifiedEditor: editor };
×
180
  };
181

182
  const handleDiffEditorDidMount = (editor, monaco) => {
3✔
183
    const modifiedEditor = editor.getModifiedEditor();
×
184
    modifiedEditor.onDidChangeModelContent(() => setUpdated(modifiedEditor.getValue()));
×
185
    editor.onDidUpdateDiff(onDidUpdateDiff);
×
186
    editorRef.current = { editor, monaco, modifiedEditor };
×
187
  };
188

189
  const onDidUpdateDiff = () => {
3✔
190
    const changes = editorRef.current.editor.getLineChanges();
×
191
    setDiffCount(changes.length);
×
192
    setInitialized(true);
×
193
  };
194

195
  const onApplyClick = () => {
3✔
196
    let update = {};
×
197
    try {
×
198
      update = JSON.parse(updated);
×
199
    } catch (error) {
200
      setErrorMessage('There was an error parsing the device twin changes, please ensure that it is valid JSON.');
×
201
      return;
×
202
    }
203
    editorRef.current.modifiedEditor.getAction('editor.action.formatDocument').run();
×
204
    setUpdated(stringifyTwin(update));
×
205
    setErrorMessage('');
×
206
    setDeviceTwin(device.id, integration, update).then(() => setIsEditing(false));
×
207
  };
208

209
  const onCancelClick = () => {
3✔
210
    const textContent = stringifyTwin(configuredTwin);
×
211
    setUpdated(textContent);
×
212
    editorRef.current.modifiedEditor.getModel().setValue(textContent);
×
213
    setIsEditing(false);
×
214
  };
215

216
  const onRefreshClick = () => {
3✔
217
    setIsRefreshing(true);
×
218
    getDeviceTwin(device.id, integration).finally(() => setTimeout(() => setIsRefreshing(false), TIMEOUTS.halfASecond));
×
219
  };
220

221
  const onEditClick = () => setIsEditing(true);
3✔
222

223
  const widthStyle = { maxWidth: isSync ? maxWidth : 'initial' };
3!
224

225
  return (
3✔
226
    <DeviceDataCollapse
227
      header={
228
        <div className="flexbox column">
229
          {initialized ? (
3!
230
            <TwinSyncStatus diffCount={diffCount} providerTitle={externalProvider.title} twinError={twinError} updateTime={updateTime} />
231
          ) : (
232
            <Loader show={!initialized} />
233
          )}
234
        </div>
235
      }
236
      title={<Title providerTitle={externalProvider.title} twinTitle={externalProvider.twinTitle} updateTime={updateTime} />}
237
    >
238
      <div className={`flexbox column ${isEditing ? 'twin-editing' : ''}`}>
3!
239
        <div style={widthStyle}>
240
          {!initialized || (!(isEmpty(reported) && isEmpty(configured)) && !isSync) ? (
6!
241
            <>
242
              <div className="two-columns">
243
                <h4>Desired configuration</h4>
244
                <h4>Reported configuration</h4>
245
              </div>
246
              <DiffEditor
247
                {...editorProps}
248
                original={reported}
249
                modified={configured}
250
                onMount={handleDiffEditorDidMount}
251
                options={{
252
                  ...editorProps.options,
253
                  readOnly: !isEditing
254
                }}
255
              />
256
            </>
257
          ) : (
258
            <>
259
              <h4>{!deviceTwin.reported || isEditing ? 'Desired' : 'Reported'} configuration</h4>
×
260
              <Editor
261
                {...editorProps}
262
                options={{
263
                  ...editorProps.options,
264
                  readOnly: !isEditing
265
                }}
266
                className="editor modified"
267
                onMount={handleEditorDidMount}
268
                value={reported || configured}
×
269
                onChange={setUpdated}
270
              />
271
            </>
272
          )}
273
          {!!errorMessage && <p className="warning">{errorMessage}</p>}
3!
274
        </div>
275
        <div className="two-columns margin-top" style={isSync ? { gridTemplateColumns: `${maxWidth}px 1fr` } : widthStyle}>
3!
276
          <div className="flexbox" style={{ alignItems: 'flex-start', justifyContent: 'flex-end' }}>
277
            {isEditing ? (
3!
278
              <>
279
                <Button onClick={onCancelClick}>Cancel</Button>
280
                <Button className={classes.buttonSpacer} color="secondary" onClick={onApplyClick} variant="contained">
281
                  Save
282
                </Button>
283
              </>
284
            ) : (
285
              <Button color="secondary" onClick={onEditClick} variant="contained">
286
                Edit desired configuration
287
              </Button>
288
            )}
289
          </div>
290
          <div className="flexbox" style={{ justifyContent: 'flex-end' }}>
291
            <Loader show={isRefreshing} small table />
292
            {!isEditing && (
6✔
293
              <Button onClick={onRefreshClick} startIcon={<RefreshIcon />}>
294
                Refresh
295
              </Button>
296
            )}
297
          </div>
298
        </div>
299
      </div>
300
    </DeviceDataCollapse>
301
  );
302
};
303

304
export default DeviceTwin;
305

306
export const IntegrationTab = ({ device, integrations, getDeviceTwin, setDeviceTwin }) => (
11✔
307
  <div>
×
308
    {integrations.map(integration => (
309
      <DeviceTwin key={integration.id} device={device} integration={integration} getDeviceTwin={getDeviceTwin} setDeviceTwin={setDeviceTwin} />
×
310
    ))}
311
  </div>
312
);
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