blob: 2e2f1a8f597e51a5deafeeb81fe0e7a006e3c91e [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 {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 {SplitPanel} from '../../widgets/split_panel';
import {Stack, StackAuto} from '../../widgets/stack';
import {Switch} from '../../widgets/switch';
import {TextInput} from '../../widgets/text_input';
import {InMemoryDataSource} from '../../components/widgets/datagrid/in_memory_data_source';
import {getBigtraceEndpoint} from '../settings/endpoint_storage';
import {BigtraceAsyncDataSource} from '../query/bigtrace_async_data_source';
import {setHistoryActiveTab} from '../query/query_history';
import {formatPerfettoSql} from '../query/sql_formatter';
import {BigtraceQueryClient} from '../query/bigtrace_query_client';
import type {QueryRunner} from '../query/query_runner';
import {
type BigTraceEditorTab,
type QueryTabsState,
deriveTitleFromQuery,
effectiveTabSettings,
} from './query_tabs_state';
import {renderResultsPanel} from './results_panel';
import type {SettingCategory, SettingFilter} from '../settings/settings_types';
import type {SettingsBindings} from '../settings/tab_bound_setting';
import {BigtraceSettingsBar} from './bigtrace_settings_bar';
export interface EditorTabViewAttrs {
readonly tab: BigTraceEditorTab;
readonly tabsState: QueryTabsState;
readonly runner: QueryRunner;
readonly useBigtraceBackend: boolean;
}
// Split pane with editor on top, results on bottom.
// Rendering lives in results_panel.ts and status_box.ts.
export class EditorTabView implements m.ClassComponent<EditorTabViewAttrs> {
view({attrs}: m.Vnode<EditorTabViewAttrs>): m.Children {
const {tab, tabsState, runner, useBigtraceBackend} = attrs;
// Tabs reopened from history wire up their dataSource on first render.
if (tab.queryUuid && !tab.dataSource) {
attachAsyncDataSource(tab, runner);
}
if (tab.dataSource && tab.queryResult && tab.materialize && tab.execution) {
tab.queryResult.totalRowCount = tab.execution.processedRows;
}
return m('.pf-bt-editor-tab', [
m(BigtraceSettingsBar, {
tab,
tabsState,
bindings: buildTabBindings(tab, tabsState),
}),
m(SplitPanel, {
direction: 'vertical',
initialSplit: {percent: 22},
minSize: 100,
firstPanel: renderEditorPanel(
tab,
tabsState,
runner,
useBigtraceBackend,
),
secondPanel: renderResultsPanel(tab, tabsState),
}),
]);
}
}
// ---------------------------------------------------------------------------
// Per-tab bindings shared between the chip strip and any modal it opens.
// Getters read live; setters mutate in place and mark dirty.
// getEffectiveSettings layers per-tab overrides over global defaults so
// /trace_metadata sees a complete settings array even before the user edits.
// ---------------------------------------------------------------------------
function buildTabBindings(
tab: BigTraceEditorTab,
tabsState: QueryTabsState,
): SettingsBindings {
return {
getEffectiveSettings: () => effectiveTabSettings(tab),
getSettingValue: (id) => {
const entry = tab.querySettings.find((s) => s.settingId === id);
return entry?.values;
},
setSettingValue: (id, values, category) => {
const next = [...tab.querySettings];
const idx = next.findIndex((s) => s.settingId === id);
const entry: SettingFilter = {
settingId: id,
values: [...values],
category: category as SettingCategory,
};
if (idx >= 0) next[idx] = entry;
else next.push(entry);
tab.querySettings = next;
tabsState.markDirty();
},
getTraceFilters: () => tab.traceFilters,
setTraceFilters: (filters) => {
tab.traceFilters = [...filters];
tabsState.markDirty();
},
getTraceMetadataColumns: () => tab.traceMetadataColumns,
setTraceMetadataColumns: (cols) => {
tab.traceMetadataColumns = cols === null ? null : [...cols];
tabsState.markDirty();
},
getTraceOrderBy: () => tab.traceOrderBy,
setTraceOrderBy: (orderBy) => {
tab.traceOrderBy = orderBy;
tabsState.markDirty();
},
isSettingDisabled: (id) => tab.disabledSettings.includes(id),
setSettingDisabled: (id, disabled) => {
const set = new Set(tab.disabledSettings);
if (disabled) set.add(id);
else set.delete(id);
tab.disabledSettings = [...set];
tabsState.markDirty();
},
getSql: () => tab.editorText,
setQueryAndTitle: (perfettoSql, title) => {
tab.editorText = perfettoSql;
// A title other than "Query N" sticks — maybeAutoNameTab won't replace it.
if (title) tab.title = title;
tabsState.markDirty();
m.redraw();
},
};
}
// Format a tab's query in place; no-op when formatting fails (the error is
// logged) or the text is already clean. Bound to Alt+Shift+F in the editor.
async function formatTabQuery(
tab: BigTraceEditorTab,
tabsState: QueryTabsState,
text: string,
): Promise<void> {
const formatted = await formatPerfettoSql(text);
if (formatted === undefined || formatted === tab.editorText) return;
tab.editorText = formatted;
tabsState.markDirty();
m.redraw();
}
// ---------------------------------------------------------------------------
// Editor panel: toolbar (Run/Cancel + limit + Persistent) and the editor.
// ---------------------------------------------------------------------------
function renderEditorPanel(
tab: BigTraceEditorTab,
tabsState: QueryTabsState,
runner: QueryRunner,
useBigtraceBackend: boolean,
): m.Children {
return m('.pf-bt-query-page__editor-panel', [
m(Box, {className: 'pf-bt-query-page__toolbar'}, [
m(Stack, {orientation: 'horizontal'}, [
tab.isLoading
? m(Button, {
label: 'Cancel',
icon: 'stop',
intent: Intent.Warning,
variant: ButtonVariant.Filled,
onclick: () => runner.cancel(tab),
})
: m(Button, {
label: 'Run Query',
icon: 'play_arrow',
intent: Intent.Primary,
variant: ButtonVariant.Filled,
disabled: deriveTitleFromQuery(tab.editorText) === undefined,
onclick: () => {
setHistoryActiveTab(tab.materialize);
tabsState.maybeAutoNameTab(tab.id, tab.editorText);
runner.run(tab, tab.editorText);
},
}),
m(
Stack,
{orientation: 'horizontal', className: 'pf-bt-query-page__hotkeys'},
'or press',
m(HotkeyGlyphs, {hotkey: 'Mod+Enter'}),
),
m(StackAuto),
// Icon-only to keep the toolbar lean; the editor binds the same chord.
m(Button, {
icon: 'format_align_left',
title: 'Format query (Alt+Shift+F)',
disabled: deriveTitleFromQuery(tab.editorText) === undefined,
onclick: () => void formatTabQuery(tab, tabsState, tab.editorText),
}),
useBigtraceBackend && [
m('span.pf-bt-toolbar-divider', {'aria-hidden': 'true'}),
m(Switch, {
label: 'Persistent',
title:
'ON: results saved to History (Persistent tab) — reopen later. ' +
'OFF: results shown inline and discarded when the tab closes.',
checked: tab.materialize,
disabled: tab.isLoading,
onchange: (e: Event) => {
tab.materialize = (e.target as HTMLInputElement).checked;
setHistoryActiveTab(tab.materialize);
tabsState.markDirty();
},
}),
m('span.pf-bt-toolbar-divider', {'aria-hidden': 'true'}),
m('span', 'Limit:'),
m(TextInput, {
type: 'number',
value: String(tab.limit),
placeholder: 'Limit',
disabled: tab.isLoading,
onInput: (value: string) => {
const newLimit = parseInt(value, 10);
if (!isNaN(newLimit) && newLimit > 0) {
tab.limit = newLimit;
}
},
}),
],
]),
]),
tab.editorText.includes('"') &&
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, {
text: tab.editorText,
language: 'perfetto-sql',
autofocus: true,
onSave: () => {},
// Alt+Shift+F (option+shift+F on Mac); listed in the help modal.
onFormat: (text: string) => formatTabQuery(tab, tabsState, text),
onUpdate: (text: string) => {
tab.editorText = text;
tabsState.markDirty();
},
onExecute: (query: string) => {
setHistoryActiveTab(tab.materialize);
tabsState.maybeAutoNameTab(tab.id, query);
runner.run(tab, query);
},
}),
]);
}
// ---------------------------------------------------------------------------
// Lazily build the data source for tabs restored from localStorage.
// ---------------------------------------------------------------------------
function attachAsyncDataSource(
tab: BigTraceEditorTab,
runner: QueryRunner,
): void {
if (!tab.queryUuid) return;
const queryClient = new BigtraceQueryClient(getBigtraceEndpoint());
tab.queryClient = queryClient;
if (!tab.materialize) {
tab.dataSource = new InMemoryDataSource([]);
return;
}
tab.dataSource = new BigtraceAsyncDataSource(
tab.queryUuid,
queryClient,
() => tab.execution?.processedRows ?? 0,
tab.lifecycle.signal,
);
tab.isLoading = true;
runner.startPolling(tab);
if (tab.queryResult === undefined) {
tab.queryResult = {
rows: [],
columns: [],
error: undefined,
totalRowCount: 0,
durationMs: 0,
statementWithOutputCount: 0,
statementCount: 1,
lastStatementSql: tab.editorText,
query: tab.editorText,
};
}
}