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

mendersoftware / gui / 1493849842

13 Oct 2024 07:39AM UTC coverage: 83.457% (-16.5%) from 99.965%
1493849842

Pull #4531

gitlab-ci

web-flow
chore: Bump send and express in /tests/e2e_tests

Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4531: chore: Bump send and express in /tests/e2e_tests

4486 of 6422 branches covered (69.85%)

8551 of 10246 relevant lines covered (83.46%)

151.3 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@${xterm}/css/xterm.css" />
51

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

292
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