• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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