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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

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

69.77
/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 { useDispatch } from 'react-redux';
16
import { Link } from 'react-router-dom';
17

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

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

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

34
loader.config({ paths: { vs: '/ui/vs' } });
9✔
35

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

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

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

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

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

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

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

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

140
const stringifyTwin = twin => JSON.stringify(twin, undefined, indentation) ?? '';
9!
141

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

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

160
  useEffect(() => {
3✔
161
    const textContent = stringifyTwin(configuredTwin);
1✔
162
    setConfigured(textContent);
1✔
163
    setUpdated(textContent);
1✔
164
    setReported(stringifyTwin(reportedTwin));
1✔
165
    // eslint-disable-next-line react-hooks/exhaustive-deps
166
  }, []);
167

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

178
  useEffect(() => {
3✔
179
    setIsSync(deepCompare(reported, configured));
2✔
180
  }, [configured, reported]);
181

182
  const handleEditorDidMount = (editor, monaco) => {
3✔
183
    editorRef.current = { editor, monaco, modifiedEditor: editor };
×
184
  };
185

186
  const handleDiffEditorDidMount = (editor, monaco) => {
3✔
187
    const modifiedEditor = editor.getModifiedEditor();
×
188
    modifiedEditor.onDidChangeModelContent(() => setUpdated(modifiedEditor.getValue()));
×
189
    editor.onDidUpdateDiff(onDidUpdateDiff);
×
190
    editorRef.current = { editor, monaco, modifiedEditor };
×
191
  };
192

193
  const onDidUpdateDiff = () => {
3✔
194
    const changes = editorRef.current.editor.getLineChanges();
×
195
    setDiffCount(changes.length);
×
196
    setInitialized(true);
×
197
  };
198

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

213
  const onCancelClick = () => {
3✔
214
    const textContent = stringifyTwin(configuredTwin);
×
215
    setUpdated(textContent);
×
216
    editorRef.current.modifiedEditor.getModel().setValue(textContent);
×
217
    setIsEditing(false);
×
218
  };
219

220
  const onRefreshClick = () => {
3✔
221
    setIsRefreshing(true);
×
222
    dispatch(getDeviceTwin(device.id, integration)).finally(() => setTimeout(() => setIsRefreshing(false), TIMEOUTS.halfASecond));
×
223
  };
224

225
  const onEditClick = () => setIsEditing(true);
3✔
226

227
  const widthStyle = { maxWidth: isSync ? maxWidth : 'initial' };
3!
228

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

308
export default DeviceTwin;
309

310
export const IntegrationTab = ({ device, integrations }) => (
9✔
311
  <div>
×
312
    {integrations.map(integration => (
313
      <DeviceTwin key={integration.id} device={device} integration={integration} />
×
314
    ))}
315
  </div>
316
);
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