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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

41.18
/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 { FitAddon } from 'xterm-addon-fit';
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 } from '../../../helpers';
27
import { blobToString, byteArrayToString } from '../../../utils/sockethook';
28
import XTerm from '../../common/xterm';
29

30
const MessagePack = msgpack5();
7✔
31
const fitAddon = new FitAddon();
7✔
32

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

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

41
const generateHtml = (versions, content) => {
7✔
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 }) => {
7✔
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

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

177
  useEffect(() => {
1✔
178
    if (!sessionInitialized) {
1!
179
      return;
×
180
    }
181
    socket = new WebSocket(`wss://${window.location.host}${deviceConnect}/sessions/${item.meta.session_id[0]}/playback`);
1✔
182

183
    socket.onopen = () => {
1✔
184
      setSocketInitialized(true);
×
185
    };
186
  }, [sessionInitialized]);
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.reset();
×
219
      }
220
      if (buffer[bufferIndex].content) {
×
221
        xtermRef.current.terminal.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
        fitAddon.fit();
×
244
        xtermRef.current.terminal.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');
×
261
  };
262

263
  return (
1✔
264
    <div className={`${className} `}>
265
      <div className="relative">
266
        <XTerm addons={[fitAddon]} className="xterm-min-screen" 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