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

mendersoftware / gui / 1057188406

01 Nov 2023 04:24AM UTC coverage: 82.824% (-17.1%) from 99.964%
1057188406

Pull #4134

gitlab-ci

web-flow
chore: Bump uuid from 9.0.0 to 9.0.1

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4134: chore: Bump uuid from 9.0.0 to 9.0.1

4349 of 6284 branches covered (0.0%)

8313 of 10037 relevant lines covered (82.82%)

200.97 hits per line

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

46.48
/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

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

29
const MessagePack = msgpack5();
7✔
30

31
let socket = null;
7✔
32
let buffer = [];
7✔
33
let timer;
34

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

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

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

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

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

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

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

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

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

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

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

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

230
  const resetPlayer = () => {
1✔
231
    setIsPlaying(false);
1✔
232
    setBufferIndex(0);
1✔
233
  };
234

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

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

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

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

288
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