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