| // 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(); |