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

mendersoftware / gui / 947088195

pending completion
947088195

Pull #2661

gitlab-ci

mzedel
chore: improved device filter scrolling behaviour

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #2661: chore: added lint rules for hooks usage

4411 of 6415 branches covered (68.76%)

297 of 440 new or added lines in 62 files covered. (67.5%)

1617 existing lines in 163 files now uncovered.

8311 of 10087 relevant lines covered (82.39%)

192.12 hits per line

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

49.74
/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 { useDispatch, useSelector } 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 { DEVICE_MESSAGE_TYPES as MessageTypes } from '../../../constants/deviceConstants';
30
import { createDownload } from '../../../helpers';
31
import { getFeatures, getIsEnterprise, getIsPreview, getTenantCapabilities, getUserCapabilities } from '../../../selectors';
32
import { useSession } from '../../../utils/sockethook';
33
import { TwoColumns } from '../../common/configurationobject';
34
import MaterialDesignIcon from '../../common/materialdesignicon';
35
import { MaybeTime } from '../../common/time';
36
import FileTransfer from '../troubleshoot/filetransfer';
37
import Terminal from '../troubleshoot/terminal';
38
import ListOptions from '../widgets/listoptions';
39
import DeviceIdentityDisplay from './../../common/deviceidentity';
40
import { getCode } from './make-gateway-dialog';
41

42
momentDurationFormatSetup(moment);
12✔
43

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

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

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

84
export const TroubleshootDialog = ({ device, onCancel, open, setSocketClosed, type = tabs.terminal.value }) => {
12✔
85
  const [currentTab, setCurrentTab] = useState(type);
2✔
86
  const [availableTabs, setAvailableTabs] = useState(Object.values(tabs));
2✔
87
  const [downloadPath, setDownloadPath] = useState('');
2✔
88
  const [elapsed, setElapsed] = useState(moment());
2✔
89
  const [file, setFile] = useState();
2✔
90
  const [socketInitialized, setSocketInitialized] = useState(false);
2✔
91
  const [startTime, setStartTime] = useState();
2✔
92
  const [uploadPath, setUploadPath] = useState('');
2✔
93
  const [terminalInput, setTerminalInput] = useState('');
2✔
94
  const [snackbarAlreadySet, setSnackbarAlreadySet] = useState(false);
2✔
95
  const snackTimer = useRef();
2✔
96
  const timer = useRef();
2✔
97
  const termRef = useRef({ terminal: React.createRef(), terminalRef: React.createRef() });
2✔
98
  const { classes } = useStyles();
2✔
99
  const { isHosted } = useSelector(getFeatures);
2✔
100
  const isEnterprise = useSelector(getIsEnterprise);
2✔
101
  const canPreview = useSelector(getIsPreview);
2✔
102
  const userCapabilities = useSelector(getUserCapabilities);
2✔
103
  const { canAuditlog, canTroubleshoot, canWriteDevices } = userCapabilities;
2✔
104
  const { hasAuditlogs } = useSelector(getTenantCapabilities);
2✔
105
  const dispatch = useDispatch();
2✔
106
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
2✔
107

108
  const onSocketOpen = useCallback(() => {
2✔
NEW
109
    setSocketInitialized(true);
×
110
  }, []);
111

112
  const onNotify = useCallback(
2✔
113
    content => {
NEW
114
      if (snackbarAlreadySet) {
×
NEW
115
        return;
×
116
      }
NEW
117
      setSnackbarAlreadySet(true);
×
NEW
118
      dispatch(setSnackbar(content, TIMEOUTS.fiveSeconds));
×
NEW
119
      snackTimer.current = setTimeout(() => setSnackbarAlreadySet(false), TIMEOUTS.fiveSeconds + TIMEOUTS.debounceShort);
×
120
    },
121
    [dispatch, snackbarAlreadySet]
122
  );
123

124
  const onHealthCheckFailed = useCallback(() => {
2✔
NEW
125
    if (!socketInitialized) {
×
NEW
126
      return;
×
127
    }
NEW
128
    onNotify('Health check failed: connection with the device lost.');
×
129
  }, [onNotify, socketInitialized]);
130

131
  const onSocketClose = useCallback(
2✔
132
    event => {
NEW
133
      if (!socketInitialized) {
×
NEW
134
        return;
×
135
      }
NEW
136
      if (event.wasClean) {
×
NEW
137
        onNotify(`Connection with the device closed.`);
×
NEW
138
      } else if (event.code == 1006) {
×
139
        // 1006: abnormal closure
NEW
140
        onNotify('Connection to the remote terminal is forbidden.');
×
141
      } else {
NEW
142
        onNotify('Connection with the device died.');
×
143
      }
NEW
144
      setSocketClosed(true);
×
145
    },
146
    [onNotify, setSocketClosed, socketInitialized]
147
  );
148

149
  const onMessageReceived = useCallback(message => {
2✔
NEW
150
    if (!termRef.current.terminal.current) {
×
NEW
151
      return;
×
152
    }
NEW
153
    termRef.current.terminal.current.write(new Uint8Array(message));
×
154
  }, []);
155

156
  const [connect, sendMessage, close, sessionState, sessionId] = useSession({
2✔
157
    onClose: onSocketClose,
158
    onHealthCheckFailed,
159
    onMessageReceived,
160
    onNotify,
161
    onOpen: onSocketOpen,
162
    onReady: setSocketInitialized
163
  });
164

165
  useEffect(() => {
2✔
166
    if (open) {
1!
167
      setCurrentTab(type);
1✔
168
      return;
1✔
169
    }
UNCOV
170
    setDownloadPath('');
×
UNCOV
171
    setUploadPath('');
×
UNCOV
172
    setFile();
×
UNCOV
173
    return () => {
×
UNCOV
174
      clearTimeout(snackTimer.current);
×
NEW
175
      if (!open) {
×
NEW
176
        close();
×
177
      }
178
    };
179
  }, [open, type, close]);
180

181
  useEffect(() => {
2✔
182
    const allowedTabs = Object.values(tabs).reduce((accu, tab) => {
1✔
183
      if (tab.canShow(userCapabilities)) {
2!
184
        accu.push(tab);
2✔
185
      }
186
      return accu;
2✔
187
    }, []);
188
    setAvailableTabs(allowedTabs);
1✔
189
  }, [canTroubleshoot, canWriteDevices, userCapabilities]);
190

191
  useEffect(() => {
2✔
192
    if (socketInitialized === undefined) {
1!
UNCOV
193
      return;
×
194
    }
195
    clearInterval(timer.current);
1✔
196
    if (socketInitialized) {
1!
UNCOV
197
      setStartTime(new Date());
×
NEW
198
      dispatch(setSnackbar('Connection with the device established.', TIMEOUTS.fiveSeconds));
×
UNCOV
199
      timer.current = setInterval(() => setElapsed(moment()), TIMEOUTS.halfASecond);
×
200
    } else {
201
      close();
1✔
202
    }
203
    return () => {
1✔
204
      clearInterval(timer.current);
1✔
205
    };
206
  }, [close, dispatch, socketInitialized]);
207

208
  useEffect(() => {
2✔
209
    if (!canTroubleshoot || !open || sessionId || sessionState === WebSocket.CONNECTING) {
1!
210
      return;
1✔
211
    }
NEW
212
    if (sessionState === WebSocket.OPEN) {
×
NEW
213
      sendMessage({ typ: MessageTypes.New, props: { terminal_height: 1, terminal_width: 1 } });
×
NEW
214
      return;
×
215
    }
NEW
216
    connect(device.id);
×
UNCOV
217
    return () => {
×
NEW
218
      if (sessionState === WebSocket.OPEN) {
×
NEW
219
        close();
×
220
      }
221
    };
222
  }, [canTroubleshoot, close, connect, device.id, open, sessionId, sessionState, sendMessage]);
223

224
  const onConnectionToggle = () => {
2✔
UNCOV
225
    if (socketInitialized) {
×
UNCOV
226
      close();
×
227
    } else {
UNCOV
228
      setSocketInitialized(false);
×
UNCOV
229
      connect(device.id);
×
230
    }
231
  };
232

233
  const onDrop = acceptedFiles => {
2✔
UNCOV
234
    if (acceptedFiles.length === 1) {
×
UNCOV
235
      setFile(acceptedFiles[0]);
×
UNCOV
236
      setUploadPath(`/tmp/${acceptedFiles[0].name}`);
×
UNCOV
237
      setCurrentTab(tabs.transfer.value);
×
238
    }
239
  };
240

241
  const onDownloadClick = useCallback(
2✔
242
    path => {
NEW
243
      setDownloadPath(path);
×
NEW
244
      dispatch(getDeviceFileDownloadLink(device.id, path)).then(address => {
×
NEW
245
        const filename = path.substring(path.lastIndexOf('/') + 1) || 'file';
×
NEW
246
        createDownload(address, filename);
×
247
      });
248
    },
249
    [dispatch, device.id]
250
  );
251

252
  const onMakeGatewayClick = () => {
2✔
UNCOV
253
    const code = getCode(canPreview);
×
UNCOV
254
    setTerminalInput(code);
×
255
  };
256

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

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

344
export default 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