1// Copyright (C) 2025 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16import {Button, ButtonBar} from '../../../../widgets/button'; 17import {Intent} from '../../../../widgets/common'; 18import {isSqlColumnEqual, SqlColumn, sqlColumnId} from './sql_column'; 19import { 20 SqlValue, 21 sqlValueToSqliteString, 22} from '../../../../trace_processor/sql_utils'; 23 24// A filter which can be applied to the table. 25export interface Filter { 26 // Operation: it takes a list of column names and should return a valid SQL expression for this filter. 27 op: (cols: string[]) => string; 28 // Columns that the `op` should reference. The number of columns should match the number of interpolations in `op`. 29 columns: SqlColumn[]; 30 // Returns a human-readable title for the filter. If not set, `op` will be used. 31 // TODO(altimin): This probably should return m.Children, but currently Button expects its label to be string. 32 getTitle?(): string; 33} 34 35// A class representing a set of filters. As it's common for multiple components to share the same set of filters (e.g. 36// table viewer and associated charts), this class allows sharing the same set of filters between multiple components 37// and them being notified when the filters change. 38export class Filters { 39 private filters: Filter[] = []; 40 // Use WeakRef to allow observers to be reclaimed. 41 private observers: (() => void)[] = []; 42 43 constructor(filters: Filter[] = []) { 44 this.filters = [...filters]; 45 } 46 47 addFilter(filter: Filter) { 48 this.filters.push(filter); 49 this.notify(); 50 } 51 52 addFilters(filter: ReadonlyArray<Filter>) { 53 this.filters.push(...filter); 54 this.notify(); 55 } 56 57 removeFilter(filter: Filter) { 58 const idx = this.filters.findIndex((f) => isFilterEqual(f, filter)); 59 if (idx === -1) throw new Error('Filter not found'); 60 this.filters.splice(idx, 1); 61 this.notify(); 62 } 63 64 setFilters(filters: ReadonlyArray<Filter>) { 65 this.filters = [...filters]; 66 this.notify(); 67 } 68 69 clear() { 70 this.setFilters([]); 71 } 72 73 get(): Filter[] { 74 return this.filters; 75 } 76 77 addObserver(observer: () => void) { 78 this.observers.push(observer); 79 } 80 81 private notify() { 82 this.observers.forEach((observer) => observer()); 83 } 84} 85 86// Returns a default string representation of the filter. 87export function formatFilter(filter: Filter): string { 88 return filter.op(filter.columns.map((c) => sqlColumnId(c))); 89} 90 91// Returns a human-readable title for the filter. 92export function filterTitle(filter: Filter): string { 93 if (filter.getTitle !== undefined) { 94 return filter.getTitle(); 95 } 96 return formatFilter(filter); 97} 98 99export function isFilterEqual(a: Filter, b: Filter): boolean { 100 return ( 101 a.op === b.op && 102 a.columns.length === b.columns.length && 103 a.columns.every((c, i) => isSqlColumnEqual(c, b.columns[i])) 104 ); 105} 106 107export function areFiltersEqual( 108 a: ReadonlyArray<Filter>, 109 b: ReadonlyArray<Filter>, 110) { 111 if (a.length !== b.length) return false; 112 return a.every((f, i) => isFilterEqual(f, b[i])); 113} 114 115export function renderFilters(filters: Filters): m.Children { 116 return m( 117 ButtonBar, 118 filters.get().map((filter) => 119 m(Button, { 120 label: filterTitle(filter), 121 icon: 'close', 122 intent: Intent.Primary, 123 onclick: () => filters.removeFilter(filter), 124 }), 125 ), 126 ); 127} 128 129export class StandardFilters { 130 static valueEquals(col: SqlColumn, value: SqlValue): Filter { 131 if (value === null) { 132 return { 133 columns: [col], 134 op: (cols) => `${cols[0]} IS NULL`, 135 }; 136 } 137 return { 138 columns: [col], 139 op: (cols) => `${cols[0]} = ${sqlValueToSqliteString(value)}`, 140 }; 141 } 142} 143