Skip to content

Commit e64407d

Browse files
authored
fix: Save/load plugin data with layout (#1866)
- Plugin data was not getting saved with the workspace - Reload the dashboard when a layout is imported by updating the key - Fixes #1861
1 parent f302fb9 commit e64407d

5 files changed

Lines changed: 87 additions & 22 deletions

File tree

packages/code-studio/src/main/AppDashboards.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface AppDashboardsProps {
1818
dashboards: {
1919
id: string;
2020
layoutConfig: ItemConfigType[];
21+
key?: string;
2122
}[];
2223
activeDashboard: string;
2324
onGoldenLayoutChange: (goldenLayout: LayoutManager) => void;
@@ -65,6 +66,7 @@ export function AppDashboards({
6566
>
6667
<LazyDashboard
6768
id={d.id}
69+
key={d.key}
6870
isActive={d.id === activeDashboard}
6971
emptyDashboard={
7072
d.id === DEFAULT_DASHBOARD_ID ? (

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

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import React from 'react';
33
import { Provider } from 'react-redux';
4-
import { render, screen } from '@testing-library/react';
4+
import { act, render, screen } from '@testing-library/react';
55
import { ConnectionContext } from '@deephaven/app-utils';
66
import { ToolType } from '@deephaven/dashboard-core-plugins';
77
import {
@@ -112,17 +112,39 @@ function renderAppMainContainer({
112112
</Provider>
113113
);
114114
}
115+
116+
const EMPTY_LAYOUT = {
117+
filterSets: [],
118+
layoutConfig: [],
119+
links: [],
120+
version: 2,
121+
};
122+
115123
let mockProp = {};
116124
let mockId = DEFAULT_DASHBOARD_ID;
125+
let mockIteration = 0;
117126
jest.mock('@deephaven/dashboard', () => ({
118127
...jest.requireActual('@deephaven/dashboard'),
119128
__esModule: true,
120129
LazyDashboard: jest.fn(({ hydrate }) => {
130+
const { useMemo } = jest.requireActual('react');
131+
// We use the `key` to determine how many times this LazyDashboard component was re-rendered with a new key
132+
// When rendered with a new key, the `useMemo` will be useless and will return a new key
133+
const key = useMemo(() => {
134+
const newKey = `${mockIteration}`;
135+
mockIteration += 1;
136+
return newKey;
137+
}, []);
121138
const result = hydrate(mockProp, mockId);
122139
if (result.fetch != null) {
123140
result.fetch();
124141
}
125-
return <p>{JSON.stringify(result)}</p>;
142+
return (
143+
<>
144+
<p>{JSON.stringify(result)}</p>
145+
<p data-testid="dashboard-key">{key}</p>
146+
</>
147+
);
126148
}),
127149
default: jest.fn(),
128150
}));
@@ -136,6 +158,8 @@ beforeEach(() => {
136158
cb(0);
137159
return 0;
138160
});
161+
mockProp = {};
162+
mockIteration = 0;
139163
});
140164

141165
afterEach(() => {
@@ -241,3 +265,36 @@ describe('hydrates widgets correctly', () => {
241265
expect(objectFetcher).toHaveBeenCalled();
242266
});
243267
});
268+
269+
describe('imports layout correctly', () => {
270+
it('uses a new key when layout is imported', async () => {
271+
renderAppMainContainer();
272+
273+
expect(screen.getByText('{"localDashboardId":"default"}')).toBeTruthy();
274+
275+
const oldKey = screen.getByTestId('dashboard-key').textContent ?? '';
276+
expect(oldKey.length).not.toBe(0);
277+
278+
await act(async () => {
279+
const text = JSON.stringify(EMPTY_LAYOUT);
280+
const file = TestUtils.createMockProxy<File>({
281+
text: () => Promise.resolve(text),
282+
name: 'layout.json',
283+
type: 'application/json',
284+
});
285+
286+
// Technically, the "Import Layout" button in the panels list is what the user clicks on to show the file picker
287+
// However, the testing library uses the `.upload` command on the `input` element directly, which we don't display
288+
// So just fetch it by testid and use the `.upload` command: https://testing-library.com/docs/user-event/utility/#upload
289+
const importInput = screen.getByTestId('input-import-layout');
290+
await userEvent.upload(importInput, file);
291+
});
292+
293+
expect(screen.getByText('{"localDashboardId":"default"}')).toBeTruthy();
294+
295+
const newKey = screen.getByTestId('dashboard-key').textContent ?? '';
296+
297+
expect(newKey.length).not.toBe(0);
298+
expect(newKey).not.toBe(oldKey);
299+
});
300+
});

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ import {
5050
getDashboardSessionWrapper,
5151
ControlType,
5252
ToolType,
53-
FilterSet,
54-
Link,
5553
getDashboardConnection,
5654
NotebookPanel,
5755
} from '@deephaven/dashboard-core-plugins';
@@ -155,6 +153,9 @@ interface AppMainContainerState {
155153
widgets: DhType.ide.VariableDefinition[];
156154
tabs: NavTabItem[];
157155
activeTabKey: string;
156+
157+
// Number of times the layout has been re-initialized
158+
layoutIteration: number;
158159
}
159160

160161
export class AppMainContainer extends Component<
@@ -255,6 +256,7 @@ export class AppMainContainer extends Component<
255256
title: value.title ?? 'Untitled',
256257
})),
257258
activeTabKey: DEFAULT_DASHBOARD_ID,
259+
layoutIteration: 0,
258260
};
259261
}
260262

@@ -577,13 +579,7 @@ export class AppMainContainer extends Component<
577579
try {
578580
const { workspace } = this.props;
579581
const { data } = workspace;
580-
const exportedConfig = UserLayoutUtils.exportLayout(
581-
data as {
582-
filterSets: FilterSet[];
583-
links: Link[];
584-
layoutConfig: ItemConfigType[];
585-
}
586-
);
582+
const exportedConfig = UserLayoutUtils.exportLayout(data);
587583

588584
log.info('handleExportLayoutClick exportedConfig', exportedConfig);
589585

@@ -658,14 +654,18 @@ export class AppMainContainer extends Component<
658654
const { updateDashboardData, updateWorkspaceData } = this.props;
659655
const fileText = await file.text();
660656
const exportedLayout = JSON.parse(fileText);
661-
const { filterSets, layoutConfig, links } =
657+
const { filterSets, layoutConfig, links, pluginDataMap } =
662658
UserLayoutUtils.normalizeLayout(exportedLayout);
663659

664660
updateWorkspaceData({ layoutConfig });
665661
updateDashboardData(DEFAULT_DASHBOARD_ID, {
666662
filterSets,
667663
links,
664+
pluginDataMap,
668665
});
666+
this.setState(({ layoutIteration }) => ({
667+
layoutIteration: layoutIteration + 1,
668+
}));
669669
} catch (e) {
670670
log.error('Unable to import layout', e);
671671
}
@@ -832,8 +832,9 @@ export class AppMainContainer extends Component<
832832
getDashboards(): {
833833
id: string;
834834
layoutConfig: ItemConfigType[];
835+
key?: string;
835836
}[] {
836-
const { tabs } = this.state;
837+
const { layoutIteration, tabs } = this.state;
837838
const { allDashboardData, workspace } = this.props;
838839
const { data: workspaceData } = workspace;
839840
const { layoutConfig } = workspaceData;
@@ -842,11 +843,13 @@ export class AppMainContainer extends Component<
842843
{
843844
id: DEFAULT_DASHBOARD_ID,
844845
layoutConfig: layoutConfig as ItemConfigType[],
846+
key: `${DEFAULT_DASHBOARD_ID}-${layoutIteration}`,
845847
},
846848
...tabs.map(tab => ({
847849
id: tab.key,
848850
layoutConfig: (allDashboardData[tab.key]?.layoutConfig ??
849851
EMPTY_ARRAY) as ItemConfigType[],
852+
key: `${tab.key}-${layoutIteration}`,
850853
})),
851854
];
852855
}
@@ -1019,6 +1022,7 @@ export class AppMainContainer extends Component<
10191022
accept=".json"
10201023
style={{ display: 'none' }}
10211024
onChange={this.handleImportLayoutFiles}
1025+
data-testid="input-import-layout"
10221026
/>
10231027
<DebouncedModal
10241028
isOpen={isDisconnected && !isAuthFailed}

packages/code-studio/src/main/UserLayoutUtils.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import {
22
CommandHistoryPanel,
33
ConsolePanel,
44
FileExplorerPanel,
5-
FilterSet,
6-
Link,
75
LogPanel,
86
} from '@deephaven/dashboard-core-plugins';
9-
import type { ItemConfigType } from '@deephaven/golden-layout';
107
import Log from '@deephaven/log';
8+
import { CustomizableWorkspaceData } from '@deephaven/redux';
119
import LayoutStorage, {
1210
ExportedLayout,
1311
ExportedLayoutV2,
@@ -137,16 +135,18 @@ export async function getDefaultLayout(
137135
: DEFAULT_LAYOUT_CONFIG_NO_CONSOLE;
138136
}
139137

140-
export function exportLayout(data: {
141-
filterSets: FilterSet[];
142-
links: Link[];
143-
layoutConfig: ItemConfigType[];
144-
}): ExportedLayoutV2 {
145-
const { filterSets, layoutConfig, links } = data;
138+
export function exportLayout(
139+
data: CustomizableWorkspaceData
140+
): ExportedLayoutV2 {
141+
const { filterSets, layoutConfig, links, pluginDataMap } = data as Omit<
142+
ExportedLayoutV2,
143+
'version'
144+
>;
146145
const exportedLayout: ExportedLayoutV2 = {
147146
filterSets,
148147
layoutConfig,
149148
links,
149+
pluginDataMap,
150150
version: 2,
151151
};
152152
return exportedLayout;

packages/code-studio/src/storage/LayoutStorage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ItemConfigType } from '@deephaven/golden-layout';
22
import { FilterSet, Link } from '@deephaven/dashboard-core-plugins';
3+
import { PluginDataMap } from '@deephaven/redux';
34

45
/**
56
* Have a different version to support legacy layout exports
@@ -10,6 +11,7 @@ export type ExportedLayoutV2 = {
1011
filterSets: FilterSet[];
1112
links: Link[];
1213
layoutConfig: ItemConfigType[];
14+
pluginDataMap?: PluginDataMap;
1315
version: 2;
1416
};
1517

0 commit comments

Comments
 (0)