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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

60.23
/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✔
UNCOV
101
    if (!termRef.current.terminal.current) {
×
UNCOV
102
      return;
×
103
    }
UNCOV
104
    termRef.current.terminal.current.write(new Uint8Array(message));
×
105
  }, []);
106

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

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

126
  const onSocketClose = useCallback(
6✔
127
    event => {
128
      // abnormal socket close might happen without socket being initialized, in case of forbidden permissions
129
      // this should be checked before socketInitialized condition
UNCOV
130
      if (event.code === 1006) {
×
131
        // 1006: abnormal closure
UNCOV
132
        onNotify('Connection to the remote terminal is forbidden.');
×
133
      }
134

UNCOV
135
      if (!socketInitialized) {
×
UNCOV
136
        return;
×
137
      }
UNCOV
138
      if (event.wasClean) {
×
UNCOV
139
        onNotify(`Connection with the device closed.`);
×
140
      } else {
UNCOV
141
        onNotify('Connection with the device died.');
×
142
      }
UNCOV
143
      setSocketInitialized(false);
×
UNCOV
144
      setSocketClosed(true);
×
145
    },
146
    [onNotify, setSocketClosed, setSocketInitialized, socketInitialized]
147
  );
148

149
  const [connect, sendMessage, close, sessionState] = useSession({
6✔
150
    onClose: onSocketClose,
151
    onHealthCheckFailed,
152
    onMessageReceived,
153
    onNotify,
154
    onOpen: setSocketInitialized,
155
    token
156
  });
157

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

170
  useEffect(() => {
6✔
171
    return () => {
4✔
172
      clearTimeout(snackTimer.current);
4✔
173
      if (socketInitialized !== undefined) {
4✔
174
        close();
1✔
175
      }
176
    };
177
  }, [close, socketInitialized]);
178

179
  useEffect(() => {
6✔
180
    if (sessionState !== WebSocket.OPEN) {
4!
UNCOV
181
      return;
×
182
    }
183
    return close;
4✔
184
  }, [close, sessionState]);
185

186
  const onConnectionToggle = () => {
6✔
UNCOV
187
    if (sessionState === WebSocket.CLOSED) {
×
UNCOV
188
      setStartTime();
×
UNCOV
189
      setSocketInitialized(undefined);
×
UNCOV
190
      setSocketClosed(false);
×
UNCOV
191
      connect(device.id);
×
UNCOV
192
      Tracking.event({ category: 'devices', action: 'open_terminal' });
×
193
    } else {
UNCOV
194
      setSocketInitialized(false);
×
UNCOV
195
      close();
×
196
    }
197
  };
198

199
  const onMakeGatewayClick = () => {
6✔
UNCOV
200
    const code = getCode(canPreview);
×
UNCOV
201
    setTerminalInput(code);
×
202
  };
203

204
  const onDrop = acceptedFiles => {
6✔
UNCOV
205
    if (acceptedFiles.length === 1) {
×
UNCOV
206
      setFile(acceptedFiles[0]);
×
UNCOV
207
      setUploadPath(`/tmp/${acceptedFiles[0].name}`);
×
208
    }
209
  };
210

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

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

251
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