• 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 {Intent} from '../../../widgets/common';
17import {Popup, PopupPosition} from '../../../widgets/popup';
18import {EmptyState} from '../../../widgets/empty_state';
19import {Button} from '../../../widgets/button';
20import {Icons} from '../../../base/semantic_icons';
21import {Checkbox} from '../../../widgets/checkbox';
22import {SqlColumn} from '../../dev.perfetto.SqlModules/sql_modules';
23import {TextInput} from '../../../widgets/text_input';
24
25export interface ColumnControllerRow {
26  // The ID is used to indentify this option, and is used in callbacks.
27  id: string;
28  // Whether this column is selected or not.
29  checked: boolean;
30  // This is the name displayed and used for searching.
31  column: SqlColumn;
32  // What is the data source of the column. Used for formatting for SQL.
33  source?: string;
34  // Word column was renamed to.
35  alias?: string;
36}
37
38export function columnControllerRowFromSqlColumn(
39  column: SqlColumn,
40  checked: boolean = false,
41): ColumnControllerRow {
42  return {
43    id: column.name,
44    checked,
45    column: column,
46  };
47}
48
49export function columnControllerRowFromName(
50  name: string,
51  checked: boolean = false,
52): ColumnControllerRow {
53  return {
54    id: name,
55    checked,
56    column: {name: name, type: {name: 'NA', shortName: 'NA'}},
57  };
58}
59
60export function newColumnControllerRow(
61  oldCol: ColumnControllerRow,
62  checked?: boolean | undefined,
63) {
64  return {
65    id: oldCol.alias ?? oldCol.column.name,
66    column: oldCol.column,
67    alias: undefined,
68    checked: checked ?? oldCol.checked,
69  };
70}
71
72export function newColumnControllerRows(
73  oldCols: ColumnControllerRow[],
74  checked?: boolean | undefined,
75) {
76  return oldCols.map((col) => newColumnControllerRow(col, checked));
77}
78
79export interface ColumnControllerDiff {
80  id: string;
81  checked: boolean;
82  alias?: string;
83}
84
85export interface ColumnControllerAttrs {
86  options: ColumnControllerRow[];
87  onChange?: (diffs: ColumnControllerDiff[]) => void;
88  fixedSize?: boolean;
89  allowAlias?: boolean;
90}
91
92export class ColumnController
93  implements m.ClassComponent<ColumnControllerAttrs>
94{
95  view({attrs}: m.CVnode<ColumnControllerAttrs>) {
96    const {options, fixedSize = false, allowAlias = true} = attrs;
97
98    const filteredItems = options;
99
100    return m(
101      fixedSize
102        ? '.pf-column-controller-panel.pf-column-controller-fixed-size'
103        : '.pf-column-controller-panel',
104      this.renderListOfItems(attrs, filteredItems, allowAlias),
105    );
106  }
107
108  private renderListOfItems(
109    attrs: ColumnControllerAttrs,
110    options: ColumnControllerRow[],
111    allowAlias: boolean,
112  ) {
113    const {onChange = () => {}} = attrs;
114    const allChecked = options.every(({checked}) => checked);
115    const anyChecked = options.some(({checked}) => checked);
116
117    if (options.length === 0) {
118      return m(EmptyState, {
119        title: `No results.'`,
120      });
121    } else {
122      return [
123        m(
124          '.pf-list',
125          m(
126            '.pf-column-controller-container',
127            m(
128              '.pf-column-controller-header',
129              m(Button, {
130                label: 'Select All',
131                icon: Icons.SelectAll,
132                compact: true,
133                onclick: () => {
134                  const diffs = options
135                    .filter(({checked}) => !checked)
136                    .map(({id, alias}) => ({id, checked: true, alias: alias}));
137                  onChange(diffs);
138                },
139                disabled: allChecked,
140              }),
141              m(Button, {
142                label: 'Clear All',
143                icon: Icons.Deselect,
144                compact: true,
145                onclick: () => {
146                  const diffs = options
147                    .filter(({checked}) => checked)
148                    .map(({id, alias}) => ({id, checked: false, alias: alias}));
149                  onChange(diffs);
150                },
151                disabled: !anyChecked,
152              }),
153            ),
154            this.renderColumnRows(attrs, options, allowAlias),
155          ),
156        ),
157      ];
158    }
159  }
160
161  private renderColumnRows(
162    attrs: ColumnControllerAttrs,
163    options: ColumnControllerRow[],
164    allowAlias: boolean,
165  ): m.Children {
166    const {onChange = () => {}} = attrs;
167
168    return options.map((item) => {
169      const {id, checked, column, alias} = item;
170      return m(
171        '',
172        {key: id},
173        m(Checkbox, {
174          label: column.name,
175          checked,
176          className: 'pf-column-controller-item',
177          onchange: () => {
178            onChange([{id, alias, checked: !checked}]);
179          },
180        }),
181        allowAlias && [
182          ' as ',
183          m(TextInput, {
184            placeholder: item.alias ? item.alias : column.name,
185            type: 'string',
186            oninput: (e: KeyboardEvent) => {
187              if (!e.target) return;
188              onChange([
189                {
190                  id,
191                  checked,
192                  alias: (e.target as HTMLInputElement).value.trim(),
193                },
194              ]);
195            },
196          }),
197        ],
198      );
199    });
200  }
201}
202
203export type PopupColumnControllerAttrs = ColumnControllerAttrs & {
204  intent?: Intent;
205  compact?: boolean;
206  icon?: string;
207  label: string;
208  popupPosition?: PopupPosition;
209};
210
211// The same multi-select component that functions as a drop-down instead of
212// a list.
213export class PopupColumnController
214  implements m.ClassComponent<PopupColumnControllerAttrs>
215{
216  view({attrs}: m.CVnode<PopupColumnControllerAttrs>) {
217    const {icon, popupPosition = PopupPosition.Auto, intent, compact} = attrs;
218
219    return m(
220      Popup,
221      {
222        trigger: m(Button, {
223          label: this.labelText(attrs),
224          icon,
225          intent,
226          compact,
227        }),
228        position: popupPosition,
229      },
230      m(ColumnController, attrs as ColumnControllerAttrs),
231    );
232  }
233
234  private labelText(attrs: PopupColumnControllerAttrs): string {
235    const {label} = attrs;
236    return label;
237  }
238}
239
240export function hasDuplicateColumnsSelected(
241  cols: ColumnControllerRow[],
242): string[] {
243  const seenNames: {[key: string]: boolean} = {};
244  const duplicates: string[] = [];
245
246  for (const col of cols) {
247    const name = col.alias || col.column.name;
248    if (seenNames[name] && col.checked) {
249      duplicates.push(name);
250    } else {
251      seenNames[name] = true;
252    }
253  }
254
255  return duplicates;
256}
257