Enrich nodegraph demo
diff --git a/ui/src/assets/widgets/nodegraph.scss b/ui/src/assets/widgets/nodegraph.scss
index c59efb3..ea543d8 100644
--- a/ui/src/assets/widgets/nodegraph.scss
+++ b/ui/src/assets/widgets/nodegraph.scss
@@ -184,13 +184,14 @@
 .pf-node-header {
   /* Default background when hue is not set */
   background: var(--pf-color-background);
-  padding: 6px 6px;
+  padding: 4px 6px;
   border-radius: 6px 6px 0 0;
   display: flex;
   border-bottom: 1px solid var(--pf-color-border);
   justify-content: space-between;
   align-items: center;
-  gap: 8px;
+  gap: 4px;
+  font-size: 12px;
 }
 
 /* Override with hue-based color when --pf-node-hue is set via inline styles */
@@ -225,7 +226,10 @@
 
 .pf-node-title {
   flex: 1;
-  padding-left: 6px;
+}
+
+.pf-node-title-icon {
+  font-size: 16px;
 }
 
 .pf-node-context-menu {
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts
index 708f7fb..07d1875 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts
@@ -16,7 +16,6 @@
 import {produce} from 'immer';
 import {uuidv4} from '../../../base/uuid';
 import {Button, ButtonVariant} from '../../../widgets/button';
-import {Checkbox} from '../../../widgets/checkbox';
 import {MenuItem, PopupMenu} from '../../../widgets/menu';
 import {
   Connection,
@@ -41,14 +40,14 @@
 }
 
 // Individual node type interfaces
-interface TableNodeData extends BaseNodeData {
-  readonly type: 'table';
+interface FromNodeData extends BaseNodeData {
+  readonly type: 'from';
   readonly table: string;
 }
 
 interface SelectNodeData extends BaseNodeData {
   readonly type: 'select';
-  readonly columns: Record<string, boolean>;
+  readonly columns: ReadonlyArray<string>;
 }
 
 interface FilterNodeData extends BaseNodeData {
@@ -79,7 +78,7 @@
 
 // Discriminated union of all node types
 type NodeData =
-  | TableNodeData
+  | FromNodeData
   | SelectNodeData
   | FilterNodeData
   | SortNodeData
@@ -102,20 +101,23 @@
   readonly canDockTop?: boolean;
   readonly canDockBottom?: boolean;
   readonly hue: number;
+  readonly icon: string;
 }
 
 const NODE_CONFIGS: Record<NodeData['type'], NodeConfig> = {
-  table: {
+  from: {
     outputs: [{content: 'Output', direction: 'bottom'}],
     canDockBottom: true,
-    hue: 200,
+    hue: 100,
+    icon: 'table_chart',
   },
   select: {
     inputs: [{content: 'Input', direction: 'top'}],
     outputs: [{content: 'Output', direction: 'bottom'}],
     canDockTop: true,
     canDockBottom: true,
-    hue: 100,
+    hue: 200,
+    icon: 'checklist',
   },
   filter: {
     inputs: [{content: 'Input', direction: 'top'}],
@@ -123,6 +125,7 @@
     canDockTop: true,
     canDockBottom: true,
     hue: 50,
+    icon: 'filter_alt',
   },
   sort: {
     inputs: [{content: 'Input', direction: 'top'}],
@@ -130,6 +133,7 @@
     canDockTop: true,
     canDockBottom: true,
     hue: 150,
+    icon: 'sort',
   },
   join: {
     inputs: [
@@ -140,6 +144,7 @@
     canDockTop: true,
     canDockBottom: true,
     hue: 300,
+    icon: 'join',
   },
   union: {
     inputs: [
@@ -150,18 +155,20 @@
     canDockTop: true,
     canDockBottom: true,
     hue: 240,
+    icon: 'merge',
   },
   result: {
     inputs: [{content: 'Input', direction: 'top'}],
     canDockTop: true,
     hue: 0,
+    icon: 'output',
   },
 };
 
 // Factory functions for creating node data
-function createTableNode(id: string, x: number, y: number): TableNodeData {
+function createFromNode(id: string, x: number, y: number): FromNodeData {
   return {
-    type: 'table',
+    type: 'from',
     id,
     x,
     y,
@@ -175,13 +182,7 @@
     id,
     x,
     y,
-    columns: {
-      id: true,
-      name: true,
-      cpu: false,
-      duration: false,
-      timestamp: false,
-    },
+    columns: ['id', 'name'],
   };
 }
 
@@ -237,9 +238,9 @@
 }
 
 // Pure render functions for each node type
-function renderTableNode(
-  node: TableNodeData,
-  updateNode: (updates: Partial<Omit<TableNodeData, 'type' | 'id'>>) => void,
+function renderFromNode(
+  node: FromNodeData,
+  updateNode: (updates: Partial<Omit<FromNodeData, 'type' | 'id'>>) => void,
 ): m.Children {
   return m(
     Select,
@@ -265,20 +266,44 @@
   return m(
     '',
     {style: {display: 'flex', flexDirection: 'column', gap: '4px'}},
-    Object.entries(node.columns).map(([col, checked]) =>
-      m(Checkbox, {
-        label: col,
-        checked,
-        onchange: () => {
-          updateNode({
-            columns: {
-              ...node.columns,
-              [col]: !checked,
-            },
-          });
+    [
+      ...node.columns.map((col, index) =>
+        m(
+          '',
+          {
+            style: {display: 'flex', alignItems: 'center', gap: '4px'},
+            key: index,
+          },
+          [
+            m(TextInput, {
+              value: col,
+              oninput: (e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                const newColumns = [...node.columns];
+                newColumns[index] = target.value;
+                updateNode({columns: newColumns});
+              },
+            }),
+            m(Button, {
+              icon: 'close',
+              minimal: true,
+              onclick: () => {
+                const newColumns = node.columns.filter((_, i) => i !== index);
+                updateNode({columns: newColumns});
+              },
+            }),
+          ],
+        ),
+      ),
+      m(Button, {
+        key: 'add-button',
+        icon: 'add',
+        label: 'Add',
+        onclick: () => {
+          updateNode({columns: [...node.columns, '']});
         },
       }),
-    ),
+    ],
   );
 }
 
@@ -403,8 +428,8 @@
   updateNode: (updates: Partial<Omit<NodeData, 'id'>>) => void,
 ): m.Children {
   switch (node.type) {
-    case 'table':
-      return renderTableNode(node, updateNode);
+    case 'from':
+      return renderFromNode(node, updateNode);
     case 'select':
       return renderSelectNode(node, updateNode);
     case 'filter':
@@ -432,10 +457,10 @@
 export function NodeGraphDemo(): m.Component<NodeGraphDemoAttrs> {
   let graphApi: NodeGraphApi | undefined;
 
-  // Initialize store with a single table node
+  // Initialize store with a single from node
   const initialId = uuidv4();
   let store: NodeGraphStore = {
-    nodes: new Map([[initialId, createTableNode(initialId, 150, 100)]]),
+    nodes: new Map([[initialId, createFromNode(initialId, 150, 100)]]),
     connections: [],
     labels: [
       {
@@ -614,7 +639,7 @@
 
       // Node factory options
       const nodeFactories = [
-        createTableNode,
+        createFromNode,
         createSelectNode,
         createFilterNode,
         createSortNode,
@@ -743,37 +768,47 @@
     const connectedInputs = findConnectedInputs(nodes, connections, nodeId);
 
     switch (node.type) {
-      case 'table': {
+      case 'from': {
         return node.table || 'unknown_table';
       }
 
       case 'select': {
-        const selectedCols = Object.entries(node.columns)
-          .filter(([_, checked]) => checked)
-          .map(([col]) => col);
-        const colList = selectedCols.length > 0 ? selectedCols.join(', ') : '*';
+        const validCols = node.columns.filter((col) => col.trim() !== '');
+        const colList = validCols.length > 0 ? validCols.join(', ') : '*';
 
-        const inputSql = dockedParent
-          ? buildSqlFromNode(nodes, connections, dockedParent.id)
-          : connectedInputs.get(0)
-            ? buildSqlFromNode(nodes, connections, connectedInputs.get(0)!.id)
-            : '';
+        const inputNode = dockedParent ?? connectedInputs.get(0);
+        if (!inputNode) return `SELECT ${colList}`;
 
+        const inputSql = buildSqlFromNode(nodes, connections, inputNode.id);
         if (!inputSql) return `SELECT ${colList}`;
+
+        // If input is a raw table name (from 'from' node), use it directly
+        if (inputNode.type === 'from') {
+          return `SELECT ${colList} FROM ${inputSql}`;
+        }
         return `SELECT ${colList} FROM (${inputSql})`;
       }
 
       case 'filter': {
         const filterExpr = node.filterExpression || '';
 
-        const inputSql = dockedParent
-          ? buildSqlFromNode(nodes, connections, dockedParent.id)
-          : connectedInputs.get(0)
-            ? buildSqlFromNode(nodes, connections, connectedInputs.get(0)!.id)
-            : '';
+        const inputNode = dockedParent ?? connectedInputs.get(0);
+        if (!inputNode) return '';
 
+        const inputSql = buildSqlFromNode(nodes, connections, inputNode.id);
         if (!inputSql) return '';
-        if (!filterExpr) return inputSql;
+        if (!filterExpr) {
+          // If input is a raw table name, wrap it in SELECT *
+          if (inputNode.type === 'from') {
+            return `SELECT * FROM ${inputSql}`;
+          }
+          return inputSql;
+        }
+
+        // If input is a raw table name (from 'from' node), use it directly
+        if (inputNode.type === 'from') {
+          return `SELECT * FROM ${inputSql} WHERE ${filterExpr}`;
+        }
         return `SELECT * FROM (${inputSql}) WHERE ${filterExpr}`;
       }
 
@@ -781,14 +816,23 @@
         const sortColumn = node.sortColumn || '';
         const sortOrder = node.sortOrder || 'ASC';
 
-        const inputSql = dockedParent
-          ? buildSqlFromNode(nodes, connections, dockedParent.id)
-          : connectedInputs.get(0)
-            ? buildSqlFromNode(nodes, connections, connectedInputs.get(0)!.id)
-            : '';
+        const inputNode = dockedParent ?? connectedInputs.get(0);
+        if (!inputNode) return '';
 
+        const inputSql = buildSqlFromNode(nodes, connections, inputNode.id);
         if (!inputSql) return '';
-        if (!sortColumn) return inputSql;
+        if (!sortColumn) {
+          // If input is a raw table name, wrap it in SELECT *
+          if (inputNode.type === 'from') {
+            return `SELECT * FROM ${inputSql}`;
+          }
+          return inputSql;
+        }
+
+        // If input is a raw table name (from 'from' node), use it directly
+        if (inputNode.type === 'from') {
+          return `SELECT * FROM ${inputSql} ORDER BY ${sortColumn} ${sortOrder}`;
+        }
         return `SELECT * FROM (${inputSql}) ORDER BY ${sortColumn} ${sortOrder}`;
       }
 
@@ -797,45 +841,67 @@
         const joinOn = node.joinOn || 'true';
 
         // Join needs two inputs: one docked (or from top connection) and one from left connection
-        const leftInput = dockedParent
-          ? buildSqlFromNode(nodes, connections, dockedParent.id)
-          : connectedInputs.get(0)
-            ? buildSqlFromNode(nodes, connections, connectedInputs.get(0)!.id)
-            : '';
+        const leftInputNode = dockedParent ?? connectedInputs.get(0);
+        const rightInputNode = connectedInputs.get(1);
 
-        const rightInput = connectedInputs.get(1)
-          ? buildSqlFromNode(nodes, connections, connectedInputs.get(1)!.id)
+        const leftInput = leftInputNode
+          ? buildSqlFromNode(nodes, connections, leftInputNode.id)
+          : '';
+        const rightInput = rightInputNode
+          ? buildSqlFromNode(nodes, connections, rightInputNode.id)
           : '';
 
         if (!leftInput || !rightInput) return leftInput || rightInput || '';
-        return `SELECT * FROM (${leftInput}) ${joinType} JOIN (${rightInput}) ON ${joinOn}`;
+
+        const leftSql =
+          leftInputNode?.type === 'from' ? leftInput : `(${leftInput})`;
+        const rightSql =
+          rightInputNode?.type === 'from' ? rightInput : `(${rightInput})`;
+        return `SELECT * FROM ${leftSql} ${joinType} JOIN ${rightSql} ON ${joinOn}`;
       }
 
       case 'union': {
         const unionType = node.unionType || '';
 
+        const inputNodes: Array<NodeData | undefined> = [];
         const inputs: string[] = [];
 
         // Collect all inputs (docked + connections)
         if (dockedParent) {
+          inputNodes.push(dockedParent);
           inputs.push(buildSqlFromNode(nodes, connections, dockedParent.id));
         }
         for (const [_, inputNode] of connectedInputs) {
+          inputNodes.push(inputNode);
           inputs.push(buildSqlFromNode(nodes, connections, inputNode.id));
         }
 
-        const validInputs = inputs.filter((sql) => sql);
+        const validInputs = inputs
+          .map((sql, i) => ({sql, node: inputNodes[i]}))
+          .filter(({sql}) => sql);
         if (validInputs.length === 0) return '';
-        if (validInputs.length === 1) return validInputs[0];
-        return validInputs.map((sql) => `(${sql})`).join(` ${unionType} `);
+        if (validInputs.length === 1) {
+          const {sql, node: inputNode} = validInputs[0];
+          return inputNode?.type === 'from' ? `SELECT * FROM ${sql}` : sql;
+        }
+        return validInputs
+          .map(({sql, node: inputNode}) =>
+            inputNode?.type === 'from' ? `SELECT * FROM ${sql}` : `(${sql})`,
+          )
+          .join(` ${unionType} `);
       }
 
       case 'result': {
-        const inputSql = dockedParent
-          ? buildSqlFromNode(nodes, connections, dockedParent.id)
-          : connectedInputs.get(0)
-            ? buildSqlFromNode(nodes, connections, connectedInputs.get(0)!.id)
-            : '';
+        const inputNode = dockedParent ?? connectedInputs.get(0);
+        if (!inputNode) return '';
+
+        const inputSql = buildSqlFromNode(nodes, connections, inputNode.id);
+        if (!inputSql) return '';
+
+        // If input is a raw table name, wrap it in SELECT *
+        if (inputNode.type === 'from') {
+          return `SELECT * FROM ${inputSql}`;
+        }
         return inputSql;
       }
     }
@@ -898,7 +964,7 @@
         return [
           m(MenuItem, {
             label: 'Select',
-            icon: Icons.Filter,
+            icon: 'checklist',
             onclick: () => addNode(createSelectNode, toNode),
             style: {
               borderLeft: `4px solid hsl(${NODE_CONFIGS.select.hue}, 60%, 50%)`,
@@ -906,7 +972,7 @@
           }),
           m(MenuItem, {
             label: 'Filter',
-            icon: Icons.Filter,
+            icon: 'filter_alt',
             onclick: () => addNode(createFilterNode, toNode),
             style: {
               borderLeft: `4px solid hsl(${NODE_CONFIGS.filter.hue}, 60%, 50%)`,
@@ -971,7 +1037,7 @@
             canDockTop: config.canDockTop,
             accentBar: attrs.accentBars,
             titleBar: attrs.titleBars
-              ? {title: tempNode.type.toUpperCase()}
+              ? {title: tempNode.type.toUpperCase(), icon: config.icon}
               : undefined,
             hue: attrs.colors ? config.hue : undefined,
             contextMenuItems: attrs.contextMenus
@@ -1038,7 +1104,7 @@
           next: nextModel ? renderChildNode(nextModel) : undefined,
           accentBar: attrs.accentBars,
           titleBar: attrs.titleBars
-            ? {title: nodeData.type.toUpperCase()}
+            ? {title: nodeData.type.toUpperCase(), icon: config.icon}
             : undefined,
           hue: attrs.colors ? config.hue : undefined,
           contextMenuItems: attrs.contextMenus
@@ -1071,7 +1137,7 @@
           next: nextModel ? renderChildNode(nextModel) : undefined,
           accentBar: attrs.accentBars,
           titleBar: attrs.titleBars
-            ? {title: nodeData.type.toUpperCase()}
+            ? {title: nodeData.type.toUpperCase(), icon: config.icon}
             : undefined,
           hue: attrs.colors ? config.hue : undefined,
           contextMenuItems: attrs.contextMenus
@@ -1118,16 +1184,16 @@
             },
             [
               m(MenuItem, {
-                label: 'Table',
+                label: 'From',
                 icon: 'table_chart',
-                onclick: () => addNode(createTableNode),
+                onclick: () => addNode(createFromNode),
                 style: {
-                  borderLeft: `4px solid hsl(${NODE_CONFIGS.table.hue}, 60%, 50%)`,
+                  borderLeft: `4px solid hsl(${NODE_CONFIGS.from.hue}, 60%, 50%)`,
                 },
               }),
               m(MenuItem, {
                 label: 'Select',
-                icon: Icons.Filter,
+                icon: 'checklist',
                 onclick: () => addNode(createSelectNode),
                 style: {
                   borderLeft: `4px solid hsl(${NODE_CONFIGS.select.hue}, 60%, 50%)`,
@@ -1321,8 +1387,8 @@
       renderWidget: (opts) => m(NodeGraphDemo, opts),
       initialOpts: {
         multiselect: true,
-        accentBars: true,
-        titleBars: false,
+        accentBars: false,
+        titleBars: true,
         colors: true,
         contextMenus: true,
         contextMenuOnHover: false,
diff --git a/ui/src/widgets/nodegraph.ts b/ui/src/widgets/nodegraph.ts
index d8b2b9e..ad1eb28 100644
--- a/ui/src/widgets/nodegraph.ts
+++ b/ui/src/widgets/nodegraph.ts
@@ -73,6 +73,7 @@
 
 export interface NodeTitleBar {
   readonly title: m.Children;
+  readonly icon?: string;
 }
 
 export interface NodePort {
@@ -1687,6 +1688,8 @@
         // Render node title if it exists
         titleBar !== undefined &&
           m('.pf-node-header', [
+            titleBar.icon !== undefined &&
+              m(Icon, {icon: titleBar.icon, className: 'pf-node-title-icon'}),
             m('.pf-node-title', titleBar.title),
             contextMenuItems !== undefined &&
               m(