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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

93.38
/frontend/src/js/components/auditlogs/eventdetails/TerminalPlayer.tsx
1
// Copyright 2021 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import React, { useCallback, useEffect, useRef, useState } from 'react';
2✔
15

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

2✔
20
import XTerm from '@northern.tech/common-ui/xterm';
2✔
21
import { DEVICE_MESSAGE_PROTOCOLS as MessageProtocols, DEVICE_MESSAGE_TYPES as MessageTypes, TIMEOUTS, deviceConnect } from '@northern.tech/store/constants';
2✔
22
import { blobToString, byteArrayToString, createFileDownload, toggle } from '@northern.tech/utils/helpers';
2✔
23
import msgpack5 from 'msgpack5';
2✔
24
import Cookies from 'universal-cookie';
2✔
25

2✔
26
const cookies = new Cookies();
8✔
27
const MessagePack = msgpack5();
8✔
28

2✔
29
let socket = null;
8✔
30
let buffer = [];
8✔
31
let timer;
2✔
32

2✔
33
const useStyles = makeStyles()(theme => ({
8✔
34
  playArrow: { fontSize: '7rem', color: theme.palette.text.disabled }
2✔
35
}));
2✔
36

2✔
37
const generateHtml = (versions, content) => {
8✔
38
  const { fit, search, xterm } = Object.entries(versions).reduce((accu, [key, version]) => {
2✔
39
    accu[key] = version.match(/(?<version>\d.*)/).groups.version;
2✔
40
    return accu;
2✔
41
  }, {});
2✔
42
  return `
2✔
43
  <!DOCTYPE html>
2✔
44
  <html>
2✔
45
    <head>
2✔
46
      <link rel="stylesheet" href="https://unpkg.com/@xterm/xterm@${xterm}/css/xterm.css" />
2✔
47

2✔
48
      <script src="https://unpkg.com/@xterm/xterm@${xterm}/lib/xterm.js"></script>
2✔
49
      <script src="https://unpkg.com/@xterm/addon-search@${search}/lib/addon-search.js"></script>
2✔
50
      <script src="https://unpkg.com/@xterm/addon-fit@${fit}/lib/addon-fit.js"></script>
2✔
51
      <style type="text/css">
2✔
52
        body {
2✔
53
          display: grid;
2✔
54
          justify-items: center;
2✔
55
          max-width: 80vw;
2✔
56
          margin: 10vh auto;
2✔
57
          row-gap: 5vh;
2✔
58
          font-family: 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', Helvetica, Arial, sans-serif;
2✔
59
        }
2✔
60
        h2 {
2✔
61
          color: #24444a;
2✔
62
        }
2✔
63
        button {
2✔
64
          background-color: #921267;
2✔
65
          padding: 1.3em 3.4em;
2✔
66
          color: #fff;
2✔
67
          font-weight: 700;
2✔
68
          text-transform: uppercase;
2✔
69
          border: 0;
2✔
70
          border-radius: 3px;
2✔
71
          cursor: pointer;
2✔
72
        }
2✔
73
        .disabled {
2✔
74
          background-color: lightgrey;
2✔
75
          opacity: 0.4;
2✔
76
        }
2✔
77
      </style>
2✔
78
    </head>
2✔
79
    <body>
2✔
80
      <img
2✔
81
        alt="mender-logo"
2✔
82
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAABMCAMAAACs7yB4AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACTUExURUxpcV0PQwFZaUBAQV0PQ10PQ10PQ0BAQUBAQUBAQV0PQ0BAQUBAQQFZaV0PQ0BAQV0PQ0BAQQFZaQFZaUBAQUBAQUBAQUBAQQFZaUBAQQFZaV0PQ10PQwFZaUBAQQFZaV0PQwFZaV0PQ10PQ0BAQQFZaQFZaQFZaQFZaV0PQwFZaV0PQ10PQwFZaUBAQV0PQwFZaXk7Gk0AAAAudFJOUwAQgCCAn2C/QIDPUBBAQO/vYBDv369wj2AwICC/z58wcL9Q38/fUK+fr48wj3AmLHfiAAAFCElEQVR42u2aWZuiOhCGEVBAtEVEcMF96cUe4f//uiNZKyQsMz3nIvPku2kogtSbrSpJW5aRkZGRkZGRkZGRkdH/occwL8afZ8G2et+Vp68NNA2O+SXKjwP9AL8LrJz7vtmVWF+82DnCxaKRboDDgmpMEeNFSfXBAFmxQjPEI/e8GBLbqeRakS4a8WLRVivCCyAssOsHAFje5IpgNaGF3qDnxRHZbEhYHpAth8UuOhGOBEIX2d4FwimyCcUK3QlP/xThm4LQVhCOYbFIq5kmkqeQOwRcKJpaq5nGchW+7wChLQMWb1oRDsYy4oFH/F0sA35rltO0Iv4LgIzgewgQ469bxbcCXXT8luM/Oi4uyOAaKueREUtaXT7dypovPc9LJ+Tmde1lDnnkZB7XHJuC1+XeR5eVdc3eIsLPkCYpsGfsFWZx+hMqETlgK6H3RPLQTYBvZshvaz17AgXIaWKqMHx6QV8j8khtTa6C1aKvMJHP9CJUIALANkL2TVShFGmPngkeYsKU3CxbCJ9X/NPps4PwOZv0JwSIj5F73gqAbYTMuRQ6gNxxnjLhkt61EeKitHe0ED7nHXxbnFVfIOInXnLkAiBKDiJ30EQ4I82W0hvuTij36YTYa4SoPdaoChJWdqnoMSFv4awjb4uEle2wkIQAeVAZDxoIAwKVVX89gdCXCb0l9q1GSEosWZf3WGuKhOQnr2z0N7YgSNvOSkRM9AkMDYQh8bDyKesmTHEzqQm5tYPQ6yT8lDLqoQpQyNBHasIJqfeqg867CR1c/IeEidyJa00o0JwlaLruF7BzNSH6mm9hUJEwEeIZqfgEzUx9CGcs+jm1cRg85QroXj19K9LsS/v6EDu3RF+rvj+zPPXE5wHCDFV/H0IuXzWXhn03oihhriAs+hCmaDKdI8d7EOJx+3PC5W+vgIcKwqgPoY8cD1BL9iC0UP3/mDBrD/jiOBwp2nUrj81xAyFyGbnk1wgDn2gNCVG86EO4pK/7lkwYdsR72CWjgUydK/Zzjk2ElTfrhF22z6U4XCd/OpeGNN4nv7FR4yrW/Q9iA6vIy6CJMCNR8dqPcI1SLiVh2C9a7Ps04kix/TKUQ9+WIUYPq4kQ0eFUpQ8hihd7FSFeUHTnNCFP8Ft0xpEggjk1zkFJrkr2Agh2vrUaCR2eCjeMQ9+BhHuSwALCsCqUJmJeevWFcQwqLUGhqVNn13VHYtcbjF62c21WOrru8dG8tuDrJsdqmktxc9CHoTBFBsolQ9PaAhNmfbrp3xAlxMuiBOaLk1kzIX2GV3g1QtL59q2ETndA/LuEeD0/FzLieTMhfTa3ZEI69iZJGyFeX3cugTfTX9NNzXZ42eKabXpfTdW/sA5eQnWaBgEaauHLQHqPEwCt6UNM5Vc24qwPis0nYJ8G2pGlupqAL3fsY2zwScwOOj+94fNRyG2jLcaFHeu20ca3f1fM9sX29A/UFJ+ETWKNtOH72+Wd2Fbg2IK24ge3nfQifJfPe8ExPjvIn5byybceioWTtHu9CV+K602oWSMKbYMPmvgoBOeHN8GmE+EvBaHqDLjUllDVhh8KwoW2hJtSnkLEdt1IE1K502ouhee9i1imPlny7GNbunZTux7wWScVauKmWchfyf/DBvrkSs4MeJ6ji+44Eixg3yP/cHK7g8hJsE8bSz/dbdteiV0vXr1s91oG+zLZB8vIyMjIyMjIyMjIyOil/wDgSQFggbLYfQAAAABJRU5ErkJggg=="
2✔
83
      />
2✔
84
      <h2>Terminal playback</h2>
2✔
85
      <div id="terminal"></div>
2✔
86
      <div>
2✔
87
        <button id="start" onclick="handleStart()">Start</button>
2✔
88
        <button id="pause" class="disabled" disabled onclick="handlePause()">Pause</button>
2✔
89
        <button id="stop" class="disabled" disabled onclick="handleStop()">Stop</button>
2✔
90
      </div>
2✔
91
      <script>
2✔
92
        const byteArrayToString = body => String.fromCharCode(...body);
2✔
93
        const transfer = '${JSON.stringify(content.map(item => ({ delay: item.delay, content: btoa(JSON.stringify(item.content)) })))}';
2✔
94
        const content = JSON.parse(transfer);
2✔
95
        let contentIndex = 0;
2✔
96
        let timer;
2✔
97
        const term = new Terminal();
2✔
98
        const fitAddon = new FitAddon.FitAddon();
2✔
99
        const searchAddon = new SearchAddon.SearchAddon();
2✔
100
        term.loadAddon(searchAddon);
2✔
101
        term.loadAddon(fitAddon);
2✔
102
        term.open(document.getElementById('terminal'));
2✔
103
        fitAddon.fit();
2✔
104
        const startButton = document.getElementById('start');
2✔
105
        const pauseButton = document.getElementById('pause');
2✔
106
        const stopButton = document.getElementById('stop');
2✔
107

2✔
108
        const resetPlayer = () => {
2✔
109
          contentIndex = 0;
2✔
110
          term.reset()
2✔
111
          startButton.toggleAttribute('disabled');
2✔
112
          pauseButton.toggleAttribute('disabled');
2✔
113
          stopButton.toggleAttribute('disabled');
2✔
114
          pauseButton.classList.toggle('disabled');
2✔
115
          stopButton.classList.toggle('disabled');
2✔
116
          startButton.classList.toggle('disabled');
2✔
117
        }
2✔
118

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

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

2✔
146
        const handlePause = () => {
2✔
147
          startButton.toggleAttribute('disabled');
2✔
148
          pauseButton.toggleAttribute('disabled');
2✔
149
          startButton.classList.toggle('disabled');
2✔
150
          pauseButton.classList.toggle('disabled');
2✔
151
          clearTimeout(timer);
2✔
152
        };
2✔
153

2✔
154
        const handleStop = () => {
2✔
155
          clearTimeout(timer);
2✔
156
          resetPlayer();
2✔
157
        };
2✔
158
      </script>
2✔
159
    </body>
2✔
160
  </html>`;
2✔
161
};
2✔
162

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

