diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts index be2cedf889e85..c8c988d5c461e 100644 --- a/src/plugins/dashboard/common/index.ts +++ b/src/plugins/dashboard/common/index.ts @@ -32,6 +32,7 @@ export { prefixReferencesFromPanel } from './dashboard_container/persistable_sta export { convertPanelsArrayToPanelMap, convertPanelMapToPanelsArray, + generateNewPanelIds, } from './lib/dashboard_panel_converters'; export const UI_SETTINGS = { diff --git a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts index 5fcb6522b0152..cf1dd0e949d4c 100644 --- a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -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, @@ -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, }; @@ -130,6 +133,7 @@ export function getDashboardApi({ dashboardState, controlGroupReferences, panelReferences, + searchSourceReferences, }; } @@ -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({ @@ -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, }); diff --git a/src/plugins/dashboard/public/dashboard_api/get_serialized_state.test.ts b/src/plugins/dashboard/public/dashboard_api/get_serialized_state.test.ts new file mode 100644 index 0000000000000..9cae8584e7f44 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/get_serialized_state.test.ts @@ -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); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_api/get_serialized_state.ts b/src/plugins/dashboard/public/dashboard_api/get_serialized_state.ts new file mode 100644 index 0000000000000..5c0377adf175d --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/get_serialized_state.ts @@ -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 }; +}; diff --git a/src/plugins/dashboard/public/dashboard_api/open_save_modal.tsx b/src/plugins/dashboard/public/dashboard_api/open_save_modal.tsx index e5b2676d7198f..567fd1dcf98f6 100644 --- a/src/plugins/dashboard/public/dashboard_api/open_save_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_api/open_save_modal.tsx @@ -32,6 +32,7 @@ export async function openSaveModal({ isManaged, lastSavedId, panelReferences, + searchSourceReferences, viewMode, }: { controlGroupReferences?: Reference[]; @@ -39,6 +40,7 @@ export async function openSaveModal({ isManaged: boolean; lastSavedId: string | undefined; panelReferences: Reference[]; + searchSourceReferences: Reference[]; viewMode: ViewMode; }) { if (viewMode === 'edit' && isManaged) { @@ -101,8 +103,9 @@ export async function openSaveModal({ const saveResult = await dashboardContentManagementService.saveDashboardState({ controlGroupReferences, panelReferences, + searchSourceReferences, saveOptions, - currentState: dashboardStateToSave, + dashboardState: dashboardStateToSave, lastSavedId, }); diff --git a/src/plugins/dashboard/public/dashboard_api/types.ts b/src/plugins/dashboard/public/dashboard_api/types.ts index 54b540d575695..73e7e9422641f 100644 --- a/src/plugins/dashboard/public/dashboard_api/types.ts +++ b/src/plugins/dashboard/public/dashboard_api/types.ts @@ -53,6 +53,7 @@ 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 { DashboardAttributes, DashboardOptions } from '../../server/content_management'; import { @@ -146,6 +147,10 @@ export type DashboardApi = CanExpandPanels & focusedPanelId$: PublishingSubject; forceRefresh: () => void; getSettings: () => DashboardSettings; + getSerializedState: () => { + attributes: DashboardAttributes; + references: SavedObjectReference[]; + }; getDashboardPanelFromId: (id: string) => DashboardPanelState; hasOverlays$: PublishingSubject; hasUnsavedChanges$: PublishingSubject; diff --git a/src/plugins/dashboard/public/dashboard_api/unified_search_manager.ts b/src/plugins/dashboard/public/dashboard_api/unified_search_manager.ts index 9d39961778a91..acc6d0569d2db 100644 --- a/src/plugins/dashboard/public/dashboard_api/unified_search_manager.ts +++ b/src/plugins/dashboard/public/dashboard_api/unified_search_manager.ts @@ -33,10 +33,12 @@ import fastIsEqual from 'fast-deep-equal'; import { PublishingSubject, StateComparators } from '@kbn/presentation-publishing'; import { ControlGroupApi } from '@kbn/controls-plugin/public'; import { cloneDeep } from 'lodash'; +import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; import { GlobalQueryStateFromUrl, RefreshInterval, connectToQueryState, + extractSearchSourceReferences, syncGlobalQueryStateWithUrl, } from '@kbn/data-plugin/public'; import moment, { Moment } from 'moment'; @@ -324,16 +326,30 @@ export function initializeUnifiedSearchManager( setAndSyncTimeRange(lastSavedState.timeRange); } }, - getState: (): Pick< - DashboardState, - 'filters' | 'query' | 'refreshInterval' | 'timeRange' | 'timeRestore' - > => ({ - filters: unifiedSearchFilters$.value ?? DEFAULT_DASHBOARD_INPUT.filters, - query: query$.value ?? DEFAULT_DASHBOARD_INPUT.query, - refreshInterval: refreshInterval$.value, - timeRange: timeRange$.value, - timeRestore: timeRestore$.value ?? DEFAULT_DASHBOARD_INPUT.timeRestore, - }), + getState: (): { + state: Pick< + DashboardState, + 'filters' | 'query' | 'refreshInterval' | 'timeRange' | 'timeRestore' + >; + references: SavedObjectReference[]; + } => { + // pinned filters are not serialized when saving the dashboard + const serializableFilters = unifiedSearchFilters$.value?.filter((f) => !isFilterPinned(f)); + const [{ filter, query }, references] = extractSearchSourceReferences({ + filter: serializableFilters, + query: query$.value, + }); + return { + state: { + filters: filter ?? DEFAULT_DASHBOARD_INPUT.filters, + query: (query as Query) ?? DEFAULT_DASHBOARD_INPUT.query, + refreshInterval: refreshInterval$.value, + timeRange: timeRange$.value, + timeRestore: timeRestore$.value ?? DEFAULT_DASHBOARD_INPUT.timeRestore, + }, + references, + }; + }, }, cleanup: () => { controlGroupSubscriptions.unsubscribe(); diff --git a/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts b/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts index a1b18aca3aca0..1bb1edaac96ac 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts @@ -47,7 +47,7 @@ describe('Save dashboard state', () => { it('should save the dashboard using the same ID', async () => { const result = await saveDashboardState({ - currentState: { + dashboardState: { ...getSampleDashboardState(), title: 'BOO', } as unknown as DashboardContainerInput, @@ -68,7 +68,7 @@ describe('Save dashboard state', () => { it('should save the dashboard using a new id, and return redirect required', async () => { const result = await saveDashboardState({ - currentState: { + dashboardState: { ...getSampleDashboardState(), title: 'BooToo', } as unknown as DashboardContainerInput, @@ -92,7 +92,7 @@ describe('Save dashboard state', () => { it('should generate new panel IDs for dashboard panels when save as copy is true', async () => { const result = await saveDashboardState({ - currentState: { + dashboardState: { ...getSampleDashboardState(), title: 'BooThree', panels: { aVerySpecialVeryUniqueId: { type: 'boop' } }, @@ -118,7 +118,7 @@ describe('Save dashboard state', () => { it('should update prefixes on references when save as copy is true', async () => { const result = await saveDashboardState({ - currentState: { + dashboardState: { ...getSampleDashboardState(), title: 'BooFour', panels: { idOne: { type: 'boop' } }, @@ -146,7 +146,7 @@ describe('Save dashboard state', () => { it('should return an error when the save fails.', async () => { contentManagementService.client.create = jest.fn().mockRejectedValue('Whoops'); const result = await saveDashboardState({ - currentState: { + dashboardState: { ...getSampleDashboardState(), title: 'BooThree', panels: { idOne: { type: 'boop' } }, diff --git a/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts b/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts index 58492f51f4d36..5c14732ed939b 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts @@ -7,169 +7,37 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { pick } from 'lodash'; -import moment, { Moment } from 'moment'; - -import { extractSearchSourceReferences, RefreshInterval } from '@kbn/data-plugin/public'; -import { isFilterPinned } from '@kbn/es-query'; - -import type { SavedObjectReference } from '@kbn/core/server'; import { getDashboardContentManagementCache } from '..'; -import { convertPanelMapToPanelsArray, extractReferences } from '../../../../common'; import type { - DashboardAttributes, DashboardCreateIn, DashboardCreateOut, DashboardUpdateIn, DashboardUpdateOut, } from '../../../../server/content_management'; -import { generateNewPanelIds } from '../../../../common/lib/dashboard_panel_converters'; import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants'; -import { LATEST_DASHBOARD_CONTAINER_VERSION } from '../../../dashboard_container'; import { dashboardSaveToastStrings } from '../../../dashboard_container/_dashboard_container_strings'; import { getDashboardBackupService } from '../../dashboard_backup_service'; -import { - contentManagementService, - coreServices, - dataService, - embeddableService, - savedObjectsTaggingService, -} from '../../kibana_services'; -import { DashboardSearchSource, SaveDashboardProps, SaveDashboardReturn } from '../types'; -import { convertDashboardVersionToNumber } from './dashboard_versioning'; - -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; - } -}; +import { contentManagementService, coreServices } from '../../kibana_services'; +import { SaveDashboardProps, SaveDashboardReturn } from '../types'; +import { getSerializedState } from '../../../dashboard_api/get_serialized_state'; export const saveDashboardState = async ({ controlGroupReferences, lastSavedId, saveOptions, - currentState, + dashboardState, panelReferences, + searchSourceReferences, }: SaveDashboardProps): Promise => { - const { - search: dataSearchService, - query: { - timefilter: { timefilter }, - }, - } = dataService; const dashboardContentManagementCache = getDashboardContentManagementCache(); - const { - tags, - query, - title, - filters, - timeRestore, - description, - - // Dashboard options - useMargins, - syncColors, - syncCursor, - syncTooltips, - hidePanelTitles, - controlGroupInput, - } = currentState; - - let { panels } = currentState; - let prefixedPanelReferences = panelReferences; - if (saveOptions.saveAsCopy) { - 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 ?? []), - ]; + const { attributes, references } = getSerializedState({ + controlGroupReferences, + generateNewIds: saveOptions.saveAsCopy, + dashboardState, + panelReferences, + searchSourceReferences, + }); /** * Save the saved object using the content management @@ -183,7 +51,7 @@ export const saveDashboardState = async ({ contentTypeId: DASHBOARD_CONTENT_ID, data: attributes, options: { - references: allReferences, + references, /** perform a "full" update instead, where the provided attributes will fully replace the existing ones */ mergeAttributes: false, }, @@ -192,14 +60,14 @@ export const saveDashboardState = async ({ contentTypeId: DASHBOARD_CONTENT_ID, data: attributes, options: { - references: allReferences, + references, }, }); const newId = result.item.id; if (newId) { coreServices.notifications.toasts.addSuccess({ - title: dashboardSaveToastStrings.getSuccessString(currentState.title), + title: dashboardSaveToastStrings.getSuccessString(dashboardState.title), className: 'eui-textBreakWord', 'data-test-subj': 'saveDashboardSuccess', }); @@ -209,15 +77,15 @@ export const saveDashboardState = async ({ */ if (newId !== lastSavedId) { getDashboardBackupService().clearState(lastSavedId); - return { redirectRequired: true, id: newId, references: allReferences }; + return { redirectRequired: true, id: newId, references }; } else { dashboardContentManagementCache.deleteDashboard(newId); // something changed in an existing dashboard, so delete it from the cache so that it can be re-fetched } } - return { id: newId, references: allReferences }; + return { id: newId, references }; } catch (error) { coreServices.notifications.toasts.addDanger({ - title: dashboardSaveToastStrings.getFailureString(currentState.title, error.message), + title: dashboardSaveToastStrings.getFailureString(dashboardState.title, error.message), 'data-test-subj': 'saveDashboardFailure', }); return { error }; diff --git a/src/plugins/dashboard/public/services/dashboard_content_management_service/types.ts b/src/plugins/dashboard/public/services/dashboard_content_management_service/types.ts index 3c0c37afc0cd6..0c22aa03010c2 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management_service/types.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management_service/types.ts @@ -81,12 +81,18 @@ export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolea export interface SaveDashboardProps { controlGroupReferences?: Reference[]; - currentState: DashboardState; + dashboardState: DashboardState; saveOptions: SavedDashboardSaveOpts; panelReferences?: Reference[]; + searchSourceReferences?: Reference[]; lastSavedId?: string; } +export interface GetDashboardStateReturn { + attributes: DashboardAttributes; + references: Reference[]; +} + export interface SaveDashboardReturn { id?: string; error?: string;