npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@deephaven/iris-grid

v1.27.0

Published

Deephaven Iris Grid

Readme

@deephaven/iris-grid

This is a library of Deephaven Iris Grid component. Display a grid with enhanced functionality with a Deephaven table.

Usage

Add the package to your "dependencies":

npm install --save @deephaven/iris-grid

Then, import and use the component from the package:

import { useApi } from '@deephaven/jsapi-bootstrap';
import { IrisGrid, IrisGridModelFactory } from '@deephaven/iris-grid';

// In your initialization, create the model async
const dh = useApi();
const model = await IrisGridModelFactory.makeModel(dh, table);

// In your render function
<IrisGrid dh={dh} model={model} />

Customizing the Table Options menu

The Table Options sidebar (the gear menu on the right edge of the grid) is extensible. Plugin authors can hide built-in items, relabel or reorder them, and add their own items that open a custom configuration page — without forking IrisGrid.

There is a single entry point: the transformTableOptions prop on <IrisGrid>. It is opt-in — it lives on the iris-grid-specific IrisGridTableOptionsWidgetProps, not on the generic WidgetComponentProps / WidgetPanelProps, so widgets that don't care about the Table Options menu never see it.

  • Own the render site? Pass transformTableOptions straight to <IrisGrid>.
  • A WidgetMiddlewarePlugin that doesn't render <IrisGrid> yourself? Thread the prop down the middleware chain via the Component you wrap, composing your own transform on top of the one you received (see Publishing from middleware). The panel hosts that ship with Deephaven (IrisGridPanel, GridWidgetPlugin) accept transformTableOptions as a prop and forward it to <IrisGrid>.

Writing a transform

transformTableOptions(defaults) is a pure function that receives the built-in items (already filtered by what the current model supports) and returns the items to actually render. Use it to add, hide, relabel, reorder, or replace entries.

import { OptionType, type OptionItem } from '@deephaven/iris-grid';

const transformTableOptions = (defaults: readonly OptionItem[]) => [
  // hide a built-in
  ...defaults.filter(o => o.type !== OptionType.SELECT_DISTINCT),
  // add a plugin item with its own page
  {
    type: 'plugin:my-plugin:column-inspector',
    title: 'Column Inspector',
    configPage: ColumnInspectorPage,
  },
];

Rules:

  • The transform should be referentially stable and side-effect-free (it's called inside memoization). Memoize it with useMemo / useCallback rather than rebuilding per render.
  • A throwing transform is logged once and treated as identity for that render, so the menu degrades gracefully.
  • Items without a configPage MUST have a type matching an existing OptionType enum value — those are rendered by the built-in page switch.
  • Items with a configPage SHOULD use a namespaced key, conventionally plugin:<name>:<id>, to avoid colliding with built-ins or other plugins.

Implementing a configPage

A configPage is a regular React component that receives IrisGridTableOptionsPageProps:

import { type IrisGridTableOptionsPageProps } from '@deephaven/iris-grid';

export function ColumnInspectorPage({
  model,
  viewState,
  onBack,
}: IrisGridTableOptionsPageProps): JSX.Element {
  const hidden = new Set(viewState.hiddenColumns);
  return (
    <div>
      <button type="button" onClick={onBack}>Back</button>
      <pre>
        {model.columns
          .filter(c => !hidden.has(c.name))
          .map(c => c.name)
          .join('\n')}
      </pre>
    </div>
  );
}

IrisGrid wraps each configPage render in PluginTableOptionsErrorBoundary, so a throw inside your page shows a small inline fallback instead of unmounting the whole grid.

The viewState prop is an IrisGridViewState — a curated, read-only snapshot of the grid's current view configuration (e.g. viewState.hiddenColumns). It's a small, stable subset of the grid's internal state; new fields are added additively as plugins need them.

Publishing from middleware

When you're a WidgetMiddlewarePlugin and don't render <IrisGrid> yourself, you receive transformTableOptions as a prop and pass a composed transform down to the Component (or panel) you wrap. Run the upstream transform first so contributions compose, then layer your own changes on top.

A panelComponent middleware should be built with createPanelMiddleware from @deephaven/plugin: you supply a body hook that may inject props onto the wrapped component and/or wrap the child in a wrapper element (both optional), and the factory owns the React.forwardRef ceremony and ref forwarding for you. That ref matters — golden-layout binds a ref to the registered panel to persist class-panel state (sorts, filters, column moves, etc.) into its componentState, and a middleware that swallowed it would silently break that persistence for every panel below it; the factory guarantees it can't be dropped. For the non-panel component path use createWidgetMiddleware, which is otherwise identical but takes no ref.

import { useMemo } from 'react';
import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
import {
  type IrisGridTableOptionsWidgetProps,
  type TableOptionsTransform,
} from '@deephaven/iris-grid';

function makeMyTransform(
  upstream: TableOptionsTransform | undefined
): TableOptionsTransform {
  return defaults => {
    const base = upstream != null ? upstream(defaults) : defaults;
    return [...base, myPluginItem];
  };
}

const MyMiddleware = createPanelMiddleware<
  unknown,
  WidgetPanelProps & IrisGridTableOptionsWidgetProps
>(({ transformTableOptions }) => {
  const composedTransform = useMemo(
    () => makeMyTransform(transformTableOptions),
    [transformTableOptions]
  );
  return { inject: { transformTableOptions: composedTransform } };
}, 'MyMiddleware');

The body hook receives the incoming props (minus Component) and returns an optional { inject?, wrap? }. Every incoming prop is forwarded to the wrapped component automatically; inject only adds or overrides the few props you actually change (here transformTableOptions), and wrap optionally nests the child (e.g. in a context provider). Both fields are optional — a pass-through middleware can return {}. The factory adds the ref plumbing on top.

Composition rule: each middleware layer reads the transformTableOptions it was handed, runs that transform first, then layers its own changes on top — last writer wins for any given OptionItem.type.

Model-aware menus

The transform must stay pure — it only sees defaults, never the IrisGridModel. To make a menu react to model state (e.g. relabel an item once a pivot is active), take a snapshot of the value you care about from model events and fold it into the transform's identity:

const [isPivot, setIsPivot] = useState(model.isPivot);
useEffect(() => {
  const handler = () => setIsPivot(model.isPivot);
  model.addEventListener(SOME_MODEL_EVENT, handler);
  return () => model.removeEventListener(SOME_MODEL_EVENT, handler);
}, [model]);

const composedTransform = useMemo(
  () => makeMyTransform(transformTableOptions, isPivot),
  [transformTableOptions, isPivot]
);

Because composedTransform's identity changes when the snapshot changes, IrisGrid re-runs it (its menu cache is keyed on [defaults, transform]). Keeping the snapshot in the dependency array — rather than reading model.isPivot inside the transform — is what keeps that memoization honest.

To obtain the model when the host builds it for you, pass an onModelChanged callback to IrisGridPanel (called once the panel's model is ready).

Full example

See the @deephaven/js-plugin-pivot-builder plugin for a working WidgetMiddlewarePlugin that replaces the default widget renderer and adds a configPage-backed "Rollup, Aggregate and Pivot" item to the Table Options sidebar.

Why the transform doesn't take the model

The transform signature is (defaults) => items — it deliberately does not receive the IrisGridModel or grid state. State-aware menus (e.g. "relabel an item once a pivot is active", "show Reset filters only when filters exist") are implemented in the middleware: subscribe to model events, snapshot the value you need, and fold that snapshot into the transform's identity (see Model-aware menus). The transform itself stays pure.

This isn't just about keeping the public surface small — it's also what keeps menu memoization honest. IrisGrid caches the computed item list on [defaults, transform] (see getCachedTransformedOptionItems), both of which are stable values/refs. Adding a live model argument would break that: IrisGridModel is a long-lived mutable handle whose identity does not change when isExpandable, filter, sorts, or isRollup flip, so any plugin that read those fields would silently return stale items until something unrelated invalidated the cache.

By passing a curated snapshot of values through the transform's closure instead, the memo key changes exactly when those values change and re-runs are driven by actual dependencies. Passing the model itself, or the full IrisGrid instance, is intentionally off the table: the surface is too large, too volatile, and (in the model's case) memoization-hostile.

Transforming the model

Some plugins need more than a custom menu — they need to change the model the grid renders (e.g. wrap it in a proxy that can swap its inner model in response to a config page). For that there is a second, symmetric opt-in seam: the transformModel prop.

Like transformTableOptions, it lives on an iris-grid-specific interface (IrisGridModelWidgetProps), not on the generic WidgetComponentProps / WidgetPanelProps, and is threaded down the middleware chain by the hosts that build the model for you (IrisGridPanel, GridWidgetPlugin / useIrisGridModel).

import { type IrisGridModelTransform } from '@deephaven/iris-grid';

// (model: IrisGridModel) => IrisGridModel | Promise<IrisGridModel>
const transformModel: IrisGridModelTransform = model =>
  wrapInMyProxy(model);

The host builds the model from fetch as usual, then applies transformModel to whatever it built before handing it to <IrisGrid>. The returned model must be a drop-in for the input — the host owns its lifecycle and will close() whatever you return, so wrap rather than discard the model you were given. The transform may be async if it needs to await dependencies first.

Rules:

  • transformModel must be referentially stable. It is applied when the model is (re)built; an unstable transform would rebuild the model. Memoize it with useMemo / useCallback.
  • It runs once per model build, not per render, so it is the right place for one-time wrapping — not for per-render state.
  • This is why model construction stays in the host: the host keeps ownership of fetch, error/loading state, and close(), while the plugin only augments the result. A middleware using transformModel can therefore render the wrapped Component and stay a chained layer instead of taking over model construction and becoming terminal.

Publishing transformModel from middleware

A middleware that needs both seams composes them the same way — run any upstream transform first, then layer your own — and returns both from its body hook's inject. Props you don't touch (here transformTableOptions) are forwarded automatically, so you only inject what you change:

import { useMemo } from 'react';
import { createPanelMiddleware, type WidgetPanelProps } from '@deephaven/plugin';
import {
  type IrisGridModelTransform,
  type IrisGridModelWidgetProps,
  type IrisGridTableOptionsWidgetProps,
} from '@deephaven/iris-grid';

const MyMiddleware = createPanelMiddleware<
  unknown,
  WidgetPanelProps & IrisGridModelWidgetProps & IrisGridTableOptionsWidgetProps
>(({ transformModel }) => {
  const composedModel = useMemo<IrisGridModelTransform>(
    () => async model => {
      const base = transformModel != null ? await transformModel(model) : model;
      return wrapInMyProxy(base);
    },
    [transformModel]
  );
  return { inject: { transformModel: composedModel } };
}, 'MyMiddleware');