diff --git a/src/platform/plugins/shared/dashboard/common/content_management/constants.ts b/src/platform/plugins/shared/dashboard/common/content_management/constants.ts index c29a765d7697d..18691a3024da6 100644 --- a/src/platform/plugins/shared/dashboard/common/content_management/constants.ts +++ b/src/platform/plugins/shared/dashboard/common/content_management/constants.ts @@ -8,6 +8,7 @@ */ export const LATEST_VERSION = 1; +export const DASHBOARD_API_VERSION = String(LATEST_VERSION); export const CONTENT_ID = 'dashboard'; diff --git a/src/platform/plugins/shared/dashboard/jest_setup.ts b/src/platform/plugins/shared/dashboard/jest_setup.ts index a86dc47ab043d..a1f15dfeb3a31 100644 --- a/src/platform/plugins/shared/dashboard/jest_setup.ts +++ b/src/platform/plugins/shared/dashboard/jest_setup.ts @@ -16,8 +16,6 @@ */ import { mockDashboardBackupService, - mockDashboardContentManagementCache, - mockDashboardContentManagementService, setStubKibanaServices, setStubLogger, } from './public/services/mocks'; @@ -26,14 +24,6 @@ setStubLogger(); // Start the kibana services with stubs setStubKibanaServices(); -// Mock the dashboard services -jest.mock('./public/services/dashboard_content_management_service', () => { - return { - getDashboardContentManagementCache: () => mockDashboardContentManagementCache, - getDashboardContentManagementService: () => mockDashboardContentManagementService, - }; -}); - jest.mock('./public/services/dashboard_backup_service', () => { return { getDashboardBackupService: () => mockDashboardBackupService, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index ec5c435c549c9..c47cba894b2ec 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -15,8 +15,6 @@ import { CONTROLS_GROUP_TYPE } from '@kbn/controls-constants'; import { DASHBOARD_APP_ID } from '../../common/constants'; import { getReferencesForControls, getReferencesForPanelId } from '../../common'; import type { DashboardState } from '../../common/types'; -import { getDashboardContentManagementService } from '../services/dashboard_content_management_service'; -import type { LoadDashboardReturn } from '../services/dashboard_content_management_service/types'; import { CONTROL_GROUP_EMBEDDABLE_ID, initializeControlGroupManager, @@ -37,6 +35,8 @@ import { initializeUnifiedSearchManager } from './unified_search_manager'; import { initializeUnsavedChangesManager } from './unsaved_changes_manager'; import { initializeViewModeManager } from './view_mode_manager'; import { mergeControlGroupStates } from './merge_control_group_states'; +import type { DashboardAPIGetOut } from '../../server/content_management'; +import { saveDashboard } from './save_modal/save_dashboard'; export function getDashboardApi({ creationOptions, @@ -48,15 +48,19 @@ export function getDashboardApi({ creationOptions?: DashboardCreationOptions; incomingEmbeddables?: EmbeddablePackageState[] | undefined; initialState: DashboardState; - savedObjectResult?: LoadDashboardReturn; + savedObjectResult?: DashboardAPIGetOut; savedObjectId?: string; }) { const fullScreenMode$ = new BehaviorSubject(creationOptions?.fullScreenMode ?? false); - const isManaged = savedObjectResult?.managed ?? false; + const isManaged = savedObjectResult?.meta.managed ?? false; const savedObjectId$ = new BehaviorSubject(savedObjectId); const dashboardContainerRef$ = new BehaviorSubject(null); - const viewModeManager = initializeViewModeManager(incomingEmbeddables, savedObjectResult); + const viewModeManager = initializeViewModeManager({ + incomingEmbeddables, + isManaged, + savedObjectId, + }); const trackPanel = initializeTrackPanel(async (id: string) => { await layoutManager.api.getChildApi(id); }, dashboardContainerRef$); @@ -107,7 +111,7 @@ export function getDashboardApi({ viewMode$: viewModeManager.api.viewMode$, storeUnsavedChanges: creationOptions?.useSessionStorageIntegration, controlGroupManager, - lastSavedState: savedObjectResult?.dashboardInput ?? DEFAULT_DASHBOARD_STATE, + lastSavedState: savedObjectResult?.data ?? DEFAULT_DASHBOARD_STATE, layoutManager, savedObjectId$, settingsManager, @@ -209,7 +213,7 @@ export function getDashboardApi({ runQuickSave: async () => { if (isManaged) return; const { dashboardState, references } = getState(); - const saveResult = await getDashboardContentManagementService().saveDashboardState({ + const saveResult = await saveDashboard({ dashboardState, references, saveOptions: {}, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts index 8eba52d11a34f..c702c3f57e692 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts @@ -25,6 +25,18 @@ jest.mock('@kbn/content-management-content-insights-public', () => { }; }); +jest.mock('../../dashboard_client', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const defaultState = require('../default_dashboard_state'); + return { + dashboardClient: { + get: jest.fn().mockResolvedValue({ + data: { ...defaultState.DEFAULT_DASHBOARD_STATE }, + }), + }, + }; +}); + const lastSavedQuery = { query: 'memory:>220000', language: 'kuery' }; describe('loadDashboardApi', () => { @@ -39,16 +51,6 @@ describe('loadDashboardApi', () => { internalApi: {}, }); - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('../../services/dashboard_content_management_service').getDashboardContentManagementService = - () => ({ - loadDashboardState: () => ({ - dashboardFound: true, - dashboardInput: DEFAULT_DASHBOARD_STATE, - references: [], - }), - }); - // eslint-disable-next-line @typescript-eslint/no-var-requires require('../../services/dashboard_backup_service').getDashboardBackupService = () => ({ getState: () => ({ @@ -71,10 +73,7 @@ describe('loadDashboardApi', () => { }); expect(getDashboardApiMock).toHaveBeenCalled(); // @ts-ignore - expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual({ - ...DEFAULT_DASHBOARD_STATE, - references: [], - }); + expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual(DEFAULT_DASHBOARD_STATE); }); test('should overwrite saved object state with unsaved state', async () => { @@ -88,7 +87,6 @@ describe('loadDashboardApi', () => { // @ts-ignore expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual({ ...DEFAULT_DASHBOARD_STATE, - references: [], query: lastSavedQuery, }); }); @@ -109,7 +107,6 @@ describe('loadDashboardApi', () => { // @ts-ignore expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual({ ...DEFAULT_DASHBOARD_STATE, - references: [], query: queryFromUrl, }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts index 12c107f1cbd2a..e73567e8fe582 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts @@ -8,15 +8,15 @@ */ import { ContentInsightsClient } from '@kbn/content-management-content-insights-public'; -import type { DashboardState } from '../../../common'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import { coreServices } from '../../services/kibana_services'; import { logger } from '../../services/logger'; import { getDashboardApi } from '../get_dashboard_api'; import { startQueryPerformanceTracking } from '../performance/query_performance_tracking'; import type { DashboardCreationOptions } from '../types'; import { transformPanels } from './transform_panels'; +import { dashboardClient } from '../../dashboard_client'; +import { DEFAULT_DASHBOARD_STATE } from '../default_dashboard_state'; export async function loadDashboardApi({ getCreationOptions, @@ -28,13 +28,8 @@ export async function loadDashboardApi({ const creationStartTime = performance.now(); const creationOptions = await getCreationOptions?.(); const incomingEmbeddables = creationOptions?.getIncomingEmbeddables?.(); - const savedObjectResult = await getDashboardContentManagementService().loadDashboardState({ - id: savedObjectId, - }); + const savedObjectResult = savedObjectId ? await dashboardClient.get(savedObjectId) : undefined; - // -------------------------------------------------------------------------------------- - // Run validation. - // -------------------------------------------------------------------------------------- const validationResult = savedObjectResult && creationOptions?.validateLoadedSavedObject?.(savedObjectResult); if (validationResult === 'invalid') { @@ -44,25 +39,10 @@ export async function loadDashboardApi({ return; } - // -------------------------------------------------------------------------------------- - // Combine saved object state and session storage state - // -------------------------------------------------------------------------------------- - const sessionStorageInput = ((): Partial | undefined => { - if (!creationOptions?.useSessionStorageIntegration) return; - return getDashboardBackupService().getState(savedObjectResult.dashboardId); - })(); - - const combinedSessionState: DashboardState = { - ...(savedObjectResult?.dashboardInput ?? {}), - ...sessionStorageInput, - }; - combinedSessionState.references = sessionStorageInput?.references?.length - ? sessionStorageInput?.references - : savedObjectResult?.references; + const unsavedChanges = creationOptions?.useSessionStorageIntegration + ? getDashboardBackupService().getState(savedObjectId) + : undefined; - // -------------------------------------------------------------------------------------- - // Combine state with overrides. - // -------------------------------------------------------------------------------------- const { viewMode, ...overrideState } = creationOptions?.getInitialInput?.() ?? {}; if (overrideState.panels) { overrideState.panels = await transformPanels(overrideState.panels, overrideState.references); @@ -73,14 +53,13 @@ export async function loadDashboardApi({ getDashboardBackupService().storeViewMode(viewMode); } - // -------------------------------------------------------------------------------------- - // get dashboard Api - // -------------------------------------------------------------------------------------- const { api, cleanup, internalApi } = getDashboardApi({ creationOptions, incomingEmbeddables, initialState: { - ...combinedSessionState, + ...DEFAULT_DASHBOARD_STATE, + ...savedObjectResult?.data, + ...unsavedChanges, ...overrideState, }, savedObjectResult, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/open_save_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/open_save_modal.tsx index a959bc109342e..15c52036d8f6d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/open_save_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/open_save_modal.tsx @@ -13,14 +13,14 @@ import type { Reference } from '@kbn/content-management-utils'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { showSaveModal } from '@kbn/saved-objects-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { SaveDashboardReturn } from '../../services/dashboard_content_management_service/types'; -import type { DashboardSaveOptions } from './types'; +import type { DashboardSaveOptions, SaveDashboardReturn } from './types'; import { coreServices, savedObjectsTaggingService } from '../../services/kibana_services'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import type { DashboardState } from '../../../common'; import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../utils/telemetry_constants'; import { extractTitleAndCount } from '../../utils/extract_title_and_count'; import { DashboardSaveModal } from './save_modal'; +import { checkForDuplicateDashboardTitle } from '../../dashboard_client'; +import { saveDashboard } from './save_dashboard'; /** * @description exclusively for user directed dashboard save actions, also @@ -51,7 +51,6 @@ export async function openSaveModal({ if (viewMode === 'edit' && isManaged) { return undefined; } - const dashboardContentManagementService = getDashboardContentManagementService(); const saveAsTitle = lastSavedId ? await getSaveAsTitle(title) : title; return new Promise<(SaveDashboardReturn & { savedState: DashboardState }) | undefined>( (resolve) => { @@ -73,7 +72,7 @@ export async function openSaveModal({ try { if ( - !(await dashboardContentManagementService.checkForDuplicateDashboardTitle({ + !(await checkForDuplicateDashboardTitle({ title: newTitle, onTitleDuplicate, lastSavedTitle: title, @@ -99,7 +98,7 @@ export async function openSaveModal({ const beforeAddTime = window.performance.now(); - const saveResult = await dashboardContentManagementService.saveDashboardState({ + const saveResult = await saveDashboard({ references, saveOptions, dashboardState: dashboardStateToSave, @@ -178,7 +177,7 @@ function generateDashboardNotSavedToast(title: string, errorMessage: any) { async function getSaveAsTitle(title: string) { const [baseTitle, baseCount] = extractTitleAndCount(title); let saveAsTitle = `${baseTitle} (${baseCount + 1})`; - await getDashboardContentManagementService().checkForDuplicateDashboardTitle({ + await checkForDuplicateDashboardTitle({ title: saveAsTitle, lastSavedTitle: title, copyOnSave: true, diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.test.ts similarity index 53% rename from src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.test.ts index 9e09e8d09148b..81249b78ed1cf 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.test.ts @@ -7,12 +7,24 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getSampleDashboardState } from '../../../mocks'; -import { contentManagementService, coreServices } from '../../kibana_services'; -import { saveDashboardState } from './save_dashboard_state'; -import type { DashboardPanel } from '../../../../server'; +import { getSampleDashboardState } from '../../mocks'; +import { coreServices } from '../../services/kibana_services'; +import { saveDashboard } from './save_dashboard'; +import type { DashboardState } from '../../../server'; +import type { Reference } from '@kbn/content-management-utils'; -contentManagementService.client.create = jest.fn().mockImplementation(({ options }) => { +const mockCreate = jest.fn(); +const mockUpdate = jest.fn(); +jest.mock('../../dashboard_client', () => ({ + dashboardClient: { + create: (dashboardState: DashboardState, references: Reference[]) => + mockCreate(dashboardState, references), + update: (id: string, dashboardState: DashboardState, references: Reference[]) => + mockUpdate(id, dashboardState, references), + }, +})); + +/* contentManagementService.client.create = jest.fn().mockImplementation(({ options }) => { if (options.id === undefined) { return { item: { id: 'newlyGeneratedId' } }; } @@ -25,27 +37,29 @@ contentManagementService.client.update = jest.fn().mockImplementation(({ id }) = throw new Error('Update needs an id'); } return { item: { id } }; -}); +});*/ describe('Save dashboard state', () => { beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); it('should save the dashboard using the same ID', async () => { - const result = await saveDashboardState({ - dashboardState: { - ...getSampleDashboardState(), - title: 'BOO', - }, + mockUpdate.mockResolvedValue({ item: { id: 'Boogaloo' } }); + const dashboardState = { + ...getSampleDashboardState(), + title: 'BOO', + }; + const references: Reference[] = []; + const result = await saveDashboard({ + dashboardState, lastSavedId: 'Boogaloo', + references, saveOptions: {}, }); expect(result.id).toBe('Boogaloo'); - expect(contentManagementService.client.update).toHaveBeenCalledWith( - expect.objectContaining({ id: 'Boogaloo' }) - ); + expect(mockUpdate).toHaveBeenCalledWith('Boogaloo', dashboardState, references); expect(coreServices.notifications.toasts.addSuccess).toHaveBeenCalledWith({ title: `Dashboard 'BOO' was saved`, className: 'eui-textBreakWord', @@ -54,56 +68,32 @@ describe('Save dashboard state', () => { }); it('should save the dashboard using a new id, and return redirect required', async () => { - const result = await saveDashboardState({ + mockCreate.mockResolvedValue({ item: { id: 'newlyGeneratedId' } }); + const result = await saveDashboard({ dashboardState: { ...getSampleDashboardState(), title: 'BooToo', }, lastSavedId: 'Boogaloonie', + references: [], saveOptions: { saveAsCopy: true }, }); expect(result.id).toBe('newlyGeneratedId'); expect(result.redirectRequired).toBe(true); - expect(contentManagementService.client.create).toHaveBeenCalled(); + expect(mockCreate).toHaveBeenCalled(); expect(coreServices.notifications.toasts.addSuccess).toHaveBeenCalled(); }); - it('should generate new panel IDs for dashboard panels when save as copy is true', async () => { - const result = await saveDashboardState({ - dashboardState: { - ...getSampleDashboardState(), - title: 'BooThree', - panels: [{ type: 'boop', uid: 'idOne' } as DashboardPanel], - }, - lastSavedId: 'Boogatoonie', - saveOptions: { saveAsCopy: true }, - }); - - expect(result.id).toBe('newlyGeneratedId'); - - expect(contentManagementService.client.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - panels: expect.arrayContaining([ - expect.objectContaining({ - uid: expect.not.stringContaining('aVerySpecialVeryUniqueId'), - }), - ]), - }), - }) - ); - }); - it('should return an error when the save fails.', async () => { - contentManagementService.client.create = jest.fn().mockRejectedValue('Whoops'); - const result = await saveDashboardState({ + mockCreate.mockRejectedValue('Whoops'); + const result = await saveDashboard({ dashboardState: { ...getSampleDashboardState(), title: 'BooThree', - panels: [{ type: 'boop', uid: 'idOne' } as DashboardPanel], }, lastSavedId: 'Boogatoonie', + references: [], saveOptions: { saveAsCopy: true }, }); diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts similarity index 57% rename from src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts index 8af5bb2be1b3b..cb9401723ff5a 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/save_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts @@ -8,26 +8,17 @@ */ import { i18n } from '@kbn/i18n'; -import { getDashboardContentManagementCache } from '..'; -import type { - DashboardCreateIn, - DashboardCreateOut, - DashboardUpdateIn, - DashboardUpdateOut, -} from '../../../../server/content_management'; -import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; -import { getDashboardBackupService } from '../../dashboard_backup_service'; -import { contentManagementService, coreServices } from '../../kibana_services'; -import type { SaveDashboardProps, SaveDashboardReturn } from '../types'; +import { getDashboardBackupService } from '../../services/dashboard_backup_service'; +import { coreServices } from '../../services/kibana_services'; +import { dashboardClient } from '../../dashboard_client'; +import type { SaveDashboardProps, SaveDashboardReturn } from './types'; -export const saveDashboardState = async ({ +export const saveDashboard = async ({ lastSavedId, saveOptions, dashboardState, references, }: SaveDashboardProps): Promise => { - const dashboardContentManagementCache = getDashboardContentManagementCache(); - /** * Save the saved object using the content management */ @@ -35,23 +26,9 @@ export const saveDashboardState = async ({ try { const result = idToSaveTo - ? await contentManagementService.client.update({ - id: idToSaveTo, - contentTypeId: DASHBOARD_CONTENT_ID, - data: dashboardState, - options: { - references, - /** perform a "full" update instead, where the provided attributes will fully replace the existing ones */ - mergeAttributes: false, - }, - }) - : await contentManagementService.client.create({ - contentTypeId: DASHBOARD_CONTENT_ID, - data: dashboardState, - options: { - references, - }, - }); + ? await dashboardClient.update(idToSaveTo, dashboardState, references) + : await dashboardClient.create(dashboardState, references); + const newId = result.item.id; if (newId) { @@ -70,8 +47,6 @@ export const saveDashboardState = async ({ if (newId !== lastSavedId) { getDashboardBackupService().clearState(lastSavedId); 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 }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/types.ts index 187afe0234b28..71e2bd90c8fb7 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/types.ts @@ -7,6 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { Reference } from '@kbn/content-management-utils'; +import type { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public'; +import type { DashboardState } from '../../../common'; + export interface DashboardSaveOptions { newTitle: string; newTags?: string[]; @@ -16,3 +20,20 @@ export interface DashboardSaveOptions { onTitleDuplicate: () => void; isTitleDuplicateConfirmed: boolean; } + +export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolean }; + +export interface SaveDashboardProps { + dashboardState: DashboardState; + references: Reference[]; + saveOptions: SavedDashboardSaveOpts; + searchSourceReferences?: Reference[]; + lastSavedId?: string; +} + +export interface SaveDashboardReturn { + id?: string; + error?: string; + references?: Reference[]; + redirectRequired?: boolean; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts index 8cc592262f21e..6bef0347849cd 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts @@ -51,11 +51,8 @@ import type { ControlsGroupState } from '@kbn/controls-schemas'; import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { BehaviorSubject, Observable, Subject } from 'rxjs'; import type { DashboardLocatorParams } from '../../common'; -import type { DashboardState, GridData } from '../../server/content_management'; -import type { - LoadDashboardReturn, - SaveDashboardReturn, -} from '../services/dashboard_content_management_service/types'; +import type { DashboardAPIGetOut, DashboardState, GridData } from '../../server/content_management'; +import type { SaveDashboardReturn } from './save_modal/types'; import type { DashboardLayout } from './layout_manager/types'; import type { DashboardSettings } from './settings_manager'; @@ -87,7 +84,7 @@ export interface DashboardCreationOptions { useUnifiedSearchIntegration?: boolean; unifiedSearchSettings?: { kbnUrlStateStorage: IKbnUrlStateStorage }; - validateLoadedSavedObject?: (result: LoadDashboardReturn) => 'valid' | 'invalid' | 'redirected'; + validateLoadedSavedObject?: (result: DashboardAPIGetOut) => 'valid' | 'invalid' | 'redirected'; fullScreenMode?: boolean; isEmbeddedExternally?: boolean; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts index a93fd16910e76..ed80944b4a167 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts @@ -10,24 +10,28 @@ import type { EmbeddablePackageState } from '@kbn/embeddable-plugin/public'; import type { ViewMode } from '@kbn/presentation-publishing'; import { BehaviorSubject } from 'rxjs'; -import type { LoadDashboardReturn } from '../services/dashboard_content_management_service/types'; import { getDashboardBackupService } from '../services/dashboard_backup_service'; import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; -export function initializeViewModeManager( - incomingEmbeddables?: EmbeddablePackageState[], - savedObjectResult?: LoadDashboardReturn -) { +export function initializeViewModeManager({ + incomingEmbeddables, + isManaged, + savedObjectId, +}: { + incomingEmbeddables?: EmbeddablePackageState[]; + isManaged: boolean; + savedObjectId?: string; +}) { const dashboardBackupService = getDashboardBackupService(); function getInitialViewMode() { - if (savedObjectResult?.managed || !getDashboardCapabilities().showWriteControls) { + if (isManaged || !getDashboardCapabilities().showWriteControls) { return 'view'; } if ( incomingEmbeddables?.length || - savedObjectResult?.newDashboardCreated || - dashboardBackupService.dashboardHasUnsavedEdits(savedObjectResult?.dashboardId) + !Boolean(savedObjectId) || + dashboardBackupService.dashboardHasUnsavedEdits(savedObjectId) ) return 'edit'; @@ -38,7 +42,7 @@ export function initializeViewModeManager( function setViewMode(viewMode: ViewMode) { // block the Dashboard from entering edit mode if this Dashboard is managed. - if (savedObjectResult?.managed && viewMode?.toLowerCase() === 'edit') { + if (isManaged && viewMode?.toLowerCase() === 'edit') { return; } viewMode$.next(viewMode); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx index dd1b5ba31dd37..02260eb6c4b07 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx @@ -11,9 +11,9 @@ import { useCallback, useMemo, useState } from 'react'; import type { DashboardCreationOptions } from '../..'; import { createDashboardEditUrl } from '../../utils/urls'; -import type { LoadDashboardReturn } from '../../services/dashboard_content_management_service/types'; import { screenshotModeService, spacesService } from '../../services/kibana_services'; import { useDashboardMountContext } from './dashboard_mount_context'; +import type { DashboardAPIGetOut } from '../../../server/content_management'; export const useDashboardOutcomeValidation = () => { const [aliasId, setAliasId] = useState(); @@ -24,29 +24,19 @@ export const useDashboardOutcomeValidation = () => { const scopedHistory = getScopedHistory?.(); const validateOutcome: DashboardCreationOptions['validateLoadedSavedObject'] = useCallback( - ({ dashboardFound, resolveMeta, dashboardId }: LoadDashboardReturn) => { - if (!dashboardFound) { - return 'invalid'; - } - - if (resolveMeta && dashboardId) { - const { outcome: loadOutcome, aliasTargetId: alias, aliasPurpose } = resolveMeta; - /** - * Handle saved object resolve alias outcome by redirecting. - */ - if (loadOutcome === 'aliasMatch' && dashboardId && alias) { - const path = scopedHistory.location.hash.replace(dashboardId, alias); - if (screenshotModeService.isScreenshotMode()) { - scopedHistory.replace(path); // redirect without the toast when in screenshot mode. - } else { - spacesService?.ui.redirectLegacyUrl({ path, aliasPurpose }); - } - return 'redirected'; // redirected. Stop loading dashboard. + (result: DashboardAPIGetOut) => { + if (result.meta.outcome === 'aliasMatch' && result.meta.aliasTargetId) { + const path = scopedHistory.location.hash.replace(result.id, result.meta.aliasTargetId); + if (screenshotModeService.isScreenshotMode()) { + scopedHistory.replace(path); // redirect without the toast when in screenshot mode. + } else { + spacesService?.ui.redirectLegacyUrl({ path, aliasPurpose: result.meta.aliasPurpose }); } - setAliasId(alias); - setOutcome(loadOutcome); - setSavedObjectId(dashboardId); + return 'redirected'; // redirected. Stop loading dashboard. } + setAliasId(result.meta.aliasTargetId); + setOutcome(result.meta.outcome); + setSavedObjectId(result.id); return 'valid'; }, [scopedHistory] diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.test.tsx index e01f0b945c05b..3b36c509bea72 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.test.tsx @@ -27,11 +27,8 @@ jest.mock('../../dashboard_listing/dashboard_listing', () => { import { DashboardAppNoDataPage } from '../no_data/dashboard_app_no_data'; import { dataService } from '../../services/kibana_services'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; -const dashboardContentManagementService = getDashboardContentManagementService(); const mockIsDashboardAppInNoDataState = jest.fn().mockResolvedValue(false); - jest.mock('../no_data/dashboard_app_no_data', () => { const originalModule = jest.requireActual('../no_data/dashboard_app_no_data'); return { @@ -42,6 +39,13 @@ jest.mock('../no_data/dashboard_app_no_data', () => { }; }); +const mockFindByTitle = jest.fn(); +jest.mock('../../dashboard_client', () => ({ + findService: { + findByTitle: () => mockFindByTitle(), + }, +})); + const renderDashboardListingPage = (props: Partial = {}) => render( { }); test('When given a title that matches multiple dashboards, filter on the title', async () => { - (dashboardContentManagementService.findDashboards.findByTitle as jest.Mock).mockResolvedValue( - undefined - ); - + mockFindByTitle.mockResolvedValue(undefined); const redirectTo = jest.fn(); renderDashboardListingPage({ title: 'search by title', redirectTo }); @@ -95,7 +96,7 @@ test('When given a title that matches multiple dashboards, filter on the title', }); test('When given a title that matches one dashboard, redirect to dashboard', async () => { - (dashboardContentManagementService.findDashboards.findByTitle as jest.Mock).mockResolvedValue({ + mockFindByTitle.mockResolvedValue({ id: 'you_found_me', }); const redirectTo = jest.fn(); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx index 5caa71bb006a0..e96efef1cbac8 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx @@ -20,8 +20,8 @@ import { isDashboardAppInNoDataState, } from '../no_data/dashboard_app_no_data'; import { getDashboardListItemLink } from './get_dashboard_list_item_link'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import type { DashboardRedirect } from '../types'; +import { findService } from '../../dashboard_client'; export interface DashboardListingPageProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -74,16 +74,14 @@ export const DashboardListingPage = ({ kbnUrlStateStorage ); if (title) { - getDashboardContentManagementService() - .findDashboards.findByTitle(title) - .then((result) => { - if (!result) return; - redirectTo({ - destination: 'dashboard', - id: result.id, - useReplace: true, - }); + findService.findByTitle(title).then((result) => { + if (!result) return; + redirectTo({ + destination: 'dashboard', + id: result.id, + useReplace: true, }); + }); } return () => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx index acde5a1310d9e..d5ca8271ac1da 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx @@ -29,7 +29,7 @@ import { lensService, } from '../../services/kibana_services'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; +import { dashboardClient } from '../../dashboard_client'; export const DashboardAppNoDataPage = ({ onDataViewCreated, @@ -153,8 +153,8 @@ export const isDashboardAppInNoDataState = async () => { if (getDashboardBackupService().dashboardHasUnsavedEdits()) return false; // consider has data if there is at least one dashboard - const { total } = await getDashboardContentManagementService() - .findDashboards.search({ search: '', size: 1 }) + const { total } = await dashboardClient + .search({ search: '', size: 1 }) .catch(() => ({ total: 0 })); if (total > 0) return false; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index df3d505d88816..3316251a121a4 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -20,7 +20,7 @@ import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays'; import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; -import type { SaveDashboardReturn } from '../../services/dashboard_content_management_service/types'; +import type { SaveDashboardReturn } from '../../dashboard_api/save_modal/types'; import { coreServices, shareService, dataService } from '../../services/kibana_services'; import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities'; import { topNavStrings } from '../_dashboard_app_strings'; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/check_for_duplicate_dashboard_title.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/check_for_duplicate_dashboard_title.test.ts similarity index 95% rename from src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/check_for_duplicate_dashboard_title.test.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_client/check_for_duplicate_dashboard_title.test.ts index 47fd5e9d80439..74fc6fc0dd569 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/check_for_duplicate_dashboard_title.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/check_for_duplicate_dashboard_title.test.ts @@ -8,8 +8,8 @@ */ import { checkForDuplicateDashboardTitle } from './check_for_duplicate_dashboard_title'; -import { extractTitleAndCount } from '../../../utils/extract_title_and_count'; -import { contentManagementService } from '../../kibana_services'; +import { extractTitleAndCount } from '../utils/extract_title_and_count'; +import { contentManagementService } from '../services/kibana_services'; describe('checkForDuplicateDashboardTitle', () => { const newTitle = 'Shiny dashboard (1)'; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/check_for_duplicate_dashboard_title.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/check_for_duplicate_dashboard_title.ts similarity index 80% rename from src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/check_for_duplicate_dashboard_title.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_client/check_for_duplicate_dashboard_title.ts index 5fe726e63cc1b..0c26cd0919fa9 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/check_for_duplicate_dashboard_title.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/check_for_duplicate_dashboard_title.ts @@ -7,10 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DashboardSearchIn, DashboardSearchOut } from '../../../../server/content_management'; -import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; -import { extractTitleAndCount } from '../../../utils/extract_title_and_count'; -import { contentManagementService } from '../../kibana_services'; +import { extractTitleAndCount } from '../utils/extract_title_and_count'; +import { dashboardClient } from './dashboard_client'; export interface DashboardDuplicateTitleCheckProps { title: string; @@ -53,15 +51,9 @@ export async function checkForDuplicateDashboardTitle({ const [baseDashboardName] = extractTitleAndCount(title); - const { hits } = await contentManagementService.client.search< - DashboardSearchIn, - DashboardSearchOut - >({ - contentTypeId: DASHBOARD_CONTENT_ID, - query: { - text: `${baseDashboardName}*`, - limit: 20, - }, + const { hits } = await dashboardClient.search({ + search: baseDashboardName, + size: 20, options: { onlyTitle: true, }, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_client/dashboard_client.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/dashboard_client.ts new file mode 100644 index 0000000000000..27fca3297e752 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/dashboard_client.ts @@ -0,0 +1,121 @@ +/* + * 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 { LRUCache } from 'lru-cache'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; +import type { DeleteResult } from '@kbn/content-management-plugin/common'; +import type { Reference } from '@kbn/content-management-utils'; +import { CONTENT_ID, DASHBOARD_API_VERSION } from '../../common/content_management/constants'; +import type { + DashboardAPIGetOut, + DashboardCreateIn, + DashboardCreateOut, + DashboardSearchAPIResult, + DashboardSearchIn, + DashboardState, + DashboardUpdateIn, + DashboardUpdateOut, +} from '../../server/content_management'; +import { contentManagementService, coreServices } from '../services/kibana_services'; +import type { SearchDashboardsArgs } from './types'; +import { DASHBOARD_CONTENT_ID } from '../utils/telemetry_constants'; + +const CACHE_SIZE = 20; // only store a max of 20 dashboards +const CACHE_TTL = 1000 * 60 * 5; // time to live = 5 minutes + +const cache = new LRUCache({ + max: CACHE_SIZE, + ttl: CACHE_TTL, +}); + +export const dashboardClient = { + create: async (dashboardState: DashboardState, references: Reference[]) => { + // TODO replace with call to dashboard REST create endpoint + return contentManagementService.client.create({ + contentTypeId: DASHBOARD_CONTENT_ID, + data: dashboardState, + options: { + references, + }, + }); + }, + delete: async (id: string): Promise => { + cache.delete(id); + return coreServices.http.delete(`/api/dashboards/dashboard/${id}`, { + version: DASHBOARD_API_VERSION, + }); + }, + get: async (id: string): Promise => { + if (cache.has(id)) { + return cache.get(id)!; + } + + const result = await coreServices.http + .get(`/api/dashboards/dashboard/${id}`, { + version: DASHBOARD_API_VERSION, + }) + .catch((e) => { + if (e.response?.status === 404) { + throw new SavedObjectNotFound({ type: CONTENT_ID, id }); + } + const message = (e.body as { message?: string })?.message ?? e.message; + throw new Error(message); + }); + + if (result.meta.outcome !== 'aliasMatch') { + /** + * Only add the dashboard to the cache if it does not require a redirect - otherwise, the meta + * alias info gets cached and prevents the dashboard contents from being updated + */ + cache.set(id, result); + } + return result; + }, + search: async ({ hasNoReference, hasReference, options, search, size }: SearchDashboardsArgs) => { + // TODO replace with call to dashboard REST search endpoint + // https://github.com/elastic/kibana/issues/241211 + const { + hits, + pagination: { total }, + } = await contentManagementService.client.search({ + contentTypeId: CONTENT_ID, + query: { + text: search ? `${search}*` : undefined, + limit: size, + tags: { + included: (hasReference ?? []).map(({ id }) => id), + excluded: (hasNoReference ?? []).map(({ id }) => id), + }, + }, + options, + }); + return { + total, + hits, + }; + }, + update: async (id: string, dashboardState: DashboardState, references: Reference[]) => { + // TODO replace with call to dashboard REST update endpoint + const updateResponse = await contentManagementService.client.update< + DashboardUpdateIn, + DashboardUpdateOut + >({ + contentTypeId: DASHBOARD_CONTENT_ID, + id, + data: dashboardState, + options: { + /** perform a "full" update instead, where the provided attributes will fully replace the existing ones */ + mergeAttributes: false, + references, + }, + }); + cache.delete(id); + return updateResponse; + }, +}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_client/find_service.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/find_service.ts new file mode 100644 index 0000000000000..5aa9cea12d016 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/find_service.ts @@ -0,0 +1,50 @@ +/* + * 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 { asyncMap } from '@kbn/std'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; +import { dashboardClient } from './dashboard_client'; +import type { FindDashboardsByIdResponse } from './types'; + +export const findService = { + findById: async (id: string): Promise => { + try { + const result = await dashboardClient.get(id); + return { + id, + status: 'success', + attributes: result.data, + references: result.data.references ?? [], + }; + } catch (error) { + return { id, status: 'error', notFound: error instanceof SavedObjectNotFound, error }; + } + }, + findByIds: async (ids: string[]) => { + return asyncMap(ids, async (id) => { + return findService.findById(id); + }); + }, + findByTitle: async (title: string) => { + const { hits } = await dashboardClient.search({ + search: title, + size: 10, + options: { onlyTitle: true }, + }); + + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = hits.filter( + (hit) => hit.attributes.title.toLowerCase() === title.toLowerCase() + ); + if (matchingDashboards.length === 1) { + return { id: matchingDashboards[0].id }; + } + }, + search: dashboardClient.search, +}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts new file mode 100644 index 0000000000000..fd2eaf3595e28 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts @@ -0,0 +1,18 @@ +/* + * 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". + */ + +export { checkForDuplicateDashboardTitle } from './check_for_duplicate_dashboard_title'; +export { dashboardClient } from './dashboard_client'; +export { findService } from './find_service'; + +export type { + FindDashboardsByIdResponse, + FindDashboardsService, + SearchDashboardsResponse, +} from './types'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_client/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/types.ts new file mode 100644 index 0000000000000..e50d1c472060b --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/types.ts @@ -0,0 +1,51 @@ +/* + * 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 { Reference } from '@kbn/content-management-utils'; +import type { SavedObjectsFindOptionsReference } from '@kbn/core/public'; + +import type { + DashboardSearchOptions, + DashboardSearchAPIResult, + DashboardState, +} from '../../server/content_management'; + +export interface SearchDashboardsArgs { + options?: DashboardSearchOptions; + hasNoReference?: SavedObjectsFindOptionsReference[]; + hasReference?: SavedObjectsFindOptionsReference[]; + search: string; + size: number; +} + +export interface SearchDashboardsResponse { + total: number; + hits: DashboardSearchAPIResult['hits']; +} + +/** + * Types for Finding Dashboards + */ + +export type FindDashboardsByIdResponse = { id: string } & ( + | { status: 'success'; attributes: DashboardState; references: Reference[] } + | { status: 'error'; notFound: boolean; error: Error } +); + +export interface FindDashboardsService { + search: ( + props: Pick< + SearchDashboardsArgs, + 'hasReference' | 'hasNoReference' | 'search' | 'size' | 'options' + > + ) => Promise; + findById: (id: string) => Promise; + findByIds: (ids: string[]) => Promise; + findByTitle: (title: string) => Promise<{ id: string } | undefined>; +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx index ba845dd14d4cb..efd9ca1cf0d2a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx @@ -17,7 +17,6 @@ import { DASHBOARD_PANELS_UNSAVED_ID, getDashboardBackupService, } from '../services/dashboard_backup_service'; -import { getDashboardContentManagementService } from '../services/dashboard_content_management_service'; import { coreServices } from '../services/kibana_services'; import type { DashboardUnsavedListingProps } from './dashboard_unsaved_listing'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; @@ -33,26 +32,53 @@ const renderDashboardUnsavedListing = (props: Partial ({ + findService: { + findByIds: (ids: string[]) => mockFindByIds(ids), + }, +})); + describe('Unsaved listing', () => { const dashboardBackupService = getDashboardBackupService(); - const dashboardContentManagementService = getDashboardContentManagementService(); + + beforeEach(() => { + mockFindByIds.mockReset(); + mockFindByIds.mockResolvedValue([ + { + id: `dashboardUnsavedOne`, + status: 'success', + attributes: { + title: `Dashboard Unsaved One`, + }, + }, + { + id: `dashboardUnsavedTwo`, + status: 'success', + attributes: { + title: `Dashboard Unsaved Two`, + }, + }, + { + id: `dashboardUnsavedThree`, + status: 'success', + attributes: { + title: `Dashboard Unsaved Three`, + }, + }, + ]); + }); it('Gets information for each unsaved dashboard', async () => { renderDashboardUnsavedListing(); - await waitFor(() => - expect(dashboardContentManagementService.findDashboards.findByIds).toHaveBeenCalledTimes(1) - ); + await waitFor(() => expect(mockFindByIds).toHaveBeenCalledTimes(1)); }); it('Does not attempt to get newly created dashboard', async () => { renderDashboardUnsavedListing({ unsavedDashboardIds: ['dashboardUnsavedOne', DASHBOARD_PANELS_UNSAVED_ID], }); - await waitFor(() => - expect(dashboardContentManagementService.findDashboards.findByIds).toHaveBeenCalledWith([ - 'dashboardUnsavedOne', - ]) - ); + await waitFor(() => expect(mockFindByIds).toHaveBeenCalledWith(['dashboardUnsavedOne'])); }); it('Redirects to the requested dashboard in edit mode when continue editing clicked', async () => { @@ -84,7 +110,7 @@ describe('Unsaved listing', () => { }); it('removes unsaved changes from any dashboard which errors on fetch', async () => { - (dashboardContentManagementService.findDashboards.findByIds as jest.Mock).mockResolvedValue([ + mockFindByIds.mockResolvedValue([ { id: 'failCase1', status: 'error', diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx index 567b41bb9a536..8daee04e8224f 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx @@ -28,9 +28,9 @@ import { DASHBOARD_PANELS_UNSAVED_ID, getDashboardBackupService, } from '../services/dashboard_backup_service'; -import { getDashboardContentManagementService } from '../services/dashboard_content_management_service'; import { dashboardUnsavedListingStrings, getNewDashboardTitle } from './_dashboard_listing_strings'; import { confirmDiscardUnsavedChanges } from './confirm_overlays'; +import { findService } from '../dashboard_client'; const unsavedItemStyles = { item: (euiThemeContext: UseEuiTheme) => @@ -172,34 +172,32 @@ export const DashboardUnsavedListing = ({ const existingDashboardsWithUnsavedChanges = unsavedDashboardIds.filter( (id) => id !== DASHBOARD_PANELS_UNSAVED_ID ); - getDashboardContentManagementService() - .findDashboards.findByIds(existingDashboardsWithUnsavedChanges) - .then((results) => { - const dashboardMap = {}; - if (canceled) { - return; - } - let hasError = false; - const newItems = results.reduce((map, result) => { - if (result.status === 'error') { - hasError = true; - if (result.error.statusCode === 404) { - // Save object not found error - dashboardBackupService.clearState(result.id); - } - return map; + findService.findByIds(existingDashboardsWithUnsavedChanges).then((results) => { + const dashboardMap = {}; + if (canceled) { + return; + } + let hasError = false; + const newItems = results.reduce((map, result) => { + if (result.status === 'error') { + hasError = true; + if (result.error && result.notFound) { + // Save object not found error + dashboardBackupService.clearState(result.id); } - return { - ...map, - [result.id || DASHBOARD_PANELS_UNSAVED_ID]: result.attributes, - }; - }, dashboardMap); - if (hasError) { - refreshUnsavedDashboards(); - return; + return map; } - setItems(newItems); - }); + return { + ...map, + [result.id || DASHBOARD_PANELS_UNSAVED_ID]: result.attributes, + }; + }, dashboardMap); + if (hasError) { + refreshUnsavedDashboards(); + return; + } + setItems(newItems); + }); return () => { canceled = true; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx index 8afa6b1472b49..b6fe1d1b90b65 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -10,7 +10,6 @@ import { renderHook, act } from '@testing-library/react'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import { coreServices } from '../../services/kibana_services'; import { confirmCreateWithUnsaved } from '../confirm_overlays'; import type { DashboardSavedObjectUserContent } from '../types'; @@ -19,7 +18,6 @@ import { useDashboardListingTable } from './use_dashboard_listing_table'; const clearStateMock = jest.fn(); const getDashboardUrl = jest.fn(); const goToDashboard = jest.fn(); -const deleteDashboards = jest.fn().mockResolvedValue(true); const getUiSettingsMock = jest.fn().mockImplementation((key) => { if (key === 'savedObjects:listingLimit') { return 20; @@ -46,9 +44,15 @@ jest.mock('../_dashboard_listing_strings', () => ({ }, })); +const mockDeleteDashboards = jest.fn().mockResolvedValue(true); +jest.mock('../../dashboard_client', () => ({ + dashboardClient: { + delete: () => mockDeleteDashboards(), + }, +})); + describe('useDashboardListingTable', () => { const dashboardBackupService = getDashboardBackupService(); - const dashboardContentManagementService = getDashboardContentManagementService(); beforeEach(() => { jest.clearAllMocks(); @@ -58,7 +62,6 @@ describe('useDashboardListingTable', () => { dashboardBackupService.getDashboardIdsWithUnsavedChanges = jest.fn().mockReturnValue([]); dashboardBackupService.clearState = clearStateMock; - dashboardContentManagementService.deleteDashboards = deleteDashboards; coreServices.uiSettings.get = getUiSettingsMock; coreServices.notifications.toasts.addError = jest.fn(); }); @@ -180,7 +183,7 @@ describe('useDashboardListingTable', () => { ]); }); - expect(deleteDashboards).toHaveBeenCalled(); + expect(mockDeleteDashboards).toHaveBeenCalled(); }); test('should call goToDashboard when editItem is called', () => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index 5ea650425eb3a..ac3f49de14ab3 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -16,6 +16,7 @@ import type { SavedObjectsFindOptionsReference } from '@kbn/core/public'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import type { ViewMode } from '@kbn/presentation-publishing'; +import { asyncMap } from '@kbn/std'; import type { DashboardSearchAPIResult } from '../../../server/content_management'; import { DASHBOARD_CONTENT_ID, @@ -23,7 +24,6 @@ import { SAVED_OBJECT_LOADED_TIME, } from '../../utils/telemetry_constants'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import { getDashboardRecentlyAccessedService } from '../../services/dashboard_recently_accessed_service'; import { coreServices } from '../../services/kibana_services'; import { logger } from '../../services/logger'; @@ -35,7 +35,11 @@ import { import { confirmCreateWithUnsaved } from '../confirm_overlays'; import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt'; import type { DashboardSavedObjectUserContent } from '../types'; -import type { UpdateDashboardMetaProps } from '../../services/dashboard_content_management_service/lib/update_dashboard_meta'; +import { + checkForDuplicateDashboardTitle, + dashboardClient, + findService, +} from '../../dashboard_client'; type GetDetailViewLink = TableListViewTableProps['getDetailViewLink']; @@ -107,10 +111,6 @@ export const useDashboardListingTable = ({ const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false); const dashboardBackupService = useMemo(() => getDashboardBackupService(), []); - const dashboardContentManagementService = useMemo( - () => getDashboardContentManagementService(), - [] - ); const [unsavedDashboardIds, setUnsavedDashboardIds] = useState( dashboardBackupService.getDashboardIdsWithUnsavedChanges() @@ -131,12 +131,24 @@ export const useDashboardListingTable = ({ }, [dashboardBackupService, goToDashboard, useSessionStorageIntegration]); const updateItemMeta = useCallback( - async (props: UpdateDashboardMetaProps) => { - await dashboardContentManagementService.updateDashboardMeta(props); + async ({ id, ...updatedState }: Parameters['onSave']>[0]) => { + const dashboard = await findService.findById(id); + if (dashboard.status === 'error') { + return; + } + const { references, spaces, namespaces, ...currentState } = dashboard.attributes; + await dashboardClient.update( + id, + { + ...currentState, + ...updatedState, + }, + dashboard.references + ); setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); }, - [dashboardBackupService, dashboardContentManagementService] + [dashboardBackupService] ); const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo( @@ -147,19 +159,17 @@ export const useDashboardListingTable = ({ fn: async (value: string, id: string) => { if (id) { try { - const [dashboard] = - await dashboardContentManagementService.findDashboards.findByIds([id]); + const dashboard = await findService.findById(id); if (dashboard.status === 'error') { return; } - const validTitle = - await dashboardContentManagementService.checkForDuplicateDashboardTitle({ - title: value, - copyOnSave: false, - lastSavedTitle: dashboard.attributes.title, - isTitleDuplicateConfirmed: false, - }); + const validTitle = await checkForDuplicateDashboardTitle({ + title: value, + copyOnSave: false, + lastSavedTitle: dashboard.attributes.title, + isTitleDuplicateConfirmed: false, + }); if (!validTitle) { throw new Error(dashboardListingErrorStrings.getDuplicateTitleWarning(value)); @@ -172,7 +182,7 @@ export const useDashboardListingTable = ({ }, ], }), - [dashboardContentManagementService] + [] ); const emptyPrompt = useMemo( @@ -208,7 +218,7 @@ export const useDashboardListingTable = ({ ) => { const searchStartTime = window.performance.now(); - return dashboardContentManagementService.findDashboards + return findService .search({ search: searchTerm, size: listingLimit, @@ -236,7 +246,7 @@ export const useDashboardListingTable = ({ }; }); }, - [listingLimit, dashboardContentManagementService] + [listingLimit] ); const deleteItems = useCallback( @@ -244,12 +254,10 @@ export const useDashboardListingTable = ({ try { const deleteStartTime = window.performance.now(); - await dashboardContentManagementService.deleteDashboards( - dashboardsToDelete.map(({ id }) => { - dashboardBackupService.clearState(id); - return id; - }) - ); + await asyncMap(dashboardsToDelete, async ({ id }) => { + await dashboardClient.delete(id); + dashboardBackupService.clearState(id); + }); const deleteDuration = window.performance.now() - deleteStartTime; reportPerformanceMetricEvent(coreServices.analytics, { @@ -268,7 +276,7 @@ export const useDashboardListingTable = ({ setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); }, - [dashboardBackupService, dashboardContentManagementService] + [dashboardBackupService] ); const editItem = useCallback( diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx index 173558323b2fe..63d8d733579be 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx @@ -32,9 +32,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; -import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import { savedObjectsTaggingService } from '../../services/kibana_services'; import type { DashboardSettings } from '../../dashboard_api/settings_manager'; +import { checkForDuplicateDashboardTitle } from '../../dashboard_client'; interface DashboardSettingsProps { onClose: () => void; @@ -62,15 +62,13 @@ export const DashboardSettingsFlyout = ({ onClose, ariaLabelledBy }: DashboardSe const onApply = async () => { setIsApplying(true); - const validTitle = await getDashboardContentManagementService().checkForDuplicateDashboardTitle( - { - title: localSettings.title, - copyOnSave: false, - lastSavedTitle: dashboardApi.title$.value ?? '', - onTitleDuplicate, - isTitleDuplicateConfirmed, - } - ); + const validTitle = await checkForDuplicateDashboardTitle({ + title: localSettings.title, + copyOnSave: false, + lastSavedTitle: dashboardApi.title$.value ?? '', + onTitleDuplicate, + isTitleDuplicateConfirmed, + }); if (!isMounted()) return; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index e60370af70d12..9ee7c08ea4aae 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -43,7 +43,7 @@ import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount import { useDashboardMenuItems } from '../dashboard_app/top_nav/use_dashboard_menu_items'; import type { DashboardEmbedSettings, DashboardRedirect } from '../dashboard_app/types'; import { openSettingsFlyout } from '../dashboard_renderer/settings/open_settings_flyout'; -import type { SaveDashboardReturn } from '../services/dashboard_content_management_service/types'; +import type { SaveDashboardReturn } from '../dashboard_api/save_modal/types'; import { getDashboardRecentlyAccessedService } from '../services/dashboard_recently_accessed_service'; import { coreServices, diff --git a/src/platform/plugins/shared/dashboard/public/index.ts b/src/platform/plugins/shared/dashboard/public/index.ts index 2fdc2557c31fe..3395f0b3ebadd 100644 --- a/src/platform/plugins/shared/dashboard/public/index.ts +++ b/src/platform/plugins/shared/dashboard/public/index.ts @@ -22,10 +22,7 @@ export { DashboardListingTable } from './dashboard_listing'; export { DashboardTopNav } from './dashboard_top_nav'; export type { RedirectToProps } from './dashboard_app/types'; -export { - type FindDashboardsByIdResponse, - type SearchDashboardsResponse, -} from './services/dashboard_content_management_service/lib/find_dashboards'; +export { type FindDashboardsByIdResponse, type SearchDashboardsResponse } from './dashboard_client'; export { DASHBOARD_APP_ID } from '../common/constants'; export { cleanEmptyKeys, DashboardAppLocatorDefinition } from '../common/locator/locator'; diff --git a/src/platform/plugins/shared/dashboard/public/mocks.tsx b/src/platform/plugins/shared/dashboard/public/mocks.tsx index 14a34e3432fc3..16680476e650f 100644 --- a/src/platform/plugins/shared/dashboard/public/mocks.tsx +++ b/src/platform/plugins/shared/dashboard/public/mocks.tsx @@ -13,6 +13,7 @@ import type { DashboardStart } from './plugin'; import type { DashboardState } from '../common/types'; import { getDashboardApi } from './dashboard_api/get_dashboard_api'; import { deserializeLayout } from './dashboard_api/layout_manager/deserialize_layout'; +import type { DashboardAPIGetOut } from '../server/content_management'; export type Start = jest.Mocked; @@ -90,16 +91,15 @@ export function buildMockDashboardApi({ const results = getDashboardApi({ initialState, savedObjectId, - savedObjectResult: { - dashboardFound: true, - newDashboardCreated: savedObjectId === undefined, - dashboardId: savedObjectId, - managed: false, - dashboardInput: { - ...initialState, - }, - references: [], - }, + savedObjectResult: savedObjectId + ? ({ + id: savedObjectId, + data: initialState, + meta: { + managed: false, + }, + } as unknown as DashboardAPIGetOut) + : undefined, }); results.internalApi.setControlGroupApi(mockControlGroupApi); return results; diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index cc38171149055..aa37787b57ad1 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -68,11 +68,11 @@ import type { DashboardMountContextProps } from './dashboard_app/types'; import { DASHBOARD_APP_ID, LANDING_PAGE_PATH, SEARCH_SESSION_ID } from '../common/constants'; import type { GetPanelPlacementSettings } from './panel_placement'; import { registerDashboardPanelSettings } from './panel_placement'; -import type { FindDashboardsService } from './services/dashboard_content_management_service/types'; import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; import { setLogger } from './services/logger'; import { registerActions } from './dashboard_actions/register_actions'; import { setupUrlForwarding } from './dashboard_app/url/setup_url_forwarding'; +import type { FindDashboardsService } from './dashboard_client'; export interface DashboardSetupDependencies { data: DataPublicPluginSetup; @@ -151,14 +151,13 @@ export class DashboardPlugin new DashboardAppLocatorDefinition({ useHashedUrl: core.uiSettings.get('state:storeInSessionStorage'), getDashboardFilterFields: async (dashboardId: string) => { - const [{ getDashboardContentManagementService }] = await Promise.all([ - import('./services/dashboard_content_management_service'), + const [{ dashboardClient }] = await Promise.all([ + import('./dashboard_client'), untilPluginStartServicesReady(), ]); - return ( - (await getDashboardContentManagementService().loadDashboardState({ id: dashboardId })) - .dashboardInput?.filters ?? [] - ); + + const result = await dashboardClient.get(dashboardId); + return result.data.filters ?? []; }, }) ); @@ -297,10 +296,8 @@ export class DashboardPlugin return { registerDashboardPanelSettings, findDashboardsService: async () => { - const { getDashboardContentManagementService } = await import( - './services/dashboard_content_management_service' - ); - return getDashboardContentManagementService().findDashboards; + const { findService } = await import('./dashboard_client'); + return findService; }, }; } diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/dashboard_content_management_cache.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/dashboard_content_management_cache.ts deleted file mode 100644 index 78682ffef330d..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/dashboard_content_management_cache.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { LRUCache } from 'lru-cache'; -import type { DashboardGetOut } from '../../../server/content_management'; - -const DASHBOARD_CACHE_SIZE = 20; // only store a max of 20 dashboards -const DASHBOARD_CACHE_TTL = 1000 * 60 * 5; // time to live = 5 minutes - -export class DashboardContentManagementCache { - private cache: LRUCache; - - constructor() { - this.cache = new LRUCache({ - max: DASHBOARD_CACHE_SIZE, - ttl: DASHBOARD_CACHE_TTL, - }); - } - - /** Fetch the dashboard with `id` from the cache */ - public fetchDashboard(id: string) { - return this.cache.get(id); - } - - /** Add the fetched dashboard to the cache */ - public addDashboard({ item: dashboard, meta }: DashboardGetOut) { - this.cache.set(dashboard.id, { - meta, - item: dashboard, - }); - } - - /** Delete the dashboard with `id` from the cache */ - public deleteDashboard(id: string) { - this.cache.delete(id); - } -} diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/index.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/index.ts deleted file mode 100644 index ebc5a414f9f64..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { DashboardContentManagementCache } from './dashboard_content_management_cache'; -import { checkForDuplicateDashboardTitle } from './lib/check_for_duplicate_dashboard_title'; -import { deleteDashboards } from './lib/delete_dashboards'; -import { - findDashboardById, - findDashboardIdByTitle, - findDashboardsByIds, - searchDashboards, -} from './lib/find_dashboards'; -import { loadDashboardState } from './lib/load_dashboard_state'; -import { saveDashboardState } from './lib/save_dashboard_state'; -import { updateDashboardMeta } from './lib/update_dashboard_meta'; - -let dashboardContentManagementCache: DashboardContentManagementCache; - -export const getDashboardContentManagementCache = () => { - if (!dashboardContentManagementCache) - dashboardContentManagementCache = new DashboardContentManagementCache(); - return dashboardContentManagementCache; -}; - -export const getDashboardContentManagementService = () => { - return { - loadDashboardState, - saveDashboardState, - findDashboards: { - search: searchDashboards, - findById: findDashboardById, - findByIds: findDashboardsByIds, - findByTitle: findDashboardIdByTitle, - }, - checkForDuplicateDashboardTitle, - deleteDashboards, - updateDashboardMeta, - }; -}; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/delete_dashboards.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/delete_dashboards.ts deleted file mode 100644 index 7a7b97461c8cb..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/delete_dashboards.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { DeleteIn, DeleteResult } from '@kbn/content-management-plugin/common'; -import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; -import { getDashboardContentManagementCache } from '..'; -import { contentManagementService } from '../../kibana_services'; - -export const deleteDashboards = async (ids: string[]) => { - const deletePromises = ids.map((id) => { - getDashboardContentManagementCache().deleteDashboard(id); - return contentManagementService.client.delete({ - contentTypeId: DASHBOARD_CONTENT_ID, - id, - }); - }); - - await Promise.all(deletePromises); -}; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/find_dashboards.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/find_dashboards.ts deleted file mode 100644 index 9bbf1ac533ac5..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/find_dashboards.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 { Reference } from '@kbn/content-management-utils'; -import type { SavedObjectError, SavedObjectsFindOptionsReference } from '@kbn/core/public'; - -import type { - DashboardState, - DashboardGetIn, - DashboardSearchIn, - DashboardSearchOptions, - DashboardSearchAPIResult, - DashboardGetOut, - DashboardSearchOut, -} from '../../../../server/content_management'; -import { getDashboardContentManagementCache } from '..'; -import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; -import { contentManagementService } from '../../kibana_services'; - -export interface SearchDashboardsArgs { - options?: DashboardSearchOptions; - hasNoReference?: SavedObjectsFindOptionsReference[]; - hasReference?: SavedObjectsFindOptionsReference[]; - search: string; - size: number; -} - -export interface SearchDashboardsResponse { - total: number; - hits: DashboardSearchAPIResult['hits']; -} - -export async function searchDashboards({ - hasNoReference, - hasReference, - options, - search, - size, -}: SearchDashboardsArgs): Promise<{ - total: DashboardSearchAPIResult['pagination']['total']; - hits: DashboardSearchAPIResult['hits']; -}> { - const { - hits, - pagination: { total }, - } = await contentManagementService.client.search({ - contentTypeId: DASHBOARD_CONTENT_ID, - query: { - text: search ? `${search}*` : undefined, - limit: size, - tags: { - included: (hasReference ?? []).map(({ id }) => id), - excluded: (hasNoReference ?? []).map(({ id }) => id), - }, - }, - options, - }); - return { - total, - hits, - }; -} - -export type FindDashboardsByIdResponse = { id: string } & ( - | { status: 'success'; attributes: DashboardState; references: Reference[] } - | { status: 'error'; error: SavedObjectError } -); - -export async function findDashboardById(id: string): Promise { - const dashboardContentManagementCache = getDashboardContentManagementCache(); - - /** If the dashboard exists in the cache, then return the result from that */ - const cachedDashboard = dashboardContentManagementCache.fetchDashboard(id); - if (cachedDashboard) { - return { - id, - status: 'success', - attributes: cachedDashboard.item.attributes, - references: cachedDashboard.item.references, - }; - } - - /** Otherwise, fetch the dashboard from the content management client, add it to the cache, and return the result */ - try { - const response = await contentManagementService.client.get({ - contentTypeId: DASHBOARD_CONTENT_ID, - id, - }); - - if ('error' in response.item) { - throw response.item.error; - } - - dashboardContentManagementCache.addDashboard(response); - return { - id, - status: 'success', - attributes: response.item.attributes, - references: response.item.references ?? [], - }; - } catch (e) { - return { - status: 'error', - error: e.body || e.message, - id, - }; - } -} - -export async function findDashboardsByIds(ids: string[]): Promise { - const findPromises = ids.map((id) => findDashboardById(id)); - const results = await Promise.all(findPromises); - return results as FindDashboardsByIdResponse[]; -} - -export async function findDashboardIdByTitle(title: string): Promise<{ id: string } | undefined> { - const { hits } = await contentManagementService.client.search< - DashboardSearchIn, - DashboardSearchOut - >({ - contentTypeId: DASHBOARD_CONTENT_ID, - query: { - text: title ? `${title}*` : undefined, - limit: 10, - }, - options: { onlyTitle: true }, - }); - - // The search isn't an exact match, lets see if we can find a single exact match to use - const matchingDashboards = hits.filter( - (hit) => hit.attributes.title.toLowerCase() === title.toLowerCase() - ); - if (matchingDashboards.length === 1) { - return { id: matchingDashboards[0].id }; - } -} diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.test.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.test.ts deleted file mode 100644 index b0c470de8812d..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 { getDashboardContentManagementCache } from '..'; -import { contentManagementService } from '../../kibana_services'; -import { loadDashboardState } from './load_dashboard_state'; - -describe('Load dashboard state', () => { - const dashboardContentManagementCache = getDashboardContentManagementCache(); - - it('should return cached result if available', async () => { - dashboardContentManagementCache.fetchDashboard = jest.fn().mockImplementation((id: string) => { - return { - item: { - id, - version: 1, - references: [], - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { searchSourceJSON: '' }, - title: 'Test dashboard', - }, - }, - meta: {}, - }; - }); - contentManagementService.client.get = jest.fn(); - dashboardContentManagementCache.addDashboard = jest.fn(); - const id = '123'; - const result = await loadDashboardState({ - id, - }); - expect(dashboardContentManagementCache.fetchDashboard).toBeCalled(); - expect(dashboardContentManagementCache.addDashboard).not.toBeCalled(); - expect(contentManagementService.client.get).not.toBeCalled(); - expect(result).toMatchObject({ - dashboardId: id, - dashboardFound: true, - dashboardInput: { - title: 'Test dashboard', - }, - }); - }); - - it('should not add to cache for alias redirect result', async () => { - dashboardContentManagementCache.fetchDashboard = jest.fn().mockImplementation(() => undefined); - dashboardContentManagementCache.addDashboard = jest.fn(); - contentManagementService.client.get = jest.fn().mockImplementation(({ id }) => { - return Promise.resolve({ - item: { id }, - meta: { - outcome: 'aliasMatch', - }, - }); - }); - await loadDashboardState({ - id: '123', - }); - expect(dashboardContentManagementCache.fetchDashboard).toBeCalled(); - expect(dashboardContentManagementCache.addDashboard).not.toBeCalled(); - }); -}); diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts deleted file mode 100644 index 06426e4375237..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; - -import { getDashboardContentManagementCache } from '..'; -import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management'; -import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state'; -import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; -import { contentManagementService } from '../../kibana_services'; -import type { LoadDashboardFromSavedObjectProps, LoadDashboardReturn } from '../types'; - -export const loadDashboardState = async ({ - id, -}: LoadDashboardFromSavedObjectProps): Promise => { - const dashboardContentManagementCache = getDashboardContentManagementCache(); - - const savedObjectId = id; - - const newDashboardState = { ...DEFAULT_DASHBOARD_STATE }; - - /** - * This is a newly created dashboard, so there is no saved object state to load. - */ - if (!savedObjectId) { - return { - dashboardInput: newDashboardState, - dashboardFound: true, - newDashboardCreated: true, - references: [], - }; - } - - /** - * Load the saved object from Content Management - */ - let rawDashboardContent: DashboardGetOut['item']; - let resolveMeta: DashboardGetOut['meta']; - - const cachedDashboard = dashboardContentManagementCache.fetchDashboard(id); - - if (cachedDashboard) { - /** If the dashboard exists in the cache, use the cached version to load the dashboard */ - ({ item: rawDashboardContent, meta: resolveMeta } = cachedDashboard); - } else { - /** Otherwise, fetch and load the dashboard from the content management client, and add it to the cache */ - const result = await contentManagementService.client - .get({ - contentTypeId: DASHBOARD_CONTENT_ID, - id, - }) - .catch((e) => { - if (e.response?.status === 404) { - throw new SavedObjectNotFound({ type: DASHBOARD_CONTENT_ID, id }); - } - const message = (e.body as { message?: string })?.message ?? e.message; - throw new Error(message); - }); - - ({ item: rawDashboardContent, meta: resolveMeta } = result); - const { outcome: loadOutcome } = resolveMeta; - if (loadOutcome !== 'aliasMatch') { - /** - * Only add the dashboard to the cache if it does not require a redirect - otherwise, the meta - * alias info gets cached and prevents the dashboard contents from being updated - */ - dashboardContentManagementCache.addDashboard(result); - } - } - - if (!rawDashboardContent || !rawDashboardContent.version) { - return { - dashboardInput: newDashboardState, - dashboardFound: false, - dashboardId: savedObjectId, - references: [], - }; - } - - const { references, attributes, managed, version } = rawDashboardContent; - - return { - managed, - references, - resolveMeta: { ...resolveMeta, version }, - dashboardInput: attributes, - dashboardFound: true, - dashboardId: savedObjectId, - }; -}; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/update_dashboard_meta.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/update_dashboard_meta.ts deleted file mode 100644 index d6febbeda5a31..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/update_dashboard_meta.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; -import type { - DashboardState, - DashboardUpdateIn, - DashboardUpdateOut, -} from '../../../../server/content_management'; -import { findDashboardsByIds } from './find_dashboards'; -import { contentManagementService } from '../../kibana_services'; -import { getDashboardContentManagementCache } from '..'; - -export interface UpdateDashboardMetaProps { - id: DashboardUpdateIn['id']; - title: DashboardState['title']; - description?: DashboardState['description']; - tags: string[]; -} - -export const updateDashboardMeta = async ({ - id, - title, - description = '', - tags, -}: UpdateDashboardMetaProps) => { - const [dashboard] = await findDashboardsByIds([id]); - if (dashboard.status === 'error') { - return; - } - - await contentManagementService.client.update({ - contentTypeId: DASHBOARD_CONTENT_ID, - id, - data: { ...dashboard.attributes, title, description, tags }, - options: { - references: dashboard.references, - /** perform a "full" update instead, where the provided attributes will fully replace the existing ones */ - mergeAttributes: false, - }, - }); - - getDashboardContentManagementCache().deleteDashboard(id); -}; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/types.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/types.ts deleted file mode 100644 index 85f4b0d8fa720..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 { Reference } from '@kbn/content-management-utils'; -import type { Query, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; -import type { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public'; - -import type { DashboardState, DashboardAPIGetOut } from '../../../server/content_management'; -import type { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title'; -import type { - FindDashboardsByIdResponse, - SearchDashboardsArgs, - SearchDashboardsResponse, -} from './lib/find_dashboards'; -import type { UpdateDashboardMetaProps } from './lib/update_dashboard_meta'; - -export interface DashboardContentManagementService { - findDashboards: FindDashboardsService; - deleteDashboards: (ids: string[]) => Promise; - loadDashboardState: (props: { id?: string }) => Promise; - saveDashboardState: (props: SaveDashboardProps) => Promise; - checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise; - updateDashboardMeta: (props: UpdateDashboardMetaProps) => Promise; -} - -/** - * Types for Loading Dashboards - */ -export interface LoadDashboardFromSavedObjectProps { - id?: string; -} - -type DashboardResolveMeta = DashboardAPIGetOut['meta']; - -export type DashboardSearchSource = Omit & { - query?: Query; -}; - -export interface LoadDashboardReturn { - dashboardFound: boolean; - newDashboardCreated?: boolean; - dashboardId?: string; - managed?: boolean; - resolveMeta?: DashboardResolveMeta; - dashboardInput: DashboardState; - - /** - * Raw references returned directly from the Dashboard saved object. These - * should be provided to the React Embeddable children on deserialize. - */ - references: Reference[]; -} - -/** - * Types for Saving Dashboards - */ -export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolean }; - -export interface SaveDashboardProps { - dashboardState: DashboardState; - references?: Reference[]; - saveOptions: SavedDashboardSaveOpts; - searchSourceReferences?: Reference[]; - lastSavedId?: string; -} - -export interface GetDashboardStateReturn { - attributes: DashboardState; - references: Reference[]; -} - -export interface SaveDashboardReturn { - id?: string; - error?: string; - references?: Reference[]; - redirectRequired?: boolean; -} - -/** - * Types for Finding Dashboards - */ -export interface FindDashboardsService { - search: ( - props: Pick< - SearchDashboardsArgs, - 'hasReference' | 'hasNoReference' | 'search' | 'size' | 'options' - > - ) => Promise; - findById: (id: string) => Promise; - findByIds: (ids: string[]) => Promise; - findByTitle: (title: string) => Promise<{ id: string } | undefined>; -} diff --git a/src/platform/plugins/shared/dashboard/public/services/mocks.ts b/src/platform/plugins/shared/dashboard/public/services/mocks.ts index 13dc819d87762..c7e872ec3b3f0 100644 --- a/src/platform/plugins/shared/dashboard/public/services/mocks.ts +++ b/src/platform/plugins/shared/dashboard/public/services/mocks.ts @@ -33,8 +33,6 @@ import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks import { setKibanaServices } from './kibana_services'; import { setLogger } from './logger'; import type { DashboardCapabilities } from '../../common'; -import type { LoadDashboardReturn } from './dashboard_content_management_service/types'; -import type { SearchDashboardsResponse } from './dashboard_content_management_service/lib/find_dashboards'; const defaultDashboardCapabilities: DashboardCapabilities = { show: true, @@ -77,66 +75,6 @@ export const setStubLogger = () => { setLogger(coreMock.createCoreContext().logger); }; -export const mockDashboardContentManagementService = { - loadDashboardState: jest.fn().mockImplementation(() => - Promise.resolve({ - dashboardInput: {}, - } as LoadDashboardReturn) - ), - saveDashboardState: jest.fn(), - findDashboards: { - search: jest.fn().mockImplementation(({ search, size }) => { - const sizeToUse = size ?? 10; - const hits: SearchDashboardsResponse['hits'] = []; - for (let i = 0; i < sizeToUse; i++) { - hits.push({ - type: 'dashboard', - id: `dashboard${i}`, - attributes: { - description: `dashboard${i} desc`, - title: `dashboard${i} - ${search} - title`, - }, - references: [] as SearchDashboardsResponse['hits'][0]['references'], - } as SearchDashboardsResponse['hits'][0]); - } - return Promise.resolve({ - total: sizeToUse, - hits, - }); - }), - findById: jest.fn(), - findByIds: jest.fn().mockImplementation(() => - Promise.resolve([ - { - id: `dashboardUnsavedOne`, - status: 'success', - attributes: { - title: `Dashboard Unsaved One`, - }, - }, - { - id: `dashboardUnsavedTwo`, - status: 'success', - attributes: { - title: `Dashboard Unsaved Two`, - }, - }, - { - id: `dashboardUnsavedThree`, - status: 'success', - attributes: { - title: `Dashboard Unsaved Three`, - }, - }, - ]) - ), - findByTitle: jest.fn(), - }, - deleteDashboards: jest.fn(), - checkForDuplicateDashboardTitle: jest.fn(), - updateDashboardMeta: jest.fn(), -}; - export const mockDashboardBackupService = { clearState: jest.fn(), getState: jest.fn().mockReturnValue(undefined), @@ -148,9 +86,3 @@ export const mockDashboardBackupService = { .mockReturnValue(['dashboardUnsavedOne', 'dashboardUnsavedTwo']), dashboardHasUnsavedEdits: jest.fn(), }; - -export const mockDashboardContentManagementCache = { - fetchDashboard: jest.fn(), - addDashboard: jest.fn(), - deleteDashboard: jest.fn(), -}; diff --git a/src/platform/plugins/shared/dashboard/server/api/register_routes.ts b/src/platform/plugins/shared/dashboard/server/api/register_routes.ts index a47860f5c3f7f..65a6cca6dfd93 100644 --- a/src/platform/plugins/shared/dashboard/server/api/register_routes.ts +++ b/src/platform/plugins/shared/dashboard/server/api/register_routes.ts @@ -69,6 +69,8 @@ const formatResult = (item: DashboardItem) => { error, managed, version, + // TODO rest contains spaces and namespaces + // These should not be spread into data and instead be moved to meta ...rest } = item; return { diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/types.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/types.ts index a4cb32162b24d..d02f98049621b 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/types.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/types.ts @@ -12,7 +12,6 @@ import type { CreateIn, CreateResult, GetIn, - GetResult, SearchIn, SearchResult, UpdateIn, @@ -39,11 +38,13 @@ export type PartialDashboardItem = Omit; +// TODO rename to DashboardGetRequestBody export type DashboardGetIn = GetIn; -export type DashboardAPIGetOut = GetResult< - TypeOf>, - TypeOf ->; +// REST API Get response body +// TODO rename to DashboardGetResponseBody +export type DashboardAPIGetOut = TypeOf; +// RPC Get response body +// TODO remove and have RPC endpoints return same shape as REST API or remove RPC routes altogether export type DashboardGetOut = TypeOf>; export type DashboardCreateIn = CreateIn; diff --git a/x-pack/platform/plugins/shared/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx b/x-pack/platform/plugins/shared/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx index 585ab9d357912..2e2dabfd48c77 100644 --- a/x-pack/platform/plugins/shared/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx +++ b/x-pack/platform/plugins/shared/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx @@ -106,7 +106,7 @@ export class CollectConfigContainer extends React.Component< if (!this.isMounted) return; // handle case when destination dashboard no longer exists - if (dashboardResponse.status === 'error' && dashboardResponse.error?.statusCode === 404) { + if (dashboardResponse.status === 'error' && dashboardResponse.notFound) { this.setState({ error: txtDestinationDashboardNotFound(config.dashboardId), });