ui: Move aggregation tabs into details panel for area selections
- Make a new row of tabs inside the current selection tab, which
appear when making an area selection.
- This provides a similar user journey to TabsV1 wrt aggregations and
avoids an extra click.
Bug: https://github.com/google/perfetto/issues/720
Change-Id: I5c90282c75be1ef792bb8504c527a6e83bd23bd0
diff --git a/ui/src/assets/widgets/button.scss b/ui/src/assets/widgets/button.scss
index a73c5b2..1c7ef99 100644
--- a/ui/src/assets/widgets/button.scss
+++ b/ui/src/assets/widgets/button.scss
@@ -109,3 +109,9 @@
}
}
}
+
+.pf-button-bar {
+ display: flex;
+ flex-direction: row;
+ gap: 2px;
+}
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index 0288b91..44166f0 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -16,182 +16,93 @@
import {Disposable, Trash} from '../base/disposable';
import {AggregationPanel} from './aggregation_panel';
import {globals} from './globals';
-import {Area, AreaSelection} from '../common/state';
-import {Anchor} from '../widgets/anchor';
-import {Actions} from '../common/actions';
import {isEmptyData} from '../common/aggregation_data';
import {DetailsShell} from '../widgets/details_shell';
-import {Section} from '../widgets/section';
-import {GridLayout} from '../widgets/grid_layout';
-import {Icons} from '../base/semantic_icons';
-import {Tree, TreeNode} from '../widgets/tree';
-import {Timestamp} from './widgets/timestamp';
-import {PIVOT_TABLE_REDUX_FLAG} from '../controller/pivot_table_controller';
+import {Button, ButtonBar} from '../widgets/button';
+import {raf} from '../core/raf_scheduler';
+import {EmptyState} from '../widgets/empty_state';
-interface AreaDetailsPanelAttrs {
- selection: AreaSelection;
-}
+class AreaDetailsPanel implements m.ClassComponent {
+ private currentTab: string|undefined = undefined;
-class AreaDetailsPanel implements m.ClassComponent<AreaDetailsPanelAttrs> {
- view(vnode: m.Vnode<AreaDetailsPanelAttrs>): m.Children {
- const {
- selection,
- } = vnode.attrs;
+ private getCurrentAggType(): string|undefined {
+ const types = Array.from(globals.aggregateDataStore.entries())
+ .filter(([_, value]) => !isEmptyData(value))
+ .map(([type, _]) => type);
- const areaId = selection.areaId;
- const area = globals.state.areas[areaId];
+ if (types.length === 0) {
+ return undefined;
+ }
+
+ if (this.currentTab === undefined) {
+ return types[0];
+ }
+
+ if (!types.includes(this.currentTab)) {
+ return types[0];
+ }
+
+ return this.currentTab;
+ }
+
+ view(_: m.Vnode): m.Children {
+ const aggregationButtons = Array.from(globals.aggregateDataStore.entries())
+ .filter(([_, value]) => !isEmptyData(value))
+ .map(([type, value]) => {
+ return m(Button,
+ {
+ onclick: () => {
+ this.currentTab = type;
+ raf.scheduleFullRedraw();
+ },
+ key: type,
+ label: value.tabName,
+ active: this.getCurrentAggType() === type,
+ minimal: true,
+ },
+ );
+ });
+
+ const content = this.renderAggregationContent();
+
+ if (content === undefined) {
+ return this.renderEmptyState();
+ }
return m(DetailsShell,
{
- title: 'Area Selection',
+ title: 'Aggregate',
+ description: m(ButtonBar, aggregationButtons),
},
- m(GridLayout,
- this.renderDetailsSection(area),
- this.renderLinksSection(),
- ),
+ content,
);
}
- private renderDetailsSection(area: Area) {
- return m(Section,
- {
- title: 'Details',
- },
- m(Tree,
- m(TreeNode, {left: 'Start', right: m(Timestamp, {ts: area.start})}),
- m(TreeNode, {left: 'End', right: m(Timestamp, {ts: area.end})}),
- m(TreeNode, {left: 'Track Count', right: area.tracks.length}),
- ),
- );
+ private renderAggregationContent(): m.Children {
+ const currentTab = this.getCurrentAggType();
+ if (currentTab === undefined) return undefined;
+
+ const data = globals.aggregateDataStore.get(currentTab);
+ return m(AggregationPanel, {kind: currentTab, data});
}
- private renderLinksSection() {
- const linkNodes: m.Children = [];
-
- globals.aggregateDataStore.forEach((value, type) => {
- if (!isEmptyData(value)) {
- const anchor = m(Anchor,
- {
- icon: Icons.ChangeTab,
- onclick: () => {
- globals.dispatch(Actions.showTab({uri: uriForAggType(type)}));
- },
- },
- value.tabName,
- );
- const node = m(TreeNode, {left: anchor});
- linkNodes.push(node);
- }
- });
-
- linkNodes.push(m(TreeNode, {
- left: m(
- Anchor,
- {
- icon: Icons.ChangeTab,
- onclick: () => {
- globals.dispatch(
- Actions.showTab({uri: 'perfetto.Flows#FlowEvents'}));
- },
- },
- 'Flow Events'),
- }));
-
- if (PIVOT_TABLE_REDUX_FLAG.get()) {
- linkNodes.push(m(TreeNode, {
- left: m(
- Anchor,
- {
- icon: Icons.ChangeTab,
- onclick: () => {
- globals.dispatch(
- Actions.showTab({uri: 'perfetto.PivotTable#PivotTable'}));
- },
- },
- 'Pivot Table'),
- }));
- }
-
- if (linkNodes.length === 0) return undefined;
-
- return m(Section,
- {
- title: 'Relevant Aggregations',
- },
- m(Tree, linkNodes),
- );
+ private renderEmptyState(): m.Children {
+ return m(EmptyState, {
+ className: 'pf-noselection',
+ title: 'Unsupported area selection',
+ },
+ 'No details available for this area selection');
}
}
-function uriForAggType(type: string): string {
- return `aggregationTab#${type}`;
-}
-
export class AggregationsTabs implements Disposable {
- private tabs = [
- {
- type: 'cpu_aggregation',
- title: 'CPU by thread',
- },
- {
- type: 'thread_state_aggregation',
- title: 'Thread States',
- },
- {
- type: 'cpu_by_process_aggregation',
- title: 'CPU by process',
- },
- {
- type: 'slice_aggregation',
- title: 'Slices',
- },
- {
- type: 'counter_aggregation',
- title: 'Counters',
- },
- {
- type: 'frame_aggregation',
- title: 'Frames',
- },
- ];
-
private trash = new Trash();
constructor() {
- for (const {type, title} of this.tabs) {
- const uri = uriForAggType(type);
- const unregister = globals.tabManager.registerTab({
- uri,
- isEphemeral: false,
- content: {
- hasContent: () => {
- const data = globals.aggregateDataStore.get(type);
- const hasData = Boolean(data && !isEmptyData(data));
- return hasData;
- },
- getTitle: () => title,
- render: () => {
- const data = globals.aggregateDataStore.get(type);
- return m(AggregationPanel, {kind: type, data});
- },
- },
- });
- this.trash.add(unregister);
-
- const unregisterCmd = globals.commandManager.registerCommand({
- id: uri,
- name: `Show ${title} Aggregation Tab`,
- callback: () => {
- globals.dispatch(Actions.showTab({uri}));
- },
- });
- this.trash.add(unregisterCmd);
- }
-
const unregister = globals.tabManager.registerDetailsPanel({
render(selection) {
if (selection.kind === 'AREA') {
- return m(AreaDetailsPanel, {selection});
+ return m(AreaDetailsPanel);
} else {
return undefined;
}
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 130a494..dff5daf 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -44,13 +44,10 @@
const resolvedTabs = tabMan.resolveTabs(tabList);
const tabs = resolvedTabs.map(({uri, tab: tabDesc}): TabWithContent => {
if (tabDesc) {
- const titleStr = tabDesc.content.getTitle();
return {
key: uri,
hasCloseButton: true,
- title: (tabDesc.content.hasContent?.() ?? true) ?
- titleStr :
- m('.pf-nocontent', titleStr),
+ title: tabDesc.content.getTitle(),
content: tabDesc.content.render(),
};
} else {
@@ -140,10 +137,8 @@
};
}
- const detailsPanels = globals.tabManager.detailsPanels;
-
// Get the first "truthy" details panel
- const panel = detailsPanels
+ const panel = globals.tabManager.detailsPanels
.map((dp) => {
return {
content: dp.render(cs),
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 7d9e1bd..7ea2bbe 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -312,7 +312,6 @@
}
export interface Tab {
- hasContent?(): boolean;
render(): m.Children;
getTitle(): string;
}
diff --git a/ui/src/widgets/button.ts b/ui/src/widgets/button.ts
index 3d4d230..3f73969 100644
--- a/ui/src/widgets/button.ts
+++ b/ui/src/widgets/button.ts
@@ -92,3 +92,12 @@
);
}
}
+
+/**
+ * Space buttons out with a little gap between each one.
+ */
+export class ButtonBar implements m.ClassComponent {
+ view({children}: m.CVnode): m.Children {
+ return m('.pf-button-bar', children);
+ }
+}