diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 1b9bb36366d4d..6b8e39a9f1b38 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -162,7 +162,7 @@ describe('checking migration metadata changes on all registered SO types', () => "risk-engine-configuration": "533a0a3f2dbef1c95129146ec4d5714de305be1a", "rules-settings": "53f94e5ce61f5e75d55ab8adbc1fb3d0937d2e0b", "sample-data-telemetry": "c38daf1a49ed24f2a4fb091e6e1e833fccf19935", - "search": "33a40cd7fc42cbeabe8e4237fc8377727ae375f7", + "search": "edb9e325564c504e6c49f94115509dc28ccc4b81", "search-session": "fae0dfc63274d6a3b90ca583802c48cab8760637", "search-telemetry": "1bbaf2db531b97fa04399440fa52d46e86d54dd8", "search_playground": "3eba7e7c4563f03f76aea02f5dd3a7a739bf51a3", @@ -999,6 +999,7 @@ describe('checking migration metadata changes on all registered SO types', () => "search|global: ce649a79d99c5ff5eb68d544635428ef87946d84", "search|mappings: 432d4dfdb5a33ce29d00ccdcfcda70d7c5f94b52", "search|schemas: 8d6477e08dfdf20335752a69994646f9da90741f", + "search|10.6.0: 4f14cdd86c1bdf96f5390706142438c940236220", "search|10.5.0: 976f308f7d6c229d3ff39eee58f0322aa945e0ae", "search|10.4.0: 53bcea1cfac6ec3826e47ddf2ef85b3e3d428bd1", "search|10.3.0: df30d50a6e40e7b0ed137b7a4517fa4848fee785", @@ -1327,7 +1328,7 @@ describe('checking migration metadata changes on all registered SO types', () => "risk-engine-configuration": "10.2.0", "rules-settings": "10.1.0", "sample-data-telemetry": "10.0.0", - "search": "10.5.0", + "search": "10.6.0", "search-session": "10.0.0", "search-telemetry": "10.0.0", "search_playground": "10.1.0", @@ -1474,7 +1475,7 @@ describe('checking migration metadata changes on all registered SO types', () => "risk-engine-configuration": "10.2.0", "rules-settings": "10.1.0", "sample-data-telemetry": "0.0.0", - "search": "10.5.0", + "search": "10.6.0", "search-session": "8.6.0", "search-telemetry": "7.12.0", "search_playground": "10.1.0", diff --git a/src/platform/plugins/shared/discover/common/embeddable/index.ts b/src/platform/plugins/shared/discover/common/embeddable/index.ts index f0e863af4b806..6add62fbd4ed7 100644 --- a/src/platform/plugins/shared/discover/common/embeddable/index.ts +++ b/src/platform/plugins/shared/discover/common/embeddable/index.ts @@ -8,3 +8,4 @@ */ export { inject, extract } from './search_inject_extract'; +export { searchEmbeddableTransforms } from './search_embeddable_transforms'; diff --git a/src/platform/plugins/shared/discover/common/embeddable/search_embeddable_transforms.test.ts b/src/platform/plugins/shared/discover/common/embeddable/search_embeddable_transforms.test.ts new file mode 100644 index 0000000000000..d58a417209b87 --- /dev/null +++ b/src/platform/plugins/shared/discover/common/embeddable/search_embeddable_transforms.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { searchEmbeddableTransforms } from './search_embeddable_transforms'; +import type { SearchEmbeddableSerializedState } from '../../public'; + +describe('searchEmbeddableTransforms', () => { + describe('transformOut', () => { + it('returns the state unchanged if attributes are not present', () => { + const state: SearchEmbeddableSerializedState = { + title: 'Test Title', + description: 'Test Description', + }; + const result = searchEmbeddableTransforms.transformOut?.(state); + expect(result).toEqual(state); + }); + + it('transforms the state by extracting tabs if attributes are present', () => { + const state = { + title: 'Test Title', + description: 'Test Description', + attributes: { + title: 'Test Title', + description: 'Test Description', + columns: ['column1', 'column2'], + }, + } as SearchEmbeddableSerializedState; + const expectedAttributes = { + title: 'Test Title', + description: 'Test Description', + columns: ['column1', 'column2'], + tabs: [ + { + id: expect.any(String), + label: 'Untitled', + attributes: { + columns: ['column1', 'column2'], + }, + }, + ], + }; + const result = searchEmbeddableTransforms.transformOut?.(state); + expect(result).toEqual({ + ...state, + attributes: expectedAttributes, + }); + }); + + it('handles empty attributes gracefully', () => { + const state = { + title: 'Test Title', + description: 'Test Description', + attributes: {}, + } as SearchEmbeddableSerializedState; + const expectedAttributes = { + tabs: [ + { + id: expect.any(String), + label: 'Untitled', + attributes: {}, + }, + ], + }; + const result = searchEmbeddableTransforms.transformOut?.(state); + expect(result).toEqual({ + ...state, + attributes: expectedAttributes, + }); + }); + }); +}); diff --git a/src/platform/plugins/shared/discover/common/embeddable/search_embeddable_transforms.ts b/src/platform/plugins/shared/discover/common/embeddable/search_embeddable_transforms.ts new file mode 100644 index 0000000000000..37e1cfba0016f --- /dev/null +++ b/src/platform/plugins/shared/discover/common/embeddable/search_embeddable_transforms.ts @@ -0,0 +1,20 @@ +/* + * 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 { EmbeddableTransforms } from '@kbn/embeddable-plugin/common'; +import { extractTabs } from '@kbn/saved-search-plugin/common'; +import type { SearchEmbeddableSerializedState } from '../../public'; + +export const searchEmbeddableTransforms: EmbeddableTransforms = { + transformOut: (state) => { + if (!state.attributes) return state; + const attributes = extractTabs(state.attributes); + return { ...state, attributes }; + }, +}; 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 fde7f363a1fec..8842753265345 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 @@ -165,6 +165,7 @@ export const getSearchEmbeddableFactory = ({ timeRestore: 'skip', usesAdHocDataView: 'skip', visContext: 'skip', + tabs: 'skip', }; }, onReset: async (lastSaved) => { diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts index bc7195e47418a..7784e4ed1ae92 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts @@ -129,11 +129,22 @@ describe('Serialization utils', () => { serializeDynamicActions: jest.fn(), }); + const attributes = toSavedSearchAttributes( + savedSearch, + searchSource.serialize().searchSourceJSON + ); + expect(serializedState).toEqual({ rawState: { type: 'search', attributes: { - ...toSavedSearchAttributes(savedSearch, searchSource.serialize().searchSourceJSON), + ...attributes, + tabs: [ + { + ...attributes.tabs![0]!, + id: expect.any(String), + }, + ], references: mockedSavedSearchAttributes.references, }, }, diff --git a/src/platform/plugins/shared/discover/public/plugin.tsx b/src/platform/plugins/shared/discover/public/plugin.tsx index 19b58c461bb85..84d7303eeca16 100644 --- a/src/platform/plugins/shared/discover/public/plugin.tsx +++ b/src/platform/plugins/shared/discover/public/plugin.tsx @@ -451,6 +451,11 @@ export class DiscoverPlugin discoverServices, }); }); + + plugins.embeddable.registerTransforms(SEARCH_EMBEDDABLE_TYPE, async () => { + const { searchEmbeddableTransforms } = await getEmbeddableServices(); + return searchEmbeddableTransforms; + }); } } diff --git a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts index 6379bfd0d17f4..e53c3806fe7a7 100644 --- a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts +++ b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts @@ -10,3 +10,4 @@ export { ViewSavedSearchAction } from '../embeddable/actions/view_saved_search_action'; export { getSearchEmbeddableFactory } from '../embeddable/get_search_embeddable_factory'; export { getLegacyLogStreamEmbeddableFactory } from '../embeddable/get_legacy_log_stream_embeddable_factory'; +export { searchEmbeddableTransforms } from '../../common/embeddable'; diff --git a/src/platform/plugins/shared/discover/server/plugin.ts b/src/platform/plugins/shared/discover/server/plugin.ts index d03d23eac6f73..57e71734ccb4a 100644 --- a/src/platform/plugins/shared/discover/server/plugin.ts +++ b/src/platform/plugins/shared/discover/server/plugin.ts @@ -14,6 +14,7 @@ import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import type { SharePluginSetup } from '@kbn/share-plugin/server'; import type { PluginInitializerContext } from '@kbn/core/server'; +import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils'; import type { DiscoverServerPluginStart, DiscoverServerPluginStartDeps } from '.'; import { DISCOVER_APP_LOCATOR } from '../common'; import { capabilitiesProvider } from './capabilities_provider'; @@ -24,6 +25,7 @@ import { getUiSettings } from './ui_settings'; import type { ConfigSchema } from './config'; import { appLocatorGetLocationCommon } from '../common/app_locator_get_location'; import { TRACES_PRODUCT_FEATURE_ID } from '../common/constants'; +import { searchEmbeddableTransforms } from '../common/embeddable'; export class DiscoverServerPlugin implements Plugin @@ -60,6 +62,7 @@ export class DiscoverServerPlugin } plugins.embeddable.registerEmbeddableFactory(createSearchEmbeddableFactory()); + plugins.embeddable.registerTransforms(SEARCH_EMBEDDABLE_TYPE, searchEmbeddableTransforms); core.pricing.registerProductFeatures([ { diff --git a/src/platform/plugins/shared/embeddable/server/plugin.ts b/src/platform/plugins/shared/embeddable/server/plugin.ts index 1fd3b28b4198e..872d24a22b4b7 100644 --- a/src/platform/plugins/shared/embeddable/server/plugin.ts +++ b/src/platform/plugins/shared/embeddable/server/plugin.ts @@ -32,7 +32,6 @@ import { } from './persistable_state'; import { getAllMigrations } from './persistable_state/get_all_migrations'; import { EmbeddableTransforms } from '../common'; -import { getTransforms, registerTransforms } from './transforms_registry'; export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; @@ -49,12 +48,19 @@ export class EmbeddableServerPlugin implements Plugin } = {}; public setup(core: CoreSetup) { this.migrateFn = getMigrateFunction(this.getEmbeddableFactory, this.getEnhancement); return { registerEmbeddableFactory: this.registerEmbeddableFactory, - registerTransforms, + registerTransforms: (type: string, transforms: EmbeddableTransforms) => { + if (this.transformsRegistry[type]) { + throw new Error(`Embeddable transforms for type "${type}" are already registered.`); + } + + this.transformsRegistry[type] = transforms; + }, registerEnhancement: this.registerEnhancement, telemetry: getTelemetryFunction(this.getEmbeddableFactory, this.getEnhancement), extract: getExtractFunction(this.getEmbeddableFactory, this.getEnhancement), @@ -70,7 +76,9 @@ export class EmbeddableServerPlugin implements Plugin { + return this.transformsRegistry[type]; + }, telemetry: getTelemetryFunction(this.getEmbeddableFactory, this.getEnhancement), extract: getExtractFunction(this.getEmbeddableFactory, this.getEnhancement), inject: getInjectFunction(this.getEmbeddableFactory, this.getEnhancement), diff --git a/src/platform/plugins/shared/saved_search/common/index.ts b/src/platform/plugins/shared/saved_search/common/index.ts index c29caaf9bcde7..491eaee1135eb 100644 --- a/src/platform/plugins/shared/saved_search/common/index.ts +++ b/src/platform/plugins/shared/saved_search/common/index.ts @@ -9,6 +9,7 @@ export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url'; export { fromSavedSearchAttributes } from './saved_searches_utils'; +export { extractTabs, extractTabsBackfillFn } from './service/extract_tabs'; export type { DiscoverGridSettings, diff --git a/src/platform/plugins/shared/saved_search/common/saved_searches_utils.ts b/src/platform/plugins/shared/saved_search/common/saved_searches_utils.ts index 9ed5e023a65b9..bbc48c5b64e5b 100644 --- a/src/platform/plugins/shared/saved_search/common/saved_searches_utils.ts +++ b/src/platform/plugins/shared/saved_search/common/saved_searches_utils.ts @@ -48,5 +48,6 @@ export const fromSavedSearchAttributes = < breakdownField: attributes.breakdownField, visContext: attributes.visContext, density: attributes.density, + tabs: attributes.tabs, managed, } as ReturnType); diff --git a/src/platform/plugins/shared/saved_search/common/service/extract_tabs.test.ts b/src/platform/plugins/shared/saved_search/common/service/extract_tabs.test.ts new file mode 100644 index 0000000000000..ba3de8e3686c8 --- /dev/null +++ b/src/platform/plugins/shared/saved_search/common/service/extract_tabs.test.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import type { SavedObjectModelTransformationContext } from '@kbn/core-saved-objects-server'; +import type { TypeOf } from '@kbn/config-schema'; +import type { SCHEMA_SEARCH_MODEL_VERSION_5 } from '../../server/saved_objects/schema'; +import { extractTabs, extractTabsBackfillFn, SavedSearchType, VIEW_MODE } from '..'; + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid'), +})); + +const mockContext = {} as SavedObjectModelTransformationContext; + +describe('extractTabs', () => { + describe('extractTabs', () => { + it('should extract title and description and move the rest to tabs.attributes', () => { + const attributes: TypeOf = { + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":"service.type: \\"elasticsearch\\""},"highlightAll":true,"fields":[{"field":"*","include_unmapped":true}],"sort":[{"@timestamp":{"order":"desc","format":"strict_date_optional_time"}},{"_doc":"desc"}],"filter":[{"meta":{"disabled":false,"negate":false,"alias":null,"key":"service.type","field":"service.type","params":{"query":"elasticsearch"},"type":"phrase","indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"},"query":{"match_phrase":{"service.type":"elasticsearch"}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + title: 'my_title', + sort: [['@timestamp', 'desc']], + columns: ['message'], + description: 'my description', + grid: {}, + hideChart: false, + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + isTextBasedQuery: false, + timeRestore: false, + }; + + const result = extractTabs(attributes); + + expect(result.title).toBe(attributes.title); + expect(result.description).toBe(attributes.description); + expect(result.tabs).toBeInstanceOf(Array); + expect(result).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "message", + ], + "description": "my description", + "grid": Object {}, + "hideChart": false, + "isTextBasedQuery": false, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"service.type: \\\\\\"elasticsearch\\\\\\"\\"},\\"highlightAll\\":true,\\"fields\\":[{\\"field\\":\\"*\\",\\"include_unmapped\\":true}],\\"sort\\":[{\\"@timestamp\\":{\\"order\\":\\"desc\\",\\"format\\":\\"strict_date_optional_time\\"}},{\\"_doc\\":\\"desc\\"}],\\"filter\\":[{\\"meta\\":{\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"key\\":\\"service.type\\",\\"field\\":\\"service.type\\",\\"params\\":{\\"query\\":\\"elasticsearch\\"},\\"type\\":\\"phrase\\",\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"},\\"query\\":{\\"match_phrase\\":{\\"service.type\\":\\"elasticsearch\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}],\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "sort": Array [ + Array [ + "@timestamp", + "desc", + ], + ], + "tabs": Array [ + Object { + "attributes": Object { + "columns": Array [ + "message", + ], + "grid": Object {}, + "hideChart": false, + "isTextBasedQuery": false, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"service.type: \\\\\\"elasticsearch\\\\\\"\\"},\\"highlightAll\\":true,\\"fields\\":[{\\"field\\":\\"*\\",\\"include_unmapped\\":true}],\\"sort\\":[{\\"@timestamp\\":{\\"order\\":\\"desc\\",\\"format\\":\\"strict_date_optional_time\\"}},{\\"_doc\\":\\"desc\\"}],\\"filter\\":[{\\"meta\\":{\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"key\\":\\"service.type\\",\\"field\\":\\"service.type\\",\\"params\\":{\\"query\\":\\"elasticsearch\\"},\\"type\\":\\"phrase\\",\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"},\\"query\\":{\\"match_phrase\\":{\\"service.type\\":\\"elasticsearch\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}],\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "sort": Array [ + Array [ + "@timestamp", + "desc", + ], + ], + "timeRestore": false, + "viewMode": "documents", + }, + "id": "mock-uuid", + "label": "Untitled", + }, + ], + "timeRestore": false, + "title": "my_title", + "viewMode": "documents", + } + `); + }); + }); + + describe('extractTabsBackfillFn', () => { + it('should wrap the result of extractTabs in an object', () => { + const attributes: TypeOf = { + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":"service.type: \\"elasticsearch\\""},"highlightAll":true,"fields":[{"field":"*","include_unmapped":true}],"sort":[{"@timestamp":{"order":"desc","format":"strict_date_optional_time"}},{"_doc":"desc"}],"filter":[{"meta":{"disabled":false,"negate":false,"alias":null,"key":"service.type","field":"service.type","params":{"query":"elasticsearch"},"type":"phrase","indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"},"query":{"match_phrase":{"service.type":"elasticsearch"}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + title: 'my_title', + sort: [['@timestamp', 'desc']], + columns: ['message'], + description: 'my description', + grid: {}, + hideChart: false, + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + isTextBasedQuery: false, + timeRestore: false, + }; + + const prevDoc = { id: '123', type: SavedSearchType, attributes }; + + const result = extractTabsBackfillFn(prevDoc, mockContext); + + expect(result).toHaveProperty('attributes'); + expect(result).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "columns": Array [ + "message", + ], + "description": "my description", + "grid": Object {}, + "hideChart": false, + "isTextBasedQuery": false, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"service.type: \\\\\\"elasticsearch\\\\\\"\\"},\\"highlightAll\\":true,\\"fields\\":[{\\"field\\":\\"*\\",\\"include_unmapped\\":true}],\\"sort\\":[{\\"@timestamp\\":{\\"order\\":\\"desc\\",\\"format\\":\\"strict_date_optional_time\\"}},{\\"_doc\\":\\"desc\\"}],\\"filter\\":[{\\"meta\\":{\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"key\\":\\"service.type\\",\\"field\\":\\"service.type\\",\\"params\\":{\\"query\\":\\"elasticsearch\\"},\\"type\\":\\"phrase\\",\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"},\\"query\\":{\\"match_phrase\\":{\\"service.type\\":\\"elasticsearch\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}],\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "sort": Array [ + Array [ + "@timestamp", + "desc", + ], + ], + "tabs": Array [ + Object { + "attributes": Object { + "columns": Array [ + "message", + ], + "grid": Object {}, + "hideChart": false, + "isTextBasedQuery": false, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"service.type: \\\\\\"elasticsearch\\\\\\"\\"},\\"highlightAll\\":true,\\"fields\\":[{\\"field\\":\\"*\\",\\"include_unmapped\\":true}],\\"sort\\":[{\\"@timestamp\\":{\\"order\\":\\"desc\\",\\"format\\":\\"strict_date_optional_time\\"}},{\\"_doc\\":\\"desc\\"}],\\"filter\\":[{\\"meta\\":{\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"key\\":\\"service.type\\",\\"field\\":\\"service.type\\",\\"params\\":{\\"query\\":\\"elasticsearch\\"},\\"type\\":\\"phrase\\",\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"},\\"query\\":{\\"match_phrase\\":{\\"service.type\\":\\"elasticsearch\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}],\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "sort": Array [ + Array [ + "@timestamp", + "desc", + ], + ], + "timeRestore": false, + "viewMode": "documents", + }, + "id": "mock-uuid", + "label": "Untitled", + }, + ], + "timeRestore": false, + "title": "my_title", + "viewMode": "documents", + }, + } + `); + }); + }); +}); diff --git a/src/platform/plugins/shared/saved_search/common/service/extract_tabs.ts b/src/platform/plugins/shared/saved_search/common/service/extract_tabs.ts new file mode 100644 index 0000000000000..f133fbbc70d09 --- /dev/null +++ b/src/platform/plugins/shared/saved_search/common/service/extract_tabs.ts @@ -0,0 +1,45 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { SavedObjectModelDataBackfillFn } from '@kbn/core-saved-objects-server'; +import type { TypeOf } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import type { + SCHEMA_SEARCH_MODEL_VERSION_5, + SCHEMA_SEARCH_MODEL_VERSION_6, +} from '../../server/saved_objects/schema'; + +export const extractTabsBackfillFn: SavedObjectModelDataBackfillFn< + TypeOf, + TypeOf +> = (prevDoc) => { + const attributes = extractTabs(prevDoc.attributes); + return { attributes }; +}; + +/** + * Extract tab attributes into a separate array since multiple tabs are supported + * @param attributes The previous attributes to be transformed (version 5) + */ +export const extractTabs = ( + attributes: TypeOf +): TypeOf => { + const { title, description, ...tabAttrs } = attributes; + const tabs = [ + { + id: uuidv4(), + label: i18n.translate('savedSearch.defaultTabLabel', { + defaultMessage: 'Untitled', + }), + attributes: tabAttrs, + }, + ]; + return { ...attributes, tabs }; +}; diff --git a/src/platform/plugins/shared/saved_search/common/service/get_saved_searches.test.ts b/src/platform/plugins/shared/saved_search/common/service/get_saved_searches.test.ts index f655f913118f0..2f68f2f0eecbe 100644 --- a/src/platform/plugins/shared/saved_search/common/service/get_saved_searches.test.ts +++ b/src/platform/plugins/shared/saved_search/common/service/get_saved_searches.test.ts @@ -145,6 +145,7 @@ describe('getSavedSearch', () => { "desc", ], ], + "tabs": undefined, "tags": undefined, "timeRange": undefined, "timeRestore": undefined, @@ -255,6 +256,7 @@ describe('getSavedSearch', () => { "desc", ], ], + "tabs": undefined, "tags": undefined, "timeRange": undefined, "timeRestore": undefined, diff --git a/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.test.ts b/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.test.ts index d1efd8260f1cf..fe71a4a355d3f 100644 --- a/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.test.ts +++ b/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.test.ts @@ -88,6 +88,7 @@ describe('saved_searches_utils', () => { }, "sharingSavedObjectProps": Object {}, "sort": Array [], + "tabs": undefined, "tags": Array [ "tags-1", "tags-2", @@ -119,41 +120,39 @@ describe('saved_searches_utils', () => { managed: false, }; - expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(` - Object { - "breakdownField": undefined, - "columns": Array [ - "c", - "d", - ], - "density": undefined, - "description": "description", - "grid": Object {}, - "headerRowHeight": undefined, - "hideAggregatedPreview": undefined, - "hideChart": true, - "isTextBasedQuery": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", + const result = toSavedSearchAttributes(savedSearch, '{}'); + expect(result).toEqual({ + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + title: 'title', + sort: [['a', 'asc']], + columns: ['c', 'd'], + description: 'description', + grid: {}, + hideChart: true, + isTextBasedQuery: true, + usesAdHocDataView: false, + timeRestore: false, + tabs: [ + { + id: expect.any(String), + label: 'Untitled', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + sort: [['a', 'asc']], + columns: ['c', 'd'], + grid: {}, + hideChart: true, + isTextBasedQuery: true, + usesAdHocDataView: false, + timeRestore: false, + }, }, - "refreshInterval": undefined, - "rowHeight": undefined, - "rowsPerPage": undefined, - "sampleSize": undefined, - "sort": Array [ - Array [ - "a", - "asc", - ], - ], - "timeRange": undefined, - "timeRestore": false, - "title": "title", - "usesAdHocDataView": false, - "viewMode": undefined, - "visContext": undefined, - } - `); + ], + }); }); }); }); diff --git a/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.ts b/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.ts index 1f496425eb16c..c7bdb7311aa4d 100644 --- a/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.ts +++ b/src/platform/plugins/shared/saved_search/common/service/saved_searches_utils.ts @@ -11,7 +11,7 @@ import type { SavedObjectReference } from '@kbn/core-saved-objects-server'; import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { pick } from 'lodash'; import type { SavedSearch, SavedSearchAttributes } from '..'; -import { fromSavedSearchAttributes as fromSavedSearchAttributesCommon } from '..'; +import { extractTabs, fromSavedSearchAttributes as fromSavedSearchAttributesCommon } from '..'; import type { SerializableSavedSearch } from '../types'; export { getSavedSearchFullPathUrl, getSavedSearchUrl } from '..'; @@ -34,26 +34,27 @@ export const fromSavedSearchAttributes = ( export const toSavedSearchAttributes = ( savedSearch: SavedSearch, searchSourceJSON: string -): SavedSearchAttributes => ({ - kibanaSavedObjectMeta: { searchSourceJSON }, - title: savedSearch.title ?? '', - sort: savedSearch.sort ?? [], - columns: savedSearch.columns ?? [], - description: savedSearch.description ?? '', - grid: savedSearch.grid ?? {}, - hideChart: savedSearch.hideChart ?? false, - viewMode: savedSearch.viewMode, - hideAggregatedPreview: savedSearch.hideAggregatedPreview, - rowHeight: savedSearch.rowHeight, - headerRowHeight: savedSearch.headerRowHeight, - isTextBasedQuery: savedSearch.isTextBasedQuery ?? false, - usesAdHocDataView: savedSearch.usesAdHocDataView, - timeRestore: savedSearch.timeRestore ?? false, - timeRange: savedSearch.timeRange ? pick(savedSearch.timeRange, ['from', 'to']) : undefined, - refreshInterval: savedSearch.refreshInterval, - rowsPerPage: savedSearch.rowsPerPage, - sampleSize: savedSearch.sampleSize, - density: savedSearch.density, - breakdownField: savedSearch.breakdownField, - visContext: savedSearch.visContext, -}); +): SavedSearchAttributes => + extractTabs({ + kibanaSavedObjectMeta: { searchSourceJSON }, + title: savedSearch.title ?? '', + sort: savedSearch.sort ?? [], + columns: savedSearch.columns ?? [], + description: savedSearch.description ?? '', + grid: savedSearch.grid ?? {}, + hideChart: savedSearch.hideChart ?? false, + viewMode: savedSearch.viewMode, + hideAggregatedPreview: savedSearch.hideAggregatedPreview, + rowHeight: savedSearch.rowHeight, + headerRowHeight: savedSearch.headerRowHeight, + isTextBasedQuery: savedSearch.isTextBasedQuery ?? false, + usesAdHocDataView: savedSearch.usesAdHocDataView, + timeRestore: savedSearch.timeRestore ?? false, + timeRange: savedSearch.timeRange ? pick(savedSearch.timeRange, ['from', 'to']) : undefined, + refreshInterval: savedSearch.refreshInterval, + rowsPerPage: savedSearch.rowsPerPage, + sampleSize: savedSearch.sampleSize, + density: savedSearch.density, + breakdownField: savedSearch.breakdownField, + visContext: savedSearch.visContext, + }) as SavedSearchAttributes; diff --git a/src/platform/plugins/shared/saved_search/common/types.ts b/src/platform/plugins/shared/saved_search/common/types.ts index 965787b86568b..f24cbc1cdc8c4 100644 --- a/src/platform/plugins/shared/saved_search/common/types.ts +++ b/src/platform/plugins/shared/saved_search/common/types.ts @@ -18,6 +18,7 @@ import type { SavedObjectsResolveResponse } from '@kbn/core/server'; import type { SerializableRecord } from '@kbn/utility-types'; import type { DataGridDensity } from '@kbn/unified-data-table'; import type { SortOrder } from '@kbn/discover-utils'; +import type { DiscoverSessionTab } from '../server'; import type { VIEW_MODE } from '.'; export interface DiscoverGridSettings extends SerializableRecord { @@ -69,6 +70,8 @@ export interface SavedSearchAttributes { breakdownField?: string; density?: DataGridDensity; visContext?: VisContextUnmapped; + + tabs?: DiscoverSessionTab[]; } /** @internal **/ diff --git a/src/platform/plugins/shared/saved_search/public/services/saved_searches/save_saved_searches.test.ts b/src/platform/plugins/shared/saved_search/public/services/saved_searches/save_saved_searches.test.ts index 672ac0eefd79b..ae2ab38213d8e 100644 --- a/src/platform/plugins/shared/saved_search/public/services/saved_searches/save_saved_searches.test.ts +++ b/src/platform/plugins/shared/saved_search/public/services/saved_searches/save_saved_searches.test.ts @@ -138,6 +138,23 @@ describe('saveSavedSearch', () => { title: 'title', usesAdHocDataView: undefined, viewMode: undefined, + tabs: [ + { + id: expect.any(String), + label: 'Untitled', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + sort: [], + columns: [], + grid: {}, + hideChart: false, + isTextBasedQuery: false, + timeRestore: false, + }, + }, + ], }, options: { references: [] }, }); @@ -174,6 +191,23 @@ describe('saveSavedSearch', () => { timeRestore: false, usesAdHocDataView: undefined, viewMode: undefined, + tabs: [ + { + id: expect.any(String), + label: 'Untitled', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + sort: [], + columns: [], + grid: {}, + hideChart: false, + isTextBasedQuery: false, + timeRestore: false, + }, + }, + ], }, id: 'id', options: { references: [] }, @@ -225,6 +259,23 @@ describe('saveSavedSearch', () => { title: 'title', usesAdHocDataView: undefined, viewMode: undefined, + tabs: [ + { + id: expect.any(String), + label: 'Untitled', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + sort: [], + columns: [], + grid: {}, + hideChart: false, + isTextBasedQuery: false, + timeRestore: false, + }, + }, + ], }, id: 'id', options: { references: ['tag-1', 'tag-2'] }, diff --git a/src/platform/plugins/shared/saved_search/public/services/saved_searches/to_saved_search.test.ts b/src/platform/plugins/shared/saved_search/public/services/saved_searches/to_saved_search.test.ts index 9431ea3c01064..3aa69f898c9da 100644 --- a/src/platform/plugins/shared/saved_search/public/services/saved_searches/to_saved_search.test.ts +++ b/src/platform/plugins/shared/saved_search/public/services/saved_searches/to_saved_search.test.ts @@ -100,6 +100,7 @@ describe('toSavedSearch', () => { "desc", ], ], + "tabs": undefined, "tags": undefined, "timeRange": undefined, "timeRestore": undefined, diff --git a/src/platform/plugins/shared/saved_search/server/content_management/saved_search_storage.ts b/src/platform/plugins/shared/saved_search/server/content_management/saved_search_storage.ts index b242159f675ff..5a5e5acd80788 100644 --- a/src/platform/plugins/shared/saved_search/server/content_management/saved_search_storage.ts +++ b/src/platform/plugins/shared/saved_search/server/content_management/saved_search_storage.ts @@ -48,6 +48,7 @@ export class SavedSearchStorage extends SOContentStorage { 'sampleSize', 'density', 'visContext', + 'tabs', ], logger, throwOnResultValidationError, diff --git a/src/platform/plugins/shared/saved_search/server/content_management/schema/v1/cm_services.ts b/src/platform/plugins/shared/saved_search/server/content_management/schema/v1/cm_services.ts index 33f1a6290c10a..078f86a0e525d 100644 --- a/src/platform/plugins/shared/saved_search/server/content_management/schema/v1/cm_services.ts +++ b/src/platform/plugins/shared/saved_search/server/content_management/schema/v1/cm_services.ts @@ -16,99 +16,9 @@ import { updateOptionsSchema, createResultSchema, } from '@kbn/content-management-utils'; -import { - MIN_SAVED_SEARCH_SAMPLE_SIZE, - MAX_SAVED_SEARCH_SAMPLE_SIZE, -} from '../../../../common/constants'; - -const sortSchema = schema.arrayOf(schema.string(), { maxSize: 2 }); - -const savedSearchAttributesSchema = schema.object( - { - title: schema.string(), - sort: schema.oneOf([sortSchema, schema.arrayOf(sortSchema)]), - columns: schema.arrayOf(schema.string()), - description: schema.string(), - grid: schema.object({ - columns: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - width: schema.maybe(schema.number()), - }) - ) - ), - }), - hideChart: schema.maybe(schema.boolean()), - isTextBasedQuery: schema.maybe(schema.boolean()), - usesAdHocDataView: schema.maybe(schema.boolean()), - kibanaSavedObjectMeta: schema.object({ - searchSourceJSON: schema.string(), - }), - viewMode: schema.maybe( - schema.oneOf([ - schema.literal('documents'), - schema.literal('patterns'), - schema.literal('aggregated'), - ]) - ), - hideAggregatedPreview: schema.maybe(schema.boolean()), - rowHeight: schema.maybe(schema.number()), - headerRowHeight: schema.maybe(schema.number()), - hits: schema.maybe(schema.number()), - timeRestore: schema.maybe(schema.boolean()), - timeRange: schema.maybe( - schema.object({ - from: schema.string(), - to: schema.string(), - }) - ), - refreshInterval: schema.maybe( - schema.object({ - pause: schema.boolean(), - value: schema.number(), - }) - ), - rowsPerPage: schema.maybe(schema.number()), - sampleSize: schema.maybe( - schema.number({ - min: MIN_SAVED_SEARCH_SAMPLE_SIZE, - max: MAX_SAVED_SEARCH_SAMPLE_SIZE, - }) - ), - density: schema.maybe( - schema.oneOf([ - schema.literal('compact'), - schema.literal('normal'), - schema.literal('expanded'), - ]) - ), - breakdownField: schema.maybe(schema.string()), - visContext: schema.maybe( - schema.oneOf([ - // existing value - schema.object({ - // unified histogram state - suggestionType: schema.string(), - requestData: schema.object({ - dataViewId: schema.maybe(schema.string()), - timeField: schema.maybe(schema.string()), - timeInterval: schema.maybe(schema.string()), - breakdownField: schema.maybe(schema.string()), - }), - // lens attributes - attributes: schema.recordOf(schema.string(), schema.any()), - }), - // cleared previous value - schema.object({}), - ]) - ), - version: schema.maybe(schema.number()), - }, - { unknowns: 'forbid' } -); +import { SCHEMA_SEARCH_MODEL_VERSION_6 } from '../../../saved_objects/schema'; -const savedSearchSavedObjectSchema = savedObjectSchema(savedSearchAttributesSchema); +const savedSearchSavedObjectSchema = savedObjectSchema(SCHEMA_SEARCH_MODEL_VERSION_6); const savedSearchCreateOptionsSchema = schema.maybe( schema.object({ @@ -146,7 +56,7 @@ export const serviceDefinition: ServicesDefinition = { schema: savedSearchCreateOptionsSchema, }, data: { - schema: savedSearchAttributesSchema, + schema: SCHEMA_SEARCH_MODEL_VERSION_6, }, }, out: { @@ -161,7 +71,7 @@ export const serviceDefinition: ServicesDefinition = { schema: savedSearchUpdateOptionsSchema, }, data: { - schema: savedSearchAttributesSchema, + schema: SCHEMA_SEARCH_MODEL_VERSION_6, }, }, }, diff --git a/src/platform/plugins/shared/saved_search/server/index.ts b/src/platform/plugins/shared/saved_search/server/index.ts index 4ad49f1013f2d..3e6f666bb98f0 100644 --- a/src/platform/plugins/shared/saved_search/server/index.ts +++ b/src/platform/plugins/shared/saved_search/server/index.ts @@ -9,6 +9,7 @@ import type { PluginInitializerContext } from '@kbn/core-plugins-server'; +export type { DiscoverSessionTab } from './saved_objects/schema'; export { getSavedSearch } from './services/saved_searches'; export const plugin = async (initContext: PluginInitializerContext) => { diff --git a/src/platform/plugins/shared/saved_search/server/saved_objects/index.ts b/src/platform/plugins/shared/saved_search/server/saved_objects/index.ts index e2420e681c6b2..4014c92918ad4 100644 --- a/src/platform/plugins/shared/saved_search/server/saved_objects/index.ts +++ b/src/platform/plugins/shared/saved_search/server/saved_objects/index.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +export type { DiscoverSessionTab } from './schema'; export { getSavedSearchObjectType } from './search'; diff --git a/src/platform/plugins/shared/saved_search/server/saved_objects/schema.ts b/src/platform/plugins/shared/saved_search/server/saved_objects/schema.ts index 1639604d8fb95..3a35d8fb53434 100644 --- a/src/platform/plugins/shared/saved_search/server/saved_objects/schema.ts +++ b/src/platform/plugins/shared/saved_search/server/saved_objects/schema.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import { MIN_SAVED_SEARCH_SAMPLE_SIZE, @@ -136,3 +137,19 @@ export const SCHEMA_SEARCH_MODEL_VERSION_5 = SCHEMA_SEARCH_MODEL_VERSION_4.exten schema.oneOf([schema.literal('compact'), schema.literal('normal'), schema.literal('expanded')]) ), }); + +const SCHEMA_DISCOVER_SESSION_TAB = schema.object({ + id: schema.string(), + label: schema.string(), + // Remove `title` and `description` from the tab schema as they exist at the top level of the saved object + attributes: SCHEMA_SEARCH_MODEL_VERSION_5.extends({ + title: undefined, + description: undefined, + }), +}); + +export type DiscoverSessionTab = TypeOf; + +export const SCHEMA_SEARCH_MODEL_VERSION_6 = SCHEMA_SEARCH_MODEL_VERSION_5.extends({ + tabs: schema.maybe(schema.arrayOf(SCHEMA_DISCOVER_SESSION_TAB, { minSize: 1 })), +}); diff --git a/src/platform/plugins/shared/saved_search/server/saved_objects/search.ts b/src/platform/plugins/shared/saved_search/server/saved_objects/search.ts index 63223821ec6cc..6e9fa179eee6e 100644 --- a/src/platform/plugins/shared/saved_search/server/saved_objects/search.ts +++ b/src/platform/plugins/shared/saved_search/server/saved_objects/search.ts @@ -10,6 +10,7 @@ import { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import type { SavedObjectsType } from '@kbn/core/server'; import type { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; +import { extractTabsBackfillFn } from '../../common/service/extract_tabs'; import { getAllMigrations } from './search_migrations'; import { SavedSearchTypeDisplayName } from '../../common/constants'; import { @@ -19,6 +20,7 @@ import { SCHEMA_SEARCH_MODEL_VERSION_3, SCHEMA_SEARCH_MODEL_VERSION_4, SCHEMA_SEARCH_MODEL_VERSION_5, + SCHEMA_SEARCH_MODEL_VERSION_6, } from './schema'; export function getSavedSearchObjectType( @@ -81,6 +83,18 @@ export function getSavedSearchObjectType( create: SCHEMA_SEARCH_MODEL_VERSION_5, }, }, + 6: { + changes: [ + { + type: 'data_backfill', + backfillFn: extractTabsBackfillFn, + }, + ], + schemas: { + forwardCompatibility: SCHEMA_SEARCH_MODEL_VERSION_6.extends({}, { unknowns: 'ignore' }), + create: SCHEMA_SEARCH_MODEL_VERSION_6, + }, + }, }, mappings: { dynamic: false,