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

     1  // Types and parsing logic for log filters.
     2  
     3  import { Location } from "history"
     4  import { useLocation } from "react-router"
     5  import RegexEscape from "regex-escape"
     6  
     7  export enum FilterLevel {
     8    all = "",
     9  
    10    // Only show warnings.
    11    warn = "warn",
    12  
    13    // Only show errors.
    14    error = "error",
    15  }
    16  
    17  export enum FilterSource {
    18    all = "",
    19  
    20    // Only show build logs.
    21    build = "build",
    22  
    23    // Only show runtime logs.
    24    runtime = "runtime",
    25  }
    26  
    27  export enum TermState {
    28    Empty = "empty",
    29    Parsed = "parsed",
    30    Error = "error",
    31  }
    32  
    33  type EmptyTerm = { state: TermState.Empty }
    34  
    35  type ParsedTerm = { state: TermState.Parsed; regexp: RegExp }
    36  
    37  type ErrorTerm = { state: TermState.Error; error: string }
    38  
    39  export function isErrorTerm(
    40    term: FilterTerm
    41  ): term is { input: string } & ErrorTerm {
    42    return term.state === TermState.Error
    43  }
    44  
    45  export type FilterTerm = {
    46    input: string // Unmodified string input
    47  } & (EmptyTerm | ParsedTerm | ErrorTerm)
    48  
    49  export type FilterSet = {
    50    level: FilterLevel
    51    source: FilterSource
    52    term: FilterTerm
    53  }
    54  
    55  export const EMPTY_TERM = ""
    56  export const EMPTY_FILTER_TERM: FilterTerm = {
    57    input: EMPTY_TERM,
    58    state: TermState.Empty,
    59  }
    60  
    61  // Terms are case-insensitive.
    62  // We don't want our regexp to be stateful (so no 'g' flag).
    63  const TERM_REGEXP_FLAGS = "i"
    64  
    65  export function isRegexp(input: string): boolean {
    66    return input.length > 2 && input[0] === "/" && input[input.length - 1] === "/"
    67  }
    68  
    69  export function parseTermInput(input: string): RegExp {
    70    // Input strings that are surrounded by `/` can be parsed as regular expressions
    71    if (isRegexp(input)) {
    72      const regexpInput = input.slice(1, input.length - 1)
    73  
    74      return new RegExp(regexpInput, TERM_REGEXP_FLAGS)
    75    } else {
    76      // Input strings that aren't regular expressions should have all
    77      // special characters escaped so they can be treated literally
    78      const escapedInput = RegexEscape(input)
    79  
    80      return new RegExp(escapedInput, TERM_REGEXP_FLAGS)
    81    }
    82  }
    83  
    84  export function createFilterTermState(input: string): FilterTerm {
    85    if (!input) {
    86      return EMPTY_FILTER_TERM
    87    }
    88  
    89    try {
    90      return {
    91        input,
    92        regexp: parseTermInput(input),
    93        state: TermState.Parsed,
    94      }
    95    } catch (error: any) {
    96      let message = "Invalid regexp"
    97      if (error.message) {
    98        message += `: ${error.message}`
    99      }
   100  
   101      return {
   102        input,
   103        state: TermState.Error,
   104        error: message,
   105      }
   106    }
   107  }
   108  
   109  // Infers filter set from the history React hook.
   110  export function useFilterSet(): FilterSet {
   111    return filterSetFromLocation(useLocation())
   112  }
   113  
   114  // The source of truth for log filters is the URL.
   115  // For example,
   116  // /r/(all)/overview?level=error&source=build&term=docker
   117  // will only show errors from the build, not from the pod,
   118  // and that include the string `docker`.
   119  export function filterSetFromLocation(l: Location): FilterSet {
   120    let params = new URLSearchParams(l.search)
   121    let filters: FilterSet = {
   122      level: FilterLevel.all,
   123      source: FilterSource.all,
   124      term: EMPTY_FILTER_TERM,
   125    }
   126    switch (params.get("level")) {
   127      case FilterLevel.warn:
   128        filters.level = FilterLevel.warn
   129        break
   130      case FilterLevel.error:
   131        filters.level = FilterLevel.error
   132        break
   133    }
   134  
   135    switch (params.get("source")) {
   136      case FilterSource.build:
   137        filters.source = FilterSource.build
   138        break
   139      case FilterSource.runtime:
   140        filters.source = FilterSource.runtime
   141        break
   142    }
   143  
   144    const input = params.get("term")
   145    if (input) {
   146      filters.term = createFilterTermState(input)
   147    }
   148  
   149    return filters
   150  }
   151  
   152  export function filterSetsEqual(a: FilterSet, b: FilterSet): boolean {
   153    const sourceEqual = a.source === b.source
   154    const levelEqual = a.level === b.level
   155    // Filter terms are case-insensitive, so we can ignore casing when comparing terms
   156    const termEqual = a.term.input.toLowerCase() === b.term.input.toLowerCase()
   157    return sourceEqual && levelEqual && termEqual
   158  }