diff --git a/src/platform/plugins/shared/discover/common/embeddable/types.ts b/src/platform/plugins/shared/discover/common/embeddable/types.ts index 9154d333a83ec..75c8eeccd1aa0 100644 --- a/src/platform/plugins/shared/discover/common/embeddable/types.ts +++ b/src/platform/plugins/shared/discover/common/embeddable/types.ts @@ -28,7 +28,7 @@ export type EditableSavedSearchAttributes = Partial< Pick >; -type SearchEmbeddableBaseState = SerializedTitles & +export type SearchEmbeddableBaseState = SerializedTitles & SerializedTimeRange & SerializedDrilldowns & EditableSavedSearchAttributes & { diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx index 0af55e1f1ac17..73517a95e54ff 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx @@ -29,6 +29,11 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { SearchResponseIncompleteWarning } from '@kbn/search-response-warnings/src/types'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { ON_APPLY_FILTER, ON_OPEN_PANEL_MENU } from '@kbn/ui-actions-plugin/common/trigger_ids'; +import { getSearchEmbeddableDefaults } from './get_search_embeddable_defaults'; +import { + getDiscoverSessionEmbeddableComparators, + getSearchEmbeddableComparators, +} from './utils/get_search_embeddable_comparators'; import type { DiscoverServices } from '../build_services'; import { SearchEmbeddablFieldStatsTableComponent } from './components/search_embeddable_field_stats_table_component'; import { SearchEmbeddableGridComponent } from './components/search_embeddable_grid_component'; @@ -43,7 +48,6 @@ import type { SearchEmbeddableApi, SearchEmbeddablePanelApiState } from './types import { deserializeState, serializeState } from './utils/serialization_utils'; import { ScopedServicesProvider } from '../components/scoped_services_provider'; import { isFieldStatsMode } from './utils/is_field_stats_mode'; -import { compareSelectedTabId } from './utils/compare_selected_tab_id'; import { isTabDeleted } from './utils/is_tab_deleted'; export const getSearchEmbeddableFactory = ({ @@ -100,8 +104,14 @@ export const getSearchEmbeddableFactory = ({ const tabs = runtimeState.tabs ?? []; - const isSelectedTabDeleted = (tabId: string | undefined, availableTabs: typeof tabs = tabs) => - isTabDeleted(tabId, availableTabs); + const defaultState = embeddableTransformsEnabled + ? { selected_tab_id: tabs[0]?.id } + : { + selectedTabId: tabs[0]?.id, + sort: [], + grid: {}, + ...getSearchEmbeddableDefaults(discoverServices.uiSettings), + }; /** All other state */ const blockingError$ = new BehaviorSubject(undefined); @@ -145,6 +155,7 @@ export const getSearchEmbeddableFactory = ({ const unsavedChangesApi = initializeUnsavedChanges({ uuid, parentApi, + defaultState, serializeState: () => serialize(savedObjectId$.getValue()), anyStateChange$: merge( drilldownsManager.anyStateChange$, @@ -158,52 +169,18 @@ export const getSearchEmbeddableFactory = ({ inlineEditingApi.anyStateChange$ ), getComparators: () => { - const isDeleted = isSelectedTabDeleted(selectedTabId$.getValue()); - const shouldSkipTabComparators = isDeleted || inlineEditingApi.isEditing(); - - if (embeddableTransformsEnabled) { - const isByValue = !savedObjectId$.getValue(); - return { - ...drilldownsManager.comparators, - ...titleComparators, - ...timeRangeComparators, - ref_id: 'skip', - selected_tab_id: shouldSkipTabComparators ? 'skip' : 'referenceEquality', - overrides: shouldSkipTabComparators ? 'skip' : 'deepEquality', - tabs: !isByValue || shouldSkipTabComparators ? 'skip' : 'deepEquality', - }; - } + const isByValue = !savedObjectId$.getValue(); + const shouldSkipTabComparators = + isTabDeleted(selectedTabId$.getValue(), tabs) || inlineEditingApi.isEditing(); return { ...drilldownsManager.comparators, ...titleComparators, ...timeRangeComparators, - ...searchEmbeddable.comparators, - // While the selected tab is missing or inline editing is in progress, - // skip tab-dependent comparators so unsaved-changes badges don't appear - // until the user explicitly applies a tab change. - ...(shouldSkipTabComparators - ? Object.fromEntries( - Object.keys(searchEmbeddable.comparators).map((k) => [k, 'skip']) - ) - : {}), - selectedTabId: shouldSkipTabComparators - ? 'skip' - : (last, current) => compareSelectedTabId(tabs[0]?.id, last, current), - attributes: 'skip', - breakdownField: 'skip', - hideAggregatedPreview: 'skip', - hideChart: 'skip', - isTextBasedQuery: 'skip', - kibanaSavedObjectMeta: 'skip', + ...(embeddableTransformsEnabled + ? getDiscoverSessionEmbeddableComparators(isByValue, shouldSkipTabComparators) + : getSearchEmbeddableComparators(isByValue, shouldSkipTabComparators)), nonPersistedDisplayOptions: 'skip', - refreshInterval: 'skip', - savedObjectId: 'skip', - timeRestore: 'skip', - usesAdHocDataView: 'skip', - controlGroupJson: 'skip', - visContext: 'skip', - tabs: 'skip', }; }, onReset: async (lastSaved) => { diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx b/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx index aed2bd8584c8a..0b15630134014 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx @@ -18,7 +18,6 @@ import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { PublishesWritableUnifiedSearch, PublishesWritableDataViews, - StateComparators, ProjectRoutingOverrides, PublishesProjectRoutingOverrides, } from '@kbn/presentation-publishing'; @@ -38,7 +37,6 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { getEsqlDataView } from '@kbn/discover-utils'; import type { DiscoverServices } from '../build_services'; import { EDITABLE_SAVED_SEARCH_KEYS } from '../../common/embeddable/constants'; -import { getSearchEmbeddableDefaults } from './get_search_embeddable_defaults'; import type { PublishesWritableSavedSearch, SearchEmbeddableSerializedAttributes, @@ -118,7 +116,6 @@ export const initializeSearchEmbeddableApi = async ({ PublishesProjectRoutingOverrides; stateManager: SearchEmbeddableStateManager; anyStateChange$: Observable; - comparators: StateComparators; cleanup: () => void; reinitializeState: (lastSaved: SearchEmbeddableSerializedAttributes) => Promise; }> => { @@ -130,8 +127,6 @@ export const initializeSearchEmbeddableApi = async ({ const searchSource$ = new BehaviorSubject(searchSource); const dataViews$ = new BehaviorSubject(dataView ? [dataView] : undefined); - const defaults = getSearchEmbeddableDefaults(discoverServices.uiSettings); - /** This is the state that can be initialized from the saved initial state */ const columns$ = new BehaviorSubject(initialState.columns); const grid$ = new BehaviorSubject(initialState.grid); @@ -295,19 +290,6 @@ export const initializeSearchEmbeddableApi = async ({ }, stateManager, anyStateChange$: onAnyStateChange.pipe(map(() => undefined)), - comparators: { - sort: (a, b) => deepEqual(a ?? [], b ?? []), - columns: 'deepEquality', - grid: (a, b) => deepEqual(a ?? {}, b ?? {}), - sampleSize: (a, b) => (a ?? defaults.sampleSize) === (b ?? defaults.sampleSize), - rowsPerPage: (a, b) => (a ?? defaults.rowsPerPage) === (b ?? defaults.rowsPerPage), - rowHeight: (a, b) => (a ?? defaults.rowHeight) === (b ?? defaults.rowHeight), - headerRowHeight: (a, b) => - (a ?? defaults.headerRowHeight) === (b ?? defaults.headerRowHeight), - serializedSearchSource: 'referenceEquality', - viewMode: 'referenceEquality', - density: 'referenceEquality', - }, reinitializeState, }; }; diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/compare_selected_tab_id.test.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/compare_selected_tab_id.test.ts deleted file mode 100644 index 20e053fb65700..0000000000000 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/compare_selected_tab_id.test.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 { compareSelectedTabId } from './compare_selected_tab_id'; - -describe('compareSelectedTabId', () => { - const defaultTabId = 'tab-1'; - - it('should treat undefined and the default tab as equal', () => { - expect(compareSelectedTabId(defaultTabId, undefined, 'tab-1')).toBe(true); - expect(compareSelectedTabId(defaultTabId, 'tab-1', undefined)).toBe(true); - }); - - it('should treat both undefined as equal', () => { - expect(compareSelectedTabId(defaultTabId, undefined, undefined)).toBe(true); - }); - - it('should treat identical explicit values as equal', () => { - expect(compareSelectedTabId(defaultTabId, 'tab-2', 'tab-2')).toBe(true); - }); - - it('should detect a change to a non-default tab', () => { - expect(compareSelectedTabId(defaultTabId, undefined, 'tab-2')).toBe(false); - expect(compareSelectedTabId(defaultTabId, 'tab-1', 'tab-2')).toBe(false); - }); - - it('should detect a change from a non-default tab', () => { - expect(compareSelectedTabId(defaultTabId, 'tab-2', undefined)).toBe(false); - expect(compareSelectedTabId(defaultTabId, 'tab-2', 'tab-1')).toBe(false); - }); - - it('should handle undefined defaultTabId gracefully', () => { - expect(compareSelectedTabId(undefined, undefined, undefined)).toBe(true); - expect(compareSelectedTabId(undefined, 'tab-1', 'tab-1')).toBe(true); - expect(compareSelectedTabId(undefined, 'tab-1', undefined)).toBe(false); - }); -}); diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/compare_selected_tab_id.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/compare_selected_tab_id.ts deleted file mode 100644 index 44b3101f303b7..0000000000000 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/compare_selected_tab_id.ts +++ /dev/null @@ -1,19 +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". - */ - -/** - * Compares two selectedTabId values, treating `undefined` as equivalent to the - * first (default) tab. This prevents false "unsaved changes" when the stored - * panel state omits selectedTabId and deserialization resolves it to the default. - */ -export const compareSelectedTabId = ( - defaultTabId: string | undefined, - last: string | undefined, - current: string | undefined -): boolean => (last ?? defaultTabId) === (current ?? defaultTabId); diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/get_search_embeddable_comparators.test.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/get_search_embeddable_comparators.test.ts new file mode 100644 index 0000000000000..6cb2bf6dec509 --- /dev/null +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/get_search_embeddable_comparators.test.ts @@ -0,0 +1,250 @@ +/* + * 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 { runComparator } from '@kbn/presentation-publishing'; +import { + getDiscoverSessionEmbeddableComparators, + getSearchEmbeddableComparators, +} from './get_search_embeddable_comparators'; +import { AS_CODE_DATA_VIEW_REFERENCE_TYPE } from '@kbn/as-code-data-views-schema'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/common'; + +const sharedSavedSearchComparators = { + sort: 'deepEquality', + columns: 'deepEquality', + rowHeight: 'referenceEquality', + sampleSize: 'referenceEquality', + rowsPerPage: 'referenceEquality', + headerRowHeight: 'referenceEquality', + density: 'referenceEquality', + grid: 'deepEquality', +} as const; + +describe('getSearchEmbeddableComparators', () => { + it('returns shared attribute comparators for by-value panels', () => { + const c = getSearchEmbeddableComparators(true, false); + expect(c).toMatchObject(sharedSavedSearchComparators); + expect('attributes' in c && c.attributes).toBe('skip'); + expect(c).not.toHaveProperty('selectedTabId'); + expect(c).not.toHaveProperty('savedObjectId'); + }); + + it('uses referenceEquality for selectedTabId when by-reference and tab comparators are active', () => { + const c = getSearchEmbeddableComparators(false, false); + expect(c).toMatchObject(sharedSavedSearchComparators); + expect('selectedTabId' in c && c.selectedTabId).toBe('referenceEquality'); + expect('savedObjectId' in c && c.savedObjectId).toBe('skip'); + expect(c).not.toHaveProperty('attributes'); + }); + + it('skips selectedTabId when by-reference and tab comparators should be skipped', () => { + const c = getSearchEmbeddableComparators(false, true); + expect(c).toMatchObject(sharedSavedSearchComparators); + expect('selectedTabId' in c && c.selectedTabId).toBe('skip'); + expect('savedObjectId' in c && c.savedObjectId).toBe('skip'); + }); + + it('treats selectedTabId as always equal when skipped (deleted tab / inline edit)', () => { + const c = getSearchEmbeddableComparators(false, true); + expect( + 'selectedTabId' in c && runComparator(c.selectedTabId, undefined, undefined, 'tab-a', 'tab-b') + ).toBe(true); + }); + + it('compares selectedTabId by reference when not skipped', () => { + const c = getSearchEmbeddableComparators(false, false); + expect( + 'selectedTabId' in c && runComparator(c.selectedTabId, undefined, undefined, 'a', 'a') + ).toBe(true); + expect( + 'selectedTabId' in c && runComparator(c.selectedTabId, undefined, undefined, 'a', 'b') + ).toBe(false); + }); +}); + +describe('getDiscoverSessionEmbeddableComparators', () => { + const baseTab = { + query: { language: 'kuery', query: '*' }, + filters: [], + sort: [], + column_order: [], + header_row_height: 3, + data_source: { + type: AS_CODE_DATA_VIEW_REFERENCE_TYPE, + ref_id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + }, + view_mode: VIEW_MODE.DOCUMENT_LEVEL, + }; + + it('returns by-reference comparators when not by-value', () => { + const c = getDiscoverSessionEmbeddableComparators(false, false); + expect('ref_id' in c && c.ref_id).toBe('skip'); + expect('selected_tab_id' in c && c.selected_tab_id).toBe('referenceEquality'); + expect('overrides' in c && typeof c.overrides).toBe('function'); + }); + + it('skips selected_tab_id when tab comparators should be skipped', () => { + const c = getDiscoverSessionEmbeddableComparators(false, true); + expect('ref_id' in c && c.ref_id).toBe('skip'); + expect('selected_tab_id' in c && c.selected_tab_id).toBe('skip'); + expect('overrides' in c && typeof c.overrides).toBe('function'); + }); + + it('treats selected_tab_id as always equal when skipped', () => { + const c = getDiscoverSessionEmbeddableComparators(false, true); + expect( + 'selected_tab_id' in c && + runComparator(c.selected_tab_id, undefined, undefined, 'tab-a', 'tab-b') + ).toBe(true); + }); + + it('compares selected_tab_id by reference when not skipped', () => { + const c = getDiscoverSessionEmbeddableComparators(false, false); + expect( + 'selected_tab_id' in c && runComparator(c.selected_tab_id, undefined, undefined, 'x', 'x') + ).toBe(true); + expect( + 'selected_tab_id' in c && runComparator(c.selected_tab_id, undefined, undefined, 'x', 'y') + ).toBe(false); + }); + + describe('tabs comparator', () => { + const getTabsComparator = () => { + const c = getDiscoverSessionEmbeddableComparators(true, false); + if (!('tabs' in c) || typeof c.tabs !== 'function') { + throw new Error('expected tabs function comparator'); + } + return c.tabs; + }; + + it('treats tab arrays as equal when each tab is deeply equal', () => { + const cmp = getTabsComparator(); + const tab = { ...baseTab }; + expect(runComparator(cmp, undefined, undefined, [baseTab], [tab])).toBe(true); + }); + + it('treats omitted optional fields as equal to explicit undefined (e.g. query)', () => { + const cmp = getTabsComparator(); + const { query, ...tabWithoutQuery } = baseTab; + const tabA = { ...tabWithoutQuery }; + const tabB = { ...tabWithoutQuery, query: undefined }; + expect(runComparator(cmp, undefined, undefined, [tabA], [tabB])).toBe(true); + }); + + it('treats tab arrays as not equal when a tab differs', () => { + const cmp = getTabsComparator(); + const tabA = { ...baseTab, query: { language: 'kuery', query: 'a' } }; + const tabB = { ...baseTab, query: { language: 'kuery', query: 'b' } }; + expect(runComparator(cmp, undefined, undefined, [tabA], [tabB])).toBe(false); + }); + + it('treats tab arrays as not equal when lengths differ', () => { + const cmp = getTabsComparator(); + const tab = { ...baseTab }; + expect(runComparator(cmp, undefined, undefined, [tab], [tab, tab])).toBe(false); + }); + }); + + describe('overrides comparator', () => { + const getOverridesComparator = () => { + const c = getDiscoverSessionEmbeddableComparators(false, false); + if (!('overrides' in c) || typeof c.overrides !== 'function') { + throw new Error('expected overrides function comparator'); + } + return c.overrides; + }; + + it('treats {} and { sort: [] } as equal (default sort is empty)', () => { + const cmp = getOverridesComparator(); + expect(runComparator(cmp, undefined, undefined, {}, { sort: [] })).toBe(true); + expect(runComparator(cmp, undefined, undefined, { sort: [] }, {})).toBe(true); + }); + + it('treats {} and { column_order: [] } as equal', () => { + const cmp = getOverridesComparator(); + expect(runComparator(cmp, undefined, undefined, {}, { column_order: [] })).toBe(true); + }); + + it('treats {} and { column_settings: {} } as equal', () => { + const cmp = getOverridesComparator(); + expect(runComparator(cmp, undefined, undefined, {}, { column_settings: {} })).toBe(true); + }); + + it('treats explicit defaults together as equal to {}', () => { + const cmp = getOverridesComparator(); + expect( + runComparator( + cmp, + undefined, + undefined, + {}, + { sort: [], column_order: [], column_settings: {} } + ) + ).toBe(true); + }); + + it('detects different sort', () => { + const cmp = getOverridesComparator(); + expect( + runComparator( + cmp, + undefined, + undefined, + {}, + { sort: [{ name: '@timestamp', direction: 'desc' }] } + ) + ).toBe(false); + expect( + runComparator( + cmp, + undefined, + undefined, + { sort: [] }, + { sort: [{ name: '@timestamp', direction: 'desc' }] } + ) + ).toBe(false); + }); + + it('detects different column_order', () => { + const cmp = getOverridesComparator(); + expect( + runComparator(cmp, undefined, undefined, { column_order: ['a'] }, { column_order: ['b'] }) + ).toBe(false); + }); + + it('detects different column_settings', () => { + const cmp = getOverridesComparator(); + expect( + runComparator( + cmp, + undefined, + undefined, + { column_settings: { a: { width: 100 } } }, + { column_settings: { a: { width: 200 } } } + ) + ).toBe(false); + }); + + it('compares other override keys with deep equality', () => { + const cmp = getOverridesComparator(); + expect( + runComparator(cmp, undefined, undefined, { sample_size: 500 }, { sample_size: 500 }) + ).toBe(true); + expect( + runComparator(cmp, undefined, undefined, { sample_size: 500 }, { sample_size: 100 }) + ).toBe(false); + }); + + it('treats undefined overrides as {}', () => { + const cmp = getOverridesComparator(); + expect(runComparator(cmp, undefined, undefined, undefined, { sort: [] })).toBe(true); + expect(runComparator(cmp, undefined, undefined, { sort: [] }, undefined)).toBe(true); + }); + }); +}); diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/get_search_embeddable_comparators.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/get_search_embeddable_comparators.ts new file mode 100644 index 0000000000000..19e44ad9b678e --- /dev/null +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/get_search_embeddable_comparators.ts @@ -0,0 +1,88 @@ +/* + * 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 { StateComparators } from '@kbn/presentation-publishing'; +import { isEqual, isUndefined, omit, omitBy } from 'lodash'; +import type { + EditableSavedSearchAttributes, + SearchEmbeddableBaseState, + SearchEmbeddableByReferenceState, + SearchEmbeddableByValueState, +} from '../../../common/embeddable/types'; +import type { + DiscoverSessionEmbeddableByReferenceProps, + DiscoverSessionEmbeddableByValueProps, +} from '../../../server'; + +type SearchEmbeddableStateAttrs = EditableSavedSearchAttributes & + ( + | Omit + | Omit + ); + +export function getSearchEmbeddableComparators( + isByValue: boolean, + shouldSkipTabComparators: boolean +): StateComparators { + return { + sort: 'deepEquality', + columns: 'deepEquality', + rowHeight: 'referenceEquality', + sampleSize: 'referenceEquality', + rowsPerPage: 'referenceEquality', + headerRowHeight: 'referenceEquality', + density: 'referenceEquality', + grid: 'deepEquality', + ...(isByValue + ? { attributes: 'skip' } + : { + // While the selected tab is missing or inline editing is in progress, + // skip tab-dependent comparators so unsaved-changes badges don't appear + // until the user explicitly applies a tab change. + selectedTabId: shouldSkipTabComparators ? 'skip' : 'referenceEquality', + savedObjectId: 'skip', + }), + }; +} + +export function getDiscoverSessionEmbeddableComparators( + isByValue: boolean, + shouldSkipTabComparators: boolean +): StateComparators< + DiscoverSessionEmbeddableByValueProps | DiscoverSessionEmbeddableByReferenceProps +> { + return isByValue + ? { + tabs: (prev, next) => { + if (prev == null || next == null) return prev == null && next == null; + if (prev.length !== next.length) return false; + return prev.every((tab, i) => + isEqual(omitBy(prev[i], isUndefined), omitBy(next[i], isUndefined)) + ); + }, + } + : { + // While the selected tab is missing or inline editing is in progress, + // skip tab-dependent comparators so unsaved-changes badges don't appear + // until the user explicitly applies a tab change. + selected_tab_id: shouldSkipTabComparators ? 'skip' : 'referenceEquality', + ref_id: 'skip', + overrides: (prev = {}, next = {}) => { + return ( + isEqual(prev.column_order ?? [], next.column_order ?? []) && + isEqual(prev.column_settings ?? {}, next.column_settings ?? {}) && + isEqual(prev.sort ?? [], next.sort ?? []) && + isEqual( + omit(prev, ['column_order', 'column_settings', 'sort']), + omit(next, ['column_order', 'column_settings', 'sort']) + ) + ); + }, + }; +} diff --git a/src/platform/plugins/shared/discover/server/embeddable/index.ts b/src/platform/plugins/shared/discover/server/embeddable/index.ts index b725929a76736..8935e152c26ff 100644 --- a/src/platform/plugins/shared/discover/server/embeddable/index.ts +++ b/src/platform/plugins/shared/discover/server/embeddable/index.ts @@ -13,6 +13,8 @@ export type { DiscoverSessionEsqlTab, DiscoverSessionTab, DiscoverSessionPanelOverrides, + DiscoverSessionEmbeddableByValueProps, + DiscoverSessionEmbeddableByReferenceProps, DiscoverSessionEmbeddableByValueState, DiscoverSessionEmbeddableByReferenceState, DiscoverSessionEmbeddableState, diff --git a/src/platform/plugins/shared/discover/server/embeddable/schema.ts b/src/platform/plugins/shared/discover/server/embeddable/schema.ts index d9286e7a1bb72..4da08cc28bc9d 100644 --- a/src/platform/plugins/shared/discover/server/embeddable/schema.ts +++ b/src/platform/plugins/shared/discover/server/embeddable/schema.ts @@ -349,33 +349,35 @@ function withPanelSchemas

( ); } -const getDiscoverSessionByValueEmbeddableSchema = withPanelSchemas( - schema.object({ - tabs: schema.arrayOf(tabSchema, { - minSize: 1, - maxSize: 1, - meta: { - description: - 'Inline tab configuration. Used when no `ref_id` is set. Currently supports one tab.', - }, - }), +const discoverSessionByValuePropsSchema = schema.object({ + tabs: schema.arrayOf(tabSchema, { + minSize: 1, + maxSize: 1, + meta: { + description: + 'Inline tab configuration. Used when no `ref_id` is set. Currently supports one tab.', + }, }), +}); +const getDiscoverSessionByValueEmbeddableSchema = withPanelSchemas( + discoverSessionByValuePropsSchema, { meta: BY_VALUE_SCHEMA_META } ); +const discoverSessionByReferencePropsSchema = schema.object({ + ref_id: schema.string(), + selected_tab_id: schema.maybe( + schema.string({ + meta: { + description: + 'Tab to select from the referenced saved object. If omitted, defaults to the first tab.', + }, + }) + ), + overrides: panelOverridesSchema, +}); const getDiscoverSessionByReferenceEmbeddableSchema = withPanelSchemas( - schema.object({ - ref_id: schema.string(), - selected_tab_id: schema.maybe( - schema.string({ - meta: { - description: - 'Tab to select from the referenced saved object. If omitted, defaults to the first tab.', - }, - }) - ), - overrides: panelOverridesSchema, - }), + discoverSessionByReferencePropsSchema, { meta: BY_REF_SCHEMA_META } ); @@ -391,6 +393,12 @@ export type DiscoverSessionPanelOverrides = TypeOf; export type DiscoverSessionClassicTab = TypeOf; export type DiscoverSessionEsqlTab = TypeOf; export type DiscoverSessionTab = TypeOf; +export type DiscoverSessionEmbeddableByValueProps = TypeOf< + typeof discoverSessionByValuePropsSchema +>; +export type DiscoverSessionEmbeddableByReferenceProps = TypeOf< + typeof discoverSessionByReferencePropsSchema +>; export type DiscoverSessionEmbeddableByValueState = TypeOf< ReturnType diff --git a/src/platform/plugins/shared/discover/server/index.ts b/src/platform/plugins/shared/discover/server/index.ts index 38793c79bdb04..84d39e92b48c6 100644 --- a/src/platform/plugins/shared/discover/server/index.ts +++ b/src/platform/plugins/shared/discover/server/index.ts @@ -45,6 +45,8 @@ export type { DiscoverSessionEsqlTab, DiscoverSessionTab, DiscoverSessionPanelOverrides, + DiscoverSessionEmbeddableByValueProps, + DiscoverSessionEmbeddableByReferenceProps, DiscoverSessionEmbeddableByValueState, DiscoverSessionEmbeddableByReferenceState, DiscoverSessionEmbeddableState,