github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/OverviewLogPane.tsx (about)

     1  import { History } from "history"
     2  import React, { Component } from "react"
     3  import { useHistory, useLocation } from "react-router"
     4  import styled, { keyframes } from "styled-components"
     5  import {
     6    FilterLevel,
     7    FilterSet,
     8    filterSetsEqual,
     9    FilterSource,
    10    TermState,
    11  } from "./logfilters"
    12  import "./LogLine.scss"
    13  import "./LogPane.scss"
    14  import LogStore, {
    15    LogUpdateAction,
    16    LogUpdateEvent,
    17    useLogStore,
    18  } from "./LogStore"
    19  import PathBuilder, { usePathBuilder } from "./PathBuilder"
    20  import { RafContext, useRaf } from "./raf"
    21  import { useStarredResources } from "./StarredResourcesContext"
    22  import { Color, FontSize, SizeUnit } from "./style-helpers"
    23  import Anser from "./third-party/anser/index.js"
    24  import { LogLevel, LogLine, ResourceName } from "./types"
    25  
    26  // The number of lines to display before an error.
    27  export const PROLOGUE_LENGTH = 5
    28  
    29  type OverviewLogComponentProps = {
    30    manifestName: string
    31    pathBuilder: PathBuilder
    32    logStore: LogStore
    33    raf: RafContext
    34    filterSet: FilterSet
    35    history: History
    36    scrollToStoredLineIndex: number | null
    37    starredResources: string[]
    38  }
    39  
    40  let LogPaneRoot = styled.section`
    41    padding: 0 0 ${SizeUnit(0.25)} 0;
    42    background-color: ${Color.gray10};
    43    width: 100%;
    44    height: 100%;
    45    overflow-y: auto;
    46    box-sizing: border-box;
    47    font-size: ${FontSize.smallest};
    48  `
    49  
    50  const blink = keyframes`
    51  0% {
    52    opacity: 1;
    53  }
    54  50% {
    55    opacity: 0;
    56  }
    57  100% {
    58    opacity: 1;
    59  }
    60  `
    61  
    62  let LogEnd = styled.div`
    63    animation: ${blink} 1s infinite;
    64    animation-timing-function: ease;
    65    padding-top: ${SizeUnit(0.25)};
    66    padding-left: ${SizeUnit(0.625)};
    67    font-size: var(--log-font-scale);
    68  `
    69  
    70  let anser = new Anser()
    71  
    72  function newLineEl(
    73    line: LogLine,
    74    showManifestPrefix: boolean,
    75    extraClasses: string[]
    76  ): Element {
    77    let text = line.text
    78    let level = line.level
    79    let buildEvent = line.buildEvent
    80    let classes = ["LogLine"]
    81    classes.push(...extraClasses)
    82    if (level === "WARN") {
    83      classes.push("is-warning")
    84    } else if (level === "ERROR") {
    85      classes.push("is-error")
    86    }
    87    if (buildEvent === "init") {
    88      classes.push("is-buildEvent")
    89      classes.push("is-buildEvent-init")
    90  
    91      if (showManifestPrefix) {
    92        // For build event lines, we put the manifest name is a suffix
    93        // rather than a prefix, because it looks nicer.
    94        text += ` • ${line.manifestName}`
    95      } else {
    96        // If we're viewing a single resource, we should make the build event log
    97        // lines sticky, so that we always know context of the current logs.
    98        classes.push("is-sticky")
    99      }
   100    }
   101    if (buildEvent === "fallback") {
   102      classes.push("is-buildEvent")
   103      classes.push("is-buildEvent-fallback")
   104    }
   105    let span = document.createElement("span")
   106    span.setAttribute("data-sl-index", String(line.storedLineIndex))
   107    span.classList.add(...classes)
   108  
   109    if (showManifestPrefix && buildEvent !== "init") {
   110      let prefix = document.createElement("span")
   111      let name = line.manifestName
   112      if (!name) {
   113        name = "(global)"
   114      }
   115      prefix.title = name
   116      prefix.className = "logLinePrefix"
   117      prefix.innerHTML = anser.escapeForHtml(name)
   118      span.appendChild(prefix)
   119    }
   120  
   121    let code = document.createElement("code")
   122    code.classList.add("LogLine-content")
   123  
   124    // newline ensures this takes up at least one line
   125    let spacer = "\n"
   126    code.innerHTML = anser.linkify(
   127      anser.ansiToHtml(anser.escapeForHtml(text) + spacer, {
   128        // Let anser colorize the html as it appears from various consoles
   129        use_classes: false,
   130      })
   131    )
   132    span.appendChild(code)
   133    return span
   134  }
   135  
   136  // An index of lines such that lets us find:
   137  // - The next line
   138  // - The previous line
   139  // - The line by stored line index.
   140  type LineHashListEntry = {
   141    prev?: LineHashListEntry | null
   142    next?: LineHashListEntry | null
   143    line: LogLine
   144    el?: Element
   145  }
   146  
   147  class LineHashList {
   148    private last: LineHashListEntry | null = null
   149    private byStoredLineIndex: { [key: number]: LineHashListEntry } = {}
   150  
   151    lookup(line: LogLine): LineHashListEntry | null {
   152      return this.byStoredLineIndex[line.storedLineIndex]
   153    }
   154  
   155    lookupByStoredLineIndex(storedLineIndex: number): LineHashListEntry | null {
   156      return this.byStoredLineIndex[storedLineIndex]
   157    }
   158  
   159    append(line: LogLine) {
   160      let existing = this.byStoredLineIndex[line.storedLineIndex]
   161      if (existing) {
   162        existing.line = line
   163      } else {
   164        let last = this.last
   165        let newEntry = { prev: last, line: line }
   166        this.byStoredLineIndex[line.storedLineIndex] = newEntry
   167        if (last) {
   168          last.next = newEntry
   169        }
   170        this.last = newEntry
   171      }
   172    }
   173  }
   174  
   175  // The number of lines to render at a time.
   176  export const renderWindow = 250
   177  
   178  // React is not a great system for rendering logs.
   179  // React has to build a virtual DOM, diffs the virtual DOM, and does
   180  // spot updates of the actual DOM.
   181  //
   182  // But logs are append-only, so this wastes a lot of CPU doing diffs
   183  // for things that never change. Other components (like xtermjs) manage
   184  // rendering directly, but have a thin React wrapper to mount the component.
   185  // So we use that rendering strategy here.
   186  //
   187  // This means that we can't use other react components (like styled-components)
   188  // and have to use plain css + HTML.
   189  export class OverviewLogComponent extends Component<OverviewLogComponentProps> {
   190    autoscroll: boolean = true
   191    needsScrollToLine: boolean = false
   192  
   193    // The element containing all the log lines.
   194    rootRef: React.RefObject<any> = React.createRef()
   195  
   196    // The blinking cursor at the end of the component.
   197    private cursorRef: React.RefObject<HTMLParagraphElement> = React.createRef()
   198  
   199    // Track the scrollTop of the root element to see if the user is scrolling upwards.
   200    scrollTop: number = -1
   201  
   202    // Timer for tracking autoscroll.
   203    autoscrollRafId: number | null = null
   204  
   205    // Timer for tracking render
   206    renderBufferRafId: number | null = null
   207  
   208    // Lines to render at the end of the pane.
   209    forwardBuffer: LogLine[] = []
   210  
   211    // Lines to render at the start of the pane.
   212    backwardBuffer: LogLine[] = []
   213  
   214    private logCheckpoint: number = 0
   215  
   216    private lineHashList: LineHashList = new LineHashList()
   217  
   218    // When we're displaying warnings or errors, we want to display the last
   219    // N lines before the error. So we keep track of the last N lines for each span.
   220    private prologuesBySpanId: { [key: string]: LogLine[] } = {}
   221  
   222    constructor(props: OverviewLogComponentProps) {
   223      super(props)
   224  
   225      this.onScroll = this.onScroll.bind(this)
   226      this.onLogUpdate = this.onLogUpdate.bind(this)
   227      this.renderBuffer = this.renderBuffer.bind(this)
   228    }
   229  
   230    scrollCursorIntoView() {
   231      if (this.cursorRef.current?.scrollIntoView) {
   232        this.cursorRef.current.scrollIntoView()
   233      }
   234    }
   235  
   236    onLogUpdate(e: LogUpdateEvent) {
   237      if (!this.rootRef.current || !this.cursorRef.current) {
   238        return
   239      }
   240  
   241      if (e.action === LogUpdateAction.truncate) {
   242        this.resetRender()
   243      }
   244  
   245      this.readLogsFromLogStore()
   246    }
   247  
   248    componentDidUpdate(prevProps: OverviewLogComponentProps) {
   249      if (prevProps.logStore !== this.props.logStore) {
   250        prevProps.logStore.removeUpdateListener(this.onLogUpdate)
   251        this.props.logStore.addUpdateListener(this.onLogUpdate)
   252      }
   253  
   254      if (
   255        prevProps.manifestName !== this.props.manifestName ||
   256        !filterSetsEqual(prevProps.filterSet, this.props.filterSet)
   257      ) {
   258        this.resetRender()
   259  
   260        if (typeof this.props.scrollToStoredLineIndex === "number") {
   261          this.needsScrollToLine = true
   262        }
   263        this.autoscroll = !this.needsScrollToLine
   264  
   265        this.readLogsFromLogStore()
   266      } else if (prevProps.logStore !== this.props.logStore) {
   267        this.resetRender()
   268        this.readLogsFromLogStore()
   269      }
   270    }
   271  
   272    componentDidMount() {
   273      let rootEl = this.rootRef.current
   274      if (!rootEl) {
   275        return
   276      }
   277  
   278      if (typeof this.props.scrollToStoredLineIndex == "number") {
   279        this.needsScrollToLine = true
   280      }
   281      this.autoscroll = !this.needsScrollToLine
   282  
   283      rootEl.addEventListener("scroll", this.onScroll, {
   284        passive: true,
   285      })
   286      this.resetRender()
   287      this.readLogsFromLogStore()
   288  
   289      this.props.logStore.addUpdateListener(this.onLogUpdate)
   290    }
   291  
   292    componentWillUnmount() {
   293      this.props.logStore.removeUpdateListener(this.onLogUpdate)
   294  
   295      let rootEl = this.rootRef.current
   296      if (!rootEl) {
   297        return
   298      }
   299      rootEl.removeEventListener("scroll", this.onScroll)
   300  
   301      if (this.autoscrollRafId) {
   302        this.props.raf.cancelAnimationFrame(this.autoscrollRafId)
   303      }
   304    }
   305  
   306    onScroll() {
   307      let rootEl = this.rootRef.current
   308      if (!rootEl) {
   309        return
   310      }
   311  
   312      let scrollTop = rootEl.scrollTop
   313      let oldScrollTop = this.scrollTop
   314      let autoscroll = this.autoscroll
   315  
   316      this.scrollTop = scrollTop
   317      if (oldScrollTop === -1 || oldScrollTop === scrollTop) {
   318        return
   319      }
   320  
   321      // If we're scrolled horizontally, cancel the autoscroll.
   322      if (rootEl.scrollLeft > 0) {
   323        if (this.autoscroll) {
   324          this.autoscroll = false
   325          this.maybeScheduleRender()
   326        }
   327        return
   328      }
   329  
   330      // If we're autoscrolling, and the user scrolled up,
   331      // cancel the autoscroll.
   332      if (autoscroll && scrollTop < oldScrollTop) {
   333        if (this.autoscroll) {
   334          this.autoscroll = false
   335          this.maybeScheduleRender()
   336        }
   337        return
   338      }
   339  
   340      // If we're not autoscrolling, and the user scrolled down,
   341      // we may have to re-engage the autoscroll.
   342      if (!autoscroll && scrollTop > oldScrollTop) {
   343        this.maybeEngageAutoscroll()
   344      }
   345    }
   346  
   347    private maybeEngageAutoscroll() {
   348      // We don't expect new log lines in snapshots. So when we scroll down, we don't need
   349      // to worry about re-engaging autoscroll.
   350      if (this.props.pathBuilder.isSnapshot()) {
   351        return
   352      }
   353  
   354      if (this.needsScrollToLine) {
   355        return
   356      }
   357  
   358      if (this.autoscrollRafId) {
   359        this.props.raf.cancelAnimationFrame(this.autoscrollRafId)
   360      }
   361  
   362      this.autoscrollRafId = this.props.raf.requestAnimationFrame(() => {
   363        let autoscroll = this.computeAutoScroll()
   364        if (autoscroll) {
   365          this.autoscroll = true
   366        }
   367      })
   368    }
   369  
   370    // Compute whether we should auto-scroll from the state of the DOM.
   371    // This forces a layout, so should be used sparingly.
   372    private computeAutoScroll(): boolean {
   373      let rootEl = this.rootRef.current
   374      if (!rootEl) {
   375        return true
   376      }
   377  
   378      // Always auto-scroll when we're recovering from a loading screen.
   379      let cursorEl = this.cursorRef.current
   380      if (!cursorEl) {
   381        return true
   382      }
   383  
   384      // Never auto-scroll if we're horizontally scrolled.
   385      if (rootEl.scrollLeft) {
   386        return false
   387      }
   388  
   389      let lastElInView =
   390        cursorEl.getBoundingClientRect().bottom <=
   391        rootEl.getBoundingClientRect().bottom
   392      return lastElInView
   393    }
   394  
   395    resetRender() {
   396      let root = this.rootRef.current
   397      let cursor = this.cursorRef.current
   398      if (root) {
   399        while (root.firstChild != cursor) {
   400          root.removeChild(root.firstChild)
   401        }
   402      }
   403  
   404      this.lineHashList = new LineHashList()
   405      this.prologuesBySpanId = {}
   406      this.logCheckpoint = 0
   407      this.scrollTop = -1
   408  
   409      if (this.renderBufferRafId) {
   410        this.props.raf.cancelAnimationFrame(this.renderBufferRafId)
   411        this.renderBufferRafId = 0
   412      }
   413  
   414      if (this.autoscrollRafId) {
   415        this.props.raf.cancelAnimationFrame(this.autoscrollRafId)
   416        this.autoscrollRafId = 0
   417      }
   418    }
   419  
   420    matchesTermFilter(line: LogLine): boolean {
   421      const { term } = this.props.filterSet
   422  
   423      // Don't consider a filter term if the term hasn't been parsed for matching
   424      if (!term || term.state !== TermState.Parsed) {
   425        return true
   426      }
   427  
   428      return term.regexp.test(line.text)
   429    }
   430  
   431    // If we have a level filter on, check if this line matches the level filter.
   432    matchesLevelFilter(line: LogLine): boolean {
   433      let level = this.props.filterSet.level
   434      if (level === FilterLevel.warn && line.level !== LogLevel.WARN) {
   435        return false
   436      }
   437      if (level === FilterLevel.error && line.level !== LogLevel.ERROR) {
   438        return false
   439      }
   440      return true
   441    }
   442  
   443    // Check if this line matches the current filter.
   444    matchesFilter(line: LogLine): boolean {
   445      if (line.buildEvent) {
   446        // Always leave in build event logs.
   447        // This makes it easier to see which logs belong to which builds.
   448        return true
   449      }
   450  
   451      let source = this.props.filterSet.source
   452      if (
   453        source === FilterSource.runtime &&
   454        line.spanId.indexOf("build:") === 0
   455      ) {
   456        return false
   457      }
   458      if (source === FilterSource.build && line.spanId.indexOf("build:") !== 0) {
   459        return false
   460      }
   461  
   462      return this.matchesLevelFilter(line) && this.matchesTermFilter(line)
   463    }
   464  
   465    // Index this line so that we can display prologues to errors.
   466    trackPrologueLine(line: LogLine) {
   467      if (!this.prologuesBySpanId[line.spanId]) {
   468        this.prologuesBySpanId[line.spanId] = []
   469      }
   470      this.prologuesBySpanId[line.spanId].push(line)
   471    }
   472  
   473    // Gets the prologue for the given span, and clear the lines used for prologuing.
   474    getAndClearPrologue(spanId: string): LogLine[] {
   475      let lines = this.prologuesBySpanId[spanId]
   476      if (!lines) {
   477        return []
   478      }
   479  
   480      delete this.prologuesBySpanId[spanId]
   481      return lines.slice(-PROLOGUE_LENGTH) // last N lines
   482    }
   483  
   484    // Render new logs that have come in since the current checkpoint.
   485    readLogsFromLogStore() {
   486      let mn = this.props.manifestName
   487      let logStore = this.props.logStore
   488      let startCheckpoint = this.logCheckpoint
   489  
   490      let patch = mn
   491        ? mn === ResourceName.starred
   492          ? logStore.starredLogPatchSet(
   493              this.props.starredResources,
   494              startCheckpoint
   495            )
   496          : logStore.manifestLogPatchSet(mn, startCheckpoint)
   497        : logStore.allLogPatchSet(startCheckpoint)
   498  
   499      let lines: LogLine[] = []
   500      let shouldDisplayPrologues = this.props.filterSet.level !== FilterLevel.all
   501  
   502      patch.lines.forEach((line) => {
   503        let matches = this.matchesFilter(line)
   504        if (matches) {
   505          if (shouldDisplayPrologues) {
   506            lines.push(...this.getAndClearPrologue(line.spanId))
   507          }
   508          lines.push(line)
   509          return
   510        } else if (shouldDisplayPrologues) {
   511          this.trackPrologueLine(line)
   512        }
   513      })
   514  
   515      this.logCheckpoint = patch.checkpoint
   516      lines.forEach((line) => this.lineHashList.append(line))
   517  
   518      if (startCheckpoint) {
   519        // If this is an incremental render, put the lines in the forward buffer.
   520        lines.forEach((line) => {
   521          this.forwardBuffer.push(line)
   522        })
   523      } else {
   524        // If this is the first render, put the lines in the backward buffer, so
   525        // that the last lines get rendered first.
   526        lines.forEach((line) => {
   527          this.backwardBuffer.push(line)
   528        })
   529      }
   530  
   531      this.maybeScheduleRender()
   532    }
   533  
   534    // Schedule a render job if there's not one already scheduled.
   535    maybeScheduleRender() {
   536      if (this.renderBufferRafId) return
   537      this.renderBufferRafId = this.props.raf.requestAnimationFrame(
   538        this.renderBuffer
   539      )
   540    }
   541  
   542    shouldRenderForwardBuffer(): boolean {
   543      return this.forwardBuffer.length > 0
   544    }
   545  
   546    // When we're in autoscrolling mode, rendering the backwards buffer makes the
   547    // screen jiggle, because we have to render a few rows, then scroll down, then
   548    // render a few rows, then scroll down.
   549    //
   550    // So when in autoscrol mode, only render until we have the "last window" of logs.
   551    shouldRenderBackwardBuffer(): boolean {
   552      if (this.backwardBuffer.length == 0) {
   553        // Skip rendering if there's no lines in the buffer.
   554        return false
   555      }
   556  
   557      if (!this.autoscroll) {
   558        // Do render if we're scrolling up.
   559        return true
   560      }
   561  
   562      // In autoscroll mode, only render if there aren't enough lines to fill the viewport.
   563      return this.rootRef.current.scrollTop == 0
   564    }
   565  
   566    // We have two render buffers:
   567    // - a buffer of newer logs that we haven't rendered yet.
   568    // - a buffer of older logs that we haven't rendered yet.
   569    // First, process the newer logs.
   570    // If we're out of new logs to render, go back through the old logs.
   571    //
   572    // Each invocation of this method renders up to 2x renderWindow logs.
   573    // If there are still logs left to render, it yields the thread and schedules
   574    // another render.
   575    renderBuffer() {
   576      this.renderBufferRafId = 0
   577  
   578      let root = this.rootRef.current
   579      let cursor = this.cursorRef.current
   580      if (!root || !cursor) {
   581        return
   582      }
   583  
   584      if (
   585        !this.shouldRenderForwardBuffer() &&
   586        !this.shouldRenderBackwardBuffer()
   587      ) {
   588        return
   589      }
   590  
   591      // Render the lines in the forward buffer first.
   592      let forwardLines = this.forwardBuffer.slice(0, renderWindow)
   593      this.forwardBuffer = this.forwardBuffer.slice(renderWindow)
   594      for (let i = 0; i < forwardLines.length; i++) {
   595        let line = forwardLines[i]
   596        this.renderLineHelper(line)
   597      }
   598  
   599      if (this.shouldRenderBackwardBuffer()) {
   600        let backwardStart = Math.max(0, this.backwardBuffer.length - renderWindow)
   601        let backwardLines = this.backwardBuffer.slice(backwardStart)
   602        this.backwardBuffer = this.backwardBuffer.slice(0, backwardStart)
   603  
   604        for (let i = backwardLines.length - 1; i >= 0; i--) {
   605          let line = backwardLines[i]
   606          this.renderLineHelper(line)
   607        }
   608      }
   609  
   610      if (this.autoscroll) {
   611        this.scrollCursorIntoView()
   612      }
   613  
   614      if (this.needsScrollToLine) {
   615        let entry = this.lineHashList.lookupByStoredLineIndex(
   616          this.props.scrollToStoredLineIndex as number
   617        )
   618        if (entry?.el) {
   619          entry.el.scrollIntoView({ block: "center" })
   620          this.needsScrollToLine = false
   621        }
   622      }
   623  
   624      if (this.shouldRenderForwardBuffer() || this.shouldRenderBackwardBuffer()) {
   625        this.renderBufferRafId = this.props.raf.requestAnimationFrame(
   626          this.renderBuffer
   627        )
   628      }
   629    }
   630  
   631    // Creates a DOM element with a permalink to an alert.
   632    newAlertNavEl(line: LogLine) {
   633      let div = document.createElement("button")
   634      div.className = "LogLine-alertNav"
   635      div.innerHTML = "… (more) …"
   636      div.onclick = (e) => {
   637        let storedLineIndex = line.storedLineIndex
   638        let history = this.props.history
   639        history.push(
   640          this.props.pathBuilder.encpath`/r/${line.manifestName}/overview`,
   641          { storedLineIndex }
   642        )
   643      }
   644      return div
   645    }
   646  
   647    // Helper function for rendering lines. Returns true if the line was
   648    // successfully rendered.
   649    //
   650    // If the line has already been rendered, replace the rendered line.
   651    //
   652    // If it hasn't been rendered, but the next line has, put it before the next line.
   653    //
   654    // If it hasn't been rendered, but the previous line has, put it after the previous line.
   655    //
   656    // Otherwise, iterate through the lines until we find a place to put it.
   657    renderLineHelper(line: LogLine) {
   658      let entry = this.lineHashList.lookup(line)
   659      if (!entry) {
   660        // If the entry has been removed from the hash list for some reason,
   661        // just ignore it.
   662        return
   663      }
   664  
   665      let shouldDisplayPrologues = this.props.filterSet.level !== FilterLevel.all
   666      let mn = this.props.manifestName
   667      let showManifestName = !mn || mn === ResourceName.starred
   668      let prevManifestName = entry.prev?.line.manifestName || ""
   669  
   670      let extraClasses = []
   671      let isContextChange = !!entry.prev && prevManifestName !== line.manifestName
   672      if (isContextChange) {
   673        extraClasses.push("is-contextChange")
   674      }
   675  
   676      let isEndOfAlert =
   677        shouldDisplayPrologues &&
   678        this.matchesLevelFilter(line) &&
   679        (!entry.next || entry.next?.line.level !== line.level)
   680      if (isEndOfAlert) {
   681        extraClasses.push("is-endOfAlert")
   682      }
   683  
   684      let isStartOfAlert =
   685        shouldDisplayPrologues &&
   686        !line.buildEvent &&
   687        !this.matchesLevelFilter(line) &&
   688        (!entry.prev ||
   689          this.matchesLevelFilter(entry.prev.line) ||
   690          entry.prev.line.buildEvent)
   691      if (isStartOfAlert) {
   692        extraClasses.push("is-startOfAlert")
   693      }
   694  
   695      let lineEl = newLineEl(entry.line, showManifestName, extraClasses)
   696      if (isStartOfAlert) {
   697        lineEl.appendChild(this.newAlertNavEl(entry.line))
   698      }
   699  
   700      let root = this.rootRef.current
   701      let existingLineEl = entry.el
   702      if (existingLineEl) {
   703        root.replaceChild(lineEl, existingLineEl)
   704        entry.el = lineEl
   705        return
   706      }
   707  
   708      let nextEl = entry.next?.el
   709      if (nextEl) {
   710        root.insertBefore(lineEl, nextEl)
   711        entry.el = lineEl
   712        return
   713      }
   714  
   715      let prevEl = entry.prev?.el
   716      if (prevEl) {
   717        root.insertBefore(lineEl, prevEl.nextSibling)
   718        entry.el = lineEl
   719        return
   720      }
   721  
   722      // In the worst case scenario, we iterate through all lines to find a suitable place.
   723      let cursor = this.cursorRef.current
   724      for (let i = 0; i < root.children.length; i++) {
   725        let child = root.children[i]
   726        if (
   727          child == cursor ||
   728          Number(child.getAttribute("data-sl-index")) > line.storedLineIndex
   729        ) {
   730          root.insertBefore(lineEl, child)
   731          entry.el = lineEl
   732          return
   733        }
   734      }
   735    }
   736  
   737    render() {
   738      return (
   739        <LogPaneRoot ref={this.rootRef} aria-label="Log pane">
   740          <LogEnd key="logEnd" className="logEnd" ref={this.cursorRef}>
   741            &#9608;
   742          </LogEnd>
   743        </LogPaneRoot>
   744      )
   745    }
   746  }
   747  
   748  type OverviewLogPaneProps = {
   749    manifestName: string
   750    filterSet: FilterSet
   751  }
   752  
   753  export default function OverviewLogPane(props: OverviewLogPaneProps) {
   754    let history = useHistory()
   755    let location = useLocation() as any
   756    let pathBuilder = usePathBuilder()
   757    let logStore = useLogStore()
   758    let raf = useRaf()
   759    let starredContext = useStarredResources()
   760    return (
   761      <OverviewLogComponent
   762        manifestName={props.manifestName}
   763        pathBuilder={pathBuilder}
   764        logStore={logStore}
   765        raf={raf}
   766        filterSet={props.filterSet}
   767        history={history}
   768        scrollToStoredLineIndex={location?.state?.storedLineIndex}
   769        starredResources={starredContext.starredResources}
   770      />
   771    )
   772  }