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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

56.11
/src/js/components/devices/dialogs/troubleshootdialog.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, { useCallback, useEffect, useRef, useState } from 'react';
15
import Dropzone from 'react-dropzone';
16
import { connect } from 'react-redux';
17
import { Link } from 'react-router-dom';
18

19
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Tab, Tabs } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import { mdiConsole as ConsoleIcon } from '@mdi/js';
23
import moment from 'moment';
24
import momentDurationFormatSetup from 'moment-duration-format';
25

26
import { setSnackbar } from '../../../actions/appActions';
27
import { deviceFileUpload, getDeviceFileDownloadLink } from '../../../actions/deviceActions';
28
import { BEGINNING_OF_TIME, TIMEOUTS } from '../../../constants/appConstants';
29
import { createDownload, versionCompare } from '../../../helpers';
30
import { getFeatures, getIdAttribute, getIsEnterprise, getUserRoles } from '../../../selectors';
31
import { useSession } from '../../../utils/sockethook';
32
import { TwoColumns } from '../../common/configurationobject';
33
import MaterialDesignIcon from '../../common/materialdesignicon';
34
import { MaybeTime } from '../../common/time';
35
import FileTransfer from '../troubleshoot/filetransfer';
36
import Terminal from '../troubleshoot/terminal';
37
import ListOptions from '../widgets/listoptions';
38
import DeviceIdentityDisplay from './../../common/deviceidentity';
39
import { getCode } from './make-gateway-dialog';
40

41
momentDurationFormatSetup(moment);
12✔
42

43
const useStyles = makeStyles()(theme => ({
12✔
44
  content: { padding: 0, margin: '0 24px', height: '75vh' },
45
  title: { marginRight: theme.spacing(0.5) },
46
  connectionButton: { background: theme.palette.text.primary },
47
  connectedIcon: { color: theme.palette.success.main, marginLeft: theme.spacing() },
48
  disconnectedIcon: { color: theme.palette.error.main, marginLeft: theme.spacing() },
49
  sessionInfo: { maxWidth: 'max-content' },
50
  terminalContent: {
51
    display: 'grid',
52
    gridTemplateRows: 'max-content 0',
53
    flexGrow: 1,
54
    overflow: 'hidden',
55
    '&.device-connected': {
56
      gridTemplateRows: 'max-content minmax(min-content, 1fr)'
57
    }
58
  },
59
  terminalStatePlaceholder: {
60
    width: 280
61
  }
62
}));
63

64
const ConnectionIndicator = ({ isConnected }) => {
12✔
65
  const { classes } = useStyles();
1✔
66
  return (
1✔
67
    <div className="flexbox center-aligned">
68
      Remote terminal {<MaterialDesignIcon className={isConnected ? classes.connectedIcon : classes.disconnectedIcon} path={ConsoleIcon} />}
1!
69
    </div>
70
  );
71
};
72

73
const tabs = {
12✔
74
  terminal: {
75
    link: 'session logs',
76
    title: ConnectionIndicator,
77
    value: 'terminal',
78
    canShow: ({ canTroubleshoot, canWriteDevices }) => canTroubleshoot && canWriteDevices
1✔
79
  },
80
  transfer: { link: 'file transfer logs', title: () => 'File transfer', value: 'transfer', canShow: ({ canTroubleshoot }) => canTroubleshoot }
1✔
81
};
82

