argo-cd

Форк
0
262 строки · 8.0 Кб
1
import {Terminal} from 'xterm';
2
import {FitAddon} from 'xterm-addon-fit';
3
import * as models from '../../../shared/models';
4
import * as React from 'react';
5
import './pod-terminal-viewer.scss';
6
import 'xterm/css/xterm.css';
7
import {useCallback, useEffect} from 'react';
8
import {debounceTime, takeUntil} from 'rxjs/operators';
9
import {fromEvent, ReplaySubject, Subject} from 'rxjs';
10
import {Context} from '../../../shared/context';
11
import {ErrorNotification, NotificationType} from 'argo-ui';
12
export interface PodTerminalViewerProps {
13
    applicationName: string;
14
    applicationNamespace: string;
15
    projectName: string;
16
    selectedNode: models.ResourceNode;
17
    podState: models.State;
18
    containerName: string;
19
    onClickContainer?: (group: any, i: number, tab: string) => any;
20
}
21
export interface ShellFrame {
22
    operation: string;
23
    data?: string;
24
    rows?: number;
25
    cols?: number;
26
}
27

28
export const PodTerminalViewer: React.FC<PodTerminalViewerProps> = ({
29
    selectedNode,
30
    applicationName,
31
    applicationNamespace,
32
    projectName,
33
    podState,
34
    containerName,
35
    onClickContainer
36
}) => {
37
    const terminalRef = React.useRef(null);
38
    const appContext = React.useContext(Context); // used to show toast
39
    const fitAddon = new FitAddon();
40
    let terminal: Terminal;
41
    let webSocket: WebSocket;
42
    const keyEvent = new ReplaySubject<KeyboardEvent>(2);
43
    let connSubject = new ReplaySubject<ShellFrame>(100);
44
    let incommingMessage = new Subject<ShellFrame>();
45
    const unsubscribe = new Subject<void>();
46
    let connected = false;
47

48
    function showErrorMsg(msg: string, err: any) {
49
        appContext.notifications.show({
50
            content: <ErrorNotification title={msg} e={err} />,
51
            type: NotificationType.Error
52
        });
53
    }
54

55
    const onTerminalSendString = (str: string) => {
56
        if (connected) {
57
            webSocket.send(JSON.stringify({operation: 'stdin', data: str, rows: terminal.rows, cols: terminal.cols}));
58
        }
59
    };
60

61
    const onTerminalResize = () => {
62
        if (connected) {
63
            webSocket.send(
64
                JSON.stringify({
65
                    operation: 'resize',
66
                    cols: terminal.cols,
67
                    rows: terminal.rows
68
                })
69
            );
70
        }
71
    };
72

73
    const onConnectionMessage = (e: MessageEvent) => {
74
        const msg = JSON.parse(e.data);
75
        if (!msg?.Code) {
76
            connSubject.next(msg);
77
        } else {
78
            // Do reconnect due to refresh token event
79
            onConnectionClose();
80
            setupConnection();
81
        }
82
    };
83

84
    const onConnectionOpen = () => {
85
        connected = true;
86
        onTerminalResize(); // fit the screen first time
87
        terminal.focus();
88
    };
89

90
    const onConnectionClose = () => {
91
        if (!connected) return;
92
        if (webSocket) webSocket.close();
93
        connected = false;
94
    };
95

96
    const handleConnectionMessage = (frame: ShellFrame) => {
97
        terminal.write(frame.data);
98
        incommingMessage.next(frame);
99
    };
100

101
    const disconnect = () => {
102
        if (webSocket) {
103
            webSocket.close();
104
        }
105

106
        if (connSubject) {
107
            connSubject.complete();
108
            connSubject = new ReplaySubject<ShellFrame>(100);
109
        }
110

111
        if (terminal) {
112
            terminal.dispose();
113
        }
114

115
        incommingMessage.complete();
116
        incommingMessage = new Subject<ShellFrame>();
117
    };
118

119
    function initTerminal(node: HTMLElement) {
120
        if (connSubject) {
121
            connSubject.complete();
122
            connSubject = new ReplaySubject<ShellFrame>(100);
123
        }
124

125
        if (terminal) {
126
            terminal.dispose();
127
        }
128

129
        terminal = new Terminal({
130
            convertEol: true,
131
            fontFamily: 'Menlo, Monaco, Courier New, monospace',
132
            bellStyle: 'sound',
133
            fontSize: 14,
134
            fontWeight: 400,
135
            cursorBlink: true
136
        });
137
        terminal.options = {
138
            theme: {
139
                background: '#333'
140
            }
141
        };
142
        terminal.loadAddon(fitAddon);
143
        terminal.open(node);
144
        fitAddon.fit();
145

146
        connSubject.pipe(takeUntil(unsubscribe)).subscribe(frame => {
147
            handleConnectionMessage(frame);
148
        });
149

150
        terminal.onResize(onTerminalResize);
151
        terminal.onKey(key => {
152
            keyEvent.next(key.domEvent);
153
        });
154
        terminal.onData(onTerminalSendString);
155
    }
156

157
    function setupConnection() {
158
        const {name = '', namespace = ''} = selectedNode || {};
159
        const url = `${location.host}${appContext.baseHref}`.replace(/\/$/, '');
160
        webSocket = new WebSocket(
161
            `${
162
                location.protocol === 'https:' ? 'wss' : 'ws'
163
            }://${url}/terminal?pod=${name}&container=${containerName}&appName=${applicationName}&appNamespace=${applicationNamespace}&projectName=${projectName}&namespace=${namespace}`
164
        );
165
        webSocket.onopen = onConnectionOpen;
166
        webSocket.onclose = onConnectionClose;
167
        webSocket.onerror = e => {
168
            showErrorMsg('Terminal Connection Error', e);
169
            onConnectionClose();
170
        };
171
        webSocket.onmessage = onConnectionMessage;
172
    }
173

174
    const setTerminalRef = useCallback(
175
        node => {
176
            if (terminal && connected) {
177
                disconnect();
178
            }
179

180
            if (node) {
181
                initTerminal(node);
182
                setupConnection();
183
            }
184

185
            // Save a reference to the node
186
            terminalRef.current = node;
187
        },
188
        [containerName]
189
    );
190

191
    useEffect(() => {
192
        const resizeHandler = fromEvent(window, 'resize')
193
            .pipe(debounceTime(1000))
194
            .subscribe(() => {
195
                if (fitAddon) {
196
                    fitAddon.fit();
197
                }
198
            });
199
        return () => {
200
            resizeHandler.unsubscribe(); // unsubscribe resize callback
201
            unsubscribe.next();
202
            unsubscribe.complete();
203

204
            // clear connection and close terminal
205
            if (webSocket) {
206
                webSocket.close();
207
            }
208

209
            if (connSubject) {
210
                connSubject.complete();
211
            }
212

213
            if (terminal) {
214
                terminal.dispose();
215
            }
216

217
            incommingMessage.complete();
218
        };
219
    }, [containerName]);
220

221
    const containerGroups = [
222
        {
223
            offset: 0,
224
            title: 'CONTAINERS',
225
            containers: podState.spec.containers || []
226
        },
227
        {
228
            offset: (podState.spec.containers || []).length,
229
            title: 'INIT CONTAINERS',
230
            containers: podState.spec.initContainers || []
231
        }
232
    ];
233

234
    return (
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
242
                                className='application-details__container'
243
                                key={container.name}
244
                                onClick={() => {
245
                                    if (container.name !== containerName) {
246
                                        disconnect();
247
                                        onClickContainer(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

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.