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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 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