[Discover] Optimize Discover plugin page load bundle (#208298)

## Summary

This PR optimizes the Discover page load bundle by reducing it to only
code which is actually required on startup, and dynamically loading
other code when it's needed, resulting in a 55% decrease in the bundle
size.

Before (44.15 KB):

![before](https://github.com/user-attachments/assets/989d1626-4dd7-4710-a9bc-8d80220101eb)

After (20.12 KB):

![after](https://github.com/user-attachments/assets/ff68b367-3293-47cf-9d3f-5c35d0aea27a)

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis McPhee 2025-03-11 17:30:25 -03:00 committed by GitHub
parent 99d8400328
commit e1bffa6a9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 991 additions and 739 deletions

View file

@ -34,7 +34,7 @@ pageLoadAssetSize:
dataViews: 65000
dataVisualizer: 30000
devTools: 38637
discover: 99999
discover: 25000
discoverEnhanced: 42730
discoverShared: 17111
embeddable: 24000

View file

@ -14,9 +14,11 @@ import {
} from '@kbn/kibana-utils-plugin/public';
import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock';
import { FilterStateStore } from '@kbn/es-query';
import { DiscoverAppLocatorDefinition } from './app_locator';
import type { DiscoverAppLocatorParams } from './app_locator';
import { DISCOVER_APP_LOCATOR } from './app_locator';
import type { SerializableRecord } from '@kbn/utility-types';
import { createDataViewDataSource, createEsqlDataSource } from './data_sources';
import { appLocatorGetLocationCommon } from './app_locator_get_location';
const dataViewId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d';
@ -26,11 +28,14 @@ interface SetupParams {
}
const setup = async ({ useHash = false }: SetupParams = {}) => {
const locator = new DiscoverAppLocatorDefinition({ useHash, setStateToKbnUrl });
return {
locator,
const locator = {
id: DISCOVER_APP_LOCATOR,
getLocation: (params: DiscoverAppLocatorParams) => {
return appLocatorGetLocationCommon({ useHash, setStateToKbnUrl }, params);
},
};
return { locator };
};
beforeEach(() => {
@ -267,7 +272,7 @@ describe('Discover url generator', () => {
const { locator } = await setup();
const { state } = await locator.getLocation({ dataViewSpec: dataViewSpecMock });
expect(state.dataViewSpec).toEqual(dataViewSpecMock);
expect((state as Record<string, unknown>).dataViewSpec).toEqual(dataViewSpecMock);
});
describe('useHash property', () => {

View file

@ -9,15 +9,11 @@
import type { SerializableRecord } from '@kbn/utility-types';
import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query';
import { isOfAggregateQueryType } from '@kbn/es-query';
import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public';
import type { RefreshInterval } from '@kbn/data-plugin/public';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
import type { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common';
import type { VIEW_MODE } from './constants';
import type { DiscoverAppState } from '../public';
import { createDataViewDataSource, createEsqlDataSource } from './data_sources';
export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR';
@ -113,11 +109,6 @@ export interface DiscoverAppLocatorParams extends SerializableRecord {
export type DiscoverAppLocator = LocatorPublic<DiscoverAppLocatorParams>;
export interface DiscoverAppLocatorDependencies {
useHash: boolean;
setStateToKbnUrl: typeof setStateToKbnUrl;
}
/**
* Location state of scoped history (history instance of Kibana Platform application service)
*/
@ -126,83 +117,5 @@ export interface MainHistoryLocationState {
isAlertResults?: boolean;
}
export class DiscoverAppLocatorDefinition implements LocatorDefinition<DiscoverAppLocatorParams> {
public readonly id = DISCOVER_APP_LOCATOR;
constructor(protected readonly deps: DiscoverAppLocatorDependencies) {}
public readonly getLocation = async (params: DiscoverAppLocatorParams) => {
const {
useHash = this.deps.useHash,
filters,
dataViewId,
indexPatternId,
dataViewSpec,
query,
refreshInterval,
savedSearchId,
timeRange,
searchSessionId,
columns,
grid,
savedQuery,
sort,
interval,
viewMode,
hideAggregatedPreview,
breakdownField,
isAlertResults,
} = params;
const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
const appState: Partial<DiscoverAppState> = {};
const queryState: GlobalQueryStateFromUrl = {};
const { isFilterPinned } = await import('@kbn/es-query');
if (query) appState.query = query;
if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f));
if (indexPatternId)
appState.dataSource = createDataViewDataSource({ dataViewId: indexPatternId });
if (dataViewId) appState.dataSource = createDataViewDataSource({ dataViewId });
if (isOfAggregateQueryType(query)) appState.dataSource = createEsqlDataSource();
if (columns) appState.columns = columns;
if (grid) appState.grid = grid;
if (savedQuery) appState.savedQuery = savedQuery;
if (sort) appState.sort = sort;
if (interval) appState.interval = interval;
if (timeRange) queryState.time = timeRange;
if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
if (viewMode) appState.viewMode = viewMode;
if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview;
if (breakdownField) appState.breakdownField = breakdownField;
const state: MainHistoryLocationState = {};
if (dataViewSpec) state.dataViewSpec = dataViewSpec;
if (isAlertResults) state.isAlertResults = isAlertResults;
let path = `#/${savedSearchPath}`;
if (searchSessionId) {
path = `${path}?searchSessionId=${searchSessionId}`;
}
if (Object.keys(queryState).length) {
path = this.deps.setStateToKbnUrl<GlobalQueryStateFromUrl>(
'_g',
queryState,
{ useHash },
path
);
}
if (Object.keys(appState).length) {
path = this.deps.setStateToKbnUrl('_a', appState, { useHash }, path);
}
return {
app: 'discover',
path,
state,
};
};
}
export type DiscoverAppLocatorGetLocation =
LocatorDefinition<DiscoverAppLocatorParams>['getLocation'];

View file

@ -0,0 +1,93 @@
/*
* 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 { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
import { isFilterPinned, isOfAggregateQueryType } from '@kbn/es-query';
import type { setStateToKbnUrl as setStateToKbnUrlCommon } from '@kbn/kibana-utils-plugin/common';
import type { DiscoverAppLocatorGetLocation, MainHistoryLocationState } from './app_locator';
import type { DiscoverAppState } from '../public';
import { createDataViewDataSource, createEsqlDataSource } from './data_sources';
export const appLocatorGetLocationCommon = async (
{
useHash: useHashOriginal,
setStateToKbnUrl,
}: {
useHash: boolean;
setStateToKbnUrl: typeof setStateToKbnUrlCommon;
},
...[params]: Parameters<DiscoverAppLocatorGetLocation>
): ReturnType<DiscoverAppLocatorGetLocation> => {
const {
useHash = useHashOriginal,
filters,
dataViewId,
indexPatternId,
dataViewSpec,
query,
refreshInterval,
savedSearchId,
timeRange,
searchSessionId,
columns,
grid,
savedQuery,
sort,
interval,
viewMode,
hideAggregatedPreview,
breakdownField,
isAlertResults,
} = params;
const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
const appState: Partial<DiscoverAppState> = {};
const queryState: GlobalQueryStateFromUrl = {};
if (query) appState.query = query;
if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f));
if (indexPatternId)
appState.dataSource = createDataViewDataSource({ dataViewId: indexPatternId });
if (dataViewId) appState.dataSource = createDataViewDataSource({ dataViewId });
if (isOfAggregateQueryType(query)) appState.dataSource = createEsqlDataSource();
if (columns) appState.columns = columns;
if (grid) appState.grid = grid;
if (savedQuery) appState.savedQuery = savedQuery;
if (sort) appState.sort = sort;
if (interval) appState.interval = interval;
if (timeRange) queryState.time = timeRange;
if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
if (viewMode) appState.viewMode = viewMode;
if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview;
if (breakdownField) appState.breakdownField = breakdownField;
const state: MainHistoryLocationState = {};
if (dataViewSpec) state.dataViewSpec = dataViewSpec;
if (isAlertResults) state.isAlertResults = isAlertResults;
let path = `#/${savedSearchPath}`;
if (searchSessionId) {
path = `${path}?searchSessionId=${searchSessionId}`;
}
if (Object.keys(queryState).length) {
path = setStateToKbnUrl<GlobalQueryStateFromUrl>('_g', queryState, { useHash }, path);
}
if (Object.keys(appState).length) {
path = setStateToKbnUrl('_a', appState, { useHash }, path);
}
return {
app: 'discover',
path,
state,
};
};

View file

@ -7,36 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DISCOVER_ESQL_LOCATOR } from '@kbn/deeplinks-analytics';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common';
import type { SerializableRecord } from '@kbn/utility-types';
import { getIndexForESQLQuery, getInitialESQLQuery, getESQLAdHocDataview } from '@kbn/esql-utils';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
export type DiscoverESQLLocatorParams = SerializableRecord;
export interface DiscoverESQLLocatorDependencies {
discoverAppLocator: LocatorPublic<SerializableRecord>;
dataViews: DataViewsPublicPluginStart;
}
export type DiscoverESQLLocator = LocatorPublic<DiscoverESQLLocatorParams>;
export class DiscoverESQLLocatorDefinition implements LocatorDefinition<DiscoverESQLLocatorParams> {
public readonly id = DISCOVER_ESQL_LOCATOR;
constructor(protected readonly deps: DiscoverESQLLocatorDependencies) {}
public readonly getLocation = async () => {
const { discoverAppLocator, dataViews } = this.deps;
const indexName = (await getIndexForESQLQuery({ dataViews })) ?? '*';
const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews);
const esql = getInitialESQLQuery(dataView);
const params = {
query: { esql },
};
return await discoverAppLocator.getLocation(params);
};
}
export type DiscoverESQLLocatorGetLocation =
LocatorDefinition<DiscoverESQLLocatorParams>['getLocation'];

View file

@ -0,0 +1,32 @@
/*
* 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 { LocatorPublic } from '@kbn/share-plugin/common';
import type { SerializableRecord } from '@kbn/utility-types';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { getESQLAdHocDataview, getIndexForESQLQuery, getInitialESQLQuery } from '@kbn/esql-utils';
import type { DiscoverESQLLocatorGetLocation } from './esql_locator';
export const esqlLocatorGetLocation = async ({
discoverAppLocator,
dataViews,
}: {
discoverAppLocator: LocatorPublic<SerializableRecord>;
dataViews: DataViewsPublicPluginStart;
}): ReturnType<DiscoverESQLLocatorGetLocation> => {
const indexName = (await getIndexForESQLQuery({ dataViews })) ?? '*';
const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews);
const esql = getInitialESQLQuery(dataView);
const params = {
query: { esql },
};
return await discoverAppLocator.getLocation(params);
};

View file

@ -10,12 +10,11 @@
export const PLUGIN_ID = 'discover';
export const APP_ICON = 'discoverApp';
export { DISCOVER_APP_LOCATOR, DiscoverAppLocatorDefinition } from './app_locator';
export { DISCOVER_APP_LOCATOR } from './app_locator';
export type {
DiscoverAppLocator,
DiscoverAppLocatorParams,
MainHistoryLocationState,
} from './app_locator';
export { DiscoverESQLLocatorDefinition } from './esql_locator';
export type { DiscoverESQLLocator, DiscoverESQLLocatorParams } from './esql_locator';

View file

@ -46,7 +46,7 @@ import type { SearchSourceDependencies } from '@kbn/data-plugin/common';
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { createElement } from 'react';
import { createContextAwarenessMocks } from '../context_awareness/__mocks__';
import { DiscoverEBTManager } from '../services/discover_ebt_manager';
import { DiscoverEBTManager } from '../plugin_imports/discover_ebt_manager';
import { discoverSharedPluginMock } from '@kbn/discover-shared-plugin/public/mocks';
import { createUrlTrackerMock } from './url_tracker.mock';

View file

@ -8,7 +8,9 @@
*/
import { getStatesFromKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { DiscoverContextAppLocatorDefinition } from './locator';
import type { DiscoverContextAppLocatorParams } from './locator';
import { DISCOVER_CONTEXT_APP_LOCATOR } from './locator';
import { contextAppLocatorGetLocation } from './locator_get_location';
const dataViewId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
@ -17,7 +19,13 @@ interface SetupParams {
}
const setup = async ({ useHash = false }: SetupParams = {}) => {
const locator = new DiscoverContextAppLocatorDefinition({ useHash });
const locator = {
id: DISCOVER_CONTEXT_APP_LOCATOR,
getLocation: async (params: DiscoverContextAppLocatorParams) => {
return contextAppLocatorGetLocation({ useHash }, params);
},
};
return { locator };
};

View file

@ -9,9 +9,7 @@
import type { SerializableRecord } from '@kbn/utility-types';
import type { Filter } from '@kbn/es-query';
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
export const DISCOVER_CONTEXT_APP_LOCATOR = 'DISCOVER_CONTEXT_APP_LOCATOR';
@ -26,58 +24,10 @@ export interface DiscoverContextAppLocatorParams extends SerializableRecord {
export type DiscoverContextAppLocator = LocatorPublic<DiscoverContextAppLocatorParams>;
export interface DiscoverContextAppLocatorDependencies {
useHash: boolean;
}
export interface ContextHistoryLocationState {
referrer: string;
dataViewSpec?: DataViewSpec;
}
export class DiscoverContextAppLocatorDefinition
implements LocatorDefinition<DiscoverContextAppLocatorParams>
{
public readonly id = DISCOVER_CONTEXT_APP_LOCATOR;
constructor(protected readonly deps: DiscoverContextAppLocatorDependencies) {}
public readonly getLocation = async (params: DiscoverContextAppLocatorParams) => {
const useHash = this.deps.useHash;
const { index, rowId, columns, filters, referrer } = params;
const appState: { filters?: Filter[]; columns?: string[] } = {};
const queryState: GlobalQueryStateFromUrl = {};
const { isFilterPinned } = await import('@kbn/es-query');
if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f));
if (columns) appState.columns = columns;
if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f));
let dataViewId;
const state: ContextHistoryLocationState = { referrer };
if (typeof index === 'object') {
state.dataViewSpec = index;
dataViewId = index.id!;
} else {
dataViewId = index;
}
let path = `#/context/${dataViewId}/${encodeURIComponent(rowId)}`;
if (Object.keys(queryState).length) {
path = setStateToKbnUrl<GlobalQueryStateFromUrl>('_g', queryState, { useHash }, path);
}
if (Object.keys(appState).length) {
path = setStateToKbnUrl('_a', appState, { useHash }, path);
}
return {
app: 'discover',
path,
state,
};
};
}
export type DiscoverContextAppLocatorGetLocation =
LocatorDefinition<DiscoverContextAppLocatorParams>['getLocation'];

View file

@ -0,0 +1,54 @@
/*
* 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 { Filter } from '@kbn/es-query';
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import type { ContextHistoryLocationState, DiscoverContextAppLocatorGetLocation } from './locator';
export const contextAppLocatorGetLocation = async (
{ useHash }: { useHash: boolean },
...[params]: Parameters<DiscoverContextAppLocatorGetLocation>
): ReturnType<DiscoverContextAppLocatorGetLocation> => {
const { index, rowId, columns, filters, referrer } = params;
const appState: { filters?: Filter[]; columns?: string[] } = {};
const queryState: GlobalQueryStateFromUrl = {};
const { isFilterPinned } = await import('@kbn/es-query');
if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f));
if (columns) appState.columns = columns;
if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f));
let dataViewId;
const state: ContextHistoryLocationState = { referrer };
if (typeof index === 'object') {
state.dataViewSpec = index;
dataViewId = index.id!;
} else {
dataViewId = index;
}
let path = `#/context/${dataViewId}/${encodeURIComponent(rowId)}`;
if (Object.keys(queryState).length) {
path = setStateToKbnUrl<GlobalQueryStateFromUrl>('_g', queryState, { useHash }, path);
}
if (Object.keys(appState).length) {
path = setStateToKbnUrl('_a', appState, { useHash }, path);
}
return {
app: 'discover',
path,
state,
};
};

View file

@ -7,12 +7,20 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DiscoverSingleDocLocatorDefinition } from './locator';
import type { DiscoverSingleDocLocatorParams } from './locator';
import { DISCOVER_SINGLE_DOC_LOCATOR } from './locator';
import { singleDocLocatorGetLocation } from './locator_get_location';
const dataViewId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
const setup = () => {
const locator = new DiscoverSingleDocLocatorDefinition();
const locator = {
id: DISCOVER_SINGLE_DOC_LOCATOR,
getLocation: async (params: DiscoverSingleDocLocatorParams) => {
return singleDocLocatorGetLocation(params);
},
};
return { locator };
};

View file

@ -27,31 +27,5 @@ export interface DocHistoryLocationState {
dataViewSpec?: DataViewSpec;
}
export class DiscoverSingleDocLocatorDefinition
implements LocatorDefinition<DiscoverSingleDocLocatorParams>
{
public readonly id = DISCOVER_SINGLE_DOC_LOCATOR;
constructor() {}
public readonly getLocation = async (params: DiscoverSingleDocLocatorParams) => {
const { index, rowId, rowIndex, referrer } = params;
let dataViewId;
const state: DocHistoryLocationState = { referrer };
if (typeof index === 'object') {
state.dataViewSpec = index;
dataViewId = index.id!;
} else {
dataViewId = index;
}
const path = `#/doc/${dataViewId}/${rowIndex}?id=${encodeURIComponent(rowId)}`;
return {
app: 'discover',
path,
state,
};
};
}
export type DiscoverSingleDocLocatorGetLocation =
LocatorDefinition<DiscoverSingleDocLocatorParams>['getLocation'];

View file

@ -0,0 +1,33 @@
/*
* 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 { DiscoverSingleDocLocatorGetLocation, DocHistoryLocationState } from './locator';
export const singleDocLocatorGetLocation = async (
...[params]: Parameters<DiscoverSingleDocLocatorGetLocation>
): ReturnType<DiscoverSingleDocLocatorGetLocation> => {
const { index, rowId, rowIndex, referrer } = params;
let dataViewId;
const state: DocHistoryLocationState = { referrer };
if (typeof index === 'object') {
state.dataViewSpec = index;
dataViewId = index.id!;
} else {
dataViewId = index;
}
const path = `#/doc/${dataViewId}/${rowIndex}?id=${encodeURIComponent(rowId)}`;
return {
app: 'discover',
path,
state,
};
};

View file

@ -21,7 +21,7 @@ import type { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
import { isEqual, isFunction } from 'lodash';
import { i18n } from '@kbn/i18n';
import { VIEW_MODE } from '../../../../common/constants';
import { restoreStateFromSavedSearch } from '../../../services/saved_searches/restore_from_saved_search';
import { restoreStateFromSavedSearch } from './utils/restore_from_saved_search';
import { updateSavedSearch } from './utils/update_saved_search';
import { addLog } from '../../../utils/add_log';
import { handleSourceColumnState } from '../../../utils/state_helpers';

View file

@ -24,7 +24,7 @@ import { isOfAggregateQueryType, isOfQueryType } from '@kbn/es-query';
import { isFunction } from 'lodash';
import type { DiscoverServices } from '../../..';
import { loadSavedSearch as loadSavedSearchFn } from './utils/load_saved_search';
import { restoreStateFromSavedSearch } from '../../../services/saved_searches/restore_from_saved_search';
import { restoreStateFromSavedSearch } from './utils/restore_from_saved_search';
import { FetchStatus } from '../../types';
import { changeDataView } from './utils/change_data_view';
import { buildStateSubscribe } from './utils/build_state_subscribe';

View file

@ -9,7 +9,7 @@
import type { TimefilterContract } from '@kbn/data-plugin/public';
import type { TimeRange, RefreshInterval } from '@kbn/data-plugin/common';
import { savedSearchMock, savedSearchMockWithTimeField } from '../../__mocks__/saved_search';
import { savedSearchMock, savedSearchMockWithTimeField } from '../../../../__mocks__/saved_search';
import { restoreStateFromSavedSearch } from './restore_from_saved_search';
describe('discover restore state from saved search', () => {

View file

@ -9,7 +9,7 @@
import type { TimefilterContract } from '@kbn/data-plugin/public';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { isRefreshIntervalValid, isTimeRangeValid } from '../../utils/validate_time';
import { isRefreshIntervalValid, isTimeRangeValid } from '../../../../utils/validate_time';
export const restoreStateFromSavedSearch = ({
savedSearch,

View file

@ -55,7 +55,7 @@ import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { ContentClient } from '@kbn/content-management-plugin/public';
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import { memoize, noop } from 'lodash';
import { noop } from 'lodash';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
@ -68,7 +68,7 @@ import type { DiscoverContextAppLocator } from './application/context/services/l
import type { DiscoverSingleDocLocator } from './application/doc/locator';
import type { DiscoverAppLocator } from '../common';
import type { ProfilesManager } from './context_awareness';
import type { DiscoverEBTManager } from './services/discover_ebt_manager';
import type { DiscoverEBTManager } from './plugin_imports/discover_ebt_manager';
/**
* Location state of internal Discover history instance
@ -144,98 +144,96 @@ export interface DiscoverServices {
embeddableEnhanced?: EmbeddableEnhancedPluginStart;
}
export const buildServices = memoize(
({
export const buildServices = ({
core,
plugins,
context,
locator,
contextLocator,
singleDocLocator,
history,
scopedHistory,
urlTracker,
profilesManager,
ebtManager,
setHeaderActionMenu = noop,
}: {
core: CoreStart;
plugins: DiscoverStartPlugins;
context: PluginInitializerContext;
locator: DiscoverAppLocator;
contextLocator: DiscoverContextAppLocator;
singleDocLocator: DiscoverSingleDocLocator;
history: History<HistoryLocationState>;
scopedHistory?: ScopedHistory;
urlTracker: UrlTracker;
profilesManager: ProfilesManager;
ebtManager: DiscoverEBTManager;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
}): DiscoverServices => {
const { usageCollection } = plugins;
const storage = new Storage(localStorage);
return {
aiops: plugins.aiops,
application: core.application,
addBasePath: core.http.basePath.prepend,
analytics: core.analytics,
capabilities: core.application.capabilities,
chrome: core.chrome,
core,
plugins,
context,
data: plugins.data,
dataVisualizer: plugins.dataVisualizer,
discoverShared: plugins.discoverShared,
docLinks: core.docLinks,
embeddable: plugins.embeddable,
i18n: core.i18n,
theme: core.theme,
userProfile: core.userProfile,
fieldFormats: plugins.fieldFormats,
filterManager: plugins.data.query.filterManager,
history,
getScopedHistory: <T>() => scopedHistory as ScopedHistory<T | undefined>,
setHeaderActionMenu,
dataViews: plugins.data.dataViews,
inspector: plugins.inspector,
metadata: {
branch: context.env.packageInfo.branch,
},
navigation: plugins.navigation,
share: plugins.share,
urlForwarding: plugins.urlForwarding,
urlTracker,
timefilter: plugins.data.query.timefilter.timefilter,
toastNotifications: core.notifications.toasts,
notifications: core.notifications,
uiSettings: core.uiSettings,
settings: core.settings,
storage,
trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'),
dataViewFieldEditor: plugins.dataViewFieldEditor,
http: core.http,
spaces: plugins.spaces,
dataViewEditor: plugins.dataViewEditor,
triggersActionsUi: plugins.triggersActionsUi,
locator,
contextLocator,
singleDocLocator,
history,
scopedHistory,
urlTracker,
expressions: plugins.expressions,
charts: plugins.charts,
savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(),
savedObjectsManagement: plugins.savedObjectsManagement,
savedSearch: plugins.savedSearch,
unifiedSearch: plugins.unifiedSearch,
lens: plugins.lens,
uiActions: plugins.uiActions,
contentClient: plugins.contentManagement.client,
noDataPage: plugins.noDataPage,
observabilityAIAssistant: plugins.observabilityAIAssistant,
profilesManager,
ebtManager,
setHeaderActionMenu = noop,
}: {
core: CoreStart;
plugins: DiscoverStartPlugins;
context: PluginInitializerContext;
locator: DiscoverAppLocator;
contextLocator: DiscoverContextAppLocator;
singleDocLocator: DiscoverSingleDocLocator;
history: History<HistoryLocationState>;
scopedHistory?: ScopedHistory;
urlTracker: UrlTracker;
profilesManager: ProfilesManager;
ebtManager: DiscoverEBTManager;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
}): DiscoverServices => {
const { usageCollection } = plugins;
const storage = new Storage(localStorage);
return {
aiops: plugins.aiops,
application: core.application,
addBasePath: core.http.basePath.prepend,
analytics: core.analytics,
capabilities: core.application.capabilities,
chrome: core.chrome,
core,
data: plugins.data,
dataVisualizer: plugins.dataVisualizer,
discoverShared: plugins.discoverShared,
docLinks: core.docLinks,
embeddable: plugins.embeddable,
i18n: core.i18n,
theme: core.theme,
userProfile: core.userProfile,
fieldFormats: plugins.fieldFormats,
filterManager: plugins.data.query.filterManager,
history,
getScopedHistory: <T>() => scopedHistory as ScopedHistory<T | undefined>,
setHeaderActionMenu,
dataViews: plugins.data.dataViews,
inspector: plugins.inspector,
metadata: {
branch: context.env.packageInfo.branch,
},
navigation: plugins.navigation,
share: plugins.share,
urlForwarding: plugins.urlForwarding,
urlTracker,
timefilter: plugins.data.query.timefilter.timefilter,
toastNotifications: core.notifications.toasts,
notifications: core.notifications,
uiSettings: core.uiSettings,
settings: core.settings,
storage,
trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'),
dataViewFieldEditor: plugins.dataViewFieldEditor,
http: core.http,
spaces: plugins.spaces,
dataViewEditor: plugins.dataViewEditor,
triggersActionsUi: plugins.triggersActionsUi,
locator,
contextLocator,
singleDocLocator,
expressions: plugins.expressions,
charts: plugins.charts,
savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(),
savedObjectsManagement: plugins.savedObjectsManagement,
savedSearch: plugins.savedSearch,
unifiedSearch: plugins.unifiedSearch,
lens: plugins.lens,
uiActions: plugins.uiActions,
contentClient: plugins.contentManagement.client,
noDataPage: plugins.noDataPage,
observabilityAIAssistant: plugins.observabilityAIAssistant,
profilesManager,
ebtManager,
fieldsMetadata: plugins.fieldsMetadata,
logsDataAccess: plugins.logsDataAccess,
embeddableEnhanced: plugins.embeddableEnhanced,
};
}
);
fieldsMetadata: plugins.fieldsMetadata,
logsDataAccess: plugins.logsDataAccess,
embeddableEnhanced: plugins.embeddableEnhanced,
};
};

View file

@ -39,7 +39,7 @@ const TestComponent = (props: Partial<DiscoverContainerInternalProps>) => {
overrideServices={props.overrideServices ?? mockOverrideService}
customizationCallbacks={props.customizationCallbacks ?? [customizeMock]}
scopedHistory={props.scopedHistory ?? getScopedHistory()!}
getDiscoverServices={getDiscoverServicesMock}
getDiscoverServices={() => Promise.resolve(getDiscoverServicesMock())}
/>
);
};

View file

@ -13,6 +13,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import React, { useMemo } from 'react';
import { css } from '@emotion/react';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import useAsync from 'react-use/lib/useAsync';
import { DiscoverMainRoute } from '../../application/main';
import type { DiscoverServices } from '../../build_services';
import type { CustomizationCallback, DiscoverCustomizationContext } from '../../customizations';
@ -26,7 +27,7 @@ export interface DiscoverContainerInternalProps {
* already consumes.
*/
overrideServices: Partial<DiscoverServices>;
getDiscoverServices: () => DiscoverServices;
getDiscoverServices: () => Promise<DiscoverServices>;
scopedHistory: ScopedHistory;
customizationCallbacks: CustomizationCallback[];
stateStorageContainer?: IKbnUrlStateStorage;
@ -54,15 +55,20 @@ export const DiscoverContainerInternal = ({
stateStorageContainer,
isLoading = false,
}: DiscoverContainerInternalProps) => {
const services = useMemo<DiscoverServices>(() => {
const { value: discoverServices } = useAsync(getDiscoverServices, [getDiscoverServices]);
const services = useMemo(() => {
if (!discoverServices) {
return undefined;
}
return {
...getDiscoverServices(),
...discoverServices,
...overrideServices,
getScopedHistory: <T,>() => scopedHistory as ScopedHistory<T | undefined>,
};
}, [getDiscoverServices, overrideServices, scopedHistory]);
}, [discoverServices, overrideServices, scopedHistory]);
if (isLoading) {
if (isLoading || !services) {
return (
<EuiFlexGroup css={discoverContainerWrapperCss}>
<LoadingIndicator type="spinner" />

View file

@ -25,7 +25,7 @@ import {
} from '../profiles';
import type { ProfileProviderServices } from '../profile_providers/profile_provider_services';
import { ProfilesManager } from '../profiles_manager';
import { DiscoverEBTManager } from '../../services/discover_ebt_manager';
import { DiscoverEBTManager } from '../../plugin_imports/discover_ebt_manager';
import { createLogsContextServiceMock } from '@kbn/discover-utils/src/__mocks__';
import { discoverSharedPluginMock } from '@kbn/discover-shared-plugin/public/mocks';

View file

@ -25,7 +25,7 @@ import type {
DocumentContext,
} from './profiles';
import type { ContextWithProfileId } from './profile_service';
import type { DiscoverEBTManager } from '../services/discover_ebt_manager';
import type { DiscoverEBTManager } from '../plugin_imports/discover_ebt_manager';
import type { AppliedProfile } from './composable_profile';
interface SerializedRootProfileParams {

View file

@ -9,6 +9,5 @@
export * from './customization_types';
export * from './customization_provider';
export * from './defaults';
export * from './types';
export type { DiscoverCustomization, DiscoverCustomizationService } from './customization_service';

View file

@ -11,11 +11,10 @@ import type { ApplicationStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
import type { Action } from '@kbn/ui-actions-plugin/public';
import type { DiscoverAppLocator } from '../../../common';
import { getDiscoverLocatorParams } from '../utils/get_discover_locator_params';
export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH';
import { compatibilityCheck } from './view_saved_search_compatibility_check';
import { ACTION_VIEW_SAVED_SEARCH } from '../constants';
export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
public id = ACTION_VIEW_SAVED_SEARCH;
@ -28,7 +27,6 @@ export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
) {}
async execute({ embeddable }: EmbeddableApiContext): Promise<void> {
const { compatibilityCheck } = await import('./view_saved_search_compatibility_check');
if (!compatibilityCheck(embeddable)) {
return;
}
@ -53,7 +51,7 @@ export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
(capabilities.discover_v2.show as boolean) || (capabilities.discover_v2.save as boolean);
if (!hasDiscoverPermissions) return false; // early return to delay async import until absolutely necessary
const { compatibilityCheck } = await import('./view_saved_search_compatibility_check');
return compatibilityCheck(embeddable);
}
}

View file

@ -25,7 +25,7 @@ import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import useObservable from 'react-use/lib/useObservable';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { getSortForEmbeddable } from '../../utils';
import { getSortForEmbeddable } from '../../utils/sorting';
import { getAllowedSampleSize, getMaxAllowedSampleSize } from '../../utils/get_allowed_sample_size';
import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from '../constants';
import { isEsqlMode } from '../initialize_fetch';

View file

@ -23,6 +23,8 @@ export const SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER: Trigger = {
'This trigger is used to replace the cell actions for Discover session embeddable grid.',
} as const;
export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH';
export const DEFAULT_HEADER_ROW_HEIGHT_LINES = 3;
/** This constant refers to the parts of the saved search state that can be edited from a dashboard */

View file

@ -0,0 +1,32 @@
/*
* 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 { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils';
import type { DiscoverServices } from '../build_services';
import { deserializeState } from './utils/serialization_utils';
export const getOnAddSearchEmbeddable =
(
discoverServices: DiscoverServices
): Parameters<EmbeddableSetup['registerAddFromLibraryType']>[0]['onAdd'] =>
async (container, savedObject) => {
const initialState = await deserializeState({
serializedState: {
rawState: { savedObjectId: savedObject.id },
references: savedObject.references,
},
discoverServices,
});
container.addNewPanel({
panelType: SEARCH_EMBEDDABLE_TYPE,
initialState,
});
};

View file

@ -9,17 +9,18 @@
import { omit, pick } from 'lodash';
import deepEqual from 'react-fast-compare';
import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import type {
SerializedTimeRange,
SerializedTitles,
SerializedPanelState,
} from '@kbn/presentation-publishing';
import type { SavedSearch, SavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { toSavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import {
toSavedSearchAttributes,
type SavedSearch,
type SavedSearchAttributes,
} from '@kbn/saved-search-plugin/common';
import type { SavedSearchUnwrapResult } from '@kbn/saved-search-plugin/public';
import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin';
import { extract, inject } from '../../../common/embeddable/search_inject_extract';
import type { DiscoverServices } from '../../build_services';

View file

@ -1,90 +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 { ApplicationStart } from '@kbn/core/public';
import { from, of } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import type { GlobalSearchResultProvider } from '@kbn/global-search-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { getInitialESQLQuery } from '@kbn/esql-utils';
import type { DiscoverAppLocator } from '../../common';
/**
* Global search provider adding an ES|QL and ESQL entry.
* This is necessary because ES|QL is part of the Discover application.
*
* It navigates to Discover with a default query extracted from the default dataview
*/
export const getESQLSearchProvider: (
isESQLEnabled: boolean,
uiCapabilities: Promise<ApplicationStart['capabilities']>,
data: Promise<DataPublicPluginStart>,
locator?: DiscoverAppLocator
) => GlobalSearchResultProvider = (isESQLEnabled, uiCapabilities, data, locator) => ({
id: 'esql',
find: ({ term = '', types, tags }) => {
if (tags || (types && !types.includes('application')) || !locator || !isESQLEnabled) {
return of([]);
}
return from(
Promise.all([uiCapabilities, data]).then(async ([{ navLinks }, { dataViews }]) => {
if (!navLinks.discover) {
return [];
}
const title = i18n.translate('discover.globalSearch.esqlSearchTitle', {
defaultMessage: 'Create ES|QL queries',
description: 'ES|QL is a product name and should not be translated',
});
const defaultDataView = await dataViews.getDefaultDataView({ displayErrors: false });
if (!defaultDataView) {
return [];
}
const params = {
query: {
esql: getInitialESQLQuery(defaultDataView),
},
dataViewSpec: defaultDataView?.toSpec(),
};
const discoverLocation = await locator?.getLocation(params);
term = term.toLowerCase();
let score = 0;
if (term === 'es|ql' || term === 'esql') {
score = 100;
} else if (term && ('es|ql'.includes(term) || 'esql'.includes(term))) {
score = 90;
}
if (score === 0) return [];
return [
{
id: 'esql',
title,
type: 'application',
icon: 'logoKibana',
meta: {
categoryId: DEFAULT_APP_CATEGORIES.kibana.id,
categoryLabel: DEFAULT_APP_CATEGORIES.kibana.label,
},
score,
url: `/app/${discoverLocation.app}${discoverLocation.path}`,
},
];
})
);
},
getSearchableTypes: () => ['application'],
});

View file

@ -39,5 +39,6 @@ export {
type SearchEmbeddableApi,
type NonPersistedDisplayOptions,
} from './embeddable';
export { loadSharingDataHelpers } from './utils';
export type { DiscoverServices } from './build_services';
export const loadSharingDataHelpers = () => import('./utils/get_sharing_data');

View file

@ -20,31 +20,34 @@ import type {
} from '@kbn/core/public';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils';
import type { SavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { SavedSearchType } from '@kbn/saved-search-plugin/common';
import type { SavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { i18n } from '@kbn/i18n';
import { PLUGIN_ID } from '../common';
import { registerFeature } from './register_feature';
import { once } from 'lodash';
import { DISCOVER_ESQL_LOCATOR } from '@kbn/deeplinks-analytics';
import { DISCOVER_APP_LOCATOR, PLUGIN_ID, type DiscoverAppLocator } from '../common';
import {
DISCOVER_CONTEXT_APP_LOCATOR,
type DiscoverContextAppLocator,
} from './application/context/services/locator';
import {
DISCOVER_SINGLE_DOC_LOCATOR,
type DiscoverSingleDocLocator,
} from './application/doc/locator';
import { registerFeature } from './plugin_imports/register_feature';
import type { UrlTracker } from './build_services';
import { buildServices } from './build_services';
import { ViewSavedSearchAction } from './embeddable/actions/view_saved_search_action';
import { initializeKbnUrlTracking } from './utils/initialize_kbn_url_tracking';
import type { DiscoverContextAppLocator } from './application/context/services/locator';
import { DiscoverContextAppLocatorDefinition } from './application/context/services/locator';
import type { DiscoverSingleDocLocator } from './application/doc/locator';
import { DiscoverSingleDocLocatorDefinition } from './application/doc/locator';
import type { DiscoverAppLocator } from '../common';
import { DiscoverAppLocatorDefinition, DiscoverESQLLocatorDefinition } from '../common';
import { defaultCustomizationContext } from './customizations';
import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER } from './embeddable/constants';
import { defaultCustomizationContext } from './customizations/defaults';
import {
SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER,
ACTION_VIEW_SAVED_SEARCH,
} from './embeddable/constants';
import {
DiscoverContainerInternal,
type DiscoverContainerProps,
} from './components/discover_container';
import { getESQLSearchProvider } from './global_search/search_provider';
import { HistoryService } from './history_service';
import { getESQLSearchProvider } from './plugin_imports/search_provider';
import type { ConfigSchema, ExperimentalFeatures } from '../server/config';
import type {
DiscoverSetup,
@ -52,13 +55,14 @@ import type {
DiscoverStart,
DiscoverStartPlugins,
} from './types';
import { deserializeState } from './embeddable/utils/serialization_utils';
import { DISCOVER_CELL_ACTIONS_TRIGGER } from './context_awareness/types';
import { RootProfileService } from './context_awareness/profiles/root_profile';
import { DataSourceProfileService } from './context_awareness/profiles/data_source_profile';
import { DocumentProfileService } from './context_awareness/profiles/document_profile';
import { ProfilesManager } from './context_awareness/profiles_manager';
import { DiscoverEBTManager } from './services/discover_ebt_manager';
import type {
DiscoverEBTContextProps,
DiscoverEBTManager,
} from './plugin_imports/discover_ebt_manager';
import type { ProfilesManager } from './context_awareness';
import { forwardLegacyUrls } from './plugin_imports/forward_legacy_urls';
import { registerDiscoverEBTManagerAnalytics } from './plugin_imports/discover_ebt_manager_registrations';
/**
* Contains Discover, one of the oldest parts of Kibana
@ -67,8 +71,10 @@ import { DiscoverEBTManager } from './services/discover_ebt_manager';
export class DiscoverPlugin
implements Plugin<DiscoverSetup, DiscoverStart, DiscoverSetupPlugins, DiscoverStartPlugins>
{
private readonly discoverEbtContext$ = new BehaviorSubject<DiscoverEBTContextProps>({
discoverProfiles: [],
});
private readonly appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private readonly historyService = new HistoryService();
private readonly experimentalFeatures: ExperimentalFeatures;
private scopedHistory?: ScopedHistory<unknown>;
@ -96,35 +102,41 @@ export class DiscoverPlugin
if (plugins.share) {
const useHash = core.uiSettings.get('state:storeInSessionStorage');
this.locator = plugins.share.url.locators.create(
new DiscoverAppLocatorDefinition({ useHash, setStateToKbnUrl })
);
this.contextLocator = plugins.share.url.locators.create(
new DiscoverContextAppLocatorDefinition({ useHash })
);
this.singleDocLocator = plugins.share.url.locators.create(
new DiscoverSingleDocLocatorDefinition()
);
this.locator = plugins.share.url.locators.create({
id: DISCOVER_APP_LOCATOR,
getLocation: async (params) => {
const { appLocatorGetLocation } = await getLocators();
return appLocatorGetLocation({ useHash }, params);
},
});
this.contextLocator = plugins.share.url.locators.create({
id: DISCOVER_CONTEXT_APP_LOCATOR,
getLocation: async (params) => {
const { contextAppLocatorGetLocation } = await getLocators();
return contextAppLocatorGetLocation({ useHash }, params);
},
});
this.singleDocLocator = plugins.share.url.locators.create({
id: DISCOVER_SINGLE_DOC_LOCATOR,
getLocation: async (params) => {
const { singleDocLocatorGetLocation } = await getLocators();
return singleDocLocatorGetLocation(params);
},
});
}
if (plugins.globalSearch) {
const enableESQL = core.uiSettings.get(ENABLE_ESQL);
plugins.globalSearch.registerResultProvider(
getESQLSearchProvider(
enableESQL,
core.getStartServices().then(
([
{
application: { capabilities },
},
]) => capabilities
),
core.getStartServices().then((deps) => {
const { data } = deps[1];
return data;
}),
this.locator
)
getESQLSearchProvider({
isESQLEnabled: core.uiSettings.get(ENABLE_ESQL),
locator: this.locator,
getServices: async () => {
const [coreStart, startPlugins] = await core.getStartServices();
return [coreStart, startPlugins];
},
})
);
}
@ -146,13 +158,15 @@ export class DiscoverPlugin
this.urlTracker = { setTrackedUrl, restorePreviousUrl, setTrackingEnabled };
this.stopUrlTracking = stopUrlTracker;
const ebtManager = new DiscoverEBTManager();
ebtManager.initialize({
core,
shouldInitializeCustomContext: true,
shouldInitializeCustomEvents: true,
const getEbtManager = once(async () => {
const { DiscoverEBTManager } = await getSharedServices();
const ebtManager = new DiscoverEBTManager();
ebtManager.initialize({ core, discoverEbtContext$: this.discoverEbtContext$ });
return ebtManager;
});
registerDiscoverEBTManagerAnalytics(core, this.discoverEbtContext$);
core.application.register({
id: PLUGIN_ID,
title: 'Discover',
@ -168,7 +182,7 @@ export class DiscoverPlugin
// Store the current scoped history so initializeKbnUrlTracking can access it
this.scopedHistory = params.history;
this.historyService.syncHistoryLocations();
(await getHistoryService()).syncHistoryLocations();
appMounted();
// dispatch synthetic hash change event to update hash history objects
@ -177,24 +191,14 @@ export class DiscoverPlugin
window.dispatchEvent(new HashChangeEvent('hashchange'));
});
const ebtManager = await getEbtManager();
ebtManager.onDiscoverAppMounted();
const services = buildServices({
const services = await this.getDiscoverServicesWithProfiles({
core: coreStart,
plugins: discoverStartPlugins,
context: this.initializerContext,
locator: this.locator!,
contextLocator: this.contextLocator!,
singleDocLocator: this.singleDocLocator!,
history: this.historyService.getHistory(),
scopedHistory: this.scopedHistory,
urlTracker: this.urlTracker!,
profilesManager: await this.createProfilesManager({
core: coreStart,
plugins: discoverStartPlugins,
ebtManager,
}),
ebtManager,
scopedHistory: this.scopedHistory,
setHeaderActionMenu: params.setHeaderActionMenu,
});
@ -222,63 +226,48 @@ export class DiscoverPlugin
},
});
plugins.urlForwarding.forwardApp('doc', 'discover', (path) => {
return `#${path}`;
});
plugins.urlForwarding.forwardApp('context', 'discover', (path) => {
const urlParts = path.split('/');
// take care of urls containing legacy url, those split in the following way
// ["", "context", indexPatternId, _type, id + params]
if (urlParts[4]) {
// remove _type part
const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/');
return `#${newPath}`;
}
return `#${path}`;
});
plugins.urlForwarding.forwardApp('discover', 'discover', (path) => {
const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || [];
if (!id) {
return `#${path.replace('/discover', '') || '/'}`;
}
return `#/view/${id}${tail || ''}`;
});
if (plugins.home) {
registerFeature(plugins.home);
}
forwardLegacyUrls(plugins.urlForwarding);
this.registerEmbeddable(core, plugins);
return { locator: this.locator };
}
start(core: CoreStart, plugins: DiscoverStartPlugins): DiscoverStart {
const viewSavedSearchAction = new ViewSavedSearchAction(core.application, this.locator!);
plugins.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
plugins.uiActions.addTriggerActionAsync(
'CONTEXT_MENU_TRIGGER',
ACTION_VIEW_SAVED_SEARCH,
async () => {
const { ViewSavedSearchAction } = await getEmbeddableServices();
return new ViewSavedSearchAction(core.application, this.locator!);
}
);
plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
plugins.uiActions.registerTrigger(DISCOVER_CELL_ACTIONS_TRIGGER);
const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL);
if (plugins.share && this.locator && isEsqlEnabled) {
plugins.share?.url.locators.create(
new DiscoverESQLLocatorDefinition({
discoverAppLocator: this.locator,
dataViews: plugins.dataViews,
})
);
const discoverAppLocator = this.locator;
plugins.share?.url.locators.create({
id: DISCOVER_ESQL_LOCATOR,
getLocation: async () => {
const { esqlLocatorGetLocation } = await getLocators();
return esqlLocatorGetLocation({
discoverAppLocator,
dataViews: plugins.dataViews,
});
},
});
}
const getDiscoverServicesInternal = () => {
const ebtManager = new DiscoverEBTManager(); // It is not initialized outside of Discover
return this.getDiscoverServices(
core,
plugins,
this.createEmptyProfilesManager({ ebtManager }),
ebtManager
);
const getDiscoverServicesInternal = async () => {
const ebtManager = await getEmptyEbtManager();
const { profilesManager } = await this.createProfileServices(ebtManager);
return this.getDiscoverServices({ core, plugins, profilesManager, ebtManager });
};
return {
@ -295,29 +284,17 @@ export class DiscoverPlugin
}
}
private createProfileServices() {
private async createProfileServices(ebtManager: DiscoverEBTManager) {
const {
RootProfileService,
DataSourceProfileService,
DocumentProfileService,
ProfilesManager,
} = await getSharedServices();
const rootProfileService = new RootProfileService();
const dataSourceProfileService = new DataSourceProfileService();
const documentProfileService = new DocumentProfileService();
return { rootProfileService, dataSourceProfileService, documentProfileService };
}
private async createProfilesManager({
core,
plugins,
ebtManager,
}: {
core: CoreStart;
plugins: DiscoverStartPlugins;
ebtManager: DiscoverEBTManager;
}) {
const { registerProfileProviders } = await import('./context_awareness/profile_providers');
const { rootProfileService, dataSourceProfileService, documentProfileService } =
this.createProfileServices();
const enabledExperimentalProfileIds = this.experimentalFeatures.enabledProfiles ?? [];
const profilesManager = new ProfilesManager(
rootProfileService,
dataSourceProfileService,
@ -325,32 +302,70 @@ export class DiscoverPlugin
ebtManager
);
return {
rootProfileService,
dataSourceProfileService,
documentProfileService,
profilesManager,
};
}
private async getDiscoverServicesWithProfiles({
core,
plugins,
ebtManager,
scopedHistory,
setHeaderActionMenu,
}: {
core: CoreStart;
plugins: DiscoverStartPlugins;
ebtManager: DiscoverEBTManager;
scopedHistory?: ScopedHistory;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
}) {
const {
rootProfileService,
dataSourceProfileService,
documentProfileService,
profilesManager,
} = await this.createProfileServices(ebtManager);
const services = await this.getDiscoverServices({
core,
plugins,
profilesManager,
ebtManager,
scopedHistory,
setHeaderActionMenu,
});
const { registerProfileProviders } = await import('./context_awareness/profile_providers');
await registerProfileProviders({
rootProfileService,
dataSourceProfileService,
documentProfileService,
enabledExperimentalProfileIds,
services: this.getDiscoverServices(core, plugins, profilesManager, ebtManager),
enabledExperimentalProfileIds: this.experimentalFeatures.enabledProfiles ?? [],
services,
});
return profilesManager;
return services;
}
private createEmptyProfilesManager({ ebtManager }: { ebtManager: DiscoverEBTManager }) {
return new ProfilesManager(
new RootProfileService(),
new DataSourceProfileService(),
new DocumentProfileService(),
ebtManager
);
}
private getDiscoverServices = (
core: CoreStart,
plugins: DiscoverStartPlugins,
profilesManager: ProfilesManager,
ebtManager: DiscoverEBTManager
) => {
private getDiscoverServices = async ({
core,
plugins,
profilesManager,
ebtManager,
scopedHistory,
setHeaderActionMenu,
}: {
core: CoreStart;
plugins: DiscoverStartPlugins;
profilesManager: ProfilesManager;
ebtManager: DiscoverEBTManager;
scopedHistory?: ScopedHistory;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
}) => {
const { buildServices } = await getSharedServices();
return buildServices({
core,
plugins,
@ -358,16 +373,16 @@ export class DiscoverPlugin
locator: this.locator!,
contextLocator: this.contextLocator!,
singleDocLocator: this.singleDocLocator!,
history: this.historyService.getHistory(),
history: (await getHistoryService()).getHistory(),
scopedHistory,
urlTracker: this.urlTracker!,
profilesManager,
ebtManager,
setHeaderActionMenu,
});
};
private registerEmbeddable(core: CoreSetup<DiscoverStartPlugins>, plugins: DiscoverSetupPlugins) {
const ebtManager = new DiscoverEBTManager(); // It is not initialized outside of Discover
const getStartServices = async () => {
const [coreStart, deps] = await core.getStartServices();
return {
@ -378,29 +393,19 @@ export class DiscoverPlugin
const getDiscoverServicesForEmbeddable = async () => {
const [coreStart, deps] = await core.getStartServices();
const profilesManager = await this.createProfilesManager({
const ebtManager = await getEmptyEbtManager();
return this.getDiscoverServicesWithProfiles({
core: coreStart,
plugins: deps,
ebtManager,
});
return this.getDiscoverServices(coreStart, deps, profilesManager, ebtManager);
};
plugins.embeddable.registerAddFromLibraryType<SavedSearchAttributes>({
onAdd: async (container, savedObject) => {
onAdd: async (...params) => {
const services = await getDiscoverServicesForEmbeddable();
const initialState = await deserializeState({
serializedState: {
rawState: { savedObjectId: savedObject.id },
references: savedObject.references,
},
discoverServices: services,
});
container.addNewPanel({
panelType: SEARCH_EMBEDDABLE_TYPE,
initialState,
});
const { getOnAddSearchEmbeddable } = await getEmbeddableServices();
return getOnAddSearchEmbeddable(services)(...params);
},
savedObjectType: SavedSearchType,
savedObjectName: i18n.translate('discover.savedSearch.savedObjectName', {
@ -413,7 +418,7 @@ export class DiscoverPlugin
const [startServices, discoverServices, { getSearchEmbeddableFactory }] = await Promise.all([
getStartServices(),
getDiscoverServicesForEmbeddable(),
import('./embeddable/get_search_embeddable_factory'),
getEmbeddableServices(),
]);
return getSearchEmbeddableFactory({
@ -423,3 +428,17 @@ export class DiscoverPlugin
});
}
}
const getLocators = () => import('./plugin_imports/locators');
const getEmbeddableServices = () => import('./plugin_imports/embeddable_services');
const getSharedServices = () => import('./plugin_imports/shared_services');
const getHistoryService = once(async () => {
const { HistoryService } = await getSharedServices();
return new HistoryService();
});
const getEmptyEbtManager = once(async () => {
const { DiscoverEBTManager } = await getSharedServices();
return new DiscoverEBTManager(); // It is not initialized outside of Discover
});

View file

@ -9,12 +9,14 @@
import { BehaviorSubject } from 'rxjs';
import { coreMock } from '@kbn/core/public/mocks';
import { DiscoverEBTManager } from './discover_ebt_manager';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { type DiscoverEBTContextProps, DiscoverEBTManager } from './discover_ebt_manager';
import { registerDiscoverEBTManagerAnalytics } from './discover_ebt_manager_registrations';
import { ContextualProfileLevel } from '../context_awareness/profiles_manager';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
describe('DiscoverEBTManager', () => {
let discoverEBTContextManager: DiscoverEBTManager;
let discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>;
const coreSetupMock = coreMock.createSetup();
@ -32,15 +34,19 @@ describe('DiscoverEBTManager', () => {
beforeEach(() => {
discoverEBTContextManager = new DiscoverEBTManager();
discoverEbtContext$ = new BehaviorSubject<DiscoverEBTContextProps>({
discoverProfiles: [],
});
(coreSetupMock.analytics.reportEvent as jest.Mock).mockClear();
});
describe('register', () => {
it('should register the context provider and custom events', () => {
registerDiscoverEBTManagerAnalytics(coreSetupMock, discoverEbtContext$);
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: true,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
expect(coreSetupMock.analytics.registerContextProvider).toHaveBeenCalledWith({
@ -94,8 +100,7 @@ describe('DiscoverEBTManager', () => {
const dscProfiles2 = ['profile21', 'profile22'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: true,
shouldInitializeCustomEvents: false,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
@ -111,8 +116,7 @@ describe('DiscoverEBTManager', () => {
const dscProfiles2 = ['profile1', 'profile2'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: true,
shouldInitializeCustomEvents: false,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
@ -127,8 +131,7 @@ describe('DiscoverEBTManager', () => {
const dscProfiles = ['profile1', 'profile2'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: true,
shouldInitializeCustomEvents: false,
discoverEbtContext$,
});
discoverEBTContextManager.updateProfilesContextWith(dscProfiles);
@ -139,8 +142,7 @@ describe('DiscoverEBTManager', () => {
const dscProfiles = ['profile1', 'profile2'];
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: true,
shouldInitializeCustomEvents: false,
discoverEbtContext$,
});
discoverEBTContextManager.onDiscoverAppMounted();
discoverEBTContextManager.updateProfilesContextWith(dscProfiles);
@ -159,8 +161,7 @@ describe('DiscoverEBTManager', () => {
it('should track the field usage when a field is added to the table', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: false,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
await discoverEBTContextManager.trackDataTableSelection({
@ -186,8 +187,7 @@ describe('DiscoverEBTManager', () => {
it('should track the field usage when a field is removed from the table', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: false,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
await discoverEBTContextManager.trackDataTableRemoval({
@ -213,8 +213,7 @@ describe('DiscoverEBTManager', () => {
it('should track the field usage when a filter is created', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: false,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
await discoverEBTContextManager.trackFilterAddition({
@ -246,8 +245,7 @@ describe('DiscoverEBTManager', () => {
it('should track the event when a next contextual profile is resolved', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: false,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
discoverEBTContextManager.trackContextualProfileResolvedEvent({
@ -296,8 +294,7 @@ describe('DiscoverEBTManager', () => {
it('should not trigger duplicate requests', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: false,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
discoverEBTContextManager.trackContextualProfileResolvedEvent({
@ -325,8 +322,7 @@ describe('DiscoverEBTManager', () => {
it('should trigger similar requests after remount', async () => {
discoverEBTContextManager.initialize({
core: coreSetupMock,
shouldInitializeCustomContext: false,
shouldInitializeCustomEvents: true,
discoverEbtContext$,
});
discoverEBTContextManager.trackContextualProfileResolvedEvent({

View file

@ -7,19 +7,20 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { BehaviorSubject } from 'rxjs';
import type { BehaviorSubject } from 'rxjs';
import { isEqual } from 'lodash';
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { ContextualProfileLevel } from '../context_awareness/profiles_manager';
/**
* Field usage events i.e. when a field is selected in the data table, removed from the data table, or a filter is added
*/
const FIELD_USAGE_EVENT_TYPE = 'discover_field_usage';
const FIELD_USAGE_EVENT_NAME = 'eventName';
const FIELD_USAGE_FIELD_NAME = 'fieldName';
const FIELD_USAGE_FILTER_OPERATION = 'filterOperation';
import {
CONTEXTUAL_PROFILE_ID,
CONTEXTUAL_PROFILE_LEVEL,
CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE,
FIELD_USAGE_EVENT_NAME,
FIELD_USAGE_EVENT_TYPE,
FIELD_USAGE_FIELD_NAME,
FIELD_USAGE_FILTER_OPERATION,
} from './discover_ebt_manager_registrations';
type FilterOperation = '+' | '-' | '_exists_';
@ -34,14 +35,6 @@ interface FieldUsageEventData {
[FIELD_USAGE_FILTER_OPERATION]?: FilterOperation;
}
/**
* Contextual profile resolved event i.e. when a different contextual profile is resolved at root, data source, or document level
* Duplicated events for the same profile level will not be sent.
*/
const CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE = 'discover_profile_resolved';
const CONTEXTUAL_PROFILE_LEVEL = 'contextLevel';
const CONTEXTUAL_PROFILE_ID = 'profileId';
interface ContextualProfileResolvedEventData {
[CONTEXTUAL_PROFILE_LEVEL]: ContextualProfileLevel;
[CONTEXTUAL_PROFILE_ID]: string;
@ -73,85 +66,13 @@ export class DiscoverEBTManager {
// https://docs.elastic.dev/telemetry/collection/event-based-telemetry
public initialize({
core,
shouldInitializeCustomContext,
shouldInitializeCustomEvents,
discoverEbtContext$,
}: {
core: CoreSetup;
shouldInitializeCustomContext: boolean;
shouldInitializeCustomEvents: boolean;
discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>;
}) {
if (shouldInitializeCustomContext) {
// Register Discover specific context to be used in EBT
const context$ = new BehaviorSubject<DiscoverEBTContextProps>({
discoverProfiles: [],
});
core.analytics.registerContextProvider({
name: 'discover_context',
context$,
schema: {
discoverProfiles: {
type: 'array',
items: {
type: 'keyword',
_meta: {
description: 'List of active Discover context awareness profiles',
},
},
},
// If we decide to extend EBT context with more properties, we can do it here
},
});
this.customContext$ = context$;
}
if (shouldInitializeCustomEvents) {
// Register Discover events to be used with EBT
core.analytics.registerEventType({
eventType: FIELD_USAGE_EVENT_TYPE,
schema: {
[FIELD_USAGE_EVENT_NAME]: {
type: 'keyword',
_meta: {
description:
'The name of the event that is tracked in the metrics i.e. dataTableSelection, dataTableRemoval',
},
},
[FIELD_USAGE_FIELD_NAME]: {
type: 'keyword',
_meta: {
description: "Field name if it's a part of ECS schema",
optional: true,
},
},
[FIELD_USAGE_FILTER_OPERATION]: {
type: 'keyword',
_meta: {
description: "Operation type when a filter is added i.e. '+', '-', '_exists_'",
optional: true,
},
},
},
});
core.analytics.registerEventType({
eventType: CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE,
schema: {
[CONTEXTUAL_PROFILE_LEVEL]: {
type: 'keyword',
_meta: {
description:
'The context level at which it was resolved i.e. rootLevel, dataSourceLevel, documentLevel',
},
},
[CONTEXTUAL_PROFILE_ID]: {
type: 'keyword',
_meta: {
description: 'The resolved name of the active profile',
},
},
},
});
this.reportEvent = core.analytics.reportEvent;
}
this.customContext$ = discoverEbtContext$;
this.reportEvent = core.analytics.reportEvent;
}
public onDiscoverAppMounted() {

View file

@ -0,0 +1,103 @@
/*
* 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 { CoreSetup } from '@kbn/core/public';
import type { BehaviorSubject } from 'rxjs';
import type { DiscoverStartPlugins } from '../types';
import type { DiscoverEBTContextProps } from './discover_ebt_manager';
/**
* Field usage events i.e. when a field is selected in the data table, removed from the data table, or a filter is added
*/
export const FIELD_USAGE_EVENT_TYPE = 'discover_field_usage';
export const FIELD_USAGE_EVENT_NAME = 'eventName';
export const FIELD_USAGE_FIELD_NAME = 'fieldName';
export const FIELD_USAGE_FILTER_OPERATION = 'filterOperation';
/**
* Contextual profile resolved event i.e. when a different contextual profile is resolved at root, data source, or document level
* Duplicated events for the same profile level will not be sent.
*/
export const CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE = 'discover_profile_resolved';
export const CONTEXTUAL_PROFILE_LEVEL = 'contextLevel';
export const CONTEXTUAL_PROFILE_ID = 'profileId';
/**
* This function is statically imported since analytics registrations must happen at setup,
* while the EBT manager is loaded dynamically when needed to avoid page load bundle bloat
*/
export const registerDiscoverEBTManagerAnalytics = (
core: CoreSetup<DiscoverStartPlugins>,
discoverEbtContext$: BehaviorSubject<DiscoverEBTContextProps>
) => {
// Register Discover specific context to be used in EBT
core.analytics.registerContextProvider({
name: 'discover_context',
context$: discoverEbtContext$,
schema: {
discoverProfiles: {
type: 'array',
items: {
type: 'keyword',
_meta: {
description: 'List of active Discover context awareness profiles',
},
},
},
// If we decide to extend EBT context with more properties, we can do it here
},
});
// Register Discover events to be used with EBT
core.analytics.registerEventType({
eventType: FIELD_USAGE_EVENT_TYPE,
schema: {
[FIELD_USAGE_EVENT_NAME]: {
type: 'keyword',
_meta: {
description:
'The name of the event that is tracked in the metrics i.e. dataTableSelection, dataTableRemoval',
},
},
[FIELD_USAGE_FIELD_NAME]: {
type: 'keyword',
_meta: {
description: "Field name if it's a part of ECS schema",
optional: true,
},
},
[FIELD_USAGE_FILTER_OPERATION]: {
type: 'keyword',
_meta: {
description: "Operation type when a filter is added i.e. '+', '-', '_exists_'",
optional: true,
},
},
},
});
core.analytics.registerEventType({
eventType: CONTEXTUAL_PROFILE_RESOLVED_EVENT_TYPE,
schema: {
[CONTEXTUAL_PROFILE_LEVEL]: {
type: 'keyword',
_meta: {
description:
'The context level at which it was resolved i.e. rootLevel, dataSourceLevel, documentLevel',
},
},
[CONTEXTUAL_PROFILE_ID]: {
type: 'keyword',
_meta: {
description: 'The resolved name of the active profile',
},
},
},
});
};

View file

@ -7,11 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
/*
* Allows the getSharingData function to be lazy loadable
*/
export async function loadSharingDataHelpers() {
return await import('./get_sharing_data');
}
export { getSortForEmbeddable } from './sorting';
export { ViewSavedSearchAction } from '../embeddable/actions/view_saved_search_action';
export { getOnAddSearchEmbeddable } from '../embeddable/get_on_add_search_embeddable';
export { getSearchEmbeddableFactory } from '../embeddable/get_search_embeddable_factory';

View file

@ -0,0 +1,36 @@
/*
* 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 { UrlForwardingSetup } from '@kbn/url-forwarding-plugin/public';
export const forwardLegacyUrls = (urlForwarding: UrlForwardingSetup) => {
urlForwarding.forwardApp('doc', 'discover', (path) => {
return `#${path}`;
});
urlForwarding.forwardApp('context', 'discover', (path) => {
const urlParts = path.split('/');
// take care of urls containing legacy url, those split in the following way
// ["", "context", indexPatternId, _type, id + params]
if (urlParts[4]) {
// remove _type part
const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/');
return `#${newPath}`;
}
return `#${path}`;
});
urlForwarding.forwardApp('discover', 'discover', (path) => {
const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || [];
if (!id) {
return `#${path.replace('/discover', '') || '/'}`;
}
return `#/view/${id}${tail || ''}`;
});
};

View file

@ -7,9 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { History } from 'history';
import { createHashHistory } from 'history';
import type { HistoryLocationState } from './build_services';
import { createHashHistory, type History } from 'history';
import type { HistoryLocationState } from '../build_services';
export class HistoryService {
private history?: History<HistoryLocationState>;

View file

@ -0,0 +1,25 @@
/*
* 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 { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import type { DiscoverAppLocatorParams } from '../../common';
import { appLocatorGetLocationCommon } from '../../common/app_locator_get_location';
export const appLocatorGetLocation = (
{
useHash,
}: {
useHash: boolean;
},
params: DiscoverAppLocatorParams
) => appLocatorGetLocationCommon({ useHash, setStateToKbnUrl }, params);
export { contextAppLocatorGetLocation } from '../application/context/services/locator_get_location';
export { singleDocLocatorGetLocation } from '../application/doc/locator_get_location';
export { esqlLocatorGetLocation } from '../../common/esql_locator_get_location';

View file

@ -8,19 +8,21 @@
*/
import { NEVER, lastValueFrom } from 'rxjs';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ApplicationStart } from '@kbn/core/public';
import type { CoreStart } from '@kbn/core/public';
import { getESQLSearchProvider } from './search_provider';
import { createDiscoverDataViewsMock } from '../__mocks__/data_views';
import type { DiscoverAppLocator } from '../../common';
import type { DiscoverStartPlugins } from '../types';
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public';
describe('ES|QL search provider', () => {
const uiCapabilitiesMock = new Promise<ApplicationStart['capabilities']>((resolve) => {
resolve({ navLinks: { discover: true } } as unknown as ApplicationStart['capabilities']);
});
const dataMock = new Promise<DataPublicPluginStart>((resolve) => {
resolve({ dataViews: createDiscoverDataViewsMock() } as unknown as DataPublicPluginStart);
});
const getServices = (): Promise<[CoreStart, DiscoverStartPlugins]> =>
Promise.resolve([
{
application: { capabilities: { navLinks: { discover: true } } },
} as unknown as CoreStart,
{ dataViews: createDiscoverDataViewsMock() } as unknown as DiscoverStartPlugins,
]);
const locator = {
useUrl: jest.fn(() => ''),
navigate: jest.fn(),
@ -33,7 +35,11 @@ describe('ES|QL search provider', () => {
getRedirectUrl: jest.fn(() => ''),
} as unknown as DiscoverAppLocator;
test('returns score 100 if term is esql', async () => {
const esqlSearchProvider = getESQLSearchProvider(true, uiCapabilitiesMock, dataMock, locator);
const esqlSearchProvider = getESQLSearchProvider({
isESQLEnabled: true,
locator,
getServices,
});
const observable = esqlSearchProvider.find(
{ term: 'esql' },
{ aborted$: NEVER, maxResults: 100, preference: '' }
@ -53,7 +59,11 @@ describe('ES|QL search provider', () => {
});
test('returns score 90 if user tries to write es|ql', async () => {
const esqlSearchProvider = getESQLSearchProvider(true, uiCapabilitiesMock, dataMock, locator);
const esqlSearchProvider = getESQLSearchProvider({
isESQLEnabled: true,
locator,
getServices,
});
const observable = esqlSearchProvider.find(
{ term: 'es|' },
{ aborted$: NEVER, maxResults: 100, preference: '' }
@ -73,7 +83,11 @@ describe('ES|QL search provider', () => {
});
test('returns empty results if user tries to write something irrelevant', async () => {
const esqlSearchProvider = getESQLSearchProvider(true, uiCapabilitiesMock, dataMock, locator);
const esqlSearchProvider = getESQLSearchProvider({
isESQLEnabled: true,
locator,
getServices,
});
const observable = esqlSearchProvider.find(
{ term: 'woof' },
{ aborted$: NEVER, maxResults: 100, preference: '' }
@ -83,7 +97,11 @@ describe('ES|QL search provider', () => {
});
test('returns empty results if ESQL is disabled', async () => {
const esqlSearchProvider = getESQLSearchProvider(false, uiCapabilitiesMock, dataMock, locator);
const esqlSearchProvider = getESQLSearchProvider({
isESQLEnabled: false,
locator,
getServices,
});
const observable = esqlSearchProvider.find(
{ term: 'esql' },
{ aborted$: NEVER, maxResults: 100, preference: '' }
@ -94,17 +112,18 @@ describe('ES|QL search provider', () => {
test('returns empty results if no default dataview', async () => {
const dataViewMock = createDiscoverDataViewsMock();
const updatedDataMock = new Promise<DataPublicPluginStart>((resolve) => {
resolve({
dataViews: { ...dataViewMock, getDefaultDataView: jest.fn(() => undefined) },
} as unknown as DataPublicPluginStart);
const esqlSearchProvider = getESQLSearchProvider({
isESQLEnabled: true,
locator,
getServices: async () => {
const [core, start] = await getServices();
start.dataViews = {
...dataViewMock,
getDefaultDataView: jest.fn(() => undefined),
} as unknown as DataViewsServicePublic;
return [core, start];
},
});
const esqlSearchProvider = getESQLSearchProvider(
true,
uiCapabilitiesMock,
updatedDataMock,
locator
);
const observable = esqlSearchProvider.find(
{ term: 'woof' },
{ aborted$: NEVER, maxResults: 100, preference: '' }

View file

@ -0,0 +1,35 @@
/*
* 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 { CoreStart } from '@kbn/core/public';
import { defer } from 'rxjs';
import type { GlobalSearchResultProvider } from '@kbn/global-search-plugin/public';
import type { DiscoverAppLocator } from '../../common';
import type { DiscoverStartPlugins } from '../types';
/**
* Global search provider adding an ES|QL and ESQL entry.
* This is necessary because ES|QL is part of the Discover application.
*
* It navigates to Discover with a default query extracted from the default dataview
*/
export const getESQLSearchProvider = (options: {
isESQLEnabled: boolean;
locator?: DiscoverAppLocator;
getServices: () => Promise<[CoreStart, DiscoverStartPlugins]>;
}): GlobalSearchResultProvider => ({
id: 'esql',
find: (...findParams) => {
return defer(async () => {
const { searchProviderFind } = await import('./search_provider_find');
return searchProviderFind(options, ...findParams);
});
},
getSearchableTypes: () => ['application'],
});

View file

@ -0,0 +1,85 @@
/*
* 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 {
GlobalSearchProviderResult,
GlobalSearchResultProvider,
} from '@kbn/global-search-plugin/public';
import { DEFAULT_APP_CATEGORIES, type CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { getInitialESQLQuery } from '@kbn/esql-utils';
import type { DiscoverAppLocator } from '../../common';
import type { DiscoverStartPlugins } from '../types';
export const searchProviderFind: (
options: {
isESQLEnabled: boolean;
locator?: DiscoverAppLocator;
getServices: () => Promise<[CoreStart, DiscoverStartPlugins]>;
},
...findParams: Parameters<GlobalSearchResultProvider['find']>
) => Promise<GlobalSearchProviderResult[]> = async (
{ isESQLEnabled, locator, getServices },
{ term = '', types, tags }
) => {
if (tags || (types && !types.includes('application')) || !locator || !isESQLEnabled) {
return [];
}
const [core, { dataViews }] = await getServices();
if (!core.application.capabilities.navLinks.discover) {
return [];
}
const title = i18n.translate('discover.globalSearch.esqlSearchTitle', {
defaultMessage: 'Create ES|QL queries',
description: 'ES|QL is a product name and should not be translated',
});
const defaultDataView = await dataViews.getDefaultDataView({ displayErrors: false });
if (!defaultDataView) {
return [];
}
const params = {
query: {
esql: getInitialESQLQuery(defaultDataView),
},
dataViewSpec: defaultDataView?.toSpec(),
};
const discoverLocation = await locator?.getLocation(params);
term = term.toLowerCase();
let score = 0;
if (term === 'es|ql' || term === 'esql') {
score = 100;
} else if (term && ('es|ql'.includes(term) || 'esql'.includes(term))) {
score = 90;
}
if (score === 0) return [];
return [
{
id: 'esql',
title,
type: 'application',
icon: 'logoKibana',
meta: {
categoryId: DEFAULT_APP_CATEGORIES.kibana.id,
categoryLabel: DEFAULT_APP_CATEGORIES.kibana.label,
},
score,
url: `/app/${discoverLocation.app}${discoverLocation.path}`,
},
];
};

View file

@ -0,0 +1,16 @@
/*
* 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 { HistoryService } from './history_service';
export { DiscoverEBTManager } from './discover_ebt_manager';
export { RootProfileService } from '../context_awareness/profiles/root_profile';
export { DataSourceProfileService } from '../context_awareness/profiles/data_source_profile';
export { DocumentProfileService } from '../context_awareness/profiles/document_profile';
export { ProfilesManager } from '../context_awareness/profiles_manager';
export { buildServices } from '../build_services';

View file

@ -45,7 +45,7 @@ import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/pub
import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public';
import type { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public';
import type { DiscoverAppLocator } from '../common';
import { type DiscoverContainerProps } from './components/discover_container';
import type { DiscoverContainerProps } from './components/discover_container';
/**
* @public
@ -116,6 +116,11 @@ export interface DiscoverStart {
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
/**
* @deprecated
* Embedding Discover in other applications is discouraged and will be removed in the future.
* Use the Discover context awareness framework instead to register a custom Discover profile.
*/
readonly DiscoverContainer: ComponentType<DiscoverContainerProps>;
}

View file

@ -9,10 +9,9 @@
import type { AppUpdater, CoreSetup, ScopedHistory } from '@kbn/core/public';
import type { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs';
import { filter, switchMap } from 'rxjs';
import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import { isFilterPinned } from '@kbn/es-query';
import { SEARCH_SESSION_ID_QUERY_PARAM } from '../constants';
import type { DiscoverSetupPlugins } from '../types';
@ -69,10 +68,13 @@ export function initializeKbnUrlTracking({
filter(
({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval)
),
map(({ state }) => ({
...state,
filters: state.filters?.filter(isFilterPinned),
}))
switchMap(async ({ state }) => {
const { isFilterPinned } = await import('@kbn/es-query');
return {
...state,
filters: state.filters?.filter(isFilterPinned),
};
})
),
},
],

View file

@ -15,13 +15,14 @@ import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common';
import type { SharePluginSetup } from '@kbn/share-plugin/server';
import type { PluginInitializerContext } from '@kbn/core/server';
import type { DiscoverServerPluginStart, DiscoverServerPluginStartDeps } from '.';
import { DiscoverAppLocatorDefinition } from '../common';
import { DISCOVER_APP_LOCATOR } from '../common';
import { capabilitiesProvider } from './capabilities_provider';
import { createSearchEmbeddableFactory } from './embeddable';
import { initializeLocatorServices } from './locator';
import { registerSampleData } from './sample_data';
import { getUiSettings } from './ui_settings';
import type { ConfigSchema } from './config';
import { appLocatorGetLocationCommon } from '../common/app_locator_get_location';
export class DiscoverServerPlugin
implements Plugin<object, DiscoverServerPluginStart, object, DiscoverServerPluginStartDeps>
@ -49,9 +50,12 @@ export class DiscoverServerPlugin
}
if (plugins.share) {
plugins.share.url.locators.create(
new DiscoverAppLocatorDefinition({ useHash: false, setStateToKbnUrl })
);
plugins.share.url.locators.create({
id: DISCOVER_APP_LOCATOR,
getLocation: (params) => {
return appLocatorGetLocationCommon({ useHash: false, setStateToKbnUrl }, params);
},
});
}
plugins.embeddable.registerEmbeddableFactory(createSearchEmbeddableFactory());