Add generic tabs demo (WIP)

Change-Id: Ic52abb653f94bdaad6575e523418d65634612ae0
diff --git a/ui/src/components/details/sql_table_tab.ts b/ui/src/components/details/sql_table_tab.ts
index 81f410e..15567e7 100644
--- a/ui/src/components/details/sql_table_tab.ts
+++ b/ui/src/components/details/sql_table_tab.ts
@@ -46,6 +46,9 @@
   imports?: string[];
 }
 
+/**
+ * @deprecated New code should not depend on this.
+ */
 export function addLegacyTableTab(
   trace: Trace,
   config: AddSqlTableTabParams,
diff --git a/ui/src/components/extensions.ts b/ui/src/components/extensions.ts
index dcd5a54..ef5a90b 100644
--- a/ui/src/components/extensions.ts
+++ b/ui/src/components/extensions.ts
@@ -31,6 +31,10 @@
 export interface ExtensionApi {
   addDebugSliceTrack: typeof addDebugSliceTrack;
   addDebugCounterTrack: typeof addDebugCounterTrack;
+
+  /**
+   * @deprecated New code should not depend on this.
+   */
   addLegacySqlTableTab: typeof addLegacyTableTab;
   addVisualizedArgTracks: typeof addVisualizedArgTracks;
   addQueryResultsTab: typeof addQueryResultsTab;
diff --git a/ui/src/components/widgets/datagrid/sql_schema.ts b/ui/src/components/widgets/datagrid/sql_schema.ts
index 10bc6ee..1a3383e 100644
--- a/ui/src/components/widgets/datagrid/sql_schema.ts
+++ b/ui/src/components/widgets/datagrid/sql_schema.ts
@@ -257,7 +257,7 @@
     this.registry = registry;
     this.rootSchemaName = rootSchemaName;
     this.baseAlias =
-      baseAlias ?? `${registry[rootSchemaName]?.table ?? 'base'}_0`;
+      baseAlias ?? `${rootSchemaName.replace(/[^a-zA-Z0-9_]/g, '_')}_0`;
   }
 
   /**
diff --git a/ui/src/components/widgets/sql/pivot_table/pivot_table.ts b/ui/src/components/widgets/sql/pivot_table/pivot_table.ts
index 85137f5..2ef25bd 100644
--- a/ui/src/components/widgets/sql/pivot_table/pivot_table.ts
+++ b/ui/src/components/widgets/sql/pivot_table/pivot_table.ts
@@ -42,6 +42,9 @@
   extraRowButton?(node: PivotTreeNode): m.Children;
 }
 
+/**
+ * @deprecated Use DataGrid instead.
+ */
 export class PivotTable implements m.ClassComponent<PivotTableAttrs> {
   view({attrs}: m.CVnode<PivotTableAttrs>) {
     const state = attrs.state;
diff --git a/ui/src/core/tab_manager.ts b/ui/src/core/tab_manager.ts
index e99603e..ea5ceb3 100644
--- a/ui/src/core/tab_manager.ts
+++ b/ui/src/core/tab_manager.ts
@@ -13,8 +13,14 @@
 // limitations under the License.
 
 import {DetailsPanel} from '../public/details_panel';
-import {TabDescriptor, TabManager} from '../public/tab';
+import {
+  GenericTab,
+  GenericTabInstance,
+  TabDescriptor,
+  TabManager,
+} from '../public/tab';
 import {DrawerPanelVisibility, toggleVisibility} from '../widgets/drawer_panel';
+import {Registry} from '../base/registry';
 
 export interface ResolvedTab {
   uri: string;
@@ -154,6 +160,27 @@
    * @returns List of resolved tabs.
    */
   resolveTabs(tabUris: string[]): ResolvedTab[] {
+    const genericTabs = this.genericTabs.map(
+      ({id, tabId, config}): ResolvedTab => {
+        return {
+          uri: id,
+          tab: {
+            uri: id,
+            isEphemeral: true,
+            content: {
+              render: () => {
+                const resolvedTabRenderer = this.genericTabsReg.get(tabId);
+                const {schema, render} = resolvedTabRenderer;
+                const parsedConfig = schema.parse(config);
+                return render(id, parsedConfig);
+              },
+              getTitle: () => `Generic Tab (${tabId})`,
+            },
+          },
+        };
+      },
+    );
+
     // Refresh the list of old tabs
     const newTabs = new Map<string, TabDescriptor>();
     const tabs: ResolvedTab[] = [];
@@ -185,7 +212,7 @@
 
     this._instantiatedTabs = newTabs;
 
-    return tabs;
+    return tabs.concat(genericTabs);
   }
 
   setTabPanelVisibility(visibility: DrawerPanelVisibility): void {
@@ -222,4 +249,18 @@
       this._registry.delete(tab.uri);
     }
   }
+
+  private readonly genericTabsReg = new Registry<GenericTab<unknown>>(
+    ({id}) => id,
+  );
+  private readonly genericTabs: GenericTabInstance[] = [];
+
+  registerGenericTab<T>(tab: GenericTab<T>) {
+    this.genericTabsReg.register(tab as unknown as GenericTab<unknown>);
+  }
+
+  openGenericTab(config: GenericTabInstance) {
+    this.genericTabs.push(config);
+    this._currentTab = config.id;
+  }
 }
diff --git a/ui/src/plugins/dev.perfetto.PivotTable/index.ts b/ui/src/plugins/dev.perfetto.PivotTable/index.ts
new file mode 100644
index 0000000..99e018f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.PivotTable/index.ts
@@ -0,0 +1,79 @@
+// 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 m from 'mithril';
+import z from 'zod';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {shortUuid} from '../../base/uuid';
+import {
+  DataGrid,
+  DataGridAttrs,
+} from '../../components/widgets/datagrid/datagrid';
+import {SQLDataSource} from '../../components/widgets/datagrid/sql_data_source';
+import {createSimpleSchema} from '../../components/widgets/datagrid/sql_schema';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.PivotTable';
+
+  private readonly dataSourceMap = new Map<string, SQLDataSource>();
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.commands.registerCommand({
+      id: 'dev.perfetto.PivotTable.openExamplePivotTableTab',
+      name: 'Open example Pivot Table Tab',
+      callback: () => {
+        trace.tabs.openGenericTab({
+          id: shortUuid(),
+          tabId: 'dev.perfetto.PivotTableTab',
+          config: {query: 'SELECT * FROM slice LIMIT 1000'},
+        });
+      },
+    });
+
+    trace.tabs.registerGenericTab({
+      id: 'dev.perfetto.PivotTableTab',
+      schema: z.object({
+        query: z.string(),
+      }),
+      render: (id, config) => {
+        console.log('Rendering Pivot Table Tab with config:', config);
+        const tableName = 'query';
+        let dataSource = this.dataSourceMap.get(id);
+        if (!dataSource) {
+          const sqlSchema = createSimpleSchema(config.query, tableName);
+          dataSource = new SQLDataSource({
+            engine: trace.engine,
+            sqlSchema: sqlSchema,
+            rootSchemaName: tableName,
+          });
+          this.dataSourceMap.set(id, dataSource);
+        }
+        return m(DataGrid, {
+          fillHeight: true,
+          data: dataSource,
+          schema: {
+            [tableName]: {
+              id: {},
+              name: {},
+              ts: {},
+              dur: {},
+            },
+          },
+          rootSchema: tableName,
+        } satisfies DataGridAttrs);
+      },
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/pivot_table_tab.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/pivot_table_tab.ts
index b8308f8..650ef0f 100644
--- a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/pivot_table_tab.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/pivot_table_tab.ts
@@ -29,6 +29,9 @@
 import {SLICE_TABLE} from '../../components/widgets/sql/table_definitions';
 import {resolveTableDefinition} from '../../components/widgets/sql/table/columns';
 
+/**
+ * @deprecated Use DataGrid in a custom tab instead.
+ */
 export class PivotTableTab implements AreaSelectionTab {
   readonly id = 'pivot_table';
   readonly name = 'Pivot Table';
diff --git a/ui/src/public/tab.ts b/ui/src/public/tab.ts
index 97d7065..c811b9d 100644
--- a/ui/src/public/tab.ts
+++ b/ui/src/public/tab.ts
@@ -13,12 +13,32 @@
 // limitations under the License.
 
 import m from 'mithril';
+import z from 'zod';
+
+export interface GenericTabInstance {
+  // Id of the tab renderer instance
+  readonly tabId: string;
+
+  // Unique ID for this tab instance
+  readonly id: string;
+
+  // This tab's configuration
+  readonly config: unknown;
+}
+
+export interface GenericTab<T> {
+  readonly id: string;
+  readonly render: (id: string, config: T) => m.Children;
+  readonly schema: z.ZodType<T>;
+}
 
 export interface TabManager {
   registerTab(tab: TabDescriptor): void;
   showTab(uri: string): void;
   hideTab(uri: string): void;
   addDefaultTab(uri: string): void;
+  registerGenericTab<T>(tab: GenericTab<T>): void;
+  openGenericTab(config: GenericTabInstance): void;
 }
 
 export interface Tab {