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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 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