Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboards] Add getSerializedState method to Dashboard API #204140

Merged
merged 10 commits into from
Dec 19, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ export function getDashboardApi({
unifiedSearchManager.internalApi.controlGroupReload$,
unifiedSearchManager.internalApi.panelsReload$
).pipe(debounceTime(0)),
getSerializedState: async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

#203662 made getState sync so this method could now become sync as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh good call! Updated in b58c33d.

Unfortunately, the getSerializedState method still needs to be async for now since the getDashboardState method on the CM service relies on a Promise to extract searchSource references. But this might change when we move reference handling to the server in #192758.

const { controlGroupReferences, dashboardState, panelReferences } = await getState();
return getDashboardContentManagementService().getDashboardState({
controlGroupReferences,
currentState: dashboardState,
panelReferences,
});
},
runInteractiveSave: async () => {
trackOverlayApi.clearOverlays();
const saveResult = await openSaveModal({
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/dashboard/public/dashboard_api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { PublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';
import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session';
import { LocatorPublic } from '@kbn/share-plugin/common';
import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server';
import { DashboardPanelMap, DashboardPanelState } from '../../common';
import type { DashboardOptions } from '../../server/content_management';
import type { DashboardAttributes, DashboardOptions } from '../../server/content_management';
import {
LoadDashboardReturn,
SaveDashboardReturn,
Expand Down Expand Up @@ -153,6 +154,10 @@ export type DashboardApi = CanExpandPanels &
focusedPanelId$: PublishingSubject<string | undefined>;
forceRefresh: () => void;
getSettings: () => DashboardStateFromSettingsFlyout;
getSerializedState: () => Promise<{
attributes: DashboardAttributes;
references: SavedObjectReference[];
}>;
getDashboardPanelFromId: (id: string) => DashboardPanelState;
hasOverlays$: PublishingSubject<boolean>;
hasUnsavedChanges$: PublishingSubject<boolean>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
findDashboardsByIds,
searchDashboards,
} from './lib/find_dashboards';
import { getDashboardState } from './lib/get_dashboard_state';
import { loadDashboardState } from './lib/load_dashboard_state';
import { saveDashboardState } from './lib/save_dashboard_state';
import { updateDashboardMeta } from './lib/update_dashboard_meta';
Expand All @@ -32,6 +33,7 @@ export const getDashboardContentManagementService = () => {
return {
loadDashboardState,
saveDashboardState,
getDashboardState,
findDashboards: {
search: searchDashboards,
findById: findDashboardById,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 { dataService, embeddableService, savedObjectsTaggingService } from '../../kibana_services';
import { getSampleDashboardState } from '../../../mocks';
import { DashboardState } from '../../../dashboard_api/types';
import { getDashboardState } from './get_dashboard_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),
},
});
}

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

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

expect(result.attributes.panels).toEqual([]);
expect(result.references).toEqual([]);
});

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

expect(result.attributes.panels).toEqual(
expect.arrayContaining([
expect.objectContaining({
panelIndex: expect.not.stringMatching('oldPanelId'),
type: 'visualization',
}),
])
);
expect(result.references).toEqual(
expect.arrayContaining([
{
name: expect.not.stringMatching(/^oldPanelId:/),
id: 'bizzbuzz',
type: 'index-pattern',
},
])
);
});

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

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

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

expect(result.references).toEqual(panelReferences);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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 { isFilterPinned } from '@kbn/es-query';
import type { SavedObjectReference } from '@kbn/core/server';
import { RefreshInterval, extractSearchSourceReferences } from '@kbn/data-plugin/public';

import {
convertPanelMapToPanelsArray,
extractReferences,
generateNewPanelIds,
} from '../../../../common';
import type { DashboardAttributes } from '../../../../server';

import { convertDashboardVersionToNumber } from './dashboard_versioning';
import { DashboardSearchSource, GetDashboardStateProps } from '../types';
import { dataService, embeddableService, savedObjectsTaggingService } from '../../kibana_services';
import { LATEST_DASHBOARD_CONTAINER_VERSION } from '../../../dashboard_container';

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 getDashboardState = async ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this part of content management service instead of just a function that can be imported where needed? Does it need to be part of content management service?

Copy link
Member Author

Choose a reason for hiding this comment

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

It does not need to be on content management service. I moved the module into the dashboard_api directory in ed68550.

controlGroupReferences,
generateNewIds,
currentState,
panelReferences,
}: GetDashboardStateProps) => {
const {
search: dataSearchService,
query: {
timefilter: { timefilter },
},
} = dataService;

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

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

let { panels } = currentState;
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, searchSourceReferences } = await (async () => {
const searchSourceFields = await dataSearchService.searchSource.create();
searchSourceFields.setField(
'filter', // save only unpinned filters
filters.filter((filter) => !isFilterPinned(filter))
);
searchSourceFields.setField('query', query);

const rawSearchSourceFields = searchSourceFields.getSerializedFields();
const [fields, references] = extractSearchSourceReferences(rawSearchSourceFields) as [
DashboardSearchSource,
SavedObjectReference[]
];
return { searchSourceReferences: references, searchSource: fields };
})();

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 ?? []),
];
return { attributes, references: allReferences };
};
Loading