1// Copyright (C) 2023 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 {copyToClipboard} from '../../base/clipboard'; 17import {Icons} from '../../base/semantic_icons'; 18import {exists} from '../../base/utils'; 19import {Button, ButtonBar} from '../../widgets/button'; 20import {DetailsShell} from '../../widgets/details_shell'; 21import {Popup, PopupPosition} from '../../widgets/popup'; 22import {AddDebugTrackMenu} from '../tracks/add_debug_track_menu'; 23import {SqlTableState} from '../widgets/sql/table/state'; 24import {SqlTable} from '../widgets/sql/table/table'; 25import {SqlTableDescription} from '../widgets/sql/table/table_description'; 26import {Trace} from '../../public/trace'; 27import {MenuItem, PopupMenu} from '../../widgets/menu'; 28import {addEphemeralTab} from './add_ephemeral_tab'; 29import {Tab} from '../../public/tab'; 30import {addChartTab} from '../widgets/charts/chart_tab'; 31import {ChartType} from '../widgets/charts/chart'; 32import {AddChartMenuItem} from '../widgets/charts/add_chart_menu'; 33import {Filter, Filters, renderFilters} from '../widgets/sql/table/filters'; 34import {PivotTableState} from '../widgets/sql/pivot_table/pivot_table_state'; 35import {TableColumn} from '../widgets/sql/table/table_column'; 36import {PivotTable} from '../widgets/sql/pivot_table/pivot_table'; 37import {pivotId} from '../widgets/sql/pivot_table/ids'; 38 39export interface AddSqlTableTabParams { 40 table: SqlTableDescription; 41 filters?: Filter[]; 42 imports?: string[]; 43} 44 45export function addLegacyTableTab( 46 trace: Trace, 47 config: AddSqlTableTabParams, 48): void { 49 addSqlTableTabWithState( 50 new SqlTableState(trace, config.table, { 51 filters: new Filters(config.filters), 52 imports: config.imports, 53 }), 54 ); 55} 56 57function addSqlTableTabWithState(state: SqlTableState) { 58 addEphemeralTab('sqlTable', new LegacySqlTableTab(state)); 59} 60 61class LegacySqlTableTab implements Tab { 62 constructor(private readonly state: SqlTableState) { 63 this.selected = state; 64 } 65 66 private selected: SqlTableState | PivotTableState; 67 68 private pivots: PivotTableState[] = []; 69 70 private getTableButtons() { 71 const range = this.state.getDisplayedRange(); 72 const rowCount = this.state.getTotalRowCount(); 73 const navigation = [ 74 exists(range) && 75 exists(rowCount) && 76 `Showing rows ${range.from}-${range.to} of ${rowCount}`, 77 m(Button, { 78 icon: Icons.GoBack, 79 disabled: !this.state.canGoBack(), 80 onclick: () => this.state.goBack(), 81 }), 82 m(Button, { 83 icon: Icons.GoForward, 84 disabled: !this.state.canGoForward(), 85 onclick: () => this.state.goForward(), 86 }), 87 ]; 88 const {selectStatement, columns} = this.state.getCurrentRequest(); 89 const debugTrackColumns = Object.values(columns).filter( 90 (c) => !c.startsWith('__'), 91 ); 92 const addDebugTrack = m( 93 Popup, 94 { 95 trigger: m(Button, {label: 'Show debug track'}), 96 position: PopupPosition.Top, 97 }, 98 m(AddDebugTrackMenu, { 99 trace: this.state.trace, 100 dataSource: { 101 sqlSource: `SELECT ${debugTrackColumns.join(', ')} FROM (${selectStatement})`, 102 columns: debugTrackColumns, 103 }, 104 }), 105 ); 106 return [ 107 ...navigation, 108 addDebugTrack, 109 m( 110 PopupMenu, 111 { 112 trigger: m(Button, { 113 icon: Icons.Menu, 114 }), 115 }, 116 m(MenuItem, { 117 label: 'Duplicate', 118 icon: 'tab_duplicate', 119 onclick: () => addSqlTableTabWithState(this.state.clone()), 120 }), 121 m(MenuItem, { 122 label: 'Copy SQL query', 123 icon: Icons.Copy, 124 onclick: () => copyToClipboard(this.state.getNonPaginatedSQLQuery()), 125 }), 126 ), 127 ]; 128 } 129 130 private tableMenuItems(column: TableColumn, alias: string) { 131 const chartAttrs = { 132 data: this.state.nonPaginatedData?.rows, 133 columns: [alias], 134 }; 135 136 return [ 137 m(AddChartMenuItem, { 138 chartOptions: [ 139 { 140 chartType: ChartType.BAR_CHART, 141 ...chartAttrs, 142 }, 143 { 144 chartType: ChartType.HISTOGRAM, 145 ...chartAttrs, 146 }, 147 ], 148 addChart: (chart) => addChartTab(chart), 149 }), 150 m(MenuItem, { 151 label: 'Pivot', 152 onclick: () => { 153 const state = new PivotTableState({ 154 pivots: [column], 155 table: this.state.config, 156 trace: this.state.trace, 157 filters: this.state.filters, 158 }); 159 this.selected = state; 160 this.pivots.push(state); 161 }, 162 }), 163 ]; 164 } 165 166 render() { 167 return m( 168 DetailsShell, 169 { 170 title: 'Table', 171 description: this.getDisplayName(), 172 buttons: this.getTableButtons(), 173 }, 174 m('div', renderFilters(this.state.filters)), 175 this.pivots.length > 0 && 176 m( 177 ButtonBar, 178 m(Button, { 179 label: 'Table', 180 active: this.selected === this.state, 181 onclick: () => { 182 this.selected = this.state; 183 }, 184 }), 185 this.pivots.map((pivot) => 186 m(Button, { 187 label: `Pivot: ${pivot.getPivots().map(pivotId).join(', ')}`, 188 active: this.selected === pivot, 189 onclick: () => { 190 this.selected = pivot; 191 }, 192 }), 193 ), 194 ), 195 this.selected === this.state && 196 m(SqlTable, { 197 state: this.state, 198 addColumnMenuItems: this.tableMenuItems.bind(this), 199 }), 200 this.selected instanceof PivotTableState && 201 m(PivotTable, { 202 state: this.selected, 203 extraRowButton: (node) => 204 // Do not show any buttons for root as it doesn't have any filters anyway. 205 !node.isRoot() && 206 m( 207 PopupMenu, 208 { 209 trigger: m(Button, { 210 icon: Icons.GoTo, 211 }), 212 }, 213 m(MenuItem, { 214 label: 'Add filters', 215 onclick: () => { 216 this.state.filters.addFilters(node.getFilters()); 217 }, 218 }), 219 m(MenuItem, { 220 label: 'Open tab with filters', 221 onclick: () => { 222 const newState = this.state.clone(); 223 newState.filters.addFilters(node.getFilters()); 224 addSqlTableTabWithState(newState); 225 }, 226 }), 227 ), 228 }), 229 ); 230 } 231 232 getTitle(): string { 233 const rowCount = this.state.getTotalRowCount(); 234 const rows = rowCount === undefined ? '' : ` (${rowCount})`; 235 return `Table ${this.getDisplayName()}${rows}`; 236 } 237 238 private getDisplayName(): string { 239 return this.state.config.displayName ?? this.state.config.name; 240 } 241 242 isLoading(): boolean { 243 return this.state.isLoading(); 244 } 245} 246