Skip to content

Commit 32dde3c

Browse files
authored
feat: Multiple dashboards (#1714)
Fixes #1683 Test with the dh.ui plugin from deephaven/deephaven-plugins#176 Example dashboard code ``` from deephaven import ui, time_table from deephaven.ui import use_memo, use_state from deephaven.plot.figure import Figure def use_wave_input(): """ Demonstrating a custom hook. Creates an input panel that controls the amplitude, frequency, and phase for a wave """ amplitude, set_amplitude = use_state(1.0) frequency, set_frequency = use_state(1.0) phase, set_phase = use_state(1.0) input_panel = ui.flex( ui.slider( label="Amplitude", default_value=amplitude, min_value=-100.0, max_value=100.0, on_change=set_amplitude, step=0.1, ), ui.slider( label="Frequency", default_value=frequency, min_value=-100.0, max_value=100.0, on_change=set_frequency, step=0.1, ), ui.slider( label="Phase", default_value=phase, min_value=-100.0, max_value=100.0, on_change=set_phase, step=0.1, ), direction="column", ) return amplitude, frequency, phase, input_panel @ui.component def multiwave(): amplitude, frequency, phase, wave_input = use_wave_input() tt = use_memo(lambda: time_table("PT1s").update("x=i"), []) t = use_memo( lambda: tt.update( [ f"y_sin={amplitude}*Math.sin({frequency}*x+{phase})", f"y_cos={amplitude}*Math.cos({frequency}*x+{phase})", f"y_tan={amplitude}*Math.tan({frequency}*x+{phase})", ] ), [amplitude, frequency, phase], ) p_sin = use_memo( lambda: Figure().plot_xy(series_name="Sine", t=t, x="x", y="y_sin").show(), [t] ) p_cos = use_memo( lambda: Figure().plot_xy(series_name="Cosine", t=t, x="x", y="y_cos").show(), [t], ) p_tan = use_memo( lambda: Figure().plot_xy(series_name="Tangent", t=t, x="x", y="y_tan").show(), [t], ) return [ ui.column( ui.row( ui.stack( ui.panel(wave_input, title="Wave Input"), ui.panel(t, title="Wave Table"), activeItemIndex=0 ), height=25 ), ui.row( ui.stack(ui.panel(p_sin, title="Sine"), width=50), ui.stack(ui.panel(p_cos, title="Cosine"), width=30), ui.stack(ui.panel(p_tan, title="Tangent")) ) ) ] mw = ui.dashboard(multiwave()) ```
1 parent e1b4562 commit 32dde3c

13 files changed

Lines changed: 490 additions & 212 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React, { useCallback } from 'react';
2+
import classNames from 'classnames';
3+
import {
4+
DashboardUtils,
5+
DEFAULT_DASHBOARD_ID,
6+
DehydratedDashboardPanelProps,
7+
LazyDashboard,
8+
} from '@deephaven/dashboard';
9+
import { useConnection } from '@deephaven/jsapi-components';
10+
import { VariableDefinition } from '@deephaven/jsapi-types';
11+
import LayoutManager, { ItemConfigType } from '@deephaven/golden-layout';
12+
import { LoadingOverlay } from '@deephaven/components';
13+
import EmptyDashboard from './EmptyDashboard';
14+
15+
interface AppDashboardsProps {
16+
dashboards: {
17+
id: string;
18+
layoutConfig: ItemConfigType[];
19+
}[];
20+
activeDashboard: string;
21+
onGoldenLayoutChange: (goldenLayout: LayoutManager) => void;
22+
plugins: JSX.Element[];
23+
onAutoFillClick: (event: React.MouseEvent) => void;
24+
}
25+
26+
export function AppDashboards({
27+
dashboards,
28+
activeDashboard,
29+
onGoldenLayoutChange,
30+
plugins,
31+
onAutoFillClick,
32+
}: AppDashboardsProps): JSX.Element {
33+
const connection = useConnection();
34+
35+
const hydratePanel = useCallback(
36+
(hydrateProps: DehydratedDashboardPanelProps, id: string) => {
37+
const { metadata } = hydrateProps;
38+
if (
39+
metadata?.type != null &&
40+
(metadata?.id != null || metadata?.name != null)
41+
) {
42+
// Looks like a widget, hydrate it as such
43+
const widget: VariableDefinition =
44+
metadata.id != null
45+
? {
46+
type: metadata.type,
47+
id: metadata.id,
48+
}
49+
: {
50+
type: metadata.type,
51+
name: metadata.name,
52+
title: metadata.name,
53+
};
54+
return {
55+
fetch: async () => connection?.getObject(widget),
56+
...hydrateProps,
57+
localDashboardId: id,
58+
};
59+
}
60+
return DashboardUtils.hydrate(hydrateProps, id);
61+
},
62+
[connection]
63+
);
64+
65+
return (
66+
<div className="tab-content">
67+
{dashboards.map(d => (
68+
<div
69+
key={d.id}
70+
className={classNames('tab-pane', {
71+
active: d.id === activeDashboard,
72+
})}
73+
>
74+
<LazyDashboard
75+
id={d.id}
76+
isActive={d.id === activeDashboard}
77+
emptyDashboard={
78+
d.id === DEFAULT_DASHBOARD_ID ? (
79+
<EmptyDashboard onAutoFillClick={onAutoFillClick} />
80+
) : (
81+
<LoadingOverlay />
82+
)
83+
}
84+
layoutConfig={d.layoutConfig}
85+
onGoldenLayoutChange={onGoldenLayoutChange}
86+
hydrate={hydratePanel}
87+
plugins={plugins}
88+
/>
89+
</div>
90+
))}
91+
</div>
92+
);
93+
}
94+
95+
export default AppDashboards;

packages/code-studio/src/main/AppMainContainer.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na
4242
width: 100%;
4343
justify-content: space-between;
4444
align-items: center;
45+
}
4546

