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 }