Skip to content

Commit

Permalink
[Dashboards] Add getSerializedState method to Dashboard API (#204140)
Browse files Browse the repository at this point in the history
Adds a `getSerializedState` method to the Dashboard API.
  • Loading branch information
nickpeihl authored Dec 19, 2024
1 parent 65a75ff commit d7280a1
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 170 deletions.
1 change: 1 addition & 0 deletions src/plugins/dashboard/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { prefixReferencesFromPanel } from './dashboard_container/persistable_sta
export {
convertPanelsArrayToPanelMap,
convertPanelMapToPanelsArray,
generateNewPanelIds,
} from './lib/dashboard_panel_converters';

export const UI_SETTINGS = {
Expand Down
13 changes: 10 additions & 3 deletions src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { initializeSearchSessionManager } from './search_session_manager';
import { initializeViewModeManager } from './view_mode_manager';
import { UnsavedPanelState } from '../dashboard_container/types';
import { initializeTrackContentfulRender } from './track_contentful_render';
import { getSerializedState } from './get_serialized_state';

export function getDashboardApi({
creationOptions,
Expand Down Expand Up @@ -110,9 +111,11 @@ export function getDashboardApi({
});
function getState() {
const { panels, references: panelReferences } = panelsManager.internalApi.getState();
const { state: unifiedSearchState, references: searchSourceReferences } =
unifiedSearchManager.internalApi.getState();
const dashboardState: DashboardState = {
...settingsManager.internalApi.getState(),
...unifiedSearchManager.internalApi.getState(),
...unifiedSearchState,
panels,
viewMode: viewModeManager.api.viewMode.value,
};
Expand All @@ -130,6 +133,7 @@ export function getDashboardApi({
dashboardState,
controlGroupReferences,
panelReferences,
searchSourceReferences,
};
}

Expand Down Expand Up @@ -168,6 +172,7 @@ export function getDashboardApi({
unifiedSearchManager.internalApi.controlGroupReload$,
unifiedSearchManager.internalApi.panelsReload$
).pipe(debounceTime(0)),
getSerializedState: () => getSerializedState(getState()),
runInteractiveSave: async () => {
trackOverlayApi.clearOverlays();
const saveResult = await openSaveModal({
Expand Down Expand Up @@ -197,11 +202,13 @@ export function getDashboardApi({
},
runQuickSave: async () => {
if (isManaged) return;
const { controlGroupReferences, dashboardState, panelReferences } = getState();
const { controlGroupReferences, dashboardState, panelReferences, searchSourceReferences } =
getState();
const saveResult = await getDashboardContentManagementService().saveDashboardState({
controlGroupReferences,
currentState: dashboardState,
dashboardState,
panelReferences,
searchSourceReferences,
saveOptions: {},
lastSavedId: savedObjectId$.value,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { DashboardPanelState } from '../../common';

import {
dataService,
embeddableService,
savedObjectsTaggingService,
} from '../services/kibana_services';
import { getSampleDashboardState } from '../mocks';
import { getSerializedState } from './get_serialized_state';

dataService.search.searchSource.create = jest.fn().mockResolvedValue({
setField: jest.fn(),
getSerializedFields: jest.fn().mockReturnValue({}),
});

dataService.query.timefilter.timefilter.getTime = jest
.fn()
.mockReturnValue({ from: 'now-15m', to: 'now' });

dataService.query.timefilter.timefilter.getRefreshInterval = jest
.fn()
.mockReturnValue({ pause: true, value: 0 });

embeddableService.extract = jest
.fn()
.mockImplementation((attributes) => ({ state: attributes, references: [] }));

if (savedObjectsTaggingService) {
savedObjectsTaggingService.getTaggingApi = jest.fn().mockReturnValue({
ui: {
updateTagsReferences: jest.fn((references, tags) => references),
},
});
}

jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('54321'),
}));

describe('getSerializedState', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should return the current state attributes and references', () => {
const dashboardState = getSampleDashboardState();
const result = getSerializedState({
controlGroupReferences: [],
generateNewIds: false,
dashboardState,
panelReferences: [],
searchSourceReferences: [],
});

expect(result.attributes).toMatchInlineSnapshot(`
Object {
"controlGroupInput": undefined,
"description": "",
"kibanaSavedObjectMeta": Object {
"searchSource": Object {
"filter": Array [],
"query": Object {
"language": "kuery",
"query": "hi",
},
},
},
"options": Object {
"hidePanelTitles": false,
"syncColors": false,
"syncCursor": true,
"syncTooltips": false,
"useMargins": true,
},
"panels": Array [],
"refreshInterval": undefined,
"timeFrom": undefined,
"timeRestore": false,
"timeTo": undefined,
"title": "My Dashboard",
"version": 3,
}
`);
expect(result.references).toEqual([]);
});

it('should generate new IDs for panels and references when generateNewIds is true', () => {
const dashboardState = {
...getSampleDashboardState(),
panels: { oldPanelId: { type: 'visualization' } as unknown as DashboardPanelState },
};
const result = getSerializedState({
controlGroupReferences: [],
generateNewIds: true,
dashboardState,
panelReferences: [
{
name: 'oldPanelId:indexpattern_foobar',
type: 'index-pattern',
id: 'bizzbuzz',
},
],
searchSourceReferences: [],
});

expect(result.attributes.panels).toMatchInlineSnapshot(`
Array [
Object {
"gridData": Object {
"i": "54321",
},
"panelConfig": Object {},
"panelIndex": "54321",
"type": "visualization",
"version": undefined,
},
]
`);
expect(result.references).toMatchInlineSnapshot(`
Array [
Object {
"id": "bizzbuzz",
"name": "54321:indexpattern_foobar",
"type": "index-pattern",
},
]
`);
});

it('should include control group references', () => {
const dashboardState = getSampleDashboardState();
const controlGroupReferences = [
{ name: 'control1:indexpattern', type: 'index-pattern', id: 'foobar' },
];
const result = getSerializedState({
controlGroupReferences,
generateNewIds: false,
dashboardState,
panelReferences: [],
searchSourceReferences: [],
});

expect(result.references).toEqual(controlGroupReferences);
});

it('should include panel references', () => {
const dashboardState = getSampleDashboardState();
const panelReferences = [
{ name: 'panel1:boogiewoogie', type: 'index-pattern', id: 'fizzbuzz' },
];
const result = getSerializedState({
controlGroupReferences: [],
generateNewIds: false,
dashboardState,
panelReferences,
searchSourceReferences: [],
});

expect(result.references).toEqual(panelReferences);
});
});
150 changes: 150 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/get_serialized_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { pick } from 'lodash';
import moment, { Moment } from 'moment';
import { RefreshInterval } from '@kbn/data-plugin/public';

import type { Reference } from '@kbn/content-management-utils';
import { convertPanelMapToPanelsArray, extractReferences, generateNewPanelIds } from '../../common';
import type { DashboardAttributes } from '../../server';

import { convertDashboardVersionToNumber } from '../services/dashboard_content_management_service/lib/dashboard_versioning';
import {
dataService,
embeddableService,
savedObjectsTaggingService,
} from '../services/kibana_services';
import { LATEST_DASHBOARD_CONTAINER_VERSION } from '../dashboard_container';
import { DashboardState } from './types';

export const convertTimeToUTCString = (time?: string | Moment): undefined | string => {
if (moment(time).isValid()) {
return moment(time).utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
} else {
// If it's not a valid moment date, then it should be a string representing a relative time
// like 'now' or 'now-15m'.
return time as string;
}
};

export const getSerializedState = ({
controlGroupReferences,
generateNewIds,
dashboardState,
panelReferences,
searchSourceReferences,
}: {
controlGroupReferences?: Reference[];
generateNewIds?: boolean;
dashboardState: DashboardState;
panelReferences?: Reference[];
searchSourceReferences?: Reference[];
}) => {
const {
query: {
timefilter: { timefilter },
},
} = dataService;

const {
tags,
query,
title,
filters,
timeRestore,
description,

// Dashboard options
useMargins,
syncColors,
syncCursor,
syncTooltips,
hidePanelTitles,
controlGroupInput,
} = dashboardState;

let { panels } = dashboardState;
let prefixedPanelReferences = panelReferences;
if (generateNewIds) {
const { panels: newPanels, references: newPanelReferences } = generateNewPanelIds(
panels,
panelReferences
);
panels = newPanels;
prefixedPanelReferences = newPanelReferences;
//
// do not need to generate new ids for controls.
// ControlGroup Component is keyed on dashboard id so changing dashboard id mounts new ControlGroup Component.
//
}

const searchSource = { filter: filters, query };
const options = {
useMargins,
syncColors,
syncCursor,
syncTooltips,
hidePanelTitles,
};
const savedPanels = convertPanelMapToPanelsArray(panels, true);

/**
* Parse global time filter settings
*/
const { from, to } = timefilter.getTime();
const timeFrom = timeRestore ? convertTimeToUTCString(from) : undefined;
const timeTo = timeRestore ? convertTimeToUTCString(to) : undefined;
const refreshInterval = timeRestore
? (pick(timefilter.getRefreshInterval(), [
'display',
'pause',
'section',
'value',
]) as RefreshInterval)
: undefined;

const rawDashboardAttributes: DashboardAttributes = {
version: convertDashboardVersionToNumber(LATEST_DASHBOARD_CONTAINER_VERSION),
controlGroupInput: controlGroupInput as DashboardAttributes['controlGroupInput'],
kibanaSavedObjectMeta: { searchSource },
description: description ?? '',
refreshInterval,
timeRestore,
options,
panels: savedPanels,
timeFrom,
title,
timeTo,
};

/**
* Extract references from raw attributes and tags into the references array.
*/
const { attributes, references: dashboardReferences } = extractReferences(
{
attributes: rawDashboardAttributes,
references: searchSourceReferences ?? [],
},
{ embeddablePersistableStateService: embeddableService }
);

const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi();
const references = savedObjectsTaggingApi?.ui.updateTagsReferences
? savedObjectsTaggingApi?.ui.updateTagsReferences(dashboardReferences, tags)
: dashboardReferences;

const allReferences = [
...references,
...(prefixedPanelReferences ?? []),
...(controlGroupReferences ?? []),
...(searchSourceReferences ?? []),
];
return { attributes, references: allReferences };
};
Loading

0 comments on commit d7280a1

Please sign in to comment.