• 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

59.77
/src/js/components/devices/troubleshoot/terminal-wrapper.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 { useSelector } from 'react-redux';
17
import { Link } from 'react-router-dom';
18

19
import { Button } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import moment from 'moment';
23
import momentDurationFormatSetup from 'moment-duration-format';
24

25
import { BEGINNING_OF_TIME, TIMEOUTS } from '../../../constants/appConstants';
26
import { getCurrentSession, getFeatures, getIsPreview, getTenantCapabilities, getUserCapabilities } from '../../../selectors';
27
import Tracking from '../../../tracking';
28
import { useSession } from '../../../utils/sockethook';
29
import { MaybeTime } from '../../common/time';
30
import { getCode } from '../dialogs/make-gateway-dialog';
31
import Terminal from '../troubleshoot/terminal';
32
import ListOptions from '../widgets/listoptions';
33

34
momentDurationFormatSetup(moment);
11✔
35

36
const useStyles = makeStyles()(theme => ({
11✔
37
  connectionActions: { marginTop: theme.spacing() },
38
  connectionButton: { background: theme.palette.background.terminal, display: 'grid', placeContent: 'center' },
39
  sessionInfo: { gap: theme.spacing(3), marginBottom: theme.spacing(), '&>div': { gap: theme.spacing(2) } },
40
  terminalContent: {
41
    display: 'grid',
42
    gridTemplateRows: `max-content 0 minmax(${theme.spacing(60)}, 1fr) max-content`,
43
    flexGrow: 1,
44
    overflow: 'hidden',
45
    '&.device-connected': {
46
      gridTemplateRows: `max-content minmax(${theme.spacing(80)}, 1fr) max-content`
47
    }
48
  }
49
}));
50

51
const SessionInfo = ({ socketInitialized, startTime }) => {
11✔
52
  const [elapsed, setElapsed] = useState(moment());
6✔
53
  const timer = useRef();
6✔
54
  const { classes } = useStyles();
6✔
55

56
  useEffect(() => {
6✔
57
    clearInterval(timer.current);
4✔
58
    if (!socketInitialized) {
4✔
59
      return;
3✔
60
    }
61
    timer.current = setInterval(() => setElapsed(moment()), TIMEOUTS.halfASecond);
1✔
62
    return () => {
1✔
63
      clearInterval(timer.current);
1✔
64
    };
65
  }, [socketInitialized]);
66

67
  return (
6✔
68
    <div className={`flexbox ${classes.sessionInfo}`}>
69
      {[
70
        { key: 'status', title: 'Session status', content: socketInitialized ? 'connected' : 'disconnected' },
6✔
71
        { key: 'start', title: 'Connection start', content: <MaybeTime value={startTime} /> },
72
        {
73
          key: 'duration',
74
          title: 'Duration',
75
          content: startTime ? `${moment.duration(elapsed.diff(moment(startTime))).format('hh:mm:ss', { trim: false })}` : '-'
6✔
76
        }
77
      ].map(({ key, title, content }) => (
78
        <div key={key} className="flexbox">
18✔
79
          <div>{title}</div>
80
          <b>{content}</b>
81
        </div>
82
      ))}
83
    </div>
84
  );
85
};
86

