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);
+  }
+}