blob: 12f5fdf3edad12bf9ea9b766501f4b4a9c836181 [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 {initLiveReload} from '../core/live_reload';
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 {bigTraceSettingsStorage} from './settings/bigtrace_settings_storage';
import {Topbar} from './layout/topbar';
import {BigTraceApp as BigTraceAppSingleton} from './bigtrace_app';
import {type HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
import {initAssets} from '../base/assets';
import {toggleHelp} from './help_modal';
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'`],
// wasm-unsafe-eval: the SQL formatter runs as WebAssembly; this allows
// Wasm compilation without enabling JS eval().
'script-src': [`'self'`, `'wasm-unsafe-eval'`],
'object-src': [`'none'`],
'connect-src': [
`'self'`,
'https://autopush-brush-googleapis.corp.google.com',
'https://brush-googleapis.corp.google.com',
],
'img-src': [`'self'`, 'data:', 'blob:'],
// unsafe-inline: the editor (CodeMirror) generates its own stylesheet at
// runtime, so no fixed hash can cover it — pinned hashes left the editor
// unstyled in any browser that enforces the policy. Same allowance the
// main Perfetto UI makes, for the same reason.
'style-src': [`'self'`, `'unsafe-inline'`],
};
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());
}
class BigTraceLayout implements m.ClassComponent {
oninit() {
bigTraceSettingsStorage.loadSettings();
}
view(vnode: m.Vnode) {
return m('main.pf-ui-main', [
m(Topbar),
m('.pf-ui-main__page-container', vnode.children),
maybeRenderFullscreenModalDialog(),
]);
}
}
// Root: theme + hotkeys around the single page. 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 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, m(QueryPage, {useBigtraceBackend: true})),
),
),
]);
}
}
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.ToggleHistorySidebar',
name: 'Toggle history sidebar',
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 = '';
m.mount(document.body, BigTraceRoot);
initLiveReload();
registerCommands();
}
main();