• 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

47.95
/src/js/components/auditlogs/eventdetails/terminalplayer.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

16
import { CloudDownload, Pause, PlayArrow, Refresh } from '@mui/icons-material';
17
import { Button } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import msgpack5 from 'msgpack5';
21
import Cookies from 'universal-cookie';
22

23
import { deviceConnect } from '../../../actions/deviceActions';
24
import { TIMEOUTS } from '../../../constants/appConstants';
25
import { DEVICE_MESSAGE_PROTOCOLS as MessageProtocols, DEVICE_MESSAGE_TYPES as MessageTypes } from '../../../constants/deviceConstants';
26
import { createFileDownload, toggle } from '../../../helpers';
27
import { blobToString, byteArrayToString } from '../../../utils/sockethook';
28
import XTerm from '../../common/xterm';
29

30
const cookies = new Cookies();
6✔
31
const MessagePack = msgpack5();
6✔
32

33
let socket = null;
6✔
34
let buffer = [];
6✔
35
let timer;
36

37
const useStyles = makeStyles()(theme => ({
6✔
38
  playArrow: { fontSize: '7rem', color: theme.palette.text.disabled }
39
}));
40

41
const generateHtml = (versions, content) => {
6✔
42
  const { fit, search, xterm } = Object.entries(versions).reduce((accu, [key, version]) => {
×
43
    accu[key] = version.match(/(?<version>\d.*)/).groups.version;
×
44
    return accu;
×
45
  }, {});
46
  return `
×
47
  <!DOCTYPE html>
48
  <html>
49
    <head>
50
      <link rel="stylesheet" href="https://unpkg.com/xterm@${xterm}/css/xterm.css" />
51
      <script src="https://unpkg.com/xterm@${xterm}/lib/xterm.js"></script>
52
      <script src="https://unpkg.com/xterm-addon-search@${search}/lib/xterm-addon-search.js"></script>
53
      <script src="https://unpkg.com/xterm-addon-fit@${fit}/lib/xterm-addon-fit.js"></script>
54
      <style type="text/css">
55
        body {
56
          display: grid;
57
          justify-items: center;
58
          max-width: 80vw;
59
          margin: 10vh auto;
60
          row-gap: 5vh;
61
          font-family: 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Helvetica, Arial, sans-serif;
62
        }
63
        h2 {
64
          color: #24444a;
65
        }
66
        button {
67
          background-color: #921267;
68
          padding: 1.3em 3.4em;
69
          color: #fff;
70
          font-weight: 700;
71
          text-transform: uppercase;
72
          border: 0;
73
          border-radius: 3px;
74
          cursor: pointer;
75
        }
76
        .disabled {
77
          background-color: lightgrey;
78
          opacity: 0.4;
79
        }
80
      </style>
81
    </head>
82
    <body>
83
      <img
84
        alt="mender-logo"
85
        src="data:image/png;base64,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"
86
      />
87
      <h2>Terminal playback</h2>
88
      <div id="terminal"></div>
89
      <div>
90
        <button id="start" onclick="handleStart()">Start</button>
91
        <button id="pause" class="disabled" disabled onclick="handlePause()">Pause</button>
92
        <button id="stop" class="disabled" disabled onclick="handleStop()">Stop</button>
93
      </div>
94
      <script>
95
        const byteArrayToString = body => String.fromCharCode(...body);
96
        const transfer = '${JSON.stringify(content.map(item => ({ delay: item.delay, content: btoa(JSON.stringify(item.content)) })))}';
×
97
        const content = JSON.parse(transfer);
98
        let contentIndex = 0;
99
        let timer;
100
        const term = new Terminal();
101
        const fitAddon = new FitAddon.FitAddon();
102
        const searchAddon = new SearchAddon.SearchAddon();
103
        term.loadAddon(searchAddon);
104
        term.loadAddon(fitAddon);
105
        term.open(document.getElementById('terminal'));
106
        fitAddon.fit();
107
        const startButton = document.getElementById('start');
108
        const pauseButton = document.getElementById('pause');
109
        const stopButton = document.getElementById('stop');
110

111
        const resetPlayer = () => {
112
          contentIndex = 0;
113
          term.reset()
114
          startButton.toggleAttribute('disabled');
115
          pauseButton.toggleAttribute('disabled');
116
          stopButton.toggleAttribute('disabled');
117
          pauseButton.classList.toggle('disabled');
118
          stopButton.classList.toggle('disabled');
119
          startButton.classList.toggle('disabled');
120
        }
121

122
        const processContent = () => {
123
          if (contentIndex === content.length) {
124
            return handlePause();
125
          }
126
          const item = content[contentIndex];
127
          contentIndex += 1;
128
          let delay = 1;
129
          if (item.delay) {
130
            delay = item.delay;
131
          } else if (item.content) {
132
            const buffer = JSON.parse(atob(item.content));
133
            term.write(byteArrayToString(buffer.data || []))
134
          }
135
          timer = setTimeout(processContent, delay)
136
        };
137

138
        const handleStart = () => {
139
          contentIndex = 0;
140
          startButton.toggleAttribute('disabled');
141
          pauseButton.toggleAttribute('disabled');
142
          stopButton.toggleAttribute('disabled');
143
          startButton.classList.toggle('disabled');
144
          pauseButton.classList.toggle('disabled');
145
          stopButton.classList.toggle('disabled');
146
          timer = setTimeout(processContent, 1);
147
        };
148

149
        const handlePause = () => {
150
          startButton.toggleAttribute('disabled');
151
          pauseButton.toggleAttribute('disabled');
152
          startButton.classList.toggle('disabled');
153
          pauseButton.classList.toggle('disabled');
154
          clearTimeout(timer);
155
        };
156

157
        const handleStop = () => {
158
          clearTimeout(timer);
159
          resetPlayer();
160
        };
161
      </script>
162
    </body>
163
  </html>`;
164
};
165