2✔
173
  const { classes } = useStyles();
3✔
174

2✔
175
  const resetPlayer = () => {
3✔
176
    setIsPlaying(false);
3✔
177
    setBufferIndex(0);
3✔
178
  };
2✔
179

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

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

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

2✔
235
  const onTogglePlayClick = useCallback(() => {
3✔
UNCOV
236
    if (!wasStarted) {
2!
237
      setWasStarted(true);
2✔
238
      return setTimeout(() => {
2✔
239
        setFitTrigger(toggle);
2✔
240
        xtermRef.current.terminal.current.focus();
2✔
241
        setIsPlaying(!isPlaying);
2✔
242
      }, TIMEOUTS.debounceShort);
2✔
243
    }
2✔
244
    setIsPaused(isPlaying);
2✔
245
    setIsPlaying(!isPlaying);
2✔
246
  }, [isPlaying, wasStarted]);
2✔
247

2✔
248
  const onReplayClick = () => {
3✔
249
    resetPlayer();
2✔
250
    setIsPlaying(true);
2✔
251
  };
2✔
252

2✔
253
  const onDownloadClick = () => {
3✔
254
    const text = generateHtml({ fit: XTERM_FIT_VERSION, search: XTERM_SEARCH_VERSION, xterm: XTERM_VERSION }, buffer);
2✔
255
    createFileDownload(text, 'terminalsession.html', token);
2✔
256
  };
2✔
257

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

2✔
287
export default TerminalPlayer;
2✔
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