@deephaven/iris-grid 1.21.1 → 1.22.1-alpha-pivot-builder.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +284 -1
  2. package/dist/AdvancedFilterCreator.css +42 -0
  3. package/dist/AdvancedFilterCreator.css.map +1 -1
  4. package/dist/AdvancedFilterCreator.d.ts +2 -0
  5. package/dist/AdvancedFilterCreator.d.ts.map +1 -1
  6. package/dist/AdvancedFilterCreator.js +13 -3
  7. package/dist/AdvancedFilterCreator.js.map +1 -1
  8. package/dist/CommonTypes.d.ts +62 -2
  9. package/dist/CommonTypes.d.ts.map +1 -1
  10. package/dist/CommonTypes.js.map +1 -1
  11. package/dist/IrisGrid.d.ts +89 -3
  12. package/dist/IrisGrid.d.ts.map +1 -1
  13. package/dist/IrisGrid.js +294 -91
  14. package/dist/IrisGrid.js.map +1 -1
  15. package/dist/IrisGridModel.d.ts +30 -1
  16. package/dist/IrisGridModel.d.ts.map +1 -1
  17. package/dist/IrisGridModel.js +36 -1
  18. package/dist/IrisGridModel.js.map +1 -1
  19. package/dist/IrisGridModelWidgetProps.d.ts +26 -0
  20. package/dist/IrisGridModelWidgetProps.d.ts.map +1 -0
  21. package/dist/IrisGridModelWidgetProps.js +2 -0
  22. package/dist/IrisGridModelWidgetProps.js.map +1 -0
  23. package/dist/IrisGridProxyModel.d.ts.map +1 -1
  24. package/dist/IrisGridProxyModel.js +34 -2
  25. package/dist/IrisGridProxyModel.js.map +1 -1
  26. package/dist/IrisGridTextCellRenderer.d.ts.map +1 -1
  27. package/dist/IrisGridTextCellRenderer.js +1 -1
  28. package/dist/IrisGridTextCellRenderer.js.map +1 -1
  29. package/dist/IrisGridUtils.d.ts +25 -2
  30. package/dist/IrisGridUtils.d.ts.map +1 -1
  31. package/dist/IrisGridUtils.js +99 -42
  32. package/dist/IrisGridUtils.js.map +1 -1
  33. package/dist/LazyIrisGrid.d.ts +1 -1
  34. package/dist/RemoteComponentModules.d.ts +12 -0
  35. package/dist/RemoteComponentModules.d.ts.map +1 -0
  36. package/dist/RemoteComponentModules.js +16 -0
  37. package/dist/RemoteComponentModules.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/sidebar/IrisGridTableOptionsWidgetProps.d.ts +22 -0
  43. package/dist/sidebar/IrisGridTableOptionsWidgetProps.d.ts.map +1 -0
  44. package/dist/sidebar/IrisGridTableOptionsWidgetProps.js +2 -0
  45. package/dist/sidebar/IrisGridTableOptionsWidgetProps.js.map +1 -0
  46. package/dist/sidebar/OptionType.d.ts +8 -0
  47. package/dist/sidebar/OptionType.d.ts.map +1 -1
  48. package/dist/sidebar/OptionType.js +7 -0
  49. package/dist/sidebar/OptionType.js.map +1 -1
  50. package/dist/sidebar/PluginTableOptionsErrorBoundary.d.ts +30 -0
  51. package/dist/sidebar/PluginTableOptionsErrorBoundary.d.ts.map +1 -0
  52. package/dist/sidebar/PluginTableOptionsErrorBoundary.js +60 -0
  53. package/dist/sidebar/PluginTableOptionsErrorBoundary.js.map +1 -0
  54. package/dist/sidebar/index.d.ts +3 -2
  55. package/dist/sidebar/index.d.ts.map +1 -1
  56. package/dist/sidebar/index.js.map +1 -1
  57. package/package.json +16 -16
package/README.md CHANGED
@@ -20,4 +20,287 @@ const model = await IrisGridModelFactory.makeModel(dh, table);
20
20
 
21
21
  // In your render function
22
22
  <IrisGrid dh={dh} model={model} />
23
- ```
23
+ ```
24
+
25
+ ## Customizing the Table Options menu
26
+
27
+ The Table Options sidebar (the gear menu on the right edge of the grid) is
28
+ extensible. Plugin authors can hide built-in items, relabel or reorder
29
+ them, and add their own items that open a custom configuration page —
30
+ without forking `IrisGrid`.
31
+
32
+ There is a single entry point: the `transformTableOptions` prop on
33
+ `<IrisGrid>`. It is **opt-in** — it lives on the iris-grid-specific
34
+ `IrisGridTableOptionsWidgetProps`, not on the generic
35
+ `WidgetComponentProps` / `WidgetPanelProps`, so widgets that don't care
36
+ about the Table Options menu never see it.
37
+
38
+ - **Own the render site?** Pass `transformTableOptions` straight to
39
+ `<IrisGrid>`.
40
+ - **A `WidgetMiddlewarePlugin` that doesn't render `<IrisGrid>`
41
+ yourself?** Thread the prop down the middleware chain via the
42
+ `Component` you wrap, composing your own transform on top of the one
43
+ you received (see [Publishing from middleware](#publishing-from-middleware)).
44
+ The panel hosts that ship with Deephaven (`IrisGridPanel`,
45
+ `GridWidgetPlugin`) accept `transformTableOptions` as a prop and
46
+ forward it to `<IrisGrid>`.
47
+
48
+ ### Writing a transform
49
+
50
+ `transformTableOptions(defaults)` is a pure function that receives the
51
+ built-in items (already filtered by what the current model supports) and
52
+ returns the items to actually render. Use it to add, hide, relabel,
53
+ reorder, or replace entries.
54
+
55
+ ```tsx
56
+ import { OptionType, type OptionItem } from '@deephaven/iris-grid';
57
+
58
+ const transformTableOptions = (defaults: readonly OptionItem[]) => [
59
+ // hide a built-in
60
+ ...defaults.filter(o => o.type !== OptionType.SELECT_DISTINCT),
61
+ // add a plugin item with its own page
62
+ {
63
+ type: 'plugin:my-plugin:column-inspector',
64
+ title: 'Column Inspector',
65
+ configPage: ColumnInspectorPage,
66
+ },
67
+ ];
68
+ ```
69
+
70
+ Rules:
71
+
72
+ - The transform should be referentially stable and side-effect-free
73
+ (it's called inside memoization). Memoize it with `useMemo` /
74
+ `useCallback` rather than rebuilding per render.
75
+ - A throwing transform is logged once and treated as identity for that
76
+ render, so the menu degrades gracefully.
77
+ - Items **without** a `configPage` MUST have a `type` matching an
78
+ existing `OptionType` enum value — those are rendered by the built-in
79
+ page switch.
80
+ - Items **with** a `configPage` SHOULD use a namespaced key,
81
+ conventionally `plugin:<name>:<id>`, to avoid colliding with built-ins
82
+ or other plugins.
83
+
84
+ ### Implementing a `configPage`
85
+
86
+ A `configPage` is a regular React component that receives
87
+ `IrisGridTableOptionsPageProps`:
88
+
89
+ ```tsx
90
+ import { type IrisGridTableOptionsPageProps } from '@deephaven/iris-grid';
91
+
92
+ export function ColumnInspectorPage({
93
+ model,
94
+ onBack,
95
+ }: IrisGridTableOptionsPageProps): JSX.Element {
96
+ return (
97
+ <div>
98
+ <button type="button" onClick={onBack}>Back</button>
99
+ <pre>{model.columns.map(c => c.name).join('\n')}</pre>
100
+ </div>
101
+ );
102
+ }
103
+ ```
104
+
105
+ `IrisGrid` wraps each `configPage` render in `PluginTableOptionsErrorBoundary`,
106
+ so a throw inside your page shows a small inline fallback instead of
107
+ unmounting the whole grid.
108
+
109
+ ### Publishing from middleware
110
+
111
+ When you're a `WidgetMiddlewarePlugin` and don't render `<IrisGrid>`
112
+ yourself, you receive `transformTableOptions` as a prop and pass a
113
+ composed transform down to the `Component` (or panel) you wrap. Run the
114
+ upstream transform first so contributions compose, then layer your own
115
+ changes on top.
116
+
117
+ A `panelComponent` middleware should be built with `createPanelMiddleware`
118
+ from `@deephaven/plugin`: you supply a body hook that may `inject` props onto
119
+ the wrapped component and/or `wrap` the child in a wrapper element (both
120
+ optional), and the factory owns the `React.forwardRef` ceremony and ref
121
+ forwarding for you. That ref matters —
122
+ golden-layout binds a ref to the registered panel to persist class-panel
123
+ state (sorts, filters, column moves, etc.) into its `componentState`, and a
124
+ middleware that swallowed it would silently break that persistence for every
125
+ panel below it; the factory guarantees it can't be dropped. For the non-panel
126
+ `component` path use `createWidgetMiddleware`, which is otherwise identical but
127
+ takes no ref.
128
+
129
+ ```tsx
130
+ import { useMemo } from 'react';
131
+ import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
132
+ import {
133
+ type IrisGridTableOptionsWidgetProps,
134
+ type TableOptionsTransform,
135
+ } from '@deephaven/iris-grid';
136
+
137
+ function makeMyTransform(
138
+ upstream: TableOptionsTransform | undefined
139
+ ): TableOptionsTransform {
140
+ return defaults => {
141
+ const base = upstream != null ? upstream(defaults) : defaults;
142
+ return [...base, myPluginItem];
143
+ };
144
+ }
145
+
146
+ const MyMiddleware = createPanelMiddleware<
147
+ unknown,
148
+ WidgetPanelProps & IrisGridTableOptionsWidgetProps
149
+ >(({ transformTableOptions }) => {
150
+ const composedTransform = useMemo(
151
+ () => makeMyTransform(transformTableOptions),
152
+ [transformTableOptions]
153
+ );
154
+ return { inject: { transformTableOptions: composedTransform } };
155
+ }, 'MyMiddleware');
156
+ ```
157
+
158
+ The body hook receives the incoming props (minus `Component`) and returns an
159
+ optional `{ inject?, wrap? }`. Every incoming prop is forwarded to the wrapped
160
+ component automatically; `inject` only adds or overrides the few props you
161
+ actually change (here `transformTableOptions`), and `wrap` optionally nests the
162
+ child (e.g. in a context provider). Both fields are optional — a pass-through
163
+ middleware can return `{}`. The factory adds the `ref` plumbing on top.
164
+
165
+
166
+ Composition rule: each middleware layer reads the `transformTableOptions`
167
+ it was handed, runs that transform first, then layers its own changes on
168
+ top — last writer wins for any given `OptionItem.type`.
169
+
170
+ #### Model-aware menus
171
+
172
+ The transform must stay **pure** — it only sees `defaults`, never the
173
+ `IrisGridModel`. To make a menu react to model state (e.g. relabel an
174
+ item once a pivot is active), take a **snapshot of the value you care
175
+ about** from model events and fold it into the transform's identity:
176
+
177
+ ```tsx
178
+ const [isPivot, setIsPivot] = useState(model.isPivot);
179
+ useEffect(() => {
180
+ const handler = () => setIsPivot(model.isPivot);
181
+ model.addEventListener(SOME_MODEL_EVENT, handler);
182
+ return () => model.removeEventListener(SOME_MODEL_EVENT, handler);
183
+ }, [model]);
184
+
185
+ const composedTransform = useMemo(
186
+ () => makeMyTransform(transformTableOptions, isPivot),
187
+ [transformTableOptions, isPivot]
188
+ );
189
+ ```
190
+
191
+ Because `composedTransform`'s identity changes when the snapshot
192
+ changes, `IrisGrid` re-runs it (its menu cache is keyed on
193
+ `[defaults, transform]`). Keeping the snapshot in the dependency array —
194
+ rather than reading `model.isPivot` inside the transform — is what keeps
195
+ that memoization honest.
196
+
197
+ To obtain the model when the host builds it for you, pass an
198
+ `onModelChanged` callback to `IrisGridPanel` (called once the panel's
199
+ model is ready).
200
+
201
+ ### Full example
202
+
203
+ See the [`@deephaven/js-plugin-pivot-builder`](https://github.com/deephaven/deephaven-plugins/tree/main/plugins/pivot-builder)
204
+ plugin for a working `WidgetMiddlewarePlugin` that replaces the default
205
+ widget renderer and adds a `configPage`-backed "Rollup, Aggregate and Pivot"
206
+ item to the Table Options sidebar.
207
+
208
+ ### Why the transform doesn't take the model
209
+
210
+ The transform signature is `(defaults) => items` — it deliberately does
211
+ **not** receive the `IrisGridModel` or grid state. State-aware menus
212
+ (e.g. "relabel an item once a pivot is active", "show *Reset filters*
213
+ only when filters exist") are implemented in the middleware: subscribe
214
+ to model events, snapshot the value you need, and fold that snapshot
215
+ into the transform's identity (see
216
+ [Model-aware menus](#model-aware-menus)). The transform itself stays
217
+ pure.
218
+
219
+ This isn't just about keeping the public surface small — it's also
220
+ what keeps menu memoization honest. `IrisGrid` caches the computed
221
+ item list on `[defaults, transform]` (see `getCachedTransformedOptionItems`),
222
+ both of which are stable values/refs. Adding a live `model` argument
223
+ would break that: `IrisGridModel` is a long-lived mutable handle whose
224
+ identity does not change when `isExpandable`, `filter`, `sorts`, or
225
+ `isRollup` flip, so any plugin that read those fields would silently
226
+ return stale items until something unrelated invalidated the cache.
227
+
228
+ By passing a **curated snapshot of values** through the transform's
229
+ closure instead, the memo key changes exactly when those values change
230
+ and re-runs are driven by actual dependencies. Passing the model itself,
231
+ or the full `IrisGrid` instance, is intentionally off the table: the
232
+ surface is too large, too volatile, and (in the model's case)
233
+ memoization-hostile.
234
+
235
+ ## Transforming the model
236
+
237
+ Some plugins need more than a custom menu — they need to change the
238
+ **model** the grid renders (e.g. wrap it in a proxy that can swap its
239
+ inner model in response to a config page). For that there is a second,
240
+ symmetric opt-in seam: the `transformModel` prop.
241
+
242
+ Like `transformTableOptions`, it lives on an iris-grid-specific
243
+ interface (`IrisGridModelWidgetProps`), not on the generic
244
+ `WidgetComponentProps` / `WidgetPanelProps`, and is threaded down the
245
+ middleware chain by the hosts that build the model for you
246
+ (`IrisGridPanel`, `GridWidgetPlugin` / `useIrisGridModel`).
247
+
248
+ ```ts
249
+ import { type IrisGridModelTransform } from '@deephaven/iris-grid';
250
+
251
+ // (model: IrisGridModel) => IrisGridModel | Promise<IrisGridModel>
252
+ const transformModel: IrisGridModelTransform = model =>
253
+ wrapInMyProxy(model);
254
+ ```
255
+
256
+ The host builds the model from `fetch` as usual, then applies
257
+ `transformModel` to whatever it built **before** handing it to
258
+ `<IrisGrid>`. The returned model must be a drop-in for the input — the
259
+ host owns its lifecycle and will `close()` whatever you return, so wrap
260
+ rather than discard the model you were given. The transform may be async
261
+ if it needs to await dependencies first.
262
+
263
+ Rules:
264
+
265
+ - `transformModel` must be **referentially stable**. It is applied when
266
+ the model is (re)built; an unstable transform would rebuild the model.
267
+ Memoize it with `useMemo` / `useCallback`.
268
+ - It runs once per model build, not per render, so it is the right place
269
+ for one-time wrapping — not for per-render state.
270
+ - This is why model construction stays in the host: the host keeps
271
+ ownership of `fetch`, error/loading state, and `close()`, while the
272
+ plugin only augments the result. A middleware using `transformModel`
273
+ can therefore render the wrapped `Component` and stay a **chained**
274
+ layer instead of taking over model construction and becoming terminal.
275
+
276
+ ### Publishing `transformModel` from middleware
277
+
278
+ A middleware that needs both seams composes them the same way — run any
279
+ upstream transform first, then layer your own — and returns both from its
280
+ body hook's `inject`. Props you don't touch (here `transformTableOptions`)
281
+ are forwarded automatically, so you only inject what you change:
282
+
283
+ ```tsx
284
+ import { useMemo } from 'react';
285
+ import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
286
+ import {
287
+ type IrisGridModelTransform,
288
+ type IrisGridModelWidgetProps,
289
+ type IrisGridTableOptionsWidgetProps,
290
+ } from '@deephaven/iris-grid';
291
+
292
+ const MyMiddleware = createPanelMiddleware<
293
+ unknown,
294
+ WidgetPanelProps & IrisGridModelWidgetProps & IrisGridTableOptionsWidgetProps
295
+ >(({ transformModel }) => {
296
+ const composedModel = useMemo<IrisGridModelTransform>(
297
+ () => async model => {
298
+ const base = transformModel != null ? await transformModel(model) : model;
299
+ return wrapInMyProxy(base);
300
+ },
301
+ [transformModel]
302
+ );
303
+ return { inject: { transformModel: composedModel } };
304
+ }, 'MyMiddleware');
305
+ ```
306
+
@@ -103,5 +103,47 @@
103
103
  .advanced-filter-creator .advanced-filter-creator-filter-operator .filter-operator.btn-link:focus {
104
104
  text-decoration: underline;
105
105
  }
106
+ .advanced-filter-creator > form {
107
+ display: flex;
108
+ flex-direction: column;
109
+ flex: 1 1 auto;
110
+ min-height: 0;
111
+ overflow: hidden;
112
+ }
113
+ .advanced-filter-creator > form > .advanced-filter-values-group {
114
+ display: flex;
115
+ flex-direction: column;
116
+ flex: 1 1 auto;
117
+ min-height: 0;
118
+ }
119
+ .advanced-filter-creator .advanced-filter-creator-select-value {
120
+ display: flex;
121
+ flex-direction: column;
122
+ flex: 1 1 auto;
123
+ min-height: 0;
124
+ }
125
+ .advanced-filter-creator .advanced-filter-creator-select-value .select-value-list-wrapper {
126
+ flex: 1 1 auto;
127
+ min-height: 120px;
128
+ position: relative;
129
+ overflow: hidden;
130
+ }
131
+ .advanced-filter-creator .advanced-filter-creator-select-value .select-value-list-scroll-pane {
132
+ position: absolute;
133
+ inset: 0;
134
+ height: auto;
135
+ max-height: none;
136
+ min-height: 0;
137
+ }
138
+
139
+ .popper-container.maximized .advanced-filter-creator {
140
+ width: min(94vw, 2200px) !important;
141
+ height: 85vh !important;
142
+ max-width: none !important;
143
+ max-height: none !important;
144
+ overflow: hidden;
145
+ display: flex;
146
+ flex-direction: column;
147
+ }
106
148
 
107
149
  /*# sourceMappingURL=AdvancedFilterCreator.css.map */