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

mendersoftware / gui / 908425489

pending completion
908425489

Pull #3799

gitlab-ci

mzedel
chore: aligned loader usage in devices list with deployment devices list

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3799: MEN-6553

4406 of 6423 branches covered (68.6%)

18 of 19 new or added lines in 3 files covered. (94.74%)

1777 existing lines in 167 files now uncovered.

8329 of 10123 relevant lines covered (82.28%)

144.7 hits per line

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

62.4
/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 { useTheme } from '@mui/material/styles';
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 { TIMEOUTS } from '../../../constants/appConstants';
26
import { EXTERNAL_PROVIDER } from '../../../constants/deviceConstants';
27
import { deepCompare, isEmpty } from '../../../helpers';
28
import InfoHint from '../../common/info-hint';
29
import Loader from '../../common/loader';
30
import Time from '../../common/time';
31
import DeviceDataCollapse from './devicedatacollapse';
32

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

35
const diffStatusStyle = makeStyles()(theme => ({
10✔
36
  root: {
37
    minHeight: 75,
38
    display: 'grid',
39
    gridTemplateColumns: 'min-content 300px max-content',
40
    gridColumnGap: theme.spacing(2),
41
    alignItems: 'center',
42
    background: theme.palette.grey[100],
43
    width: 'min-content'
44
  }
45
}));
46

47
const LastSyncNote = ({ updateTime }) => (
10✔
48
  <div className="muted slightly-smaller" style={{ alignContent: 'flex-end', marginBottom: -10 }}>
2✔
49
    Last synced: <Time value={updateTime} />
50
  </div>
51
);
52

53
const NoDiffStatus = ({ updateTime }) => {
10✔
54
  const { classes } = diffStatusStyle();
1✔
55
  return (
1✔
56
    <div className={['padding', classes.root]}>
57
      <CheckCircleOutlined className="green" />
58
      <div>No difference between desired and reported configuration</div>
59
      <LastSyncNote updateTime={updateTime} />
60
    </div>
61
  );
62
};
63

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

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

98
export const Title = ({ providerTitle, twinTitle }) => (
10✔
99
  <div className="flexbox center-aligned">
3✔
100
    <h4 className="margin-right">
101
      {providerTitle} {twinTitle}
102
    </h4>
103
    <Link to="/settings/integrations">Integration settings</Link>
104
  </div>
105
);
106

107
const editorProps = {
10✔
108
  height: 500,
109
  defaultLanguage: 'json',
110
  language: 'json',
111
  loading: <Loader show />,
112
  options: {
113
    autoClosingOvertype: 'auto',
114
    codeLens: false,
115
    contextmenu: false,
116
    enableSplitViewResizing: false,
117
    formatOnPaste: true,
118
    lightbulb: { enabled: false },
119
    minimap: { enabled: false },
120
    lineNumbersMinChars: 3,
121
    quickSuggestions: false,
122
    renderOverviewRuler: false,
123
    scrollBeyondLastLine: false,
124
    readOnly: true
125
  }
126
};
127
const maxWidth = 800;
10✔
128

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

131
const stringifyTwin = twin => JSON.stringify(twin, undefined, indentation) ?? '';
10!
132

133
export const DeviceTwin = ({ device, getDeviceTwin, integration, setDeviceTwin }) => {
10✔
134
  const theme = useTheme();
3✔
135
  const [configured, setConfigured] = useState('');
3✔
136
  const [diffCount, setDiffCount] = useState(0);
3✔
137
  const [isEditing, setIsEditing] = useState(false);
3✔
138
  const [isRefreshing, setIsRefreshing] = useState(false);
3✔
139
  const [errorMessage, setErrorMessage] = useState('');
3✔
140
  const [initialized, setInitialized] = useState(false);
3✔
141
  const [reported, setReported] = useState('');
3✔
142
  const [updated, setUpdated] = useState('');
3✔
143
  const [isSync, setIsSync] = useState(true);
3✔
144
  const editorRef = useRef(null);
3✔
145

146
  const externalProvider = EXTERNAL_PROVIDER[integration.provider];
3✔
147
  const { [integration.id]: deviceTwin = {} } = device.twinsByIntegration ?? {};
3!
148
  const { desired: configuredTwin = {}, reported: reportedTwin = {}, twinError, updated_ts: updateTime = device.created_ts } = deviceTwin;
3✔
149

150
  useEffect(() => {
3✔
151
    const textContent = stringifyTwin(configuredTwin);
1✔
152
    setConfigured(textContent);
1✔
153
    setUpdated(textContent);
1✔
154
    setReported(stringifyTwin(reportedTwin));
1✔
155
  }, [open]);
156

157
  useEffect(() => {
3✔
158
    setReported(stringifyTwin(reportedTwin));
2✔
159
    if (isEditing) {
2!
UNCOV
160
      return;
×
161
    }
162
    const textContent = stringifyTwin(configuredTwin);
2✔
163
    setConfigured(textContent);
2✔
164
    setUpdated(textContent);
2✔
165
  }, [configuredTwin, reportedTwin, isEditing]);
166

167
  useEffect(() => {
3✔
168
    setIsSync(deepCompare(reported, configured));
2✔
169
  }, [configured, reported]);
170

171
  const handleEditorDidMount = (editor, monaco) => {
3✔
UNCOV
172
    editorRef.current = { editor, monaco, modifiedEditor: editor };
×
173
  };
174

175
  const handleDiffEditorDidMount = (editor, monaco) => {
3✔
UNCOV
176
    const modifiedEditor = editor.getModifiedEditor();
×
UNCOV
177
    modifiedEditor.onDidChangeModelContent(() => setUpdated(modifiedEditor.getValue()));
×
UNCOV
178
    editor.onDidUpdateDiff(onDidUpdateDiff);
×
UNCOV
179
    editorRef.current = { editor, monaco, modifiedEditor };
×
180
  };
181

182
  const onDidUpdateDiff = () => {
3✔
UNCOV
183
    const changes = editorRef.current.editor.getLineChanges();
×
UNCOV
184
    setDiffCount(changes.length);
×
UNCOV
185
    setInitialized(true);
×
186
  };
187

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

202
  const onCancelClick = () => {
3✔
UNCOV
203
    const textContent = stringifyTwin(configuredTwin);
×
UNCOV
204
    setUpdated(textContent);
×
UNCOV
205
    editorRef.current.modifiedEditor.getModel().setValue(textContent);
×
UNCOV
206
    setIsEditing(false);
×
207
  };
208

209
  const onRefreshClick = () => {
3✔
UNCOV
210
    setIsRefreshing(true);
×
UNCOV
211
    getDeviceTwin(device.id, integration).finally(() => setTimeout(() => setIsRefreshing(false), TIMEOUTS.halfASecond));
×
212
  };
213

214
  const onEditClick = () => setIsEditing(true);
3✔
215

216
  const widthStyle = { maxWidth: isSync ? maxWidth : 'initial' };
3!
217

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

297
export default DeviceTwin;
298

299
export const IntegrationTab = ({ device, integrations, getDeviceTwin, setDeviceTwin }) => (
10✔
UNCOV
300
  <div>
×
301
    {integrations.map(integration => (
UNCOV
302
      <DeviceTwin key={integration.id} device={device} integration={integration} getDeviceTwin={getDeviceTwin} setDeviceTwin={setDeviceTwin} />
×
303
    ))}
304
  </div>
305
);
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