blob: bbdf83c3ad382cb6ea08b26cdaf149f745a7124c [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.
import './sidebar.scss';
import m from 'mithril';
import {assetSrc} from '../../base/assets';
import {AppImpl} from '../../core/app_impl';
import {getCurrentChannel} from '../../core/channels';
import {isMetatracingEnabled} from '../../core/metatracing';
import {raf} from '../../core/raf_scheduler';
import type {SidebarMenuItemInternal} from '../../core/sidebar_manager';
import type {TraceImpl} from '../../core/trace_impl';
import {SCM_REVISION, VERSION} from '../../virtual/version';
import type {App} from '../../public/app';
import {SIDEBAR_SECTIONS, type SidebarSections} from '../../public/sidebar';
import {Icon} from '../../widgets/icon';
import {Animation} from '../animation';
import {toggleHelp} from '../help_modal';
import {
convertTraceToJson,
convertTraceToSystrace,
downloadTrace,
toggleMetatrace,
} from '../trace_actions';
import {shareTrace} from '../trace_share_utils';
import {EngineStatusBadge} from './engine_status_badge';
import {HiringBanner, shouldShowHiringBanner} from './hiring_banner';
import {ServiceWorkerStatusBadge} from './service_worker_status_badge';
import {SidebarSection} from './sidebar_section';
const GITILES_URL = 'https://github.com/google/perfetto';
export interface SidebarAttrs {
readonly app: AppImpl;
}
export class Sidebar implements m.ClassComponent<SidebarAttrs> {
private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
view({attrs}: m.CVnode<SidebarAttrs>) {
const app = attrs.app;
const sidebar = app.sidebar;
if (!sidebar.enabled) return null;
return m(
'nav.pf-sidebar',
{
class: sidebar.visible ? undefined : 'pf-sidebar--hidden',
// 150 here matches --sidebar-timing in the css.
// TODO(hjd): Should link to the CSS variable.
ontransitionstart: (e: TransitionEvent) => {
if (e.target !== e.currentTarget) return;
this._redrawWhileAnimating.start(150);
},
ontransitionend: (e: TransitionEvent) => {
if (e.target !== e.currentTarget) return;
this._redrawWhileAnimating.stop();
},
},
shouldShowHiringBanner(app) && m(HiringBanner),
this.renderSidebarHeader(app),
this.renderSidebarContent(app),
this.renderSidebarFooter(app),
);
}
private renderSidebarHeader(app: AppImpl) {
const sidebar = app.sidebar;
return m(
`header.pf-sidebar__header`,
{
className: `pf-sidebar__header--${getCurrentChannel()}`,
},
m(`img[src=${assetSrc('assets/brand.png')}].pf-sidebar__brand`),
app.embedder.brandingBadge &&
m(
'span.pf-sidebar__branding-badge',
{style: {color: app.embedder.brandingBadge.color}},
app.embedder.brandingBadge.image
? m('img.pf-sidebar__branding-badge-img', {
src: app.embedder.brandingBadge.image,
})
: app.embedder.brandingBadge.icon &&
m(Icon, {
icon: app.embedder.brandingBadge.icon,
className: 'pf-sidebar__branding-badge-icon',
}),
app.embedder.brandingBadge.text,
),
m(
'button.pf-sidebar-button',
{
onclick: () => sidebar.toggleVisibility(),
title: sidebar.visible ? 'Hide sidebar' : 'Show sidebar',
},
m(Icon, {icon: 'menu'}),
),
);
}
private renderSidebarContent(app: AppImpl) {
return m(
'.pf-sidebar__content',
(Object.keys(SIDEBAR_SECTIONS) as SidebarSections[]).map((sectionId) =>
this.renderSection(app, sectionId),
),
);
}
private renderSidebarFooter(app: AppImpl) {
return m(
'.pf-sidebar__footer',
m(EngineStatusBadge, {app}),
m(ServiceWorkerStatusBadge, {app}),
m(
'.pf-sidebar__version.pf-test-volatile',
m(
'a',
{
href: `${GITILES_URL}/tree/${SCM_REVISION}/ui`,
title: `Channel: ${getCurrentChannel()}`,
target: '_blank',
},
VERSION,
),
),
);
}
private renderSection(app: AppImpl, sectionId: SidebarSections) {
const section = SIDEBAR_SECTIONS[sectionId];
const trace = app.trace;
const items: SidebarMenuItemInternal[] = app.sidebar.menuItems
.valuesAsArray()
.filter((item) => item.section === sectionId);
let defaultCollapsed = false;
let leading: m.Children;
switch (sectionId) {
case 'current_trace':
if (trace !== undefined) {
items.push(...getCurrentTraceItems(trace));
if (trace.traceInfo.traceTitle) {
leading = m(
'span.pf-sidebar__trace-file-name',
trace.traceInfo.traceTitle,
);
}
}
break;
case 'convert_trace':
if (trace !== undefined) {
items.push(...getConvertTraceItems(trace));
}
defaultCollapsed = true;
break;
case 'support':
items.push(...getSupportGlobalItems(app));
if (trace !== undefined) {
items.push(...getSupportTraceItems(trace));
}
defaultCollapsed = true;
break;
}
return m(SidebarSection, {
app,
title: section.title,
summary: section.summary,
items,
defaultCollapsed,
leading,
});
}
}
// TODO(primiano): The items below should be moved to dedicated
// plugins (most of this really belongs to core_plugins/commands/index.ts).
// For now keeping everything here as splitting these require moving some
// functions like share_trace() out of core, splitting out permalink, etc.
// Returns menu items for the 'current_trace' section.
function getCurrentTraceItems(trace: TraceImpl): SidebarMenuItemInternal[] {
const items: SidebarMenuItemInternal[] = [];
const downloadDisabled = trace.traceInfo.downloadable
? false
: 'Cannot download external trace';
if (AppImpl.instance.isInternalUser) {
items.push({
id: 'perfetto.ShareTrace',
section: 'current_trace',
sortOrder: 50,
text: 'Share',
action: async () => await shareTrace(trace),
icon: 'share',
});
}
items.push({
id: 'perfetto.DownloadTrace',
section: 'current_trace',
sortOrder: 51,
text: 'Download',
action: async () => await downloadTrace(trace),
icon: 'file_download',
disabled: downloadDisabled,
});
return items;
}
// Returns menu items for the 'convert_trace' section.
function getConvertTraceItems(trace: TraceImpl): SidebarMenuItemInternal[] {
const items: SidebarMenuItemInternal[] = [];
const downloadDisabled = trace.traceInfo.downloadable
? false
: 'Cannot download external trace';
items.push({
id: 'perfetto.ConvertToJson',
section: 'convert_trace',
text: 'Convert to .json',
action: async () => await convertTraceToJson(trace),
icon: 'file_download',
disabled: downloadDisabled,
});
if (trace.traceInfo.hasFtrace) {
items.push({
id: 'perfetto.ConvertToSystrace',
section: 'convert_trace',
text: 'Convert to .systrace',
action: async () => await convertTraceToSystrace(trace),
icon: 'file_download',
disabled: downloadDisabled,
});
}
return items;
}
// Returns global menu items for the 'support' section (always visible).
function getSupportGlobalItems(app: App): SidebarMenuItemInternal[] {
// TODO(primiano): The Open file / Open with legacy entries are registered by
// the 'perfetto.CoreCommands' plugin. These built-in items should move there too.
return [
{
id: 'perfetto.KeyboardShortcuts',
section: 'support',
text: 'Keyboard shortcuts',
action: toggleHelp,
icon: 'help',
},
{
id: 'perfetto.Documentation',
section: 'support',
text: 'Documentation',
href: 'https://perfetto.dev/docs',
icon: 'find_in_page',
},
{
id: 'perfetto.ReportBug',
section: 'support',
sortOrder: 4,
text: 'Report a bug',
href: getBugReportUrl(app),
icon: 'bug_report',
},
];
}
function getBugReportUrl(app: App): string {
if (app.isInternalUser) {
return 'https://goto.google.com/perfetto-ui-bug';
} else {
return 'https://github.com/google/perfetto/issues/new';
}
}
// Returns trace-specific menu items for the 'support' section.
function getSupportTraceItems(trace: TraceImpl): SidebarMenuItemInternal[] {
return [
{
id: 'perfetto.Metatrace',
section: 'support',
sortOrder: 5,
text: () =>
isMetatracingEnabled() ? 'Finalize metatrace' : 'Record metatrace',
action: () => toggleMetatrace(trace.engine),
icon: () => (isMetatracingEnabled() ? 'download' : 'fiber_smart_record'),
},
];
}