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  };