xref: /aosp_15_r20/external/perfetto/ui/src/components/widgets/sql/table/argument_selector.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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 {raf} from '../../../../core/raf_scheduler';
17import {Spinner} from '../../../../widgets/spinner';
18import {
19  TableColumn,
20  tableColumnId,
21  TableColumnSet,
22  TableManager,
23} from './column';
24import {TextInput} from '../../../../widgets/text_input';
25import {scheduleFullRedraw} from '../../../../widgets/raf';
26import {hasModKey, modKey} from '../../../../base/hotkeys';
27import {MenuItem} from '../../../../widgets/menu';
28import {uuidv4} from '../../../../base/uuid';
29
30const MAX_ARGS_TO_DISPLAY = 15;
31
32interface ArgumentSelectorAttrs {
33  tableManager: TableManager;
34  columnSet: TableColumnSet;
35  alreadySelectedColumnIds: Set<string>;
36  onArgumentSelected: (column: TableColumn) => void;
37}
38
39// This class is responsible for rendering a menu which allows user to select which column out of ColumnSet to add.
40export class ArgumentSelector
41  implements m.ClassComponent<ArgumentSelectorAttrs>
42{
43  searchText = '';
44  columns?: {key: string; column: TableColumn | TableColumnSet}[];
45
46  constructor({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
47    this.load(attrs);
48  }
49
50  private async load(attrs: ArgumentSelectorAttrs) {
51    this.columns = await attrs.columnSet.discover(attrs.tableManager);
52    raf.scheduleFullRedraw();
53  }
54
55  view({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
56    const columns = this.columns;
57    if (columns === undefined) return m(Spinner);
58
59    // Candidates are the columns which have not been selected yet.
60    const candidates = columns.filter(
61      ({column}) =>
62        column instanceof TableColumnSet ||
63        !attrs.alreadySelectedColumnIds.has(tableColumnId(column)),
64    );
65
66    // Filter the candidates based on the search text.
67    const filtered = candidates.filter(({key}) => {
68      return key.toLowerCase().includes(this.searchText.toLowerCase());
69    });
70
71    const displayed = filtered.slice(0, MAX_ARGS_TO_DISPLAY);
72
73    const extraItems = Math.max(0, filtered.length - MAX_ARGS_TO_DISPLAY);
74
75    const firstButtonUuid = uuidv4();
76
77    return [
78      m(
79        '.pf-search-bar',
80        m(TextInput, {
81          autofocus: true,
82          oninput: (event: Event) => {
83            const eventTarget = event.target as HTMLTextAreaElement;
84            this.searchText = eventTarget.value;
85            scheduleFullRedraw();
86          },
87          onkeydown: (event: KeyboardEvent) => {
88            if (filtered.length === 0) return;
89            if (event.key === 'Enter') {
90              // If there is only one item or Mod-Enter was pressed, select the first element.
91              if (filtered.length === 1 || hasModKey(event)) {
92                const params = {bubbles: true};
93                if (hasModKey(event)) {
94                  Object.assign(params, modKey());
95                }
96                const pointerEvent = new PointerEvent('click', params);
97                (
98                  document.getElementById(firstButtonUuid) as HTMLElement | null
99                )?.dispatchEvent(pointerEvent);
100              }
101            }
102          },
103          value: this.searchText,
104          placeholder: 'Filter...',
105          className: 'pf-search-box',
106        }),
107      ),
108      ...displayed.map(({key, column}, index) =>
109        m(
110          MenuItem,
111          {
112            id: index === 0 ? firstButtonUuid : undefined,
113            label: key,
114            onclick: (event) => {
115              if (column instanceof TableColumnSet) return;
116              attrs.onArgumentSelected(column);
117              // For Control-Click, we don't want to close the menu to allow the user
118              // to select multiple items in one go.
119              if (hasModKey(event)) {
120                event.stopPropagation();
121              }
122              // Otherwise this popup will be closed.
123            },
124          },
125          column instanceof TableColumnSet &&
126            m(ArgumentSelector, {
127              columnSet: column,
128              alreadySelectedColumnIds: attrs.alreadySelectedColumnIds,
129              onArgumentSelected: attrs.onArgumentSelected,
130              tableManager: attrs.tableManager,
131            }),
132        ),
133      ),
134      Boolean(extraItems) && m('i', `+${extraItems} more`),
135    ];
136  }
137}
138