83
export const TroubleshootDialog = ({
12✔
84
  canPreview,
85
  device,
86
  deviceFileUpload,
87
  getDeviceFileDownloadLink,
88
  hasAuditlogs,
89
  isEnterprise,
90
  isHosted,
91
  idAttribute,
92
  onCancel,
93
  open,
94
  setSnackbar,
95
  setSocketClosed,
96
  type = tabs.terminal.value,
2✔
97
  userCapabilities
98
}) => {
99
  const { canAuditlog, canTroubleshoot, canWriteDevices } = userCapabilities;
2✔
100

101
  const [currentTab, setCurrentTab] = useState(type);
2✔
102
  const [availableTabs, setAvailableTabs] = useState(Object.values(tabs));
2✔
103
  const [downloadPath, setDownloadPath] = useState('');
2✔
104
  const [elapsed, setElapsed] = useState(moment());
2✔
105
  const [file, setFile] = useState();
2✔
106
  const [socketInitialized, setSocketInitialized] = useState(false);
2✔
107
  const [startTime, setStartTime] = useState();
2✔
108
  const [uploadPath, setUploadPath] = useState('');
2✔
109
  const [terminalInput, setTerminalInput] = useState('');
2✔
110
  const [snackbarAlreadySet, setSnackbarAlreadySet] = useState(false);
2✔
111
  const closeTimer = useRef();
2✔
112
  const snackTimer = useRef();
2✔
113
  const timer = useRef();
2✔
114
  const termRef = useRef({ terminal: React.createRef(), terminalRef: React.createRef() });
2✔
115
  const { classes } = useStyles();
2✔
116

117
  useEffect(() => {
2✔
118
    if (open) {
1!
119
      setCurrentTab(type);
1✔
120
      return;
1✔
121
    }
122
    setDownloadPath('');
×
123
    setUploadPath('');
×
124
    setFile();
×
125
    return () => {
×
126
      clearTimeout(closeTimer.current);
×
127
      clearTimeout(snackTimer.current);
×
128
    };
129
  }, [open]);
130

131
  useEffect(() => {
2✔
132
    const allowedTabs = Object.values(tabs).reduce((accu, tab) => {
1✔
133
      if (tab.canShow({ canTroubleshoot, canWriteDevices })) {
2!
134
        accu.push(tab);
2✔
135
      }
136
      return accu;
2✔
137
    }, []);
138
    setAvailableTabs(allowedTabs);
1✔
139
  }, [canTroubleshoot, canWriteDevices]);
140

141
  useEffect(() => {
2✔
142
    if (socketInitialized === undefined) {
2✔
143
      return;
1✔
144
    }
145
    clearInterval(timer.current);
1✔
146
    if (socketInitialized) {
1!
147
      setStartTime(new Date());
×
148
      timer.current = setInterval(() => setElapsed(moment()), TIMEOUTS.halfASecond);
×
149
    } else {
150
      close();
1✔
151
    }
152
    return () => {
1✔
153
      clearInterval(timer.current);
1✔
154
    };
155
  }, [socketInitialized]);
156

157
  useEffect(() => {
2✔
158
    if (!(open || socketInitialized) || socketInitialized) {
1!
159
      return;
×
160
    }
161
    canTroubleshoot ? connect(device.id) : undefined;
1!
162
    return () => {
1✔
163
      close();
1✔
164
      setTimeout(() => setSocketClosed(true), TIMEOUTS.fiveSeconds);
1✔
165
    };
166
  }, [device.id, open]);
167

168
  const onConnectionToggle = () => {
2✔
169
    if (socketInitialized) {
×
170
      close();
×
171
    } else {
172
      setSocketInitialized(false);
×
173
      connect(device.id);
×
174
    }
175
  };
176

177
  const onDrop = acceptedFiles => {
2✔
178
    if (acceptedFiles.length === 1) {
×
179
      setFile(acceptedFiles[0]);
×
180
      setUploadPath(`/tmp/${acceptedFiles[0].name}`);
×
181
      setCurrentTab(tabs.transfer.value);
×
182
    }
183
  };
184

185
  const onDownloadClick = path => {
2✔
186
    setDownloadPath(path);
×
187
    getDeviceFileDownloadLink(device.id, path).then(address => {
×
188
      const filename = path.substring(path.lastIndexOf('/') + 1) || 'file';
×
189
      createDownload(address, filename);
×
190
    });
191
  };
192

193
  const onSocketOpen = () => {
2✔
194
    setSnackbar('Connection with the device established.', 5000);
×
195
    setSocketInitialized(true);
×
196
  };
197

198
  const onNotify = content => {
2✔
199
    setSnackbarAlreadySet(true);
×
200
    setSnackbar(content, 5000);
×
201
    snackTimer.current = setTimeout(() => setSnackbarAlreadySet(false), TIMEOUTS.fiveSeconds + TIMEOUTS.debounceShort);
×
202
  };
203

204
  const onHealthCheckFailed = () => {
2✔
205
    if (snackbarAlreadySet) {
×
206
      return;
×
207
    }
208
    onNotify('Health check failed: connection with the device lost.');
×
209
  };
210

211
  const onSocketClose = event => {
2✔
212
    if (snackbarAlreadySet) {
×
213
      return;
×
214
    }
215
    if (event.wasClean) {
×
216
      onNotify(`Connection with the device closed.`);
×
217
    } else if (event.code == 1006) {
×
218
      // 1006: abnormal closure
219
      onNotify('Connection to the remote terminal is forbidden.');
×
220
    } else {
221
      onNotify('Connection with the device died.');
×
222
    }
223
    closeTimer.current = setTimeout(() => setSocketClosed(true), TIMEOUTS.fiveSeconds);
×
224
  };
225

226
  const onMessageReceived = useCallback(
2✔
227
    message => {
228
      if (!termRef.current.terminal) {
×
229
        return;
×
230
      }
231
      termRef.current.terminal.write(new Uint8Array(message));
×
232
    },
233
    [termRef.current]
234
  );
235

236
  const [connect, sendMessage, close, sessionState, sessionId] = useSession({
2✔
237
    onClose: onSocketClose,
238
    onHealthCheckFailed,
239
    onMessageReceived,
240
    onNotify,
241
    onOpen: onSocketOpen
242
  });
243

244
  useEffect(() => {
2✔
245
    setSocketInitialized(sessionState === WebSocket.OPEN && sessionId);
1✔
246
  }, [sessionId, sessionState]);
247

248
  const onMakeGatewayClick = () => {
2✔
249
    const code = getCode(canPreview);
×
250
    setTerminalInput(code);
×
251
  };
252

253
  const commandHandlers = isHosted && isEnterprise ? [{ key: 'thing', onClick: onMakeGatewayClick, title: 'Promote to Mender gateway' }] : [];
2!
254

255
  const duration = moment.duration(elapsed.diff(moment(startTime)));
2✔
256
  const visibilityToggle = !socketInitialized ? { maxHeight: 0, overflow: 'hidden' } : {};
2!
257
  return (
2✔
258
    <Dialog open={open} fullWidth={true} maxWidth="lg">
259
      <DialogTitle className="flexbox">
260
        <div className={classes.title}>Troubleshoot -</div>
261
        <DeviceIdentityDisplay device={device} idAttribute={idAttribute} isEditable={false} />
262
      </DialogTitle>
263
      <DialogContent className={`dialog-content flexbox column ${classes.content}`}>
264
        <Tabs value={currentTab} onChange={(e, tab) => setCurrentTab(tab)} textColor="primary" TabIndicatorProps={{ className: 'hidden' }}>
×
265
          {availableTabs.map(({ title: Title, value }) => (
266
            <Tab key={value} label={<Title isConnected={socketInitialized} />} value={value} />
4✔
267
          ))}
268
        </Tabs>
269
        {currentTab === tabs.transfer.value && (
2!
270
          <FileTransfer
271
            deviceId={device.id}
272
            downloadPath={downloadPath}
273
            file={file}
274
            onDownload={onDownloadClick}
275
            onUpload={deviceFileUpload}
276
            setDownloadPath={setDownloadPath}
277
            setFile={setFile}
278
            setSnackbar={setSnackbar}
279
            setUploadPath={setUploadPath}
280
            uploadPath={uploadPath}
281
          />
282
        )}
283
        <div className={`${classes.terminalContent} ${socketInitialized ? 'device-connected' : ''} ${currentTab === tabs.terminal.value ? '' : 'hidden'}`}>
4!
284
          <TwoColumns
285
            className={`margin-top-small margin-bottom-small ${classes.sessionInfo}`}
286
            items={{
287
              'Session status': socketInitialized ? 'connected' : 'disconnected',
2!
288
              'Connection start': <MaybeTime value={startTime} />,
289
              'Duration': `${duration.format('hh:mm:ss', { trim: false })}`
290
            }}
291
          />
292
          <Dropzone activeClassName="active" rejectClassName="active" multiple={false} onDrop={onDrop} noClick>
293
            {({ getRootProps }) => (
294
              <div {...getRootProps()} style={{ position: 'relative', ...visibilityToggle }}>
1✔
295
                <Terminal
296
                  onDownloadClick={onDownloadClick}
297
                  sendMessage={sendMessage}
298
                  sessionId={sessionId}
299
                  setSnackbar={setSnackbar}
300
                  socketInitialized={socketInitialized}
301
                  style={{ position: 'absolute', width: '100%', height: '100%', ...visibilityToggle }}
302
                  textInput={terminalInput}
303
                  xtermRef={termRef}
304
                />
305
              </div>
306
            )}
307
          </Dropzone>
308
          {!socketInitialized && (
4✔
309
            <div className={`flexbox centered ${classes.connectionButton}`}>
310
              <Button variant="contained" color="secondary" onClick={onConnectionToggle}>
311
                Connect Terminal
312
              </Button>
313
            </div>
314
          )}
315
        </div>
316
      </DialogContent>
317
      <DialogActions className="flexbox space-between">
318
        <div>
319
          {currentTab === tabs.terminal.value ? (
2!
320
            <Button onClick={onConnectionToggle}>{socketInitialized ? 'Disconnect' : 'Connect'} Terminal</Button>
2!
321
          ) : (
322
            <div className={classes.terminalStatePlaceholder} />
323
          )}
324
          {canAuditlog && hasAuditlogs && (
2!
325
            <Button component={Link} to={`auditlog?object_id=${device.id}&start_date=${BEGINNING_OF_TIME}`}>
326
              View {tabs[currentTab].link} for this device
327
            </Button>
328
          )}
329
        </div>
330
        <div>
331
          {currentTab === tabs.terminal.value && socketInitialized && !!commandHandlers.length && (
4!
332
            <ListOptions options={commandHandlers} title="Quick commands" />
333
          )}
334
          <Button onClick={onCancel}>Close</Button>
335
        </div>
336
      </DialogActions>
337
    </Dialog>
338
  );
339
};
340

341
const actionCreators = { getDeviceFileDownloadLink, deviceFileUpload, setSnackbar };
12✔
342

343
const mapStateToProps = state => {
12✔
344
  const { isHosted } = getFeatures(state);
1✔
345
  return {
1✔
346
    canPreview: versionCompare(state.app.versionInformation.Integration, 'next') > -1,
347
    idAttribute: getIdAttribute(state),
348
    isEnterprise: getIsEnterprise(state),
349
    isHosted,
350
    userRoles: getUserRoles(state)
351
  };
352
};
353

354
export default connect(mapStateToProps, actionCreators)(TroubleshootDialog);
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