87
const TroubleshootContent = ({ device, onDownload, setSocketClosed, setUploadPath, setFile, setSnackbar, setSocketInitialized, socketInitialized }) => {
11✔
88
  const [terminalInput, setTerminalInput] = useState('');
6✔
89
  const [startTime, setStartTime] = useState();
6✔
90
  const [snackbarAlreadySet, setSnackbarAlreadySet] = useState(false);
6✔
91
  const snackTimer = useRef();
6✔
92
  const { classes } = useStyles();
6✔
93
  const termRef = useRef({ terminal: React.createRef(), terminalRef: React.createRef() });
6✔
94

95
  const { isHosted } = useSelector(getFeatures);
6✔
96
  const { hasAuditlogs, isEnterprise } = useSelector(getTenantCapabilities);
6✔
97
  const { canAuditlog } = useSelector(getUserCapabilities);
6✔
98
  const canPreview = useSelector(getIsPreview);
6✔
99
  const { token } = useSelector(getCurrentSession);
6✔
100
  const onMessageReceived = useCallback(message => {
6✔
101
    if (!termRef.current.terminal.current) {
×
102
      return;
×
103
    }
104
    termRef.current.terminal.current.write(new Uint8Array(message));
×
105
  }, []);
106

107
  const onNotify = useCallback(
6✔
108
    content => {
109
      if (snackbarAlreadySet) {
×
110
        return;
×
111
      }
112
      setSnackbarAlreadySet(true);
×
113
      setSnackbar(content, TIMEOUTS.threeSeconds);
×
114
      snackTimer.current = setTimeout(() => setSnackbarAlreadySet(false), TIMEOUTS.threeSeconds + TIMEOUTS.debounceShort);
×
115
    },
116
    [setSnackbar, snackbarAlreadySet]
117
  );
118

119
  const onHealthCheckFailed = useCallback(() => {
6✔
120
    if (!socketInitialized) {
×
121
      return;
×
122
    }
123
    onNotify('Health check failed: connection with the device lost.');
×
124
  }, [onNotify, socketInitialized]);
125

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

145
  const [connect, sendMessage, close, sessionState] = useSession({
6✔
146
    onClose: onSocketClose,
147
    onHealthCheckFailed,
148
    onMessageReceived,
149
    onNotify,
150
    onOpen: setSocketInitialized,
151
    token
152
  });
153

154
  useEffect(() => {
6✔
155
    if (socketInitialized === undefined) {
4✔
156
      return;
3✔
157
    }
158
    if (socketInitialized) {
1!
159
      setStartTime(new Date());
1✔
160
      setSnackbar('Connection with the device established.', TIMEOUTS.fiveSeconds);
1✔
161
    } else {
162
      close();
×
163
    }
164
  }, [close, setSnackbar, socketInitialized]);
165

166
  useEffect(() => {
6✔
167
    return () => {
4✔
168
      clearTimeout(snackTimer.current);
4✔
169
      close();
4✔
170
    };
171
  }, [close]);
172

173
  useEffect(() => {
6✔
174
    if (sessionState !== WebSocket.OPEN) {
4!
175
      return;
×
176
    }
177
    return close;
4✔
178
  }, [close, sessionState]);
179

180
  const onConnectionToggle = () => {
6✔
181
    if (sessionState === WebSocket.CLOSED) {
×
182
      setStartTime();
×
183
      setSocketInitialized(undefined);
×
184
      setSocketClosed(false);
×
185
      connect(device.id);
×
186
      Tracking.event({ category: 'devices', action: 'open_terminal' });
×
187
    } else {
188
      setSocketInitialized(false);
×
189
      close();
×
190
    }
191
  };
192

193
  const onMakeGatewayClick = () => {
6✔
194
    const code = getCode(canPreview);
×
195
    setTerminalInput(code);
×
196
  };
197

198
  const onDrop = acceptedFiles => {
6✔
199
    if (acceptedFiles.length === 1) {
×
200
      setFile(acceptedFiles[0]);
×
201
      setUploadPath(`/tmp/${acceptedFiles[0].name}`);
×
202
    }
203
  };
204

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

207
  const visibilityToggle = !socketInitialized ? { maxHeight: 0, overflow: 'hidden' } : {};
6✔
208
  return (
6✔
209
    <div className={`${classes.terminalContent} ${socketInitialized ? 'device-connected' : ''}`}>
6✔
210
      <SessionInfo socketInitialized={socketInitialized} startTime={startTime} />
211
      <Dropzone activeClassName="active" rejectClassName="active" multiple={false} onDrop={onDrop} noClick>
212
        {({ getRootProps }) => (
213
          <div {...getRootProps()} style={{ position: 'relative', ...visibilityToggle }}>
6✔
214
            <Terminal
215
              onDownloadClick={onDownload}
216
              sendMessage={sendMessage}
217
              socketInitialized={socketInitialized}
218
              style={{ position: 'absolute', width: '100%', height: '100%', ...visibilityToggle }}
219
              textInput={terminalInput}
220
              xtermRef={termRef}
221
            />
222
          </div>
223
        )}
224
      </Dropzone>
225
      {!socketInitialized && (
10✔
226
        <div className={classes.connectionButton}>
227
          <Button variant="contained" color="secondary" onClick={onConnectionToggle}>
228
            Connect Terminal
229
          </Button>
230
        </div>
231
      )}
232
      <div className={`flexbox space-between ${classes.connectionActions}`}>
233
        <Button onClick={onConnectionToggle}>{socketInitialized ? 'Disconnect' : 'Connect'} Terminal</Button>
6✔
234
        {canAuditlog && hasAuditlogs && (
12!
235
          <Button component={Link} to={`/auditlog?objectType=device&objectId=${device.id}&startDate=${BEGINNING_OF_TIME}`}>
236
            View Session Logs for this device
237
          </Button>
238
        )}
239
        {socketInitialized && !!commandHandlers.length && <ListOptions options={commandHandlers} title="Quick commands" />}
8!
240
      </div>
241
    </div>
242
  );
243
};
244

245
export default TroubleshootContent;
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