166
export const TerminalPlayer = ({ className, item, sessionInitialized, token }) => {
6✔
167
  const xtermRef = useRef({ terminal: React.createRef(), terminalRef: React.createRef() });
1✔
168
  const [socketInitialized, setSocketInitialized] = useState(false);
1✔
169
  const [bufferIndex, setBufferIndex] = useState(0);
1✔
170
  const [isPlaying, setIsPlaying] = useState(false);
1✔
171
  const [wasStarted, setWasStarted] = useState(false);
1✔
172
  const [isPaused, setIsPaused] = useState(false);
1✔
173
  const [isLoadingSession, setIsLoadingSession] = useState(true);
1✔
174
  const [fitTrigger, setFitTrigger] = useState(false);
1✔
175

176
  const { classes } = useStyles();
1✔
177

178
  useEffect(() => {
1✔
179
    if (!sessionInitialized) {
1!
180
      return;
×
181
    }
182
    cookies.set('JWT', token, { path: '/', secure: true, sameSite: 'strict', maxAge: 5 });
1✔
183
    socket = new WebSocket(`wss://${window.location.host}${deviceConnect}/sessions/${item.meta.session_id[0]}/playback`);
1✔
184
    socket.onopen = () => setSocketInitialized(true);
1✔
185
  }, [item.meta.session_id, sessionInitialized, token]);
186

187
  useEffect(() => {
1✔
188
    if (!socketInitialized) {
1!
189
      return;
1✔
190
    }
191
    buffer = [];
×
192
    socket.onmessage = event =>
×
193
      blobToString(event.data).then(data => {
×
194
        const {
195
          hdr: { proto, typ, props = {} },
×
196
          body
197
        } = MessagePack.decode(data);
×
198
        if (proto !== MessageProtocols.Shell) {
×
199
          return;
×
200
        }
201
        clearTimeout(timer);
×
202
        timer = setTimeout(() => setIsLoadingSession(false), TIMEOUTS.oneSecond);
×
203
        switch (typ) {
×
204
          case MessageTypes.Shell:
205
            return buffer.push({ content: body });
×
206
          case MessageTypes.Delay:
207
            return buffer.push({ delay: props.delay_value });
×
208
          default:
209
            break;
×
210
        }
211
      });
212
  }, [socketInitialized]);
213

214
  useEffect(() => {
1✔
215
    if (isPlaying && bufferIndex < buffer.length) {
1!
216
      if (bufferIndex === 0) {
×
217
        xtermRef.current.terminal.current.reset();
×
218
      }
219
      if (buffer[bufferIndex].content) {
×
220
        xtermRef.current.terminal.current.write(byteArrayToString(buffer[bufferIndex].content));
×
221
        setTimeout(() => setBufferIndex(bufferIndex + 1), 20);
×
222
      }
223
      if (buffer[bufferIndex].delay) {
×
224
        setTimeout(() => {
×
225
          setBufferIndex(bufferIndex + 1);
×
226
        }, buffer[bufferIndex].delay);
227
      }
228
    } else if (!isPaused) {
1!
229
      resetPlayer();
1✔
230
    }
231
  }, [bufferIndex, isPaused, isPlaying]);
232

233
  const resetPlayer = () => {
1✔
234
    setIsPlaying(false);
1✔
235
    setBufferIndex(0);
1✔
236
  };
237

238
  const onTogglePlayClick = useCallback(() => {
1✔
239
    if (!wasStarted) {
×
240
      setWasStarted(true);
×
241
      return setTimeout(() => {
×
242
        setFitTrigger(toggle);
×
243
        xtermRef.current.terminal.current.focus();
×
244
        setIsPlaying(!isPlaying);
×
245
      }, TIMEOUTS.debounceShort);
246
    }
247
    setIsPaused(isPlaying);
×
248
    setIsPlaying(!isPlaying);
×
249
  }, [isPlaying, wasStarted]);
250

251
  const onReplayClick = () => {
1✔
252
    resetPlayer();
×
253
    setIsPlaying(true);
×
254
  };
255

256
  const onDownloadClick = () => {
1✔
257
    // eslint-disable-next-line no-undef
258
    const text = generateHtml({ fit: XTERM_FIT_VERSION, search: XTERM_SEARCH_VERSION, xterm: XTERM_VERSION }, buffer);
×
259
    createFileDownload(text, 'terminalsession.html', token);
×
260
  };
261

262
  return (
1✔
263
    <div className={`${className} `}>
264
      <div className="relative">
265
        <XTerm className="xterm-min-screen" triggerResize={fitTrigger} xtermRef={xtermRef} />
266
        {!wasStarted && (
2✔
267
          <div
268
            className="flexbox centered clickable"
269
            style={{ background: 'black', width: '100%', height: '100%', position: 'absolute', top: 0, zIndex: 10 }}
270
            onClick={onTogglePlayClick}
271
          >
272
            <PlayArrow className={classes.playArrow} />
273
          </div>
274
        )}
275
      </div>
276
      <div className="flexbox margin-top-small margin-bottom-small">
277
        <Button color="primary" onClick={onTogglePlayClick} startIcon={isPlaying ? <Pause /> : <PlayArrow />}>
1!
278
          {isPlaying ? 'Pause' : 'Play'}
1!
279
        </Button>
280
        <Button color="primary" onClick={onReplayClick} disabled={isPlaying} startIcon={<Refresh />}>
281
          Replay
282
        </Button>
283
        <Button color="primary" onClick={onDownloadClick} startIcon={<CloudDownload />} disabled={isLoadingSession}>
284
          Download
285
        </Button>
286
      </div>
287
    </div>
288
  );
289
};
290

291
export default TerminalPlayer;
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