Skip to content

Commit d7280a1

Browse files
authored
[Dashboards] Add getSerializedState method to Dashboard API (#204140)
Adds a `getSerializedState` method to the Dashboard API.
1 parent 65a75ff commit d7280a1

File tree

10 files changed

+396
-170
lines changed

10 files changed

+396
-170
lines changed

src/plugins/dashboard/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export { prefixReferencesFromPanel } from './dashboard_container/persistable_sta
3232
export {
3333
convertPanelsArrayToPanelMap,
3434
convertPanelMapToPanelsArray,
35+
generateNewPanelIds,
3536
} from './lib/dashboard_panel_converters';
3637

3738
export const UI_SETTINGS = {

src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { initializeSearchSessionManager } from './search_session_manager';
4141
import { initializeViewModeManager } from './view_mode_manager';
4242
import { UnsavedPanelState } from '../dashboard_container/types';
4343
import { initializeTrackContentfulRender } from './track_contentful_render';
44+
import { getSerializedState } from './get_serialized_state';
4445

4546
export function getDashboardApi({
4647
creationOptions,
@@ -110,9 +111,11 @@ export function getDashboardApi({
110111
});
111112
function getState() {
112113
const { panels, references: panelReferences } = panelsManager.internalApi.getState();
114+
const { state: unifiedSearchState, references: searchSourceReferences } =
115+
unifiedSearchManager.internalApi.getState();
113116
const dashboardState: DashboardState = {
114117
...settingsManager.internalApi.getState(),
115-
...unifiedSearchManager.internalApi.getState(),
118+
...unifiedSearchState,
116119
panels,
117120
viewMode: viewModeManager.api.viewMode.value,
118121
};
@@ -130,6 +133,7 @@ export function getDashboardApi({
130133
dashboardState,
131134
controlGroupReferences,
132135
panelReferences,
136+
searchSourceReferences,
133137
};
134138
}
135139

@@ -168,6 +172,7 @@ export function getDashboardApi({
168172
unifiedSearchManager.internalApi.controlGroupReload$,
169173
unifiedSearchManager.internalApi.panelsReload$
170174
).pipe(debounceTime(0)),
175+
getSerializedState: () => getSerializedState(getState()),
171176
runInteractiveSave: async () => {
172177
trackOverlayApi.clearOverlays();
173178
const saveResult = await openSaveModal({
@@ -197,11 +202,13 @@ export function getDashboardApi({
197202
},
198203
runQuickSave: async () => {
199204
if (isManaged) return;
200-
const { controlGroupReferences, dashboardState, panelReferences } = getState();
205+
const { controlGroupReferences, dashboardState, panelReferences, searchSourceReferences } =
206+
getState();
201207
const saveResult = await getDashboardContentManagementService().saveDashboardState({
202208
controlGroupReferences,
203-
currentState: dashboardState,
209+
dashboardState,
204210
panelReferences,
211+
searchSourceReferences,
205212
saveOptions: {},
206213
lastSavedId: savedObjectId$.value,
207214
});
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { DashboardPanelState } from '../../common';
11+
12+
import {
13+
dataService,
14+
embeddableService,
15+
savedObjectsTaggingService,
16+
} from '../services/kibana_services';
17+
import { getSampleDashboardState } from '../mocks';
18+
import { getSerializedState } from './get_serialized_state';
19+
20+
dataService.search.searchSource.create = jest.fn().mockResolvedValue({
21+
setField: jest.fn(),
22+
getSerializedFields: jest.fn().mockReturnValue({}),
23+
});
24+
25+
dataService.query.timefilter.timefilter.getTime = jest
26+
.fn()
27+
.mockReturnValue({ from: 'now-15m', to: 'now' });
28+
29+
dataService.query.timefilter.timefilter.getRefreshInterval = jest
30+
.fn()
31+
.mockReturnValue({ pause: true, value: 0 });
32+
33+
embeddableService.extract = jest
34+
.fn()
35+
.mockImplementation((attributes) => ({ state: attributes, references: [] }));
36+
37+
if (savedObjectsTaggingService) {
38+
savedObjectsTaggingService.getTaggingApi = jest.fn().mockReturnValue({
39+
ui: {
40+
updateTagsReferences: jest.fn((references, tags) => references),
41+
},
42+
});
43+
}
44+
45+
jest.mock('uuid', () => ({
46+
v4: jest.fn().mockReturnValue('54321'),
47+
}));
48+
49+
describe('getSerializedState', () => {
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
});
53+
54+
it('should return the current state attributes and references', () => {
55+
const dashboardState = getSampleDashboardState();
56+
const result = getSerializedState({
57+
controlGroupReferences: [],
58+
generateNewIds: false,
59+
dashboardState,
60+
panelReferences: [],
61+
searchSourceReferences: [],
62+
});
63+
64+
expect(result.attributes).toMatchInlineSnapshot(`
65+
Object {
66+
"controlGroupInput": undefined,
67+
"description": "",
68+
"kibanaSavedObjectMeta": Object {
69+
"searchSource": Object {
70+
"filter": Array [],
71+
"query": Object {
72+
"language": "kuery",
73+
"query": "hi",
74+
},
75+
},
76+
},
77+
"options": Object {
78+
"hidePanelTitles": false,
79+
"syncColors": false,
80+
"syncCursor": true,
81+
"syncTooltips": false,
82+
"useMargins": true,
83+
},
84+
"panels": Array [],
85+
"refreshInterval": undefined,
86+
"timeFrom": undefined,
87+
"timeRestore": false,
88+
"timeTo": undefined,
89+
"title": "My Dashboard",
90+
"version": 3,
91+
}
92+
`);
93+
expect(result.references).toEqual([]);
94+
});
95+
96+
it('should generate new IDs for panels and references when generateNewIds is true', () => {
97+
const dashboardState = {
98+
...getSampleDashboardState(),
99+
panels: { oldPanelId: { type: 'visualization' } as unknown as DashboardPanelState },
100+
};
101+
const result = getSerializedState({
102+
controlGroupReferences: [],
103+
generateNewIds: true,
104+
dashboardState,
105+
panelReferences: [
106+
{
107+
name: 'oldPanelId:indexpattern_foobar',
108+
type: 'index-pattern',
109+
id: 'bizzbuzz',
110+
},
111+
],
112+
searchSourceReferences: [],
113+
});
114+
115+
expect(result.attributes.panels).toMatchInlineSnapshot(`
116+
Array [
117+
Object {
118+
"gridData": Object {
119+
"i": "54321",
120+
},
121+
"panelConfig": Object {},
122+
"panelIndex": "54321",
123+
"type": "visualization",
124+
"version": undefined,
125+
},
126+
]
127+
`);
128+
expect(result.references).toMatchInlineSnapshot(`
129+
Array [
130+
Object {
131+
"id": "bizzbuzz",
132+
"name": "54321:indexpattern_foobar",
133+
"type": "index-pattern",
134+
},
135+
]
136+
`);
137+
});
138+
139+
it('should include control group references', () => {
140+
const dashboardState = getSampleDashboardState();
141+
const controlGroupReferences = [
142+
{ name: 'control1:indexpattern', type: 'index-pattern', id: 'foobar' },
143+
];
144+
const result = getSerializedState({
145+
controlGroupReferences,
146+
generateNewIds: false,
147+
dashboardState,
148+
panelReferences: [],
149+
searchSourceReferences: [],
150+
});
151+
152+
expect(result.references).toEqual(controlGroupReferences);
153+
});
154+
155+
it('should include panel references', () => {
156+
const dashboardState = getSampleDashboardState();
157+
const panelReferences = [
158+
{ name: 'panel1:boogiewoogie', type: 'index-pattern', id: 'fizzbuzz' },
159+
];
160+
const result = getSerializedState({
161+
controlGroupReferences: [],
162+
generateNewIds: false,
163+
dashboardState,
164+
panelReferences,
165+
searchSourceReferences: [],
166+
});
167+
168+
expect(result.references).toEqual(panelReferences);
169+
});
170+
});
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { pick } from 'lodash';
11+
import moment, { Moment } from 'moment';
12+
import { RefreshInterval } from '@kbn/data-plugin/public';
13+
14+
import type { Reference } from '@kbn/content-management-utils';
15+
import { convertPanelMapToPanelsArray, extractReferences, generateNewPanelIds } from '../../common';
16+
import type { DashboardAttributes } from '../../server';
17+
18+
import { convertDashboardVersionToNumber } from '../services/dashboard_content_management_service/lib/dashboard_versioning';
19+
import {
20+
dataService,
21+
embeddableService,
22+
savedObjectsTaggingService,
23+
} from '../services/kibana_services';
24+
import { LATEST_DASHBOARD_CONTAINER_VERSION } from '../dashboard_container';
25+
import { DashboardState } from './types';
26+
27+
export const convertTimeToUTCString = (time?: string | Moment): undefined | string => {
28+
if (moment(time).isValid()) {
29+
return moment(time).utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
30+
} else {
31+
// If it's not a valid moment date, then it should be a string representing a relative time
32+
// like 'now' or 'now-15m'.
33+
return time as string;
34+
}
35+
};
36+
37+
export const getSerializedState = ({
38+
controlGroupReferences,
39+
generateNewIds,
40+
dashboardState,
41+
panelReferences,
42+
searchSourceReferences,
43+
}: {
44+
controlGroupReferences?: Reference[];
45+
generateNewIds?: boolean;
46+
dashboardState: DashboardState;
47+
panelReferences?: Reference[];
48+
searchSourceReferences?: Reference[];
49+
}) => {
50+
const {
51+
query: {
52+
timefilter: { timefilter },
53+
},
54+
} = dataService;
55+
56+
const {
57+
tags,
58+
query,
59+
title,
60+
filters,
61+
timeRestore,
62+
description,
63+
64+
// Dashboard options
65+
useMargins,
66+
syncColors,
67+
syncCursor,
68+
syncTooltips,
69+
hidePanelTitles,
70+
controlGroupInput,
71+
} = dashboardState;
72+
73+
let { panels } = dashboardState;
74+
let prefixedPanelReferences = panelReferences;
75+
if (generateNewIds) {
76+
const { panels: newPanels, references: newPanelReferences } = generateNewPanelIds(
77+
panels,
78+
panelReferences
79+
);
80+
panels = newPanels;
81+
prefixedPanelReferences = newPanelReferences;
82+
//
83+
// do not need to generate new ids for controls.
84+
// ControlGroup Component is keyed on dashboard id so changing dashboard id mounts new ControlGroup Component.
85+
//
86+
}
87+
88+
const searchSource = { filter: filters, query };
89+
const options = {
90+
useMargins,
91+
syncColors,
92+
syncCursor,
93+
syncTooltips,
94+
hidePanelTitles,
95+
};
96+
const savedPanels = convertPanelMapToPanelsArray(panels, true);
97+
98+
/**
99+
* Parse global time filter settings
100+
*/
101+
const { from, to } = timefilter.getTime();
102+
const timeFrom = timeRestore ? convertTimeToUTCString(from) : undefined;
103+
const timeTo = timeRestore ? convertTimeToUTCString(to) : undefined;
104+
const refreshInterval = timeRestore
105+
? (pick(timefilter.getRefreshInterval(), [
106+
'display',
107+
'pause',
108+
'section',
109+
'value',
110+
]) as RefreshInterval)
111+
: undefined;
112+
113+
const rawDashboardAttributes: DashboardAttributes = {
114+
version: convertDashboardVersionToNumber(LATEST_DASHBOARD_CONTAINER_VERSION),
115+
controlGroupInput: controlGroupInput as DashboardAttributes['controlGroupInput'],
116+
kibanaSavedObjectMeta: { searchSource },
117+
description: description ?? '',
118+
refreshInterval,
119+
timeRestore,
120+
options,
121+
panels: savedPanels,
122+
timeFrom,
123+
title,
124+
timeTo,
125+
};
126+
127+
/**
128+
* Extract references from raw attributes and tags into the references array.
129+
*/
130+
const { attributes, references: dashboardReferences } = extractReferences(
131+
{
132+
attributes: rawDashboardAttributes,
133+
references: searchSourceReferences ?? [],
134+
},
135+
{ embeddablePersistableStateService: embeddableService }
136+
);
137+
138+
const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi();
139+
const references = savedObjectsTaggingApi?.ui.updateTagsReferences
140+
? savedObjectsTaggingApi?.ui.updateTagsReferences(dashboardReferences, tags)
141+
: dashboardReferences;
142+
143+
const allReferences = [
144+
...references,
145+
...(prefixedPanelReferences ?? []),
146+
...(controlGroupReferences ?? []),
147+
...(searchSourceReferences ?? []),
148+
];
149+
return { attributes, references: allReferences };
150+
};

0 commit comments

Comments
 (0)