blob: 1d0958218de43a9d7a521bc3e210349a6d430983 [file] [edit]
// 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.
// Keep this import first.
import '../base/static_initializers';
import '../assets/bigtrace.scss';
import '../frontend/ui_main.scss';
import m from 'mithril';
import {defer} from '../base/deferred';
import {reportError, addErrorHandler, type ErrorDetails} from '../base/logging';
import {settingsStorage} from './settings/settings_storage';
import {ThemeProvider} from '../frontend/theme_provider';
import {OverlayContainer} from '../widgets/overlay_container';
import {QueryPage, queryRightSidebarToggleFn} from './pages/query_page';
import {HomePage} from './pages/home_page';
import {bigTraceSettingsStorage} from './settings/bigtrace_settings_storage';
import {SettingsPage} from './pages/settings_page';
import {Topbar} from './layout/topbar';
import {BigTraceApp as BigTraceAppSingleton} from './bigtrace_app';
import {OmniboxMode} from '../core/omnibox_manager';
import {Sidebar, type SidebarMenuItem} from './layout/sidebar';
import {type HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
import {initAssets} from '../base/assets';
import {getCurrentRoute, initRouter} from './router';
import {toggleHelp} from './help_modal';
import {Routes} from './routes';
function getRoot() {
// Root for serving content, e.g. `http://origin/v1.2.3/`.
const script = document.currentScript as HTMLScriptElement;
// DOM tests have no script element.
if (script === null) {
return '';
}
let root = script.src;
root = root.substr(0, root.lastIndexOf('/') + 1);
return root;
}
function setupContentSecurityPolicy() {
// Note: self and sha-xxx must be quoted, urls data: and blob: must not.
const policy = {
'default-src': [`'self'`],
'script-src': [`'self'`],
'object-src': ['none'],
'connect-src': [
`'self'`,
'https://autopush-brush-googleapis.corp.google.com',
'https://brush-googleapis.corp.google.com',
],
'img-src': [`'self'`, 'data:', 'blob:'],
'style-src': [
`'self'`,
`'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='`,
`'sha256-yRQRG6LLKMvjvigtzXD1f8VRZSYY7J8fM2ZLfdMaHKg='`,
],
};
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
let policyStr = '';
for (const [key, list] of Object.entries(policy)) {
policyStr += `${key} ${list.join(' ')}; `;
}
meta.content = policyStr;
document.head.appendChild(meta);
}
function main() {
// Unregister service workers
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (const registration of registrations) {
registration.unregister();
}
});
}
setupContentSecurityPolicy();
initAssets();
// Settings will be lazy-loaded by the UI components that require them.
// Load the css. The load is asynchronous and the CSS is not ready by the time
// appendChild returns.
const root = getRoot();
const cssLoadPromise = defer<void>();
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = root + 'bigtrace.css';
css.onload = () => cssLoadPromise.resolve();
css.onerror = (err) => cssLoadPromise.reject(err);
const favicon = document.head.querySelector('#favicon');
if (favicon instanceof HTMLLinkElement) {
favicon.href = root + 'assets/favicon.png';
}
document.head.append(css);
// Add Error handlers for JS error and for uncaught exceptions in promises.
addErrorHandler((err: ErrorDetails) => console.error(err.message, err.stack));
window.addEventListener('error', (e) => reportError(e));
window.addEventListener('unhandledrejection', (e) => reportError(e));
// Prevent pinch zoom.
document.body.addEventListener(
'wheel',
(e: MouseEvent) => {
if (e.ctrlKey) e.preventDefault();
},
{passive: false},
);
cssLoadPromise.then(() => onCssLoaded());
}
// Allows the sidebar toggle command (registered globally) to reach into the
// BigTraceApp component's local state.
let sidebarToggleFn: (() => void) | undefined;
class BigTraceLayout implements m.ClassComponent {
private sidebarVisible = true;
oninit() {
bigTraceSettingsStorage.loadSettings();
sidebarToggleFn = () => {
this.sidebarVisible = !this.sidebarVisible;
};
}
view(vnode: m.Vnode) {
const currentRoute = getCurrentRoute();
const items: SidebarMenuItem[] = [
{
section: 'bigtrace',
text: 'Query (SQL)',
href: `#!${Routes.QUERY}`,
icon: 'database',
active: currentRoute === Routes.QUERY,
onclick: () => {},
},
{
section: 'bigtrace',
text: 'Settings',
href: `#!${Routes.SETTINGS}`,
icon: 'settings',
active: currentRoute === Routes.SETTINGS,
onclick: () => {},
},
];
return m('main.pf-ui-main', [
m(Sidebar, {
items,
onToggleSidebar: () => {
this.sidebarVisible = !this.sidebarVisible;
},
visible: this.sidebarVisible,
}),
m(Topbar, {sidebarVisible: this.sidebarVisible}),
m('.pf-ui-main__page-container', vnode.children),
maybeRenderFullscreenModalDialog(),
]);
}
}
// Root: routing + theme + hotkeys. Uses m.mount (not m.route) because
// m.route bypasses the raf scheduler and breaks portal-based popups.
class BigTraceRoot implements m.ClassComponent {
view(): m.Children {
const route = getCurrentRoute();
const page = this.resolvePage(route);
const theme = settingsStorage.get('theme');
const themeValue = theme ? theme.get() : 'light';
const commands = BigTraceAppSingleton.instance.commands;
const hotkeys: HotkeyConfig[] = [];
for (const {id, defaultHotkey} of commands.getCommands()) {
if (defaultHotkey) {
hotkeys.push({
callback: () => commands.runCommand(id),
hotkey: defaultHotkey,
});
}
}
return m(ThemeProvider, {theme: themeValue as 'dark' | 'light'}, [
m(
HotkeyContext,
{hotkeys, fillHeight: true, focusable: false},
m(OverlayContainer, {fillHeight: true}, [m(BigTraceLayout, page)]),
),
]);
}
private resolvePage(route: string): m.Children {
return [
// QueryPage stays mounted across route changes to preserve DataGrid
// state (filters, scroll position, sort order).
m(
'div.pf-bt-route-pane',
{className: route === Routes.QUERY ? '' : 'pf-bt-route-pane--hidden'},
m(QueryPage, {useBigtraceBackend: true}),
),
route === Routes.SETTINGS && m(SettingsPage),
route !== Routes.QUERY && route !== Routes.SETTINGS && m(HomePage),
];
}
}
function registerCommands() {
const app = BigTraceAppSingleton.instance;
app.commands.registerCommand({
id: 'bigtrace.ToggleTheme',
name: 'Toggle UI Theme (Dark/Light)',
callback: () => {
const theme = settingsStorage.get('theme');
if (theme) theme.set(theme.get() === 'light' ? 'dark' : 'light');
},
});
app.commands.registerCommand({
id: 'bigtrace.OpenCommandPalette',
name: 'Open command palette',
callback: () => app.omnibox.setMode(OmniboxMode.Command),
defaultHotkey: '!Mod+Shift+P',
});
app.commands.registerCommand({
id: 'bigtrace.ToggleLeftSidebar',
name: 'Toggle left sidebar',
callback: () => {
sidebarToggleFn?.();
},
defaultHotkey: '!Mod+B',
});
app.commands.registerCommand({
id: 'bigtrace.ToggleQueryRightSidebar',
name: 'Toggle query right sidebar (History / Stdlib Schemas)',
callback: () => {
queryRightSidebarToggleFn?.();
},
defaultHotkey: '!Mod+Shift+B',
});
app.commands.registerCommand({
id: 'bigtrace.ShowHelp',
name: 'Show help',
callback: () => toggleHelp(),
// '!' prefix fires even when the omnibox has focus.
defaultHotkey: '!?',
});
}
function onCssLoaded() {
document.body.innerHTML = '';
initRouter();
m.mount(document.body, BigTraceRoot);
registerCommands();
}
main();