From 194d08e4e4ee09e4c06de1eca46987f9f012a7b7 Mon Sep 17 00:00:00 2001 From: mikebender Date: Thu, 23 Apr 2026 13:39:30 -0400 Subject: [PATCH 1/8] e2e: add failing tests for ui.table with rollup and tree tables --- tests/app.d/ui_table.py | 20 ++++++++++++++++++++ tests/ui_table.spec.ts | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/tests/app.d/ui_table.py b/tests/app.d/ui_table.py index 7b75096e8..ae2e10f74 100644 --- a/tests/app.d/ui_table.py +++ b/tests/app.d/ui_table.py @@ -407,3 +407,23 @@ def t_selection_component(): ), ], ) + +from deephaven import agg + +_rollup_source = empty_table(100).update( + ["Group = (int)(i % 5)", "Subgroup = (int)(i % 3)", "Value = (double)i"] +) +_rollup = _rollup_source.rollup( + aggs=agg.avg(cols="AvgValue=Value"), by=["Group", "Subgroup"] +) +t_rollup = ui.table(_rollup) + +_tree_source = empty_table(7).update( + [ + "ID = i", + "Parent = i == 0 ? NULL_INT : (int)((i - 1) / 2)", + "Value = i * 10", + ] +) +_tree = _tree_source.tree(id_col="ID", parent_col="Parent") +t_tree = ui.table(_tree) diff --git a/tests/ui_table.spec.ts b/tests/ui_table.spec.ts index 99c5ed4fc..948587221 100644 --- a/tests/ui_table.spec.ts +++ b/tests/ui_table.spec.ts @@ -74,3 +74,19 @@ test('UI table on_selection_change', async ({ page }) => { await page.keyboard.press('Escape'); await expect(page.getByText('Selection: None')).toBeVisible(); }); + +test('UI table with rollup table', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 't_rollup', REACT_PANEL_VISIBLE); + + const locator = page.locator(REACT_PANEL_VISIBLE); + await expect(locator.locator('.iris-grid')).toBeVisible(); +}); + +test('UI table with tree table', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 't_tree', REACT_PANEL_VISIBLE); + + const locator = page.locator(REACT_PANEL_VISIBLE); + await expect(locator.locator('.iris-grid')).toBeVisible(); +}); From 96ead3b525028091a1212d720af0dafb7398b093 Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 24 Apr 2026 11:27:14 -0400 Subject: [PATCH 2/8] fix: ui.table works with rollup and tree tables --- .../ui/src/deephaven/ui/components/table.py | 4 ++-- .../js/src/elements/UITable/JsTableProxy.ts | 4 ++++ .../src/js/src/elements/UITable/UITable.tsx | 4 ++-- .../js/src/elements/UITable/UITableModel.ts | 23 +++++++++++++++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/table.py b/plugins/ui/src/deephaven/ui/components/table.py index 54804a0ec..49a03d421 100644 --- a/plugins/ui/src/deephaven/ui/components/table.py +++ b/plugins/ui/src/deephaven/ui/components/table.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from typing import Literal, Any, Optional import logging -from deephaven.table import Table +from deephaven.table import Table, RollupTable, TreeTable from ..elements import Element, resolve from ..elements.UriElement import UriElement from .types import AlignSelf, DimensionValue, JustifySelf, LayoutFlex, Position @@ -292,7 +292,7 @@ class table(Element): def __init__( self, - table: Table | UriElement | str, + table: Table | RollupTable | TreeTable | UriElement | str, *, format_: TableFormat | list[TableFormat] | None = None, on_row_press: RowPressCallback | None = None, diff --git a/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts b/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts index e77c600a8..12456e223 100644 --- a/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts +++ b/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts @@ -211,6 +211,10 @@ class JsTableProxy implements dh.Table { allColumns, updateIntervalMs ); + // TreeTable.setViewport returns void; avoid wrapping undefined in a Proxy + if (viewportSubscription == null) { + return viewportSubscription as unknown as dh.TableViewportSubscription; + } return new Proxy(viewportSubscription, { get: (target, prop, receiver) => { // Need to proxy setViewport on the subscription in case it is changed diff --git a/plugins/ui/src/js/src/elements/UITable/UITable.tsx b/plugins/ui/src/js/src/elements/UITable/UITable.tsx index 0101957b0..98622d186 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITable.tsx +++ b/plugins/ui/src/js/src/elements/UITable/UITable.tsx @@ -104,7 +104,7 @@ function useUITableModel({ columnDisplayNames, }: { dh: typeof DhType | null; - table: DhType.Table | null; + table: DhType.Table | DhType.TreeTable | null; layoutHints: UITableLayoutHints; format: FormattingRule[]; columnDisplayNames: Record; @@ -233,7 +233,7 @@ export function UITable({ api: dh, isLoading, error, - } = useExportedObject(exportedTable); + } = useExportedObject(exportedTable); const theme = useTheme(); const [irisGrid, setIrisGrid] = useState(null); diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts index 8bc31d7f4..f731b6303 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts +++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts @@ -46,12 +46,31 @@ import { interpolateColor, normalizeValue } from '../utils/HeatmapUtils'; */ export async function makeUiTableModel( dh: typeof DhType, - baseTableProp: DhType.Table, + baseTableProp: DhType.Table | DhType.TreeTable, layoutHints: UITableLayoutHints, format: FormattingRule[], displayNameMap: Record ): Promise { - const baseTable = await baseTableProp.copy(); + // TreeTable (includes rollup tables) doesn't support copy(), naturalJoin, or + // getTotalsTable with the same semantics as Table. For tree tables we skip the + // format-driven column processing and use the table directly. + const isTreeTable = TableUtils.isTreeTable(baseTableProp); + if (isTreeTable) { + const uiTableProxy = new JsTableProxy({ + table: baseTableProp as DhType.Table, + layoutHints, + onClose: () => undefined, + }); + const baseModel = await IrisGridModelFactory.makeModel(dh, uiTableProxy); + return new UITableModel({ + dh, + model: baseModel, + format, + displayNameMap, + }); + } + + const baseTable = await (baseTableProp as DhType.Table).copy(); const customColumns: string[] = []; format.forEach((rule, i) => { const { if_ } = rule; From e28cde0a811ea16bb322527078aca14671334dfe Mon Sep 17 00:00:00 2001 From: mikebender Date: Mon, 27 Apr 2026 09:15:04 -0400 Subject: [PATCH 3/8] WIP rollup tables in ui.table - I don't think formatting works yet --- .../js/src/elements/UITable/UITableModel.ts | 40 ++++++++++++++++-- tests/app.d/ui_table.py | 15 +++++++ tests/ui_table.spec.ts | 2 + ...table-t-rollup-format-1-chromium-linux.png | Bin 0 -> 10797 bytes ...-table-t-rollup-format-1-firefox-linux.png | Bin 0 -> 22091 bytes ...le-t-rollup-format-if-1-chromium-linux.png | Bin 0 -> 11322 bytes ...ble-t-rollup-format-if-1-firefox-linux.png | Bin 0 -> 19971 bytes 7 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-chromium-linux.png create mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-firefox-linux.png create mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-chromium-linux.png create mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-firefox-linux.png diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts index f731b6303..8fe856f9c 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts +++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts @@ -51,13 +51,45 @@ export async function makeUiTableModel( format: FormattingRule[], displayNameMap: Record ): Promise { - // TreeTable (includes rollup tables) doesn't support copy(), naturalJoin, or - // getTotalsTable with the same semantics as Table. For tree tables we skip the - // format-driven column processing and use the table directly. + // TreeTable (includes rollup tables) supports copy() and applyCustomColumns + // for if_-based conditional formatting, but does NOT support naturalJoin() or + // getTotalsTable(), so databars/heatmaps with auto min/max are unsupported. const isTreeTable = TableUtils.isTreeTable(baseTableProp); if (isTreeTable) { + const baseTable = await (baseTableProp as DhType.Table).copy(); + + const treeCustomColumns: string[] = []; + format.forEach((rule, i) => { + const { if_ } = rule; + if (if_ != null) { + treeCustomColumns.push(`${getFormatCustomColumnName(i)}=${if_}`); + } + }); + + if (treeCustomColumns.length > 0) { + // TreeTable.applyCustomColumns is synchronous and does not fire + // TABLE_CUSTOMCOLUMNSCHANGED, so call it directly instead of via + // TableUtils.applyCustomColumns which would hang waiting for that event. + (baseTable as unknown as DhType.Table).applyCustomColumns( + treeCustomColumns + ); + format.forEach((rule, i) => { + const { if_ } = rule; + if (if_ != null) { + const columnType = (baseTable as unknown as DhType.Table).findColumn( + getFormatCustomColumnName(i) + ).type; + if (!TableUtils.isBooleanType(columnType)) { + throw new Error( + `ui.TableFormat if_ must be a boolean column. "${if_}" is a ${columnType} column` + ); + } + } + }); + } + const uiTableProxy = new JsTableProxy({ - table: baseTableProp as DhType.Table, + table: baseTable as unknown as DhType.Table, layoutHints, onClose: () => undefined, }); diff --git a/tests/app.d/ui_table.py b/tests/app.d/ui_table.py index ae2e10f74..692bb2978 100644 --- a/tests/app.d/ui_table.py +++ b/tests/app.d/ui_table.py @@ -418,6 +418,21 @@ def t_selection_component(): ) t_rollup = ui.table(_rollup) +t_rollup_format = ui.table( + _rollup, + format_=[ + ui.TableFormat(cols="AvgValue", background_color="accent-200"), + ], +) + +t_rollup_format_if = ui.table( + _rollup, + format_=[ + ui.TableFormat(cols="Group", background_color="salmon"), + ui.TableFormat(cols="Group", background_color="positive", if_="Group < 3"), + ], +) + _tree_source = empty_table(7).update( [ "ID = i", diff --git a/tests/ui_table.spec.ts b/tests/ui_table.spec.ts index 948587221..036498ff8 100644 --- a/tests/ui_table.spec.ts +++ b/tests/ui_table.spec.ts @@ -33,6 +33,8 @@ test.describe('UI table', () => { 't_heatmap_both', 't_heatmap_databar_overlay', 't_heatmap_databar_mixed', + 't_rollup_format', + 't_rollup_format_if', ].forEach(name => { test(name, async ({ page }) => { await gotoPage(page, ''); diff --git a/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a705d2fb56c401d3f45f50d0cb59dc1e3d11dc74 GIT binary patch literal 10797 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 QC9%NmTZ%U`qzoVZAEnJL(EtDd literal 0 HcmV?d00001 diff --git a/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..716b2585c88a7a5ac43732df14dad31389437a6e GIT binary patch literal 11322 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 literal 0 HcmV?d00001 diff --git a/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ae3a19558f9ed8fe9b7e8837f3ec07a722562d50 GIT binary patch literal 19971 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 Date: Thu, 30 Apr 2026 11:08:35 -0400 Subject: [PATCH 4/8] fix: reject ui.TableFormat if_ on rollup/tree tables applyCustomColumns on a rollup TreeTable adds the column to the source rather than the aggregated output, so the if_ format column is not visible on the rollup itself and findColumn throws NoSuchElementException when the table is loaded. Validate up front in Python and raise ValueError when if_ is used with a RollupTable or TreeTable. Also throw a defensive error in the JS model if the case ever slips through. Remove the t_rollup_format_if test case that exercised the now-rejected combination. --- .../ui/src/deephaven/ui/components/table.py | 16 ++++++- .../js/src/elements/UITable/UITableModel.ts | 42 ++++++------------ tests/app.d/ui_table.py | 8 ---- tests/ui_table.spec.ts | 1 - ...le-t-rollup-format-if-1-chromium-linux.png | Bin 11322 -> 0 bytes ...ble-t-rollup-format-if-1-firefox-linux.png | Bin 19971 -> 0 bytes 6 files changed, 27 insertions(+), 40 deletions(-) delete mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-chromium-linux.png delete mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-firefox-linux.png diff --git a/plugins/ui/src/deephaven/ui/components/table.py b/plugins/ui/src/deephaven/ui/components/table.py index 49a03d421..b3f77c6e2 100644 --- a/plugins/ui/src/deephaven/ui/components/table.py +++ b/plugins/ui/src/deephaven/ui/components/table.py @@ -157,22 +157,34 @@ class TableDatabar: markers: list[dict[str, Any]] | None = None -def _validate_table_format(format_: list[TableFormat] | TableFormat) -> None: +def _validate_table_format( + format_: list[TableFormat] | TableFormat, + table: Table | RollupTable | TreeTable | UriElement | str, +) -> None: """Validate format rules for the table. Args: format_: A formatting rule or list of formatting rules to validate. + table: The table the format rules will be applied to. Used to validate + that rules are compatible with the table type. Raises: ValueError: If a format rule has a mode but no cols. ValueError: If a format rule has a heatmap but no cols. ValueError: If a heatmap gradient has fewer than 2 colors. + ValueError: If a format rule uses if_ on a rollup or tree table. """ format_list = format_ if isinstance(format_, list) else [format_] + is_hierarchical = isinstance(table, (RollupTable, TreeTable)) for f in format_list: if f.mode is not None and f.cols is None: raise ValueError("TableFormat with mode requires cols to be specified.") + if is_hierarchical and f.if_ is not None: + raise ValueError( + "TableFormat if_ is not supported on rollup or tree tables." + ) + if isinstance(f.color, TableHeatmap): if f.cols is None: raise ValueError( @@ -369,7 +381,7 @@ def __init__( ) if format_ is not None: - _validate_table_format(format_) + _validate_table_format(format_, table) props["table"] = resolve(table) if isinstance(table, str) else table del props["self"] diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts index 8fe856f9c..1992aed76 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts +++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts @@ -51,41 +51,25 @@ export async function makeUiTableModel( format: FormattingRule[], displayNameMap: Record ): Promise { - // TreeTable (includes rollup tables) supports copy() and applyCustomColumns - // for if_-based conditional formatting, but does NOT support naturalJoin() or - // getTotalsTable(), so databars/heatmaps with auto min/max are unsupported. + // TreeTable (includes rollup tables) supports copy() but does NOT support + // naturalJoin() or getTotalsTable(), so databars/heatmaps with auto min/max + // are unsupported. Conditional formatting via `if_` is also unsupported: + // applyCustomColumns on a rollup adds the column to the source rather than + // the aggregated output, so the column is not present on the rollup itself. + // The Python side validates and rejects these cases up front; this is + // defense-in-depth. const isTreeTable = TableUtils.isTreeTable(baseTableProp); if (isTreeTable) { - const baseTable = await (baseTableProp as DhType.Table).copy(); + const baseTable = await (baseTableProp as unknown as DhType.Table).copy(); - const treeCustomColumns: string[] = []; - format.forEach((rule, i) => { + const hasIfRule = format.some(rule => { const { if_ } = rule; - if (if_ != null) { - treeCustomColumns.push(`${getFormatCustomColumnName(i)}=${if_}`); - } + return if_ != null; }); - - if (treeCustomColumns.length > 0) { - // TreeTable.applyCustomColumns is synchronous and does not fire - // TABLE_CUSTOMCOLUMNSCHANGED, so call it directly instead of via - // TableUtils.applyCustomColumns which would hang waiting for that event. - (baseTable as unknown as DhType.Table).applyCustomColumns( - treeCustomColumns + if (hasIfRule) { + throw new Error( + 'ui.TableFormat if_ is not supported on tree or rollup tables.' ); - format.forEach((rule, i) => { - const { if_ } = rule; - if (if_ != null) { - const columnType = (baseTable as unknown as DhType.Table).findColumn( - getFormatCustomColumnName(i) - ).type; - if (!TableUtils.isBooleanType(columnType)) { - throw new Error( - `ui.TableFormat if_ must be a boolean column. "${if_}" is a ${columnType} column` - ); - } - } - }); } const uiTableProxy = new JsTableProxy({ diff --git a/tests/app.d/ui_table.py b/tests/app.d/ui_table.py index 692bb2978..6ad9a188f 100644 --- a/tests/app.d/ui_table.py +++ b/tests/app.d/ui_table.py @@ -425,14 +425,6 @@ def t_selection_component(): ], ) -t_rollup_format_if = ui.table( - _rollup, - format_=[ - ui.TableFormat(cols="Group", background_color="salmon"), - ui.TableFormat(cols="Group", background_color="positive", if_="Group < 3"), - ], -) - _tree_source = empty_table(7).update( [ "ID = i", diff --git a/tests/ui_table.spec.ts b/tests/ui_table.spec.ts index 036498ff8..6c5f18a11 100644 --- a/tests/ui_table.spec.ts +++ b/tests/ui_table.spec.ts @@ -34,7 +34,6 @@ test.describe('UI table', () => { 't_heatmap_databar_overlay', 't_heatmap_databar_mixed', 't_rollup_format', - 't_rollup_format_if', ].forEach(name => { test(name, async ({ page }) => { await gotoPage(page, ''); diff --git a/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-chromium-linux.png deleted file mode 100644 index 716b2585c88a7a5ac43732df14dad31389437a6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11322 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 diff --git a/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-if-1-firefox-linux.png deleted file mode 100644 index ae3a19558f9ed8fe9b7e8837f3ec07a722562d50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19971 zcmeIacT`i`*Df5IC@P2wDBXex2#A98azsEyKtXy5f=Uwt(g`7OP>^n;C?!}Z(vjX0 zREmOh>5x!EC-j7bB;Sq@&+onOH{Scl{o{^1#$gPHVUxY~T5Hbv%=WAuaa&*KFdHu$ z2n0HO}!Vcx>ez z`OiiA*ECKEgJe1Xmpf>Gd~K!yGMo6)EBD{VV_;;|@cfVQ{|ptu!W=9jXk~roKcBw) zSKuM6|32iu$VKDd?fO@{{#S$i*G}kY@@HrMwUd8Q_`i9Z|1&%3WMdW=7k}@IQR~Xo zOMYKrY-}tu9rL5uZ*{_?6KZ{S&wrJ=F)-LJ2*T?`cQ05(XrLI#9}IA4?*L{0wbw}- z#0Hbo>B)NzH>6KO81$&lzHnyuf`$a=>_D;Rp_5l+hss?ovzp^S5g`pLFN$ry{B3gj z-9NX90k@@27WC6^J93H2?Yj?5`@L zLxM^QL*<_=$)F|OmE&RfKn?GA0(jc zowL5)^+CIN(Y(Zx=Q;Z5Uie`V5ezDdKUC_rcOhR_*p%v8x*FEyy;Qc~6}VGbiUB9* z0IR*7is%T0K8%Jvcy!lTjOmf}Wi80=1VZ|Rfz=-VkQcYN+T3Gi(A2Uh(8Uax91NlC z>lV!`lSAklI~DrDbEaJm)%pd@4)Q!Qdzv1EoKFO1VKwtI%8?_5P+%LVHkHr3bbL@jotO<)FJLle7Zk`hcT^J0WXWzpo+fhMP zyG|n0g2vCca+hsRlG~4BPM_tk_3$!x$ydVHC9pHSQG@NfMvtCCjvm+#(v?lBzPhK& zj>F}yhfXS;6S|>q0%b#)wWQ-78A@E4SqC?JTTfe|^%R zix;~PfC`UFKO3oW{+i|Xq) zS+(N?z40r_DswSOuPGKUa;X92_r3@6|L4(l!1>faIbm>^vxd9B5DUa9M zo1(^t177nfTR1)?g(VDLe;WPu>A@(YKtR%wR$NQsc4k95MbGkh#I8fO9TFl6Fo(v;y zMFf!-5NB1+q@GoA%RtYZ`mGiCmb2*#*o2W?X$$o$APYFFFA;nepU+sKX@>G zv+gqPOp?K|<<57SZR^Iy@iGBZNlBL-+HcnNX#O)N8fx0W!FwRPKkv0h0i|^uW8sp& zsehZtSSD!XNs^lDPz`#p+%;LL^~w5S#ev+syny~XRp@3k0~717?d@SmTeN9&tX@Yg zi4s2_98T9!=C`*1_`3f~?HXMcDj{y8)s6f3TyM`jt-1=?_}+zMn|o}g;P?GpSCwv} z623zZA7{}*u3~d{A2Ka6%F@R@i4r3~A8bg-xb)>UEj*E@W5Rw}nlau;f<2*IfFEPk zTgaakNET3C&?I4>fV}4WfU$n(=ebYL zMY?r4Up(xa8vz*SExe4TFIc!}2uRCvQwRR!UooP$`MYH<2y}O?rfMp!4X<}&tlm|W zy3j^SE9*B`zB1zYP02VIfKy3i+2``oe3@?B|kDk4R(l1DA{3i5T-BlgL;KO6MlhXhlRU0D!} zlH?mTSBu&!ZabGOE5sRWgI0-rc|xQ8OExonug>X5{(#(aV-SB!cNxM(Dhv!`YfpC4 z=z?WrR`QxX0-#WY6^j{cdLH%S(@{uJ&UeecvK$86toBs9@13W-w-~KT9q$|xQd8lU zwi;pdk`19pv9@oh)Gzs0Fx70Fdo~lS(aL?|qI=!!V6JJZw*H$-BIopzDo4T!ok2L|qD)Sfwb``7qLSRk9nrg2r}r6m>x748dWm9^&xs(*&N%(fqt%!$dOL-S$I6Qz`N zvx??wZ#S5IH{T1Y^Y`8p47L<~DiHqo0^yI^Hukl={nsZ44m*YYks$Ni8-QxuT2Z4z znBy~z8L3qJmFIefn;7MI--A3-(VWt8&#YJmp63=b*840CH5RpnWy1Ck_Xd{d5Y=^<1YT1_^7Mk;oOd|M^`;#-TkW?8*<=9{|%k4`}k$ zM+mwDki9D$?xVP4>5^YIukuo2DbpYs-PwN}Pa6-@pX*y_2B{D59$MM5gf!;bH*47( zU8LW7#L=AQJLCi{H23i8Jae@-j+^gMI5{70Y%j~xM(7cq44(_IJqz?B=UaIfW&Ds})iY$D!3I;`%cHvxeXQ@}@~ z2-ZWqW_Ni^z$FeHvHXlVj8@~P*8XU28K51e5`~F$?`mR|?$D7YX2o}V=F1xZh6ZHp zO~tJ&kAgBaiUIt@r6h~9>EvJ2x)*d~wf$WvhRYDCwAJh9(qt)tb~5-!s8;}?I)B)- zqYE|i;M6?oUNU>smH=Au{uz{rG`Wq-M*>>rI(+V_kL!YpO*}g;JI`ucl~nC*DcWa{ zbv#SHzt$I6l$NFG)bp3|ORr|=Xu`}3#A}a|Ue(Yq=$-D&f*@x5QQ%ZaTawI~raC1l zyXlhZ-tH|cmdYDS*&lWDLvQ97o)N9Raw@Y<(X}kFN0)98%qFxDW8z`jUVyPbk2@rC zi|cn3&|hHPM)HhDf9L#QhaU0Ioledrb{AOJWcEbL-z@acVsrnp!y;ox*l!%18@b?5 zm&h3)9OOID?Lwblu6QS;j?$H7fKw`bbH&vnT1Y;~7+g1$)?pmH04e%Qsmw1`36n%1 zeoVULzvfIT;GxT7zbr7h0e1;sdgNk~tPvh7U^iQIs;k^pD;07GJ~y3&5T7^MoFC0H zEp@@XYj*+g>BK8h1M^-}p75wcH(3Avh2+lkjpJ9l#4Tx2+JeI~?oAXFVhku#!CS;1 zbci|q+<@GcMeRdo-g(ZQ@cs+Y5`F^(vjoRF^;d;LTL2f(MFT!p>eGk~H#3zrNdFZlu^#R~T?Uuof| zwz8J*4N&EcVpH&v%W{gM(}9&)ZF9yH%rH`u^!{b>`-xQ8Aqj~du_hAIy zVob>Wzq)CW;y~uN_due7d-B%USPOw|OYQ&FwyOGPj%R`7`Y>HD+k`vM8Be)h@_RLF z2r*xFDMWJjWEnuhxaH8Iq>Uv3G7(?6{Gh+v1Ao(hvD+PW+Thfm?;^@D*DA0r=S4SqFefD8jttp`2++VB($- zI-P+SCf{e)J7xERqf`k0^TR;K3*-iOvkqLw1Sw5c^lQtLNXB`gl%ckGKRGq86AmqL_pH#fMHGH}M3D)_{ zo;Ib+0s?z;l^sf2*I*(gF`e!%8XoilR zlG?kV@qukB^b&DvfyZ^I)ae(#d@3ZyuG>WU0&+~HU5ItUwri0(me>U%ie%Kon!uw3-qZSJqS!qrLtO^o~ZnZ znlv;(j_iK`{+|!$ivcxg2+eib8{$6)We&#f$@rgPf zya?d>x0I1Tx%8zFBEuM7FyJh=sY%~9rI3nypvRa zCJn?N6q!!CAJq^Di9Za|i9Ny9p+iQ04PxBZuF9u2;6ZpgaS@-BU`w_>Jv=~u_-QuskP0Hpp8w9xnaS-6|hk=r# z{c3?aWjtNDgYuCS8M6xqxpWp#qI?5%M}bpQak%FXiRo|j`wk(jnLRb?Oa_~jRg zTUVxP!d}zmmi}P>evoC8f+GtZckeLPV<^~)Mz!FwLgGDZGBjvw<5*WA95wM`XxxrM zs@p=Jk*b;Ke%qhwJ~A^=HA7h~0J%H;@0Ngu&8T|oGYI16EBz)`MWaXy;n1uUuK)`q zspi4H;4<6~;~+Shgm@49_+fR_cRZ{-2K$}Us4G4~k;BVopBLr9GYC>IVbI7xe)fM{ zfwt`fJm{x1VMD)$8%pR!&WAq%hKKH>ED6D-5=hwGAQ0gl2mHJnYCRRSl2XzhRomca zj+1@;j|>2m9{Aj(`6pH^7H1gC@);6Arx3B1nCgl6oi%EZU|qnbc-r}=|Qr~^I(wsVcrXR^zw4J1{t)%ilN?P z8x>#hQbTl;>R}ky+eb{{Rc>VGIz4#-ZVnYMa4C>|5wzKW4j}&=Cz}k$*mu((xpb7;l;%d{PSH)X^{`ZAuh50>=yL~zYyB~BJ zgH49;>IlN_2HFnebvgu9CzdKOu`kZ(aI=Q)tnpASbt+H2vx^A3W=gjUmr`K$F@kC_ z^fJ~z?oYVr5d%Z|$*b<_ub$Dl0Caf!$LXav09JjJqo9DEm(oWDR(S;Umg$c={PW@F z1z_&0WAnWK=}`A>V@Oy0XY4;u`+xf(vS#T(rvk{45GQ2pu7x6;oaB8L2V}4v2-MfM zmX5^fcCra2YtUoLtpOU_z!W4m$uLjIEDFV_I3cydi*Tb zX%sD|`lYz<@K`)bPV6nxl}0IG+meM$VN3H|Zd5>r#oeklGYLPld?c>2n@;Y}D>F&- zBfMap3?T_c3k6ccW7dIydWkVr**A972P5Y*;uJn%ISJ+i?_feg_ij}6>n`f zW3Y65Ce`z=3qX)s#V{7ife~Wb$A0|__Qfi3;aC)Yv1n3sa^tdo3~Xx%^%prWJNsD& zj8anE>V}A*6-z5UCMRFuVCq3zQysh;JD=UEcU%&iQjVkROhHMcfFBQddi@($#?MSV znN$;!R0XN5&7QIAOlSB6>F+Cro0H8S{EQF$5^oCTgcExZlf?)Xo7z*RCN`wa+=*g@ z8YhZ~p<4c^xYBMklYvJidiFqey}DS^(Ue~?B6#`X&uss5)b&!zz6Bn*cI`Gcc@+VtiY`}@m;JmK za~oC3#LXBX72@?(ypCYdtu{{`et)yBIOuY<{IWj07wJ~H%D4K3!;LR1El_)hT!m`Z zQQvw|iLK5}80u#g@;i_8Tv);pt=Xyiic24XMpB2$`mo=*r?B3?z?4yT<)tGvS?=yT zgjJ6=`MRv8&EL zdyD%CzNk*CGfgnn7pB0p?;$j&=OyCipqmcy!lvWnDmsGj#UBLpx468omb~~b3YpaG z4^|c3cgRfoJmRk45+{4ao^Y*P2&xJQRTs+A*Un%mt}M^`^X=ke=Mf;3)O!~ArOVJh<|okQG97-{;)~*T zrG<~GB%5eHq=$X&){x5!rus!?m%haTIpY)hsl7^v(@R9I1zB8{}1xw~VT6}~IS~IBRuW-9WYxQGQpI3)+3!}b-%16}UN0our zHlL}Dhxg7Tj+(vQM`2y$t9mJI+DM-6fa@($Olrwn!;Lu4;b!EeD=dGhPJSD9O-#7d zk?Tal!#C=Fg$2n0!B((=>h>?_mT1Lv0d)DgtDt$bCvv%RL0JX>@JeEf2$v=9!9W%S zcY0v(ajvA%*oJn_#Trpb{Epu0EpG&Mvt!1-POq8 z23Y_dg3|0|l z6iv5atF5AaJD%3CZ;1vJyGQX`>&mO1;G7=x5d2jxWzCS7)os>W&_W*J(Kovk=T=jK z-5T8*$j@f3C4ALgIzUK!AGDi1ywwI`OWAhhZdZDK|1S=Z=Sm_|N`JSS1(VVA&uLRD z-u{n8=cOZl>h1)sO(M9d-D!ojr8pRFSP4mZD$Nrz4plWp1eya~5dBoUtmFIAe271a zsYq6?ce-ll?SZBeX}L~4mb6&>X$d8D^)-O%S`lmcaA5gdtkO-p!zEuVpHtiAlnpJM&EYg$Vo37=qP0$<~A>9ubb z^1M1!;d(p86Kxu)DuxMI$oG@}CT4_=R>oE3E9{^C!YZgA{{B!zlysdia-~W-Q`q-b z*9`wNbYM?K)L3DF|J;lk)b0@6-|x{}3u+gTY`AW6(LmB4q50v{$CG~WvsdYi)x=i!5VzGLN1X)Io1Rez3czv1Mx44Gh> zgI`dX{@CNJ_9C>V*r{p--}90vnigri z@;gr1I$XC%@LfwZkaVlTQ)-NiJEbSyu`ZYxO=Eh43Mk5nTp%nov7FftP)=NCQ7+KX z_n3%4#4&BDD@bzfxTmUuQ2vvfjcN{wR$k^^4OUBAJ)(j!kgWOKT4^f^>6QMrAzRPZ z^?2*ai8+M2fV!S~$G9yrfd_A7ZUWMsLRRc#T$gZ;L?-n$vWnq})9 z*k+o)Vtp=BUv+!qePm$}i>;dQv^|307&!jKHzy-#D@wH6S2_ZfaZ!AD+8Jr}=}n=v z?{Egza{KsrT$Y2h?q4{4#E#c0srHd4VdceY>fO17!rI#P*t_Jab#{c<-}NI~Rt%;~ z3At5|2TVq{@D_|Icv&k8_?kR2VBEhLa0JZ(Ohle9_X~tu`=KY@$~F@`)+-><*6JlX zZGW9N1w$X?@beqp4VqRHddv7JbW8nXQ=wVY+nlZ(Qdy};eHl|d|1ES%LNj)Lz{6>0 zJw6`~@S%JQnIcggc#OxewUdpC$qw0kCQxLFr^<&jc>4L`)^TB4!o4~h7i8EI8%9Z_ zHJ(mb8du#%bQFT|F()oRd^F;TNnPDOhCCr%SNUAXZ&4T9*Cx2h|SM zM#3ag9+3GOQ6oTON<@%6#}RomzxJcIc}FkK#E2zWF==O7nj#ub7#y3>4Phd^|a_9Gj|?iENh+ z!dzjAAFc6~{+d>M%g5gp+$R{UyI??Rs#fijY;wBF<`(;@cLslHL{!pRZ9T6iN|iIW zu&legH*jL1J0W@oiVPyl$y`2Z9k4pxc&7Cr2tNuzC~TI#vTrsB%E9tgDXrzW6kKnu z#h*`r^n)*@kkS$})8NZ2W#P~a3e=u8xT;%P=@;S`O?Ft{+MSxo2d)+a;4| zd;sd+e5t<<{toHu*Ju^RsvOAIC#xRbN-~}R%>yXoCE_DVKPJ^9Ay)XdyZOCxl5bRt zGIJaN;X||l;d7{OTvu5oywd75VUKpZuItfplRob;|lGI;neqWbXs8FaMPdXDPt?$|f zU;tL+XBNW4LdkuiGdnLQ&uQlGICEWE*BOI8JWW|Ig;>SE$|`93of3_2)cf_m%QFLkAFy`w&+q>15r5+p1iQ?eO8*Fnv7u_LN=bZI%tF>*uM%pP&^q~x5>L^we;->(&Jm%>P?HF zB)mhbshX=V>3H6hrp8)XPrEXAxbiPGmdaDQ!Rvo9^@d-ba?er%{rob@0pB6t71&xJ zeAiJkJO;vw)NdDs%|{m1PRS!~1uRHba6tvI^_N*~$_n{;)i$R(CJL07r4o9vM`(F6 zc;s{270YHlCv)e`Q^JGGZSt5rfA|t!79a`WS6W*Q@tB!(CEZ84sEYSo!T#GmHdrgt zaSWFel-&==5K4X-1t)TY9^zRAfz-8%M3Ge!lG1-kZaF#g>nAGXB$mn1?G|YtHmA5Y z3rx-pLadu%FUm-dBWDVrQNt)wCrnH@D5qVlUe}s2aQ2Rj+lWi!WyOtAX@Td4&;#yE znws;bpn};`!eykc0j^+OW#*}JWt8>Z1gWJX&-uMfrE!II%zo=_@&@6fBQLkmt#JLF z497JtQyYugnM^*E_0DAv54MbTn`i!TWbKokcBq?l!n2vs`)lf&_^fZ&)+k}U{c`=* zZ?>3r{@PJsiQaa;4yr5#w!EYL2Ha8{T~KQ;suGrSKqr0!PHiqk&YLGDM>($T5s1Sh~JoH>wmFg$LaI zcxBp1dx2FcoZrXY162QME-k_-u6P|8;KR8r@OWjsG0hz?+>zy1>o!49+)02pnR@`V z{XcCt({^ofKEVl(l}sHylEevAC{#9lBU$spL4J ztA+9fia_tz=F*wi#SS5TdZ!zIeD7r#i&chQlBy^|CRHNEAf)LVI~)iGBB z!~vxL7Cj~6=Q5sWDBV`vZp~^`H+uFmmns-E^@Y$Y4d>!e-Kg_wam5TcWeH5?NEAwn z7nsO`ev(_lWk(2Y$x#$Y)pUYb=K>c#Q{z0i^`LZ|67l=}RCa?Ko|#5(MYn_FA8gL7 zi>5u_dPOo*dNM`uTKQRDN&Nzi)DRasziJb9OjJJ-YFIY^m1Vsr$VdL8zu}9&t6<$h zfxgF$@ArlWPmw`zpxwa3`hhdaFk>m4^m&p<3M^tXp85-1A@s^Q%ptSaNWA(3?% zUX)#*ujZ|Y%Sie*I=oEq$w?onF^jc^O~Ye}8OQQERA4v!`k#X?Vk+Ro%eKwO6>L(! zA`eW8UWk{)9ZaIKJ_Tom>I>=(pZrsr16od33Q>i$-m8WLm%`$Trd|K2Z~maKWcXcw zRDqx>^mrB&`|Ys40jtVXkMa6L>e6t9bj}p^BQ>Z#h_ydka-k}a=&Hs)zG0Ne9Hw~W zJD2A6#*mDCQ-ct7C2K&k>|HhEMU*USfbw#s{jC&Pv38ZEJi>i8EW6_3^S7D@)|7$r z)q& zUa{2$YGd*CTjwcF)+`4K=S&(bsO{akP0}kob1e|yM?T1g6UahQ;2H616^B^0i2=I{ zm{#}ON-~7#68)(Bm)|yycdM>1I7Eh?T~lde7~|9t_?=i!8X?e%PrrJ0M>JPtX}VA< zG7tC%N>>y9wu`F{eCe(>SP_(-$g!qyW{Iz=jZ`6)aMz&++AaUt1)0DFG7(~Y$y6WM zy%K3dBb8i%*$#6wtcvpUY4$LvuVXtTa)@qvtm9Mj1M@(SC#jk6P6wIH{>v{Xn+P5I zA_DsvU$R-qPgEtN3>t5MRYcTEN5dnlEUe`$VURT3ws2A{%25y%2GA%GxwTUjI!fM3 zad4X!E7)mwigkhZi0V_+WX~sT8OK5Mu3~rfy&9JvBV&D2$;K|Pxzjd;KgG3je4mFl z+n^P5%ju$0RZwfUJULt9c_`9q{EY;|x^W%+e!;o6#1{3-VZVdj(hESDtd*A|pXN+M zOoX2&=GwrP`X?e=kqHSmDDSv7%y=xLG;{%_@&>E5`8%1SNtdMU3t^HAl<@8Y{oz6$ z1BRG*ya!MoNpt1;mWHRcW~Lf7-a|g_omr;_yn85AZxWKeOH{EvOl#&agno>mwKZ-_ zh1B9q(8*G`p6S!K&xHW>B?+HV??gg(glO4!w@n;IUGgBV^jCS^TFby&k)~ex3k#H! zmmglUd0A5`fjjvIU+6_|pIAf!q=Ss;CqDnti1{5I01q-d4D3>G@cdo}iQazv-6r|> zzW-bMk}ktcD<--WmiXlLGJ}<3@{`$JlXD=m^4i^M%j(OZ!ES& z?7zL63bi*oGRN^1Ws)`z z-D7?;9a%$g!)amnHNIC-D68F`oJK34%;nJ@ooqBdjpH;U7Z+D?*Ev3|L@8_Mzfng> zWozc|NX&iLLUMl1icnuQ0)>3L#iZf{mcoc{*=@8zZYu%CHy3vIcg|s}cMRy}8$D9- z`vx#jH9tN^W#6PO=S@KL__UTpG}6m8jENkA-#{%eu^cd_^`J^P>k@0Z_7d zTL19|q+x0!agVF!7yvA>Cz*8mH)1_w7f@=*OorV2n>H<;Y&Gt)(hNb{8Hx6*r*%Vg z)n}Ei)67y*AE;AKgk1}zMS|&a`}0s=h+VMJWhcdvmoaPB%qHO)HV@Gi5ClLli}PDm zmoA7kxTf}H2F76l7KWMx~HxHXsX?yfWa&AmSN1-Emq39x}m?Y=9G=!_|xg~ABXeJZEu_$o8|mqzm+nW zfAT8kcRH%^3P?_qQ$|>S%dPAnus!LeJHDfR?djCRN3wxHc^d86K~@d;m{FVj=(ES_ z6dA921V!vEOx)&Xuw{Ba+U>_hWoLVJy8~8SN!A4eMd*sr@QFqqaHLBCvS1(-7YuZ0;@~?Ux{xrE-)67r zB$NZL-V_UyOW+0-l(Z{(42*|2^;ts9`L!Qu$T%(kzIEWRAc>v_EW|-u>-j zh-CTD`|P2|oMven-T+H|ki3qd@{aaUUL?DgJFO11$&d0%*KHV%c&*DvwbZS*lbwIQ zxhx;6M0`f*3|BjWAunsnh(Gd_a^+16K`j!u$?*jV`tifHX8OzOao zs&D_Wm(y#xH(pS)bTnKrz+G>^&o)PP#095HqyMk=u1iPKX#A2GVgCRMk z%nBX3jdPtz)Y>uOBRw?BX5KHG0r+FFGT1H(oCOqU1vPw6=G00W<0c2gC*&#-5-360 zEQB{^CcE*@q{6oh5pW{VFe!ltmg|Em@oLbvqqye*M-Bz8#;JeLxpaUDAPAiowdaIC9( zrR$CoOE8`dWH|>g-`z}EgV(Wbah%c|;B6lNm%lgmh}k`W!NB&)f0yNv_qM(%R6Z79 zpcf43s*H*=&*K=9BsL#6+SpV>x6Ef6-)lupU@eix7Z{M^M82L70qTW>=>im`o>qo^ zRN1vpY__Jdh`6|lh|4cW=VVW`b|8G5Z5UP5rBD-}OGU?mPv2cc=UWyd#(=!!(TLBm zKyh$1rc{CX z{@TuJp2J-HSB>On2t5pY)0e5Owo2C?m1=?SHtFd|^uPBKjFF}I%)T!Ij zk3#qdxdj=uh=xG_QOMD&RlSHFb|1)QZ322U@x>i%6Zy2bQK$=&S^ zS5hvi978$gF(;D9gByMTbYs$>OD}@Zr-Des7_Qzh01w`PI&ZlJ*T39*l&mf%dRJs{Y(=dydz&b@@!pdoGKqHs)L{ft7 z*6iS!&5D3KR=IQ4u}6hF*;sNU(z5hnmu-a9-*K_c?$YobZ2(ulP9!POq+Dtk!tS&^ zm1+5C=q9vq7`Px6B0|;FvAIfFi<0JP?V?9vJ+$LUeZ&1US0Ahqqe0#5i$cQ!IDvdb zBlieUl0?dQ80OCA>i;Iuha{W>f)A%N?^S;Tr8DzTsq3-r*S+m?_l;@V)pFsYZ~Mpz zpfP{fc-mQmu5iC-3dfD#Wxe0l6-HDm`OS{acD`fc0p|MJ5a5Zj07p>2e`gK;li|}y z9C0;;yN~4!i4{<8(oTL+cI55Kkl%;d&nAf?>aDF)-6$1WC|i`t1{^DvXv$oX7NU&r zdE&RGfWT@g$lpXI=K1{_3;puMkEonN5eJ8Anv*!nl7nA&Dfu{oL%}VkGmu8-3?hEa zb#lE+GHf4;%$iBjRy93tMK1Y`|9aod~g-lZOp$B*{7%Ua+~3LdiWG zll{%kw0H{WfbpS{-+NyF_IxM6G&OTbE&E^z5VGFchdcHcoE8@!(xOz+^rubiHgMLA zThit(GmJ){J=x>XFbqMBrs%D?{ Date: Fri, 1 May 2026 10:12:13 -0400 Subject: [PATCH 5/8] refactor: extract JsBaseTableProxy and add JsTreeTableProxy Split shared table proxy logic into a generic JsBaseTableProxy base class and add a dedicated JsTreeTableProxy for TreeTable (including rollup) so TreeTables no longer reuse JsTableProxy. --- .../src/elements/UITable/JsBaseTableProxy.ts | 263 ++++++++++++++++++ .../js/src/elements/UITable/JsTableProxy.ts | 245 +--------------- .../src/elements/UITable/JsTreeTableProxy.ts | 32 +++ .../js/src/elements/UITable/UITableModel.ts | 14 +- 4 files changed, 319 insertions(+), 235 deletions(-) create mode 100644 plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts create mode 100644 plugins/ui/src/js/src/elements/UITable/JsTreeTableProxy.ts diff --git a/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts b/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts new file mode 100644 index 000000000..1739344a6 --- /dev/null +++ b/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts @@ -0,0 +1,263 @@ +import type { dh } from '@deephaven/jsapi-types'; +import { + DATABAR_MIN_SUFFIX, + DATABAR_MAX_SUFFIX, + HEATMAP_MIN_SUFFIX, + HEATMAP_MAX_SUFFIX, +} from './UITableUtils'; + +export interface UITableLayoutHints { + frontColumns?: string[]; + frozenColumns?: string[]; + backColumns?: string[]; + hiddenColumns?: string[]; + columnGroups?: dh.ColumnGroup[]; +} + +/** + * Subset of the dh.Table / dh.TreeTable surface area used by the proxy base + * class. Both Table and TreeTable expose these members, so the base proxy can + * operate on either. + */ +export interface ProxyableTable { + readonly columns: dh.Column[]; + readonly customColumns: dh.CustomColumn[]; + readonly isClosed: boolean; + applyCustomColumns: ( + customColumns: Array + ) => Array; + close: () => void; + setViewport: ( + firstRow: number, + lastRow: number, + columns?: Array | undefined | null, + updateIntervalMs?: number | undefined | null + ) => dh.TableViewportSubscription | void; +} + +/** + * Base class for proxying a JS API Table or TreeTable. + * + * The underlying table passed to the constructor may be modified, so callers + * should pass a copy. Methods implemented on this class (or a subclass) take + * precedence over the underlying table's methods; everything else is proxied + * through to the table. + */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +class JsBaseTableProxy { + static HIDDEN_COLUMN_SUFFIXES = [ + DATABAR_MIN_SUFFIX, + DATABAR_MAX_SUFFIX, + HEATMAP_MIN_SUFFIX, + HEATMAP_MAX_SUFFIX, + '__FORMAT', + ]; + + protected table: T; + + /** + * Keep a stable reference to all, visible, and hidden columns. + * Only update when needed. + */ + private stableColumns: { + allColumns: dh.Column[]; + visibleColumns: dh.Column[]; + hiddenColumns: dh.Column[]; + }; + + protected originalCustomColumns: dh.CustomColumn[]; + + private onClose: () => void; + + layoutHints: dh.LayoutHints | null = null; + + constructor({ + table, + layoutHints, + onClose, + }: { + table: T; + layoutHints: UITableLayoutHints; + onClose: () => void; + }) { + this.table = table; + this.originalCustomColumns = table.customColumns; + this.onClose = onClose; + + this.stableColumns = { + allColumns: [], + visibleColumns: [], + hiddenColumns: [], + }; + + const { + frontColumns = null, + frozenColumns = null, + backColumns = null, + hiddenColumns = null, + columnGroups = null, + } = layoutHints; + + this.layoutHints = { + frontColumns, + frozenColumns, + backColumns, + hiddenColumns, + columnGroups, + areSavedLayoutsAllowed: false, + }; + + // eslint-disable-next-line no-constructor-return + return new Proxy(this, { + // We want to use any properties on the proxy model if defined + // If not, then proxy to the underlying model + get(target, prop, receiver) { + // Walk the prototype chain so getters defined on subclasses or this + // base class are found. + let proto = Object.getPrototypeOf(target); + while (proto != null && proto !== Object.prototype) { + const descriptor = Object.getOwnPropertyDescriptor(proto, prop); + if (descriptor?.get != null) { + return Reflect.get(target, prop, receiver); + } + if (descriptor != null && typeof descriptor.value === 'function') { + return descriptor.value.bind(target); + } + proto = Object.getPrototypeOf(proto); + } + + // Does this instance implement the property + if (Object.prototype.hasOwnProperty.call(target, prop)) { + return Reflect.get(target, prop, receiver); + } + + const value = Reflect.get(target.table, prop, receiver); + if (typeof value === 'function') { + return value.bind(target.table); + } + return value; + }, + set(target, prop, value) { + // Walk the prototype chain looking for a setter. + let proto = Object.getPrototypeOf(target); + while (proto != null && proto !== Object.prototype) { + const descriptor = Object.getOwnPropertyDescriptor(proto, prop); + if (descriptor?.set != null) { + return Reflect.set(target, prop, value); + } + proto = Object.getPrototypeOf(proto); + } + + if (Object.prototype.hasOwnProperty.call(target, prop)) { + return Reflect.set(target, prop, value); + } + + return Reflect.set(target.table, prop, value, target.table); + }, + }); + } + + /** + * Update the stable columns object if needed. + * This lets us keep a stable array for columns unless the underlying table changes. + */ + private updateDisplayedColumns(): void { + if (this.stableColumns.allColumns !== this.table.columns) { + this.stableColumns.allColumns = this.table.columns; + + this.stableColumns.visibleColumns = this.table.columns.filter( + column => + !JsBaseTableProxy.HIDDEN_COLUMN_SUFFIXES.some(suffix => + column.name.endsWith(suffix) + ) + ); + + this.stableColumns.hiddenColumns = this.table.columns.filter(column => + JsBaseTableProxy.HIDDEN_COLUMN_SUFFIXES.some(suffix => + column.name.endsWith(suffix) + ) + ); + } + } + + close(): void { + // Something causes close to get called twice which will throw some log spam if we try to close the table again + if (!this.table.isClosed) { + this.onClose(); + this.table.close(); + } + } + + applyCustomColumns( + customColumns: Array + ): Array { + return this.table.applyCustomColumns([ + ...this.originalCustomColumns, + ...customColumns, + ]); + } + + get columns(): dh.Column[] { + this.updateDisplayedColumns(); + return this.stableColumns.visibleColumns; + } + + get hiddenColumns(): dh.Column[] { + this.updateDisplayedColumns(); + return this.stableColumns.hiddenColumns; + } + + setViewport( + firstRow: number, + lastRow: number, + columns?: Array | undefined | null, + updateIntervalMs?: number | undefined | null + ): R { + if (columns == null) { + return this.table.setViewport( + firstRow, + lastRow, + columns, + updateIntervalMs + ) as unknown as R; + } + + const allColumns = columns.concat(this.hiddenColumns); + const viewportSubscription = this.table.setViewport( + firstRow, + lastRow, + allColumns, + updateIntervalMs + ); + // TreeTable.setViewport returns void; avoid wrapping undefined in a Proxy + if (viewportSubscription == null) { + return viewportSubscription as unknown as R; + } + return new Proxy(viewportSubscription, { + get: (target, prop, receiver) => { + // Need to proxy setViewport on the subscription in case it is changed + // without creating an entirely new subscription + if (prop === 'setViewport') { + return ( + first: number, + last: number, + cols?: dh.Column[] | null, + interval?: number | null + ) => { + if (cols == null) { + return target.setViewport(first, last, cols, interval); + } + + const proxyAllColumns = cols.concat(this.hiddenColumns); + + return target.setViewport(first, last, proxyAllColumns, interval); + }; + } + return Reflect.get(target, prop, receiver); + }, + }) as unknown as R; + } +} + +export default JsBaseTableProxy; diff --git a/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts b/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts index 12456e223..81550ec5a 100644 --- a/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts +++ b/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts @@ -1,244 +1,29 @@ import type { dh } from '@deephaven/jsapi-types'; -import { - DATABAR_MIN_SUFFIX, - DATABAR_MAX_SUFFIX, - HEATMAP_MIN_SUFFIX, - HEATMAP_MAX_SUFFIX, -} from './UITableUtils'; +import JsBaseTableProxy, { type UITableLayoutHints } from './JsBaseTableProxy'; -export interface UITableLayoutHints { - frontColumns?: string[]; - frozenColumns?: string[]; - backColumns?: string[]; - hiddenColumns?: string[]; - columnGroups?: dh.ColumnGroup[]; -} +export type { UITableLayoutHints }; // This tricks TS into believing the class extends dh.Table // Even though it is through a proxy +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore // eslint-disable-next-line @typescript-eslint/no-empty-interface interface JsTableProxy extends dh.Table {} /** - * Class to proxy JsTable. - * The JsTable passed to the constructor may be modified, so it is recommended to pass a copy. - * Any methods implemented in this class will be utilized over the underlying JsTable methods. - * Any methods not implemented in this class will be proxied to the table. + * Class to proxy a `dh.Table`. + * + * The JsTable passed to the constructor may be modified, so it is recommended + * to pass a copy. Shared proxy behavior lives in {@link JsBaseTableProxy}; + * this subclass exists to type the proxy as a `dh.Table`. Any methods not + * implemented on the base class or this class are proxied to the underlying + * table. */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -class JsTableProxy implements dh.Table { - static HIDDEN_COLUMN_SUFFIXES = [ - DATABAR_MIN_SUFFIX, - DATABAR_MAX_SUFFIX, - HEATMAP_MIN_SUFFIX, - HEATMAP_MAX_SUFFIX, - '__FORMAT', - ]; - - private table: dh.Table; - - /** - * Keep a stable reference to all, visible, and hidden columns. - * Only update when needed. - */ - private stableColumns: { - allColumns: dh.Column[]; - visibleColumns: dh.Column[]; - hiddenColumns: dh.Column[]; - }; - - private originalCustomColumns: dh.CustomColumn[]; - - private onClose: () => void; - - layoutHints: dh.LayoutHints | null = null; - - constructor({ - table, - layoutHints, - onClose, - }: { - table: dh.Table; - layoutHints: UITableLayoutHints; - onClose: () => void; - }) { - this.table = table; - this.originalCustomColumns = table.customColumns; - this.onClose = onClose; - - this.stableColumns = { - allColumns: [], - visibleColumns: [], - hiddenColumns: [], - }; - - const { - frontColumns = null, - frozenColumns = null, - backColumns = null, - hiddenColumns = null, - columnGroups = null, - } = layoutHints; - - this.layoutHints = { - frontColumns, - frozenColumns, - backColumns, - hiddenColumns, - columnGroups, - areSavedLayoutsAllowed: false, - }; - - // eslint-disable-next-line no-constructor-return - return new Proxy(this, { - // We want to use any properties on the proxy model if defined - // If not, then proxy to the underlying model - get(target, prop, receiver) { - // Does this class have a getter for the prop - // Getter functions are on the prototype - const proxyHasGetter = - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop) - ?.get != null; - - if (proxyHasGetter) { - return Reflect.get(target, prop, receiver); - } - - // Does this class implement the property - const proxyHasProp = Object.prototype.hasOwnProperty.call(target, prop); - - // Does the class implement a function for the property - const proxyHasFn = Object.prototype.hasOwnProperty.call( - Object.getPrototypeOf(target), - prop - ); - - const trueTarget = proxyHasProp || proxyHasFn ? target : target.table; - const value = Reflect.get(trueTarget, prop, receiver); - - if (typeof value === 'function') { - return value.bind(trueTarget); - } - - return value; - }, - set(target, prop, value) { - const proxyHasSetter = - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop) - ?.set != null; - - const proxyHasProp = Object.prototype.hasOwnProperty.call(target, prop); - - if (proxyHasSetter || proxyHasProp) { - return Reflect.set(target, prop, value); - } - - return Reflect.set(target.table, prop, value, target.table); - }, - }); - } - - /** - * Update the stable columns object if needed. - * This lets us keep a stable array for columns unless the underlying table changes. - */ - private updateDisplayedColumns(): void { - if (this.stableColumns.allColumns !== this.table.columns) { - this.stableColumns.allColumns = this.table.columns; - - this.stableColumns.visibleColumns = this.table.columns.filter( - column => - !JsTableProxy.HIDDEN_COLUMN_SUFFIXES.some(suffix => - column.name.endsWith(suffix) - ) - ); - - this.stableColumns.hiddenColumns = this.table.columns.filter(column => - JsTableProxy.HIDDEN_COLUMN_SUFFIXES.some(suffix => - column.name.endsWith(suffix) - ) - ); - } - } - - close(): void { - // Something causes close to get called twice which will throw some log spam if we try to close the table again - if (!this.table.isClosed) { - this.onClose(); - this.table.close(); - } - } - - applyCustomColumns( - customColumns: Array - ): Array { - return this.table.applyCustomColumns([ - ...this.originalCustomColumns, - ...customColumns, - ]); - } - - get columns(): dh.Column[] { - this.updateDisplayedColumns(); - return this.stableColumns.visibleColumns; - } - - get hiddenColumns(): dh.Column[] { - this.updateDisplayedColumns(); - return this.stableColumns.hiddenColumns; - } - - setViewport( - firstRow: number, - lastRow: number, - columns?: Array | undefined | null, - updateIntervalMs?: number | undefined | null - ): dh.TableViewportSubscription { - if (columns == null) { - return this.table.setViewport( - firstRow, - lastRow, - columns, - updateIntervalMs - ); - } - - const allColumns = columns.concat(this.hiddenColumns); - const viewportSubscription = this.table.setViewport( - firstRow, - lastRow, - allColumns, - updateIntervalMs - ); - // TreeTable.setViewport returns void; avoid wrapping undefined in a Proxy - if (viewportSubscription == null) { - return viewportSubscription as unknown as dh.TableViewportSubscription; - } - return new Proxy(viewportSubscription, { - get: (target, prop, receiver) => { - // Need to proxy setViewport on the subscription in case it is changed - // without creating an entirely new subscription - if (prop === 'setViewport') { - return ( - first: number, - last: number, - cols?: dh.Column[] | null, - interval?: number | null - ) => { - if (cols == null) { - return target.setViewport(first, last, cols, interval); - } - - const proxyAllColumns = cols.concat(this.hiddenColumns); - - return target.setViewport(first, last, proxyAllColumns, interval); - }; - } - return Reflect.get(target, prop, receiver); - }, - }); - } -} +class JsTableProxy extends JsBaseTableProxy< + dh.Table, + dh.TableViewportSubscription +> {} export default JsTableProxy; diff --git a/plugins/ui/src/js/src/elements/UITable/JsTreeTableProxy.ts b/plugins/ui/src/js/src/elements/UITable/JsTreeTableProxy.ts new file mode 100644 index 000000000..b39e491c5 --- /dev/null +++ b/plugins/ui/src/js/src/elements/UITable/JsTreeTableProxy.ts @@ -0,0 +1,32 @@ +import type { dh } from '@deephaven/jsapi-types'; +import JsBaseTableProxy, { type UITableLayoutHints } from './JsBaseTableProxy'; + +export type { UITableLayoutHints }; + +// This tricks TS into believing the class extends dh.TreeTable +// Even though it is through a proxy +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface JsTreeTableProxy extends dh.TreeTable {} + +/** + * Class to proxy a `dh.TreeTable` (including rollup tables). + * + * The TreeTable passed to the constructor may be modified, so it is + * recommended to pass a copy. Shared proxy behavior lives in + * {@link JsBaseTableProxy}; this subclass exists to type the proxy as a + * `dh.TreeTable`. Any methods not implemented on the base class or this class + * are proxied to the underlying tree table. + * + * Note: TreeTable does NOT support `naturalJoin()` or `getTotalsTable()`, and + * `applyCustomColumns()` on a rollup adds the column to the source rather + * than the aggregated output. Callers must avoid using formatting features + * that depend on those operations (e.g. databars/heatmaps with auto min/max, + * or conditional `if_` rules). + */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +class JsTreeTableProxy extends JsBaseTableProxy {} + +export default JsTreeTableProxy; diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts index 1992aed76..0cc0572db 100644 --- a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts +++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts @@ -32,6 +32,7 @@ import { HEATMAP_MAX_SUFFIX, } from './UITableUtils'; import JsTableProxy, { type UITableLayoutHints } from './JsTableProxy'; +import JsTreeTableProxy from './JsTreeTableProxy'; import { resolveNamedScale } from './ColorScales'; import { interpolateColor, normalizeValue } from '../utils/HeatmapUtils'; @@ -60,7 +61,7 @@ export async function makeUiTableModel( // defense-in-depth. const isTreeTable = TableUtils.isTreeTable(baseTableProp); if (isTreeTable) { - const baseTable = await (baseTableProp as unknown as DhType.Table).copy(); + const baseTreeTable = await baseTableProp.copy(); const hasIfRule = format.some(rule => { const { if_ } = rule; @@ -72,12 +73,15 @@ export async function makeUiTableModel( ); } - const uiTableProxy = new JsTableProxy({ - table: baseTable as unknown as DhType.Table, + const uiTreeTableProxy = new JsTreeTableProxy({ + table: baseTreeTable, layoutHints, onClose: () => undefined, }); - const baseModel = await IrisGridModelFactory.makeModel(dh, uiTableProxy); + const baseModel = await IrisGridModelFactory.makeModel( + dh, + uiTreeTableProxy as unknown as DhType.Table + ); return new UITableModel({ dh, model: baseModel, @@ -86,7 +90,7 @@ export async function makeUiTableModel( }); } - const baseTable = await (baseTableProp as DhType.Table).copy(); + const baseTable = await baseTableProp.copy(); const customColumns: string[] = []; format.forEach((rule, i) => { const { if_ } = rule; From 8d9e89c04828192945f8d123c849ffff61d4570c Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 1 May 2026 10:34:14 -0400 Subject: [PATCH 6/8] Add missing snapshot --- .../UI-table-t-rollup-format-1-webkit-linux.png | Bin 0 -> 11385 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-webkit-linux.png diff --git a/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-table-t-rollup-format-1-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..69eae35fad98ac5df01f6435925286e6b80af9a2 GIT binary patch literal 11385 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 Date: Fri, 1 May 2026 17:11:08 -0400 Subject: [PATCH 7/8] refactor: factor out TableLike type alias for Table | RollupTable | TreeTable --- .../ui/src/deephaven/ui/components/table.py | 7 ++++--- plugins/ui/src/deephaven/ui/types/types.py | 2 ++ .../src/elements/UITable/JsBaseTableProxy.ts | 19 ++++++------------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/table.py b/plugins/ui/src/deephaven/ui/components/table.py index b3f77c6e2..b24812bd7 100644 --- a/plugins/ui/src/deephaven/ui/components/table.py +++ b/plugins/ui/src/deephaven/ui/components/table.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from typing import Literal, Any, Optional import logging -from deephaven.table import Table, RollupTable, TreeTable +from deephaven.table import RollupTable, TreeTable from ..elements import Element, resolve from ..elements.UriElement import UriElement from .types import AlignSelf, DimensionValue, JustifySelf, LayoutFlex, Position @@ -16,6 +16,7 @@ RowPressCallback, ResolvableContextMenuItem, SelectionChangeCallback, + TableLike, ) from .._internal import dict_to_react_props, RenderContext @@ -159,7 +160,7 @@ class TableDatabar: def _validate_table_format( format_: list[TableFormat] | TableFormat, - table: Table | RollupTable | TreeTable | UriElement | str, + table: TableLike | UriElement | str, ) -> None: """Validate format rules for the table. @@ -304,7 +305,7 @@ class table(Element): def __init__( self, - table: Table | RollupTable | TreeTable | UriElement | str, + table: TableLike | UriElement | str, *, format_: TableFormat | list[TableFormat] | None = None, on_row_press: RowPressCallback | None = None, diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py index 76932d4b3..290d6a75e 100644 --- a/plugins/ui/src/deephaven/ui/types/types.py +++ b/plugins/ui/src/deephaven/ui/types/types.py @@ -22,6 +22,7 @@ from deephaven import SortDirection from deephaven.dtypes import DType +from deephaven.table import Table, RollupTable, TreeTable # Color values for the DH color palette exposed to end users in spectrum components @@ -458,6 +459,7 @@ class SliderChange(TypedDict): ContextMenuMode = Union[ContextMenuModeOption, List[ContextMenuModeOption], None] # TODO: Fill in the list of Deephaven Colors we allow LockType = Literal["shared", "exclusive"] +TableLike = Table | RollupTable | TreeTable QuickFilterExpression = str RowData = Dict[ColumnName, Any] ColumnData = List[Any] diff --git a/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts b/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts index 1739344a6..6438b59d0 100644 --- a/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts +++ b/plugins/ui/src/js/src/elements/UITable/JsBaseTableProxy.ts @@ -91,20 +91,13 @@ class JsBaseTableProxy { hiddenColumns: [], }; - const { - frontColumns = null, - frozenColumns = null, - backColumns = null, - hiddenColumns = null, - columnGroups = null, - } = layoutHints; - this.layoutHints = { - frontColumns, - frozenColumns, - backColumns, - hiddenColumns, - columnGroups, + frontColumns: null, + frozenColumns: null, + backColumns: null, + hiddenColumns: null, + columnGroups: null, + ...layoutHints, areSavedLayoutsAllowed: false, }; From f2b235c64a2a631fcbceed3e68b32ccbdbcc0d9c Mon Sep 17 00:00:00 2001 From: mikebender Date: Fri, 8 May 2026 14:28:45 -0400 Subject: [PATCH 8/8] Fix python types for python 3.9 --- plugins/ui/src/deephaven/ui/types/types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py index 290d6a75e..5cd3e4a4b 100644 --- a/plugins/ui/src/deephaven/ui/types/types.py +++ b/plugins/ui/src/deephaven/ui/types/types.py @@ -24,7 +24,6 @@ from deephaven.dtypes import DType from deephaven.table import Table, RollupTable, TreeTable - # Color values for the DH color palette exposed to end users in spectrum components # https://github.com/deephaven/web-client-ui/blob/main/packages/components/src/theme/colorUtils.ts DeephavenColor = Literal[ @@ -459,7 +458,7 @@ class SliderChange(TypedDict): ContextMenuMode = Union[ContextMenuModeOption, List[ContextMenuModeOption], None] # TODO: Fill in the list of Deephaven Colors we allow LockType = Literal["shared", "exclusive"] -TableLike = Table | RollupTable | TreeTable +TableLike = Union[Table, RollupTable, TreeTable] QuickFilterExpression = str RowData = Dict[ColumnName, Any] ColumnData = List[Any]