argo-cd
262 строки · 8.0 Кб
1import {Terminal} from 'xterm';
2import {FitAddon} from 'xterm-addon-fit';
3import * as models from '../../../shared/models';
4import * as React from 'react';
5import './pod-terminal-viewer.scss';
6import 'xterm/css/xterm.css';
7import {useCallback, useEffect} from 'react';
8import {debounceTime, takeUntil} from 'rxjs/operators';
9import {fromEvent, ReplaySubject, Subject} from 'rxjs';
10import {Context} from '../../../shared/context';
11import {ErrorNotification, NotificationType} from 'argo-ui';
12export interface PodTerminalViewerProps {
13applicationName: string;
14applicationNamespace: string;
15projectName: string;
16selectedNode: models.ResourceNode;
17podState: models.State;
18containerName: string;
19onClickContainer?: (group: any, i: number, tab: string) => any;
20}
21export interface ShellFrame {
22operation: string;
23data?: string;
24rows?: number;
25cols?: number;
26}
27
28export const PodTerminalViewer: React.FC<PodTerminalViewerProps> = ({
29selectedNode,
30applicationName,
31applicationNamespace,
32projectName,
33podState,
34containerName,
35onClickContainer
36}) => {
37const terminalRef = React.useRef(null);
38const appContext = React.useContext(Context); // used to show toast
39const fitAddon = new FitAddon();
40let terminal: Terminal;
41let webSocket: WebSocket;
42const keyEvent = new ReplaySubject<KeyboardEvent>(2);
43let connSubject = new ReplaySubject<ShellFrame>(100);
44let incommingMessage = new Subject<ShellFrame>();
45const unsubscribe = new Subject<void>();
46let connected = false;
47
48function showErrorMsg(msg: string, err: any) {
49appContext.notifications.show({
50content: <ErrorNotification title={msg} e={err} />,
51type: NotificationType.Error
52});
53}
54
55const onTerminalSendString = (str: string) => {
56if (connected) {
57webSocket.send(JSON.stringify({operation: 'stdin', data: str, rows: terminal.rows, cols: terminal.cols}));
58}
59};
60
61const onTerminalResize = () => {
62if (connected) {
63webSocket.send(
64JSON.stringify({
65operation: 'resize',
66cols: terminal.cols,
67rows: terminal.rows
68})
69);
70}
71};
72
73const onConnectionMessage = (e: MessageEvent) => {
74const msg = JSON.parse(e.data);
75if (!msg?.Code) {
76connSubject.next(msg);
77} else {
78// Do reconnect due to refresh token event
79onConnectionClose();
80setupConnection();
81}
82};
83
84const onConnectionOpen = () => {
85connected = true;
86onTerminalResize(); // fit the screen first time
87terminal.focus();
88};
89
90const onConnectionClose = () => {
91if (!connected) return;
92if (webSocket) webSocket.close();
93connected = false;
94};
95
96const handleConnectionMessage = (frame: ShellFrame) => {
97terminal.write(frame.data);
98incommingMessage.next(frame);
99};
100
101const disconnect = () => {
102if (webSocket) {
103webSocket.close();
104}
105
106if (connSubject) {
107connSubject.complete();
108connSubject = new ReplaySubject<ShellFrame>(100);
109}
110
111if (terminal) {
112terminal.dispose();
113}
114
115incommingMessage.complete();
116incommingMessage = new Subject<ShellFrame>();
117};
118
119function initTerminal(node: HTMLElement) {
120if (connSubject) {
121connSubject.complete();
122connSubject = new ReplaySubject<ShellFrame>(100);
123}
124
125if (terminal) {
126terminal.dispose();
127}
128
129terminal = new Terminal({
130convertEol: true,
131fontFamily: 'Menlo, Monaco, Courier New, monospace',
132bellStyle: 'sound',
133fontSize: 14,
134fontWeight: 400,
135cursorBlink: true
136});
137terminal.options = {
138theme: {
139background: '#333'
140}
141};
142terminal.loadAddon(fitAddon);
143terminal.open(node);
144fitAddon.fit();
145
146connSubject.pipe(takeUntil(unsubscribe)).subscribe(frame => {
147handleConnectionMessage(frame);
148});
149
150terminal.onResize(onTerminalResize);
151terminal.onKey(key => {
152keyEvent.next(key.domEvent);
153});
154terminal.onData(onTerminalSendString);
155}
156
157function setupConnection() {
158const {name = '', namespace = ''} = selectedNode || {};
159const url = `${location.host}${appContext.baseHref}`.replace(/\/$/, '');
160webSocket = new WebSocket(
161`${
162location.protocol === 'https:' ? 'wss' : 'ws'
163}://${url}/terminal?pod=${name}&container=${containerName}&appName=${applicationName}&appNamespace=${applicationNamespace}&projectName=${projectName}&namespace=${namespace}`
164);
165webSocket.onopen = onConnectionOpen;
166webSocket.onclose = onConnectionClose;
167webSocket.onerror = e => {
168showErrorMsg('Terminal Connection Error', e);
169onConnectionClose();
170};
171webSocket.onmessage = onConnectionMessage;
172}
173
174const setTerminalRef = useCallback(
175node => {
176if (terminal && connected) {
177disconnect();
178}
179
180if (node) {
181initTerminal(node);
182setupConnection();
183}
184
185// Save a reference to the node
186terminalRef.current = node;
187},
188[containerName]
189);
190
191useEffect(() => {
192const resizeHandler = fromEvent(window, 'resize')
193.pipe(debounceTime(1000))
194.subscribe(() => {
195if (fitAddon) {
196fitAddon.fit();
197}
198});
199return () => {
200resizeHandler.unsubscribe(); // unsubscribe resize callback
201unsubscribe.next();
202unsubscribe.complete();
203
204// clear connection and close terminal
205if (webSocket) {
206webSocket.close();
207}
208
209if (connSubject) {
210connSubject.complete();
211}
212
213if (terminal) {
214terminal.dispose();
215}
216
217incommingMessage.complete();
218};
219}, [containerName]);
220
221const containerGroups = [
222{
223offset: 0,
224title: 'CONTAINERS',
225containers: podState.spec.containers || []
226},
227{
228offset: (podState.spec.containers || []).length,
229title: 'INIT CONTAINERS',
230containers: podState.spec.initContainers || []
231}
232];
233
234return (
235<div className='row'>
236<div className='columns small-3 medium-2'>
237{containerGroups.map(group => (
238<div key={group.title} style={{marginBottom: '1em'}}>
239{group.containers.length > 0 && <p>{group.title}</p>}
240{group.containers.map((container: any, i: number) => (
241<div
242className='application-details__container'
243key={container.name}
244onClick={() => {
245if (container.name !== containerName) {
246disconnect();
247onClickContainer(group, i, 'exec');
248}
249}}>
250{container.name === containerName && <i className='fa fa-angle-right negative-space-arrow' />}
251<span title={container.name}>{container.name}</span>
252</div>
253))}
254</div>
255))}
256</div>
257<div className='columns small-9 medium-10'>
258<div ref={setTerminalRef} className='pod-terminal-viewer' />
259</div>
260</div>
261);
262};
263