47+
.app-main-right-menu-buttons {
4648
.btn-link {
4749
font-size: $tab-font-size;
4850
text-decoration: none;
@@ -67,7 +69,9 @@ $nav-space: 4px; // give a gap around some buttons for focus area that are in na
6769
}
6870

6971
.tab-pane {
72+
height: 100%;
7073
width: 100%;
74+
flex-grow: 1;
7175
}
7276

7377
.app-main-tabs {

packages/code-studio/src/main/AppMainContainer.test.tsx

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import React from 'react';
3+
import { Provider } from 'react-redux';
34
import { render, screen } from '@testing-library/react';
45
import { ToolType } from '@deephaven/dashboard-core-plugins';
56
import { ApiContext } from '@deephaven/jsapi-bootstrap';
7+
import { ConnectionContext } from '@deephaven/jsapi-components';
68
import dh from '@deephaven/jsapi-shim';
79
import type {
810
IdeConnection,
911
IdeSession,
1012
VariableChanges,
1113
} from '@deephaven/jsapi-types';
1214
import { TestUtils } from '@deephaven/utils';
13-
import { Workspace } from '@deephaven/redux';
15+
import { Workspace, createMockStore } from '@deephaven/redux';
1416
import userEvent from '@testing-library/user-event';
1517
import { DEFAULT_DASHBOARD_ID } from '@deephaven/dashboard';
16-
import { AppMainContainer, AppDashboardData } from './AppMainContainer';
18+
import { AppMainContainer } from './AppMainContainer';
1719
import LocalWorkspaceStorage from '../storage/LocalWorkspaceStorage';
1820
import LayoutStorage from '../storage/LayoutStorage';
1921

@@ -69,38 +71,46 @@ function renderAppMainContainer({
6971
match = makeMatch(),
7072
plugins = new Map(),
7173
} = {}) {
74+
const store = createMockStore();
7275
return render(
73-
<ApiContext.Provider value={dh}>
74-
<AppMainContainer
75-
dashboardData={dashboardData as AppDashboardData}
76-
layoutStorage={layoutStorage as LayoutStorage}
77-
saveWorkspace={saveWorkspace}
78-
updateDashboardData={updateDashboardData}
79-
updateWorkspaceData={updateWorkspaceData}
80-
user={user}
81-
workspace={workspace as Workspace}
82-
workspaceStorage={workspaceStorage}
83-
activeTool={activeTool}
84-
setActiveTool={setActiveTool}
85-
setDashboardIsolatedLinkerPanelId={setDashboardIsolatedLinkerPanelId}
86-
client={client}
87-
serverConfigValues={serverConfigValues}
88-
dashboardOpenedPanelMaps={dashboardOpenedPanelMaps}
89-
connection={connection}
90-
session={session as unknown as IdeSession}
91-
sessionConfig={sessionConfig}
92-
match={match}
93-
plugins={plugins}
94-
/>
95-
</ApiContext.Provider>
76+
<Provider store={store}>
77+
<ApiContext.Provider value={dh}>
78+
<ConnectionContext.Provider value={connection}>
79+
<AppMainContainer
80+
dashboardData={dashboardData}
81+
allDashboardData={dashboardData}
82+
layoutStorage={layoutStorage as LayoutStorage}
83+
saveWorkspace={saveWorkspace}
84+
updateDashboardData={updateDashboardData}
85+
updateWorkspaceData={updateWorkspaceData}
86+
user={user}
87+
workspace={workspace as Workspace}
88+
workspaceStorage={workspaceStorage}
89+
activeTool={activeTool}
90+
setActiveTool={setActiveTool}
91+
setDashboardIsolatedLinkerPanelId={
92+
setDashboardIsolatedLinkerPanelId
93+
}
94+
client={client}
95+
serverConfigValues={serverConfigValues}
96+
dashboardOpenedPanelMaps={dashboardOpenedPanelMaps}
97+
connection={connection}
98+
session={session as unknown as IdeSession}
99+
sessionConfig={sessionConfig}
100+
match={match}
101+
plugins={plugins}
102+
/>
103+
</ConnectionContext.Provider>
104+
</ApiContext.Provider>
105+
</Provider>
96106
);
97107
}
98108
let mockProp = {};
99109
let mockId = DEFAULT_DASHBOARD_ID;
100110
jest.mock('@deephaven/dashboard', () => ({
101111
...jest.requireActual('@deephaven/dashboard'),
102112
__esModule: true,
103-
Dashboard: jest.fn(({ hydrate }) => {
113+
LazyDashboard: jest.fn(({ hydrate }) => {
104114
const result = hydrate(mockProp, mockId);
105115
if (result.fetch != null) {
106116
result.fetch();

0 commit comments

Comments
 (0)