blob: e4fc6ceb46bde741c61c9aa360ed078f9b9ec96e [file] [log] [blame] [edit]
// Copyright (C) 2025 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 {findRef, toHTMLElement} from '../../base/dom_utils';
import {assertExists} from '../../base/logging';
import {Icons} from '../../base/semantic_icons';
import {QueryResponse} from '../../components/query_table/queries';
import {DataGrid, renderCell} from '../../components/widgets/datagrid/datagrid';
import {
CellRenderer,
ColumnSchema,
SchemaRegistry,
} from '../../components/widgets/datagrid/column_schema';
import {InMemoryDataSource} from '../../components/widgets/datagrid/in_memory_data_source';
import {QueryHistoryComponent} from '../../components/widgets/query_history';
import {Trace} from '../../public/trace';
import {Box} from '../../widgets/box';
import {Button, ButtonVariant} from '../../widgets/button';
import {Callout} from '../../widgets/callout';
import {Intent} from '../../widgets/common';
import {Editor} from '../../widgets/editor';
import {HotkeyGlyphs} from '../../widgets/hotkey_glyphs';
import {ResizeHandle} from '../../widgets/resize_handle';
import {Stack, StackAuto} from '../../widgets/stack';
import {CopyToClipboardButton} from '../../widgets/copy_to_clipboard_button';
import {Anchor} from '../../widgets/anchor';
import {getSliceId, isSliceish} from '../../components/query_table/query_table';
import {DataSource} from '../../components/widgets/datagrid/data_source';
const HIDE_PERFETTO_SQL_AGENT_BANNER_KEY = 'hidePerfettoSqlAgentBanner';
export interface QueryPageAttrs {
readonly trace: Trace;
readonly editorText: string;
readonly executedQuery?: string;
readonly queryResult?: QueryResponse;
onEditorContentUpdate?(content: string): void;
onExecute?(query: string): void;
}
export class QueryPage implements m.ClassComponent<QueryPageAttrs> {
private dataSource?: DataSource;
private editorHeight: number = 0;
private editorElement?: HTMLElement;
oncreate({dom}: m.VnodeDOM<QueryPageAttrs>) {
this.editorElement = toHTMLElement(assertExists(findRef(dom, 'editor')));
this.editorElement.style.height = '200px';
}
onbeforeupdate(
vnode: m.Vnode<QueryPageAttrs>,
oldVnode: m.Vnode<QueryPageAttrs>,
) {
// Update the datasource if present
if (vnode.attrs.queryResult !== oldVnode.attrs.queryResult) {
if (vnode.attrs.queryResult) {
this.dataSource = new InMemoryDataSource(vnode.attrs.queryResult.rows);
} else {
this.dataSource = undefined;
}
}
}
view({attrs}: m.CVnode<QueryPageAttrs>) {
return m(
'.pf-query-page',
m(Box, {className: 'pf-query-page__toolbar'}, [
m(Stack, {orientation: 'horizontal'}, [
m(Button, {
label: 'Run Query',
icon: 'play_arrow',
intent: Intent.Primary,
variant: ButtonVariant.Filled,
onclick: () => {
attrs.onExecute?.(attrs.editorText);
},
}),
m(
Stack,
{
orientation: 'horizontal',
className: 'pf-query-page__hotkeys',
},
'or press',
m(HotkeyGlyphs, {hotkey: 'Mod+Enter'}),
),
m(StackAuto), // The spacer pushes the following buttons to the right.
attrs.trace.isInternalUser &&
m(Button, {
icon: 'wand_stars',
title:
'Generate SQL queries with the Perfetto SQL Agent! Give feedback: go/perfetto-llm-bug',
label: 'Generate SQL Queries with AI',
onclick: () => {
window.open('http://go/perfetto-sql-agent', '_blank');
},
}),
m(CopyToClipboardButton, {
textToCopy: attrs.editorText,
title: 'Copy query to clipboard',
label: 'Copy Query',
}),
]),
]),
this.shouldDisplayPerfettoSqlAgentBanner(attrs) &&
m(
Box,
m(
Callout,
{
icon: 'wand_stars',
dismissible: true,
onDismiss: () => {
this.hidePerfettoSqlAgentBanner();
},
},
[
'Try out the ',
m(
Anchor,
{
href: 'http://go/perfetto-sql-agent',
target: '_blank',
icon: Icons.ExternalLink,
},
'Perfetto SQL Agent',
),
' to generate SQL queries and ',
m(
Anchor,
{
href: 'http://go/perfetto-llm-user-guide#report-issues',
target: '_blank',
icon: Icons.ExternalLink,
},
'give feedback',
),
'!',
],
),
),
attrs.editorText.includes('"') &&
m(
Box,
m(
Callout,
{icon: 'warning', intent: Intent.None},
`" (double quote) character observed in query; if this is being used to ` +
`define a string, please use ' (single quote) instead. Using double quotes ` +
`can cause subtle problems which are very hard to debug.`,
),
),
m(Editor, {
ref: 'editor',
language: 'perfetto-sql',
text: attrs.editorText,
onUpdate: attrs.onEditorContentUpdate,
onExecute: attrs.onExecute,
}),
m(ResizeHandle, {
onResize: (deltaPx: number) => {
this.editorHeight += deltaPx;
this.editorElement!.style.height = `${this.editorHeight}px`;
},
onResizeStart: () => {
this.editorHeight = this.editorElement!.clientHeight;
},
}),
this.dataSource &&
attrs.queryResult &&
this.renderQueryResult(attrs.trace, attrs.queryResult, this.dataSource),
m(QueryHistoryComponent, {
className: 'pf-query-page__history',
trace: attrs.trace,
runQuery: (query: string) => {
attrs.onExecute?.(query);
},
setQuery: (query: string) => {
attrs.onEditorContentUpdate?.(query);
},
}),
);
}
private renderQueryResult(
trace: Trace,
queryResult: QueryResponse,
dataSource: DataSource,
) {
const queryTimeString = `${queryResult.durationMs.toFixed(1)} ms`;
if (queryResult.error) {
return m(
'.pf-query-page__query-error',
`SQL error: ${queryResult.error}`,
);
} else {
return [
queryResult.statementWithOutputCount > 1 &&
m(Box, [
m(Callout, {icon: 'warning', intent: Intent.None}, [
`${queryResult.statementWithOutputCount} out of ${queryResult.statementCount} `,
'statements returned a result. ',
'Only the results for the last statement are displayed.',
]),
]),
(() => {
// Build schema directly
const columnSchema: ColumnSchema = {};
for (const column of queryResult.columns) {
const cellRenderer: CellRenderer | undefined =
column === 'id'
? (value, row) => {
const sliceId = getSliceId(row);
const cell = renderCell(value, column);
if (sliceId !== undefined && isSliceish(row)) {
return m(
Anchor,
{
title: 'Go to slice on the timeline',
icon: Icons.UpdateSelection,
onclick: () => {
// Navigate to the timeline page
trace.navigate('#!/viewer');
trace.selection.selectSqlEvent('slice', sliceId, {
switchToCurrentSelectionTab: false,
scrollToSelection: true,
});
},
},
cell,
);
} else {
return renderCell(value, column);
}
}
: undefined;
columnSchema[column] = {cellRenderer};
}
const schema: SchemaRegistry = {data: columnSchema};
return m(DataGrid, {
schema,
rootSchema: 'data',
initialColumns: queryResult.columns,
className: 'pf-query-page__results',
data: dataSource,
showExportButton: true,
toolbarItemsLeft: m(
'span.pf-query-page__results-summary',
`Returned ${queryResult.totalRowCount.toLocaleString()} rows in ${queryTimeString}`,
),
toolbarItemsRight: m(CopyToClipboardButton, {
textToCopy: queryResult.query,
title: 'Copy executed query to clipboard',
label: 'Copy Query',
}),
});
})(),
];
}
}
private shouldDisplayPerfettoSqlAgentBanner(attrs: QueryPageAttrs) {
return (
attrs.trace.isInternalUser &&
localStorage.getItem(HIDE_PERFETTO_SQL_AGENT_BANNER_KEY) !== 'true'
);
}
private hidePerfettoSqlAgentBanner() {
localStorage.setItem(HIDE_PERFETTO_SQL_AGENT_BANNER_KEY, 'true');
}
}