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
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 { getDashboardState } from './get_dashboard_state';

export function getDashboardApi({
creationOptions,
Expand Down Expand Up @@ -113,9 +114,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 @@ -133,6 +136,7 @@ export function getDashboardApi({
dashboardState,
controlGroupReferences,
panelReferences,
searchSourceReferences,
};
}

Expand Down Expand Up @@ -171,6 +175,7 @@ export function getDashboardApi({
unifiedSearchManager.internalApi.controlGroupReload$,
unifiedSearchManager.internalApi.panelsReload$
).pipe(debounceTime(0)),
getSerializedState: () => getDashboardState(getState()),
runInteractiveSave: async () => {
trackOverlayApi.clearOverlays();
const saveResult = await openSaveModal({
Expand Down Expand Up @@ -200,11 +205,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
128 changes: 128 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/get_dashboard_state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 '../services/kibana_services';
import { getSampleDashboardState } from '../mocks';
import { DashboardState } from './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 dashboardState = getSampleDashboardState();
const result = await getDashboardState({
controlGroupReferences: [],
generateNewIds: false,
dashboardState,
panelReferences: [],
});

expect(result.attributes.panels).toEqual([]);
Copy link
Contributor

Choose a reason for hiding this comment

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

How about asserting the entire result.attributes

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

it('should generate new IDs for panels and references when generateNewIds is true', async () => {
const dashboardState = {
...getSampleDashboardState(),
panels: { oldPanelId: { type: 'visualization' } },
} as unknown as DashboardState;
Copy link
Contributor

@nreese nreese Dec 17, 2024

Choose a reason for hiding this comment

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

Can you move the cast from the entire dashboard state to just panel

const dashboardState = {
      ...getSampleDashboardState(),
      panels: { oldPanelId: { type: 'visualization' } as DashboardPanelState },
    };

const result = await getDashboardState({
controlGroupReferences: [],
generateNewIds: true,
dashboardState,
panelReferences: [
{
name: 'oldPanelId:indexpattern_foobar',
type: 'index-pattern',
id: 'bizzbuzz',
},
],
});

expect(result.attributes.panels).toEqual(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't you mock v4 so you know the new id. Then you can assert attributes.panels has the right shape.

jest.mock('uuid', () => ({
  v4: jest.fn().mockReturnValue('12345'),
}));
expect(result.attributes.panels).toEqual({ ...expectedValue});

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 dashboardState = getSampleDashboardState();
const controlGroupReferences = [
{ name: 'control1:indexpattern', type: 'index-pattern', id: 'foobar' },
];
const result = await getDashboardState({
controlGroupReferences,
generateNewIds: false,
dashboardState,
panelReferences: [],
});

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

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

expect(result.references).toEqual(panelReferences);
});
});
143 changes: 143 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/get_dashboard_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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 { convertPanelMapToPanelsArray, extractReferences, generateNewPanelIds } from '../../common';
import type { DashboardAttributes } from '../../server';

import { convertDashboardVersionToNumber } from '../services/dashboard_content_management_service/lib/dashboard_versioning';
import { GetDashboardStateProps } from '../services/dashboard_content_management_service/types';
import {
dataService,
embeddableService,
savedObjectsTaggingService,
} from '../services/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 = ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be renamed to getSerializedState (and rename file to get_serialized_state)? That will avoid ambiguity of what state this is returning.

controlGroupReferences,
generateNewIds,
dashboardState,
panelReferences,
searchSourceReferences,
}: GetDashboardStateProps) => {
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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export async function openSaveModal({
controlGroupReferences,
panelReferences,
saveOptions,
currentState: dashboardStateToSave,
dashboardState: dashboardStateToSave,
Copy link
Contributor

@nreese nreese Dec 17, 2024

Choose a reason for hiding this comment

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

searchSourceReferences is not passed to saveDashboardState call

lastSavedId,
});

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: () => {
attributes: DashboardAttributes;
references: SavedObjectReference[];
};
getDashboardPanelFromId: (id: string) => DashboardPanelState;
hasOverlays$: PublishingSubject<boolean>;
hasUnsavedChanges$: PublishingSubject<boolean>;
Expand Down
Loading