blob: 58ea1acb95c021d19e3241571e8c08961db1b3c0 [file]
// Copyright (C) 2026 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 {FuzzyFinder, FuzzySegment} from '../../base/fuzzy';
import {Accordion, AccordionItem} from '../../widgets/accordion';
import {Button} from '../../widgets/button';
import {CopyToClipboardButton} from '../../widgets/copy_to_clipboard_button';
import {Icon} from '../../widgets/icon';
import {TextInput} from '../../widgets/text_input';
import {SqlModules, SqlTable} from '../dev.perfetto.SqlModules/sql_modules';
import {
perfettoSqlTypeIcon,
perfettoSqlTypeToString,
} from '../../trace_processor/perfetto_sql_type';
import {EmptyState} from '../../widgets/empty_state';
interface FilteredTable {
table: SqlTable;
segments: FuzzySegment[];
}
function renderHighlightedName(segments: FuzzySegment[]): m.Children {
return segments.map(({matching, value}) =>
matching ? m('span.pf-simple-table-list__highlight', value) : value,
);
}
export interface TableListAttrs {
readonly sqlModules: SqlModules;
// Called when user wants to query a table in a new tab
onQueryTable?(tableName: string, query: string): void;
}
export class TableList implements m.ClassComponent<TableListAttrs> {
private searchQuery = '';
private expandedTable: string | undefined = undefined;
view({attrs}: m.CVnode<TableListAttrs>): m.Children {
const tables = attrs.sqlModules.listTables();
// Filter tables using fuzzy search (results ordered by relevance)
const searchTerm = this.searchQuery.trim();
let filteredTables: FilteredTable[];
if (searchTerm === '') {
filteredTables = tables.map((table) => ({
table,
segments: [{matching: false, value: table.name}],
}));
} else {
const finder = new FuzzyFinder(tables, (t) => t.name);
filteredTables = finder.find(searchTerm).map((result) => ({
table: result.item,
segments: result.segments,
}));
}
const items: AccordionItem[] = filteredTables.map(({table, segments}) => ({
id: table.name,
header: m(
'code.pf-simple-table-list__item-name',
renderHighlightedName(segments),
),
content: this.renderTableContent(table, attrs.onQueryTable),
}));
return m(
'.pf-simple-table-list',
m(TextInput, {
className: 'pf-simple-table-list__search',
placeholder: 'Search tables...',
value: this.searchQuery,
leftIcon: 'search',
onInput: (value) => {
this.searchQuery = value;
},
}),
items.length > 0
? m(
'.pf-simple-table-list__items',
m(Accordion, {
items,
expanded: this.expandedTable,
onToggle: (id) => {
this.expandedTable = id;
},
}),
)
: m(EmptyState, {
title: 'No matching tables found',
}),
);
}
private generateQuery(table: SqlTable): string {
const lines: string[] = [];
// Add INCLUDE statement if needed
if (table.includeKey) {
lines.push(`INCLUDE PERFETTO MODULE ${table.includeKey};`);
lines.push('');
}
// Build SELECT with all columns
const columns =
table.columns.length > 0
? table.columns.map((c) => c.name).join(',\n ')
: '*';
lines.push('SELECT');
lines.push(` ${columns}`);
lines.push(`FROM ${table.name}`);
lines.push('LIMIT 1000');
return lines.join('\n');
}
private renderTableContent(
table: SqlTable,
onQueryTable?: (tableName: string, query: string) => void,
): m.Children {
return [
// Description
table.description &&
m('.pf-simple-table-list__description', table.description),
m(
'.pf-simple-table-list__detail-row',
m('span.pf-simple-table-list__detail-label', 'Table name'),
m('code.pf-simple-table-list__detail-value', table.name),
m(CopyToClipboardButton, {
className: 'pf-show-on-hover',
textToCopy: table.name,
tooltip: 'Copy table name to clipboard',
}),
onQueryTable &&
m(Button, {
className: 'pf-show-on-hover',
icon: 'play_arrow',
compact: true,
tooltip: `SELECT * FROM ${table.name} in a new tab`,
onclick: () => onQueryTable(table.name, this.generateQuery(table)),
}),
),
// Module
table.includeKey &&
m(
'.pf-simple-table-list__detail-row',
m('span.pf-simple-table-list__detail-label', 'Include'),
m(
'code.pf-simple-table-list__detail-value',
`INCLUDE PERFETTO MODULE ${table.includeKey};`,
),
m(CopyToClipboardButton, {
className: 'pf-show-on-hover',
textToCopy: `INCLUDE PERFETTO MODULE ${table.includeKey};`,
tooltip: 'Copy include string to clipboard',
}),
),
// Columns
table.columns.length > 0 &&
m(
'.pf-simple-table-list__columns',
m('span.pf-simple-table-list__detail-label', 'Columns'),
m(
'.pf-simple-table-list__column-list',
table.columns.map((col) =>
m(
'.pf-simple-table-list__column',
m(Icon, {
icon: perfettoSqlTypeIcon(col.type),
className: 'pf-simple-table-list__column-icon',
}),
m(
'.pf-simple-table-list__column-info',
m(
'.pf-simple-table-list__column-header',
m('code.pf-simple-table-list__column-name', col.name),
m(
'.pf-simple-table-list__column-copy',
m(CopyToClipboardButton, {
textToCopy: col.name,
compact: true,
tooltip: 'Copy column name to clipboard',
}),
),
m(
'span.pf-simple-table-list__column-type',
perfettoSqlTypeToString(col.type),
),
),
col.description &&
m('.pf-simple-table-list__column-desc', col.description),
),
),
),
),
),
];
}
}