Skip to content

feat: DH-19717: Add per-user persistence for dashboards#1352

Open
mofojed wants to merge 5 commits into
deephaven:mainfrom
mofojed:DH-19717-per-user-persistence
Open

feat: DH-19717: Add per-user persistence for dashboards#1352
mofojed wants to merge 5 commits into
deephaven:mainfrom
mofojed:DH-19717-per-user-persistence

Conversation

@mofojed
Copy link
Copy Markdown
Member

@mofojed mofojed commented May 23, 2026

  • Nested dashboards now store the last layout config
  • When reloading the dashboard, it just uses that layout config and Rows, Columns, and Stacks do not try and build their own layout
  • Will need to test this extensively. Especially with dashboards that have changed the layout after the fact; I think in those cases we just do a "best" effort, and panels may open in weird locations. User should have the option to discard their override (at least in Enterprise).
  • Matching Enterprise PR for testing with Enterprise

mofojed added 4 commits May 19, 2026 11:09
- Nested dashboards now store the last layout config
- When reloading the dashboard, it just uses that layout config and Rows, Columns, and Stacks do not try and build their own layout
  - Will need to test this extensively. Especially with dashboards that have changed the layout after the fact; I think in those cases we just do a "best" effort, and panels may open in weird locations. User should have the option to discard their override (at least in Enterprise).
- It's not laying out correctly, plots aren't working
… loop

- Throttle handleDataChange via useThrottledCallback (1000ms) and skip setDashboardData when the merged widgetData is deep-equal to the last committed value, breaking the re-render loop.
- Accumulate partial widget data updates in a ref so intermediate changes are preserved between throttled flushes; flush on unmount.
- Preserve widgetData in onLayoutConfigChange (was overwriting the entire dashboardData with just layoutConfig).
- Falls back to the panel ID in this case, which is what is needed for on Enterprise dashboard
@mofojed mofojed self-assigned this May 23, 2026
@mofojed mofojed requested review from a team, bmingles and Copilot and removed request for a team May 23, 2026 13:06
@github-actions
Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds per-user persistence for nested dashboards by saving/restoring the GoldenLayout layoutConfig plus associated widgetData, and updates nested layout behavior to avoid rebuilding layout structure when a persisted layout exists.

Changes:

  • Persist nested dashboard layoutConfig and widgetData via usePersistentState, including throttled widget data commits to prevent render loops.
  • Introduce InitialLayoutConfigContext so Row/Column/Stack can skip creating GoldenLayout layout items when rehydrating.
  • Switch UIComponent to use useDhId() for stable IDs (instead of __dhId from props).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
plugins/ui/src/js/src/widget/DocumentHandler.tsx Expands debug logging to include initialData.
plugins/ui/src/js/src/UIComponent.tsx Uses useDhId() to derive a stable component ID for persistence.
plugins/ui/src/js/src/layout/Stack.tsx Wraps stack creation to skip layout-item creation when an initial layout is present.
plugins/ui/src/js/src/layout/Row.tsx Skips row layout-item creation when an initial layout is present.
plugins/ui/src/js/src/layout/Column.tsx Skips column layout-item creation when an initial layout is present.
plugins/ui/src/js/src/layout/NestedDashboardContent.tsx Removes the separate nested dashboard content component (logic moved into NestedDashboard).
plugins/ui/src/js/src/layout/NestedDashboard.tsx Adds persistence for layout + widget data, throttled widget data updates, and provides initial layout context.
plugins/ui/src/js/src/layout/InitialLayoutConfigContext.ts New context/hook to expose whether a persisted initial layout config exists.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

onClose,
}: DocumentHandlerProps): JSX.Element {
log.debug('Rendering document', widget);
log.debug('Rendering document', widget, initialData);
@@ -0,0 +1,17 @@
import { DashboardLayoutConfig } from '@deephaven/dashboard';

/**
* Gets the initial layout config from the nearest context.
* @returns The initial layout config or null if not set
const { layoutConfig, widgetData } = dashboardData ?? {};

// We want to know if the initial layoutConfig is set so we know if the dashboard has previously been loaded.
// User may have moved panels around, and we don't want the layout rows/columns to be blow away their changes
Comment on lines +47 to +49
// Don't add a row here
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
Comment on lines +52 to +56
if (initialLayoutConfig != null) {
// If there's already an initial layout defined, user has likely already customized their layout.
// Don't add a row here
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
Comment on lines +54 to +57
// Don't add a row here
const { children } = props;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
Comment on lines +54 to +113
const [dashboardData, setDashboardData] = usePersistentState<
DashboardData | undefined
>(undefined, { type: 'NestedDashboardData', version: 1 });
const [layoutInitialized, setLayoutInitialized] = useState(false);
const layoutSettings = useMemo(
() => ({ hasHeaders: showHeaders }),
[showHeaders]
);
const { layoutConfig, widgetData } = dashboardData ?? {};

// We want to know if the initial layoutConfig is set so we know if the dashboard has previously been loaded.
// User may have moved panels around, and we don't want the layout rows/columns to be blow away their changes
const [initialLayoutConfig] = useState(() => layoutConfig);
const [initialWidgetData] = useState(() => widgetData);

// Track the latest committed widgetData and any pending merged updates so
// we can throttle calls to setDashboardData and skip no-op updates that
// would otherwise re-render in a loop.
const lastWidgetDataRef = useRef<WidgetData | undefined>(initialWidgetData);
const pendingWidgetDataRef = useRef<WidgetData | undefined>(undefined);

const flushDataChange = useThrottledCallback(
() => {
const pending = pendingWidgetDataRef.current;
if (pending == null) {
return;
}
pendingWidgetDataRef.current = undefined;

const last = lastWidgetDataRef.current;
// Deep-equality check to avoid triggering a state update (and the
// re-render loop) when the merged widgetData hasn't actually changed.
if (last != null && JSON.stringify(last) === JSON.stringify(pending)) {
return;
}
lastWidgetDataRef.current = pending;
setDashboardData(
oldData =>
({
...oldData,
widgetData: pending,
}) as DashboardData
);
},
DATA_CHANGE_THROTTLE_MS,
{ flushOnUnmount: true }
);

useEffect(() => () => flushDataChange.flush(), [flushDataChange]);

const handleDataChange = useCallback(
(data: WidgetData) => {
// Accumulate partial updates between throttled flushes so we don't
// lose intermediate widget data changes.
const base = pendingWidgetDataRef.current ?? lastWidgetDataRef.current;
pendingWidgetDataRef.current = { ...base, ...data };
flushDataChange();
},
[flushDataChange]
);
Comment on lines +10 to +13
DashboardLayoutConfig,
Dashboard as DHCDashboard,
usePersistentState,
} from '@deephaven/dashboard';
@github-actions
Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants