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'; 16 17import {copyToClipboard} from '../../base/clipboard'; 18import {Icons} from '../../base/semantic_icons'; 19import {exists} from '../../base/utils'; 20import {AddDebugTrackMenu} from '../debug_tracks/add_debug_track_menu'; 21import {Button} from '../../widgets/button'; 22import {DetailsShell} from '../../widgets/details_shell'; 23import {Popup, PopupPosition} from '../../widgets/popup'; 24 25import {Filter, SqlTableState} from './state'; 26import {SqlTable} from './table'; 27import {SqlTableDescription, tableDisplayName} from './table_description'; 28import {Engine} from '../../public'; 29import {globals} from '../globals'; 30import {assertExists} from '../../base/logging'; 31import {uuidv4} from '../../base/uuid'; 32import {BottomTab, NewBottomTabArgs} from '../bottom_tab'; 33import {addEphemeralTab} from '../../common/addEphemeralTab'; 34 35interface SqlTableTabConfig { 36 table: SqlTableDescription; 37 displayName?: string; 38 filters?: Filter[]; 39} 40 41export function addSqlTableTab(config: SqlTableTabConfig): void { 42 const queryResultsTab = new SqlTableTab({ 43 config, 44 engine: getEngine(), 45 uuid: uuidv4(), 46 }); 47 48 addEphemeralTab(queryResultsTab, 'sqlTable'); 49} 50 51// TODO(stevegolton): Find a way to make this more elegant. 52function getEngine(): Engine { 53 const engConfig = globals.getCurrentEngine(); 54 const engineId = assertExists(engConfig).id; 55 return assertExists(globals.engines.get(engineId)).getProxy('QueryResult'); 56} 57 58export class SqlTableTab extends BottomTab<SqlTableTabConfig> { 59 static readonly kind = 'dev.perfetto.SqlTableTab'; 60 61 private state: SqlTableState; 62 63 constructor(args: NewBottomTabArgs<SqlTableTabConfig>) { 64 super(args); 65 66 this.state = new SqlTableState( 67 this.engine, 68 this.config.table, 69 this.config.filters, 70 ); 71 } 72 73 static create(args: NewBottomTabArgs<SqlTableTabConfig>): SqlTableTab { 74 return new SqlTableTab(args); 75 } 76 77 viewTab() { 78 const range = this.state.getDisplayedRange(); 79 const rowCount = this.state.getTotalRowCount(); 80 const navigation = [ 81 exists(range) && 82 exists(rowCount) && 83 `Showing rows ${range.from}-${range.to} of ${rowCount}`, 84 m(Button, { 85 icon: Icons.GoBack, 86 disabled: !this.state.canGoBack(), 87 onclick: () => this.state.goBack(), 88 }), 89 m(Button, { 90 icon: Icons.GoForward, 91 disabled: !this.state.canGoForward(), 92 onclick: () => this.state.goForward(), 93 }), 94 ]; 95 const {selectStatement, columns} = this.state.buildSqlSelectStatement(); 96 const addDebugTrack = m( 97 Popup, 98 { 99 trigger: m(Button, {label: 'Show debug track'}), 100 position: PopupPosition.Top, 101 }, 102 m(AddDebugTrackMenu, { 103 dataSource: { 104 sqlSource: selectStatement, 105 columns: columns, 106 }, 107 engine: this.engine, 108 }), 109 ); 110 111 return m( 112 DetailsShell, 113 { 114 title: 'Table', 115 description: this.getDisplayName(), 116 buttons: [ 117 ...navigation, 118 addDebugTrack, 119 m(Button, { 120 label: 'Copy SQL query', 121 onclick: () => 122 copyToClipboard(this.state.getNonPaginatedSQLQuery()), 123 }), 124 ], 125 }, 126 m(SqlTable, { 127 state: this.state, 128 }), 129 ); 130 } 131 132 getTitle(): string { 133 const rowCount = this.state.getTotalRowCount(); 134 const rows = rowCount === undefined ? '' : ` (${rowCount})`; 135 return `Table ${this.getDisplayName()}${rows}`; 136 } 137 138 private getDisplayName(): string { 139 return this.config.displayName ?? tableDisplayName(this.config.table); 140 } 141 142 isLoading(): boolean { 143 return this.state.isLoading(); 144 } 145} 146