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 { 17 ColumnController, 18 ColumnControllerDiff, 19 ColumnControllerRow, 20} from '../column_controller'; 21import {Section} from '../../../../widgets/section'; 22import {Select} from '../../../../widgets/select'; 23import {TextInput} from '../../../../widgets/text_input'; 24import {Button} from '../../../../widgets/button'; 25import protos from '../../../../protos'; 26 27export interface GroupByAgg { 28 column?: ColumnControllerRow; 29 aggregationOp: string; 30 newColumnName?: string; 31} 32 33export interface GroupByAttrs { 34 groupByColumns: ColumnControllerRow[]; 35 aggregations: GroupByAgg[]; 36} 37 38const AGGREGATION_OPS = [ 39 'COUNT', 40 'SUM', 41 'MIN', 42 'MAX', 43 'MEAN', 44 'DURATION_WEIGHTED_MEAN', 45] as const; 46 47export class GroupByOperation implements m.ClassComponent<GroupByAttrs> { 48 view({attrs}: m.CVnode<GroupByAttrs>) { 49 if (attrs.groupByColumns.length === 0) { 50 return; 51 } 52 53 const selectGroupByColumns = (): m.Child => { 54 return m(ColumnController, { 55 options: attrs.groupByColumns, 56 allowAlias: false, 57 onChange: (diffs: ColumnControllerDiff[]) => { 58 for (const diff of diffs) { 59 const column = attrs.groupByColumns.find((c) => c.id === diff.id); 60 if (column) { 61 column.checked = diff.checked; 62 if (!diff.checked) { 63 attrs.aggregations = attrs.aggregations?.filter( 64 (agg) => agg.column?.id !== diff.id, 65 ); 66 } 67 } 68 } 69 }, 70 }); 71 }; 72 73 const selectAggregationForColumn = ( 74 agg: GroupByAgg, 75 index: number, 76 ): m.Child => { 77 const columnOptions = attrs.groupByColumns.map((col) => 78 m( 79 'option', 80 { 81 value: col.id, 82 selected: agg.column?.id === col.id, 83 }, 84 col.id, 85 ), 86 ); 87 88 return m( 89 Section, 90 { 91 title: `Aggregation ${index + 1}`, 92 key: index, 93 }, 94 m(Button, { 95 label: 'X', 96 onclick: () => { 97 attrs.aggregations?.splice(index, 1); 98 }, 99 }), 100 m( 101 'Column:', 102 m( 103 Select, 104 { 105 onchange: (e: Event) => { 106 const target = e.target as HTMLSelectElement; 107 const selectedColumn = attrs.groupByColumns.find( 108 (c) => c.id === target.value, 109 ); 110 agg.column = selectedColumn; 111 }, 112 }, 113 m( 114 'option', 115 {disabled: true, selected: !agg.column}, 116 'Select a column', 117 ), 118 columnOptions, 119 ), 120 ), 121 m( 122 Select, 123 { 124 title: 'Aggregation type: ', 125 onchange: (e: Event) => { 126 agg.aggregationOp = (e.target as HTMLSelectElement).value; 127 }, 128 }, 129 AGGREGATION_OPS.map((op) => 130 m( 131 'option', 132 { 133 value: op, 134 selected: op === agg.aggregationOp, 135 }, 136 op, 137 ), 138 ), 139 ), 140 m(TextInput, { 141 title: 'New column name', 142 placeholder: agg.column 143 ? placeholderNewColumnName(agg) 144 : 'Enter column name', 145 onchange: (e: Event) => { 146 agg.newColumnName = (e.target as HTMLInputElement).value.trim(); 147 }, 148 value: agg.newColumnName, 149 }), 150 ); 151 }; 152 153 const onAddAggregation = () => { 154 attrs.aggregations.push({ 155 aggregationOp: AGGREGATION_OPS[0], 156 column: undefined, 157 newColumnName: undefined, 158 }); 159 }; 160 161 const selectAggregations = (): m.Child => { 162 return m( 163 '', 164 attrs.aggregations.map((agg, index) => 165 selectAggregationForColumn(agg, index), 166 ), 167 m(Button, { 168 label: 'Add Aggregation', 169 onclick: onAddAggregation, 170 }), 171 ); 172 }; 173 174 return m( 175 '', 176 m(Section, {title: 'Columns for group by'}, selectGroupByColumns()), 177 m(Section, {title: 'Aggregations'}, selectAggregations()), 178 ); 179 } 180} 181 182function stringToAggregateOp( 183 s: string, 184): protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate.Op { 185 if (AGGREGATION_OPS.includes(s as (typeof AGGREGATION_OPS)[number])) { 186 return protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate.Op[ 187 s as keyof typeof protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate.Op 188 ]; 189 } 190 throw new Error(`Invalid AggregateOp '${s}'`); 191} 192 193export function GroupByAggregationAttrsToProto( 194 agg: GroupByAgg, 195): protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate { 196 const newAgg = new protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate(); 197 newAgg.columnName = agg.column!.column.name; 198 newAgg.op = stringToAggregateOp(agg.aggregationOp); 199 newAgg.resultColumnName = agg.newColumnName ?? placeholderNewColumnName(agg); 200 return newAgg; 201} 202 203export function placeholderNewColumnName(agg: GroupByAgg) { 204 return agg.column 205 ? `${agg.column.id}_${agg.aggregationOp}` 206 : `agg_${agg.aggregationOp}`; 207} 208