github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx (about)

     1  import {DataLoader} from 'argo-ui';
     2  import * as classNames from 'classnames';
     3  import * as React from 'react';
     4  import {useEffect, useState, useRef} from 'react';
     5  import {bufferTime, delay, retryWhen} from 'rxjs/operators';
     6  
     7  import {LogEntry} from '../../../shared/models';
     8  import {services, ViewPreferences} from '../../../shared/services';
     9  
    10  import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
    11  
    12  import './pod-logs-viewer.scss';
    13  import {CopyLogsButton} from './copy-logs-button';
    14  import {DownloadLogsButton} from './download-logs-button';
    15  import {ContainerSelector} from './container-selector';
    16  import {FollowToggleButton} from './follow-toggle-button';
    17  import {ShowPreviousLogsToggleButton} from './show-previous-logs-toggle-button';
    18  import {TimestampsToggleButton} from './timestamps-toggle-button';
    19  import {DarkModeToggleButton} from './dark-mode-toggle-button';
    20  import {FullscreenButton} from './fullscreen-button';
    21  import {Spacer} from '../../../shared/components/spacer';
    22  import {LogMessageFilter} from './log-message-filter';
    23  import {SinceSecondsSelector} from './since-seconds-selector';
    24  import {TailSelector} from './tail-selector';
    25  import {PodNamesToggleButton} from './pod-names-toggle-button';
    26  import {AutoScrollButton} from './auto-scroll-button';
    27  import {WrapLinesButton} from './wrap-lines-button';
    28  import Ansi from 'ansi-to-react';
    29  
    30  export interface PodLogsProps {
    31      namespace: string;
    32      applicationNamespace: string;
    33      applicationName: string;
    34      podName?: string;
    35      containerName: string;
    36      group?: string;
    37      kind?: string;
    38      name?: string;
    39      timestamp?: string;
    40      containerGroups?: any[];
    41      onClickContainer?: (group: any, i: number, tab: string) => void;
    42  }
    43  
    44  // ansi colors, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
    45  const red = '\u001b[31m';
    46  const green = '\u001b[32m';
    47  const yellow = '\u001b[33m';
    48  const blue = '\u001b[34m';
    49  const magenta = '\u001b[35m';
    50  const cyan = '\u001b[36m';
    51  const colors = [red, green, yellow, blue, magenta, cyan];
    52  const reset = '\u001b[0m';
    53  const whiteOnYellow = '\u001b[1m\u001b[43;1m\u001b[37m';
    54  
    55  // cheap string hash function
    56  function stringHashCode(str: string) {
    57      let hash = 0;
    58      for (let i = 0; i < str.length; i++) {
    59          // tslint:disable-next-line:no-bitwise
    60          hash = str.charCodeAt(i) + ((hash << 5) - hash);
    61      }
    62      return hash;
    63  }
    64  
    65  // ansi color for pod name
    66  function podColor(podName: string) {
    67      return colors[Math.abs(stringHashCode(podName) % colors.length)];
    68  }
    69  
    70  // https://2ality.com/2012/09/empty-regexp.html
    71  const matchNothing = /.^/;
    72  
    73  export const PodsLogsViewer = (props: PodLogsProps) => {
    74      const {containerName, onClickContainer, timestamp, containerGroups, applicationName, applicationNamespace, namespace, podName, group, kind, name} = props;
    75      const queryParams = new URLSearchParams(location.search);
    76      const [viewPodNames, setViewPodNames] = useState(queryParams.get('viewPodNames') === 'true');
    77      const [follow, setFollow] = useState(queryParams.get('follow') !== 'false');
    78      const [viewTimestamps, setViewTimestamps] = useState(queryParams.get('viewTimestamps') === 'true');
    79      const [previous, setPreviousLogs] = useState(queryParams.get('showPreviousLogs') === 'true');
    80      const [tail, setTail] = useState<number>(parseInt(queryParams.get('tail'), 10) || 1000);
    81      const [sinceSeconds, setSinceSeconds] = useState(0);
    82      const [filter, setFilter] = useState(queryParams.get('filterText') || '');
    83      const [highlight, setHighlight] = useState<RegExp>(matchNothing);
    84      const [scrollToBottom, setScrollToBottom] = useState(true);
    85      const [logs, setLogs] = useState<LogEntry[]>([]);
    86      const logsContainerRef = useRef(null);
    87  
    88      useEffect(() => {
    89          if (viewPodNames) {
    90              setViewTimestamps(false);
    91          }
    92      }, [viewPodNames]);
    93  
    94      useEffect(() => {
    95          // https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
    96          // matchNothing this is chosen instead of empty regexp, because that would match everything and break colored logs
    97          setHighlight(filter === '' ? matchNothing : new RegExp(filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'));
    98      }, [filter]);
    99  
   100      if (!containerName || containerName === '') {
   101          return <div>Pod does not have container with name {containerName}</div>;
   102      }
   103  
   104      useEffect(() => setScrollToBottom(true), [follow]);
   105  
   106      useEffect(() => {
   107          if (scrollToBottom) {
   108              const element = logsContainerRef.current;
   109              if (element) {
   110                  element.scrollTop = element.scrollHeight;
   111              }
   112          }
   113      }, [logs, scrollToBottom]);
   114  
   115      useEffect(() => {
   116          setLogs([]);
   117          const logsSource = services.applications
   118              .getContainerLogs({
   119                  applicationName,
   120                  appNamespace: applicationNamespace,
   121                  namespace,
   122                  podName,
   123                  resource: {group, kind, name},
   124                  containerName,
   125                  tail,
   126                  follow,
   127                  sinceSeconds,
   128                  filter,
   129                  previous
   130              }) // accumulate log changes and render only once every 100ms to reduce CPU usage
   131              .pipe(bufferTime(100))
   132              .pipe(retryWhen(errors => errors.pipe(delay(500))))
   133              .subscribe(log => setLogs(previousLogs => previousLogs.concat(log)));
   134  
   135          return () => logsSource.unsubscribe();
   136      }, [applicationName, applicationNamespace, namespace, podName, group, kind, name, containerName, tail, follow, sinceSeconds, filter, previous]);
   137  
   138      const handleScroll = (event: React.WheelEvent<HTMLDivElement>) => {
   139          if (event.deltaY < 0) setScrollToBottom(false);
   140      };
   141  
   142      const renderLog = (log: LogEntry, lineNum: number) =>
   143          // show the pod name if there are multiple pods, pad with spaces to align
   144          (viewPodNames ? (lineNum === 0 || logs[lineNum - 1].podName !== log.podName ? podColor(podName) + log.podName + reset : ' '.repeat(log.podName.length)) + ' ' : '') +
   145          // show the timestamp if requested, pad with spaces to align
   146          (viewTimestamps ? (lineNum === 0 || (logs[lineNum - 1].timeStamp !== log.timeStamp ? log.timeStampStr : '').padEnd(30)) + ' ' : '') +
   147          // show the log content, highlight the filter text
   148          log.content?.replace(highlight, (substring: string) => whiteOnYellow + substring + reset);
   149      const logsContent = (width: number, height: number, isWrapped: boolean) => (
   150          <div ref={logsContainerRef} onScroll={handleScroll} style={{width, height, overflow: 'scroll'}}>
   151              {logs.map((log, lineNum) => (
   152                  <div key={lineNum} style={{whiteSpace: isWrapped ? 'normal' : 'pre', lineHeight: '16px'}} className='noscroll'>
   153                      <Ansi>{renderLog(log, lineNum)}</Ansi>
   154                  </div>
   155              ))}
   156          </div>
   157      );
   158  
   159      return (
   160          <DataLoader load={() => services.viewPreferences.getPreferences()}>
   161              {(prefs: ViewPreferences) => {
   162                  return (
   163                      <React.Fragment>
   164                          <div className='pod-logs-viewer__settings'>
   165                              <span>
   166                                  <FollowToggleButton follow={follow} setFollow={setFollow} />
   167                                  {follow && <AutoScrollButton scrollToBottom={scrollToBottom} setScrollToBottom={setScrollToBottom} />}
   168                                  <ShowPreviousLogsToggleButton setPreviousLogs={setPreviousLogs} showPreviousLogs={previous} />
   169                                  <Spacer />
   170                                  <ContainerSelector containerGroups={containerGroups} containerName={containerName} onClickContainer={onClickContainer} />
   171                                  <Spacer />
   172                                  {!follow && (
   173                                      <>
   174                                          <SinceSecondsSelector sinceSeconds={sinceSeconds} setSinceSeconds={n => setSinceSeconds(n)} />
   175                                          <TailSelector tail={tail} setTail={setTail} />
   176                                      </>
   177                                  )}
   178                                  <LogMessageFilter filterText={filter} setFilterText={setFilter} />
   179                              </span>
   180                              <Spacer />
   181                              <span>
   182                                  <WrapLinesButton prefs={prefs} />
   183                                  <PodNamesToggleButton viewPodNames={viewPodNames} setViewPodNames={setViewPodNames} />
   184                                  <TimestampsToggleButton setViewTimestamps={setViewTimestamps} viewTimestamps={viewTimestamps} timestamp={timestamp} />
   185                                  <DarkModeToggleButton prefs={prefs} />
   186                              </span>
   187                              <Spacer />
   188                              <span>
   189                                  <CopyLogsButton logs={logs} />
   190                                  <DownloadLogsButton {...props} />
   191                                  <FullscreenButton {...props} />
   192                              </span>
   193                          </div>
   194                          <div className={classNames('pod-logs-viewer', {'pod-logs-viewer--inverted': prefs.appDetails.darkMode})} onWheel={handleScroll}>
   195                              <AutoSizer>{({width, height}: {width: number; height: number}) => logsContent(width, height, prefs.appDetails.wrapLines)}</AutoSizer>
   196                          </div>
   197                      </React.Fragment>
   198                  );
   199              }}
   200          </DataLoader>
   201      );
   202  };