• 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

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