// Copyright (C) 2023 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {copyToClipboard} from '../../base/clipboard'; import {Icons} from '../../base/semantic_icons'; import {exists} from '../../base/utils'; import {AddDebugTrackMenu} from '../debug_tracks/add_debug_track_menu'; import {Button} from '../../widgets/button'; import {DetailsShell} from '../../widgets/details_shell'; import {Popup, PopupPosition} from '../../widgets/popup'; import {Filter, SqlTableState} from './state'; import {SqlTable} from './table'; import {SqlTableDescription, tableDisplayName} from './table_description'; import {Engine} from '../../public'; import {globals} from '../globals'; import {assertExists} from '../../base/logging'; import {uuidv4} from '../../base/uuid'; import {BottomTab, NewBottomTabArgs} from '../bottom_tab'; import {addEphemeralTab} from '../../common/addEphemeralTab'; interface SqlTableTabConfig { table: SqlTableDescription; displayName?: string; filters?: Filter[]; } export function addSqlTableTab(config: SqlTableTabConfig): void { const queryResultsTab = new SqlTableTab({ config, engine: getEngine(), uuid: uuidv4(), }); addEphemeralTab(queryResultsTab, 'sqlTable'); } // TODO(stevegolton): Find a way to make this more elegant. function getEngine(): Engine { const engConfig = globals.getCurrentEngine(); const engineId = assertExists(engConfig).id; return assertExists(globals.engines.get(engineId)).getProxy('QueryResult'); } export class SqlTableTab extends BottomTab { static readonly kind = 'dev.perfetto.SqlTableTab'; private state: SqlTableState; constructor(args: NewBottomTabArgs) { super(args); this.state = new SqlTableState( this.engine, this.config.table, this.config.filters, ); } static create(args: NewBottomTabArgs): SqlTableTab { return new SqlTableTab(args); } viewTab() { const range = this.state.getDisplayedRange(); const rowCount = this.state.getTotalRowCount(); const navigation = [ exists(range) && exists(rowCount) && `Showing rows ${range.from}-${range.to} of ${rowCount}`, m(Button, { icon: Icons.GoBack, disabled: !this.state.canGoBack(), onclick: () => this.state.goBack(), }), m(Button, { icon: Icons.GoForward, disabled: !this.state.canGoForward(), onclick: () => this.state.goForward(), }), ]; const {selectStatement, columns} = this.state.buildSqlSelectStatement(); const addDebugTrack = m( Popup, { trigger: m(Button, {label: 'Show debug track'}), position: PopupPosition.Top, }, m(AddDebugTrackMenu, { dataSource: { sqlSource: selectStatement, columns: columns, }, engine: this.engine, }), ); return m( DetailsShell, { title: 'Table', description: this.getDisplayName(), buttons: [ ...navigation, addDebugTrack, m(Button, { label: 'Copy SQL query', onclick: () => copyToClipboard(this.state.getNonPaginatedSQLQuery()), }), ], }, m(SqlTable, { state: this.state, }), ); } getTitle(): string { const rowCount = this.state.getTotalRowCount(); const rows = rowCount === undefined ? '' : ` (${rowCount})`; return `Table ${this.getDisplayName()}${rows}`; } private getDisplayName(): string { return this.config.displayName ?? tableDisplayName(this.config.table); } isLoading(): boolean { return this.state.isLoading(); } }