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