github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/pod-terminal-viewer/pod-terminal-viewer.tsx (about) 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 };