From ea71c10037c9d0d1702ff4e0586156d5b220200c Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 2 Aug 2024 12:18:43 -0300 Subject: [PATCH 01/13] [Discover] Add default app state extension and log integration data source profiles (#186347) ## Summary This PR adds a new `getDefaultAppState` extension that allows profiles to set default values for select app state properties, currently `columns` and `rowHeight`. It also adds logs data source sub profiles for the following integrations that consume the `getDefaultAppState` extension (only `columns` are used currently): - System logs - Kubernetes container logs - Windows logs - AWS S3 Logs - Nginx error logs - Nginx access logs - Apache error logs The index patterns and default state for the integrations are hardcoded for the initial implementation, but we should change this later to use an API and state provided by the integrations if we continue this approach. For testing, you can ingest sample data for the integrations using https://github.com/elastic/kibana-demo-data, but you'll need to reindex the data into correctly named data streams for each: ``` log-system_error -> logs-system.system-test log-k8s_container -> logs-kubernetes.container_logs-test log-aws_s3 -> logs-aws.s3access-test log-nginx_error -> logs-nginx.error-test log-nqinx -> logs-nginx.access-test log-apache_error -> logs-apache.error-test POST /_reindex { "source": { "index": "log-k8s_container" }, "dest": { "index": "logs-kubernetes.container_logs-test", "op_type": "create" } } ``` ![default_state](https://github.com/user-attachments/assets/ed73f527-bb5a-470e-b132-f626f0562e18) Resolves #186271. ### 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/packages/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 - [x] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] 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 renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Julia Rechkunova --- .../main/data_fetching/fetch_all.test.ts | 12 + .../main/data_fetching/fetch_all.ts | 31 +-- .../main/hooks/use_esql_mode.test.tsx | 92 ++++++++ .../application/main/hooks/use_esql_mode.ts | 137 +++++++----- .../discover_app_state_container.test.ts | 110 ++++++++- .../discover_app_state_container.ts | 70 +++--- .../discover_data_state_container.test.ts | 58 +++++ .../discover_data_state_container.ts | 81 +++++-- .../discover_internal_state_container.ts | 11 + .../main/state_management/discover_state.ts | 27 ++- .../utils/change_data_view.test.ts | 11 + .../utils/change_data_view.ts | 5 + .../utils/get_default_profile_state.test.ts | 99 +++++++++ .../utils/get_default_profile_state.ts | 81 +++++++ .../context_awareness/__mocks__/index.tsx | 17 ++ .../example_data_source_profile/profile.tsx | 16 ++ .../extend_profile_provider.test.ts | 34 +++ .../extend_profile_provider.ts | 21 ++ .../extract_index_pattern_from.test.ts | 40 ++++ .../extract_index_pattern_from.ts | 26 +++ .../accessors/get_default_app_state.ts | 32 +++ .../accessors/index.ts | 1 + .../logs_data_source_profile/consts.ts | 14 ++ .../create_profile_providers.ts | 34 +++ .../logs_data_source_profile/index.ts | 2 +- .../logs_data_source_profile/profile.ts | 24 +- .../sub_profiles/apache_error_logs.test.ts | 53 +++++ .../sub_profiles/apache_error_logs.ts | 26 +++ .../sub_profiles/aws_s3access_logs.test.ts | 55 +++++ .../sub_profiles/aws_s3access_logs.ts | 32 +++ .../sub_profiles/create_resolve.ts | 30 +++ .../sub_profiles/index.ts | 15 ++ .../kubernetes_container_logs.test.ts | 55 +++++ .../sub_profiles/kubernetes_container_logs.ts | 32 +++ .../sub_profiles/nginx_access_logs.test.ts | 55 +++++ .../sub_profiles/nginx_access_logs.ts | 32 +++ .../sub_profiles/nginx_error_logs.test.ts | 52 +++++ .../sub_profiles/nginx_error_logs.ts | 26 +++ .../sub_profiles/system_logs.test.ts | 54 +++++ .../sub_profiles/system_logs.ts | 31 +++ .../sub_profiles/windows_logs.test.ts | 53 +++++ .../sub_profiles/windows_logs.ts | 26 +++ .../register_profile_providers.ts | 50 +++-- .../profiles/document_profile.ts | 2 +- .../public/context_awareness/types.ts | 15 ++ .../context_awareness/_data_source_profile.ts | 32 ++- .../context_awareness/_root_profile.ts | 8 +- .../extensions/_get_cell_renderers.ts | 16 +- .../extensions/_get_default_app_state.ts | 206 +++++++++++++++++ .../extensions/_get_doc_viewer.ts | 16 +- .../extensions/_get_row_indicator_provider.ts | 12 +- .../apps/discover/context_awareness/index.ts | 1 + test/functional/services/data_grid.ts | 7 + .../use_discover_in_timeline_actions.tsx | 2 +- .../context_awareness/_data_source_profile.ts | 32 ++- .../context_awareness/_root_profile.ts | 11 +- .../extensions/_get_cell_renderers.ts | 19 +- .../extensions/_get_default_app_state.ts | 209 ++++++++++++++++++ .../extensions/_get_doc_viewer.ts | 17 +- .../extensions/_get_row_indicator_provider.ts | 12 +- .../discover/context_awareness/index.ts | 1 + 61 files changed, 2120 insertions(+), 261 deletions(-) create mode 100644 src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts create mode 100644 src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/get_default_app_state.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/consts.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/create_profile_providers.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/create_resolve.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.ts create mode 100644 test/functional/apps/discover/context_awareness/extensions/_get_default_app_state.ts create mode 100644 x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_default_app_state.ts diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.test.ts index 864c44fd6ce09..7d71e73d1b704 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.test.ts @@ -73,6 +73,10 @@ describe('test fetchAll', () => { expandedDoc: undefined, customFilters: [], overriddenVisContextAfterInvalidation: undefined, + resetDefaultProfileState: { + columns: false, + rowHeight: false, + }, }), searchSessionId: '123', initialFetchStatus: FetchStatus.UNINITIALIZED, @@ -261,6 +265,10 @@ describe('test fetchAll', () => { expandedDoc: undefined, customFilters: [], overriddenVisContextAfterInvalidation: undefined, + resetDefaultProfileState: { + columns: false, + rowHeight: false, + }, }), }; fetchAll(subjects, false, deps); @@ -379,6 +387,10 @@ describe('test fetchAll', () => { expandedDoc: undefined, customFilters: [], overriddenVisContextAfterInvalidation: undefined, + resetDefaultProfileState: { + columns: false, + rowHeight: false, + }, }), }; fetchAll(subjects, false, deps); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts index aed3e6f9a0222..2f7134e810611 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts @@ -8,7 +8,7 @@ import { Adapters } from '@kbn/inspector-plugin/common'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; -import { BehaviorSubject, filter, firstValueFrom, map, merge, scan } from 'rxjs'; +import { BehaviorSubject, combineLatest, filter, firstValueFrom, switchMap } from 'rxjs'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { isEqual } from 'lodash'; import { isOfAggregateQueryType } from '@kbn/es-query'; @@ -53,7 +53,8 @@ export interface FetchDeps { export function fetchAll( dataSubjects: SavedSearchData, reset = false, - fetchDeps: FetchDeps + fetchDeps: FetchDeps, + onFetchRecordsComplete?: () => Promise ): Promise { const { initialFetchStatus, @@ -177,10 +178,10 @@ export function fetchAll( // Return a promise that will resolve once all the requests have finished or failed return firstValueFrom( - merge( - fetchStatusByType(dataSubjects.documents$, 'documents'), - fetchStatusByType(dataSubjects.totalHits$, 'totalHits') - ).pipe(scan(toRequestFinishedMap, {}), filter(allRequestsFinished)) + combineLatest([ + isComplete(dataSubjects.documents$).pipe(switchMap(async () => onFetchRecordsComplete?.())), + isComplete(dataSubjects.totalHits$), + ]) ).then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. @@ -250,16 +251,8 @@ export async function fetchMoreDocuments( } } -const fetchStatusByType = (subject: BehaviorSubject, type: string) => - subject.pipe(map(({ fetchStatus }) => ({ type, fetchStatus }))); - -const toRequestFinishedMap = ( - currentMap: Record, - { type, fetchStatus }: { type: string; fetchStatus: FetchStatus } -) => ({ - ...currentMap, - [type]: [FetchStatus.COMPLETE, FetchStatus.ERROR].includes(fetchStatus), -}); - -const allRequestsFinished = (requests: Record) => - Object.values(requests).every((finished) => finished); +const isComplete = (subject: BehaviorSubject) => { + return subject.pipe( + filter(({ fetchStatus }) => [FetchStatus.COMPLETE, FetchStatus.ERROR].includes(fetchStatus)) + ); +}; diff --git a/src/plugins/discover/public/application/main/hooks/use_esql_mode.test.tsx b/src/plugins/discover/public/application/main/hooks/use_esql_mode.test.tsx index 5f6d35afe8434..10fe94f824681 100644 --- a/src/plugins/discover/public/application/main/hooks/use_esql_mode.test.tsx +++ b/src/plugins/discover/public/application/main/hooks/use_esql_mode.test.tsx @@ -23,6 +23,7 @@ import { DiscoverAppState } from '../state_management/discover_app_state_contain import { DiscoverStateContainer } from '../state_management/discover_state'; import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { dataViewAdHoc } from '../../../__mocks__/data_view_complex'; +import { buildDataTableRecord, EsHitRecord } from '@kbn/discover-utils'; function getHookProps( query: AggregateQuery | Query | undefined, @@ -487,4 +488,95 @@ describe('useEsqlMode', () => { }); }); }); + + it('should call setResetDefaultProfileState correctly when index pattern changes', async () => { + const { stateContainer } = renderHookWithContext(false); + const documents$ = stateContainer.dataState.data$.documents$; + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + documents$.next({ + fetchStatus: FetchStatus.PARTIAL, + query: { esql: 'from pattern1' }, + }); + await waitFor(() => + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: true, + rowHeight: true, + }) + ); + stateContainer.internalState.transitions.setResetDefaultProfileState({ + columns: false, + rowHeight: false, + }); + documents$.next({ + fetchStatus: FetchStatus.PARTIAL, + query: { esql: 'from pattern1' }, + }); + await waitFor(() => + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }) + ); + documents$.next({ + fetchStatus: FetchStatus.PARTIAL, + query: { esql: 'from pattern2' }, + }); + await waitFor(() => + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: true, + rowHeight: true, + }) + ); + }); + + it('should call setResetDefaultProfileState correctly when columns change', async () => { + const { stateContainer } = renderHookWithContext(false); + const documents$ = stateContainer.dataState.data$.documents$; + const result1 = [buildDataTableRecord({ message: 'foo' } as EsHitRecord)]; + const result2 = [buildDataTableRecord({ message: 'foo', extension: 'bar' } as EsHitRecord)]; + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + documents$.next({ + fetchStatus: FetchStatus.PARTIAL, + query: { esql: 'from pattern' }, + result: result1, + }); + await waitFor(() => + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: true, + rowHeight: true, + }) + ); + stateContainer.internalState.transitions.setResetDefaultProfileState({ + columns: false, + rowHeight: false, + }); + documents$.next({ + fetchStatus: FetchStatus.PARTIAL, + query: { esql: 'from pattern' }, + result: result1, + }); + await waitFor(() => + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }) + ); + documents$.next({ + fetchStatus: FetchStatus.PARTIAL, + query: { esql: 'from pattern' }, + result: result2, + }); + await waitFor(() => + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: true, + rowHeight: false, + }) + ); + }); }); diff --git a/src/plugins/discover/public/application/main/hooks/use_esql_mode.ts b/src/plugins/discover/public/application/main/hooks/use_esql_mode.ts index 841badc11537c..599b1ccd88ce5 100644 --- a/src/plugins/discover/public/application/main/hooks/use_esql_mode.ts +++ b/src/plugins/discover/public/application/main/hooks/use_esql_mode.ts @@ -18,6 +18,7 @@ import { getValidViewMode } from '../utils/get_valid_view_mode'; import { FetchStatus } from '../../types'; const MAX_NUM_OF_COLUMNS = 50; + /** * Hook to take care of ES|QL state transformations when a new result is returned * If necessary this is setting displayed columns and selected data view @@ -29,106 +30,122 @@ export function useEsqlMode({ stateContainer: DiscoverStateContainer; dataViews: DataViewsContract; }) { + const savedSearch = useSavedSearchInitial(); const prev = useRef<{ + initialFetch: boolean; query: string; - recentlyUpdatedToColumns: string[]; + allColumns: string[]; + defaultColumns: string[]; }>({ - recentlyUpdatedToColumns: [], + initialFetch: true, query: '', + allColumns: [], + defaultColumns: [], }); - const initialFetch = useRef(true); - const savedSearch = useSavedSearchInitial(); const cleanup = useCallback(() => { - if (prev.current.query) { - // cleanup when it's not an ES|QL query - prev.current = { - recentlyUpdatedToColumns: [], - query: '', - }; - initialFetch.current = true; + if (!prev.current.query) { + return; } + + // cleanup when it's not an ES|QL query + prev.current = { + initialFetch: true, + query: '', + allColumns: [], + defaultColumns: [], + }; }, []); useEffect(() => { const subscription = stateContainer.dataState.data$.documents$ .pipe( switchMap(async (next) => { - const { query } = next; - if (!query || next.fetchStatus === FetchStatus.ERROR) { + const { query: nextQuery } = next; + + if (!nextQuery || next.fetchStatus === FetchStatus.ERROR) { return; } - const sendComplete = () => { - stateContainer.dataState.data$.documents$.next({ - ...next, - fetchStatus: FetchStatus.COMPLETE, - }); - }; + if (!isOfAggregateQueryType(nextQuery)) { + // cleanup for a "regular" query + cleanup(); + return; + } - const { viewMode } = stateContainer.appState.getState(); - const isEsqlQuery = isOfAggregateQueryType(query); + if (next.fetchStatus !== FetchStatus.PARTIAL) { + return; + } - if (isEsqlQuery) { - const hasResults = Boolean(next.result?.length); + let nextAllColumns = prev.current.allColumns; + let nextDefaultColumns = prev.current.defaultColumns; - if (next.fetchStatus !== FetchStatus.PARTIAL) { - return; - } + if (next.result?.length) { + nextAllColumns = Object.keys(next.result[0].raw); - let nextColumns: string[] = prev.current.recentlyUpdatedToColumns; + if (hasTransformationalCommand(nextQuery.esql)) { + nextDefaultColumns = nextAllColumns.slice(0, MAX_NUM_OF_COLUMNS); + } else { + nextDefaultColumns = []; + } + } - if (hasResults) { - const firstRow = next.result![0]; - const firstRowColumns = Object.keys(firstRow.raw); + if (prev.current.initialFetch) { + prev.current.initialFetch = false; + prev.current.query = nextQuery.esql; + prev.current.allColumns = nextAllColumns; + prev.current.defaultColumns = nextDefaultColumns; + } - if (hasTransformationalCommand(query.esql)) { - nextColumns = firstRowColumns.slice(0, MAX_NUM_OF_COLUMNS); - } else { - nextColumns = []; - } - } + const indexPatternChanged = + getIndexPatternFromESQLQuery(nextQuery.esql) !== + getIndexPatternFromESQLQuery(prev.current.query); - if (initialFetch.current) { - initialFetch.current = false; - prev.current.query = query.esql; - prev.current.recentlyUpdatedToColumns = nextColumns; - } + const allColumnsChanged = !isEqual(nextAllColumns, prev.current.allColumns); - const indexPatternChanged = - getIndexPatternFromESQLQuery(query.esql) !== - getIndexPatternFromESQLQuery(prev.current.query); + const changeDefaultColumns = + indexPatternChanged || !isEqual(nextDefaultColumns, prev.current.defaultColumns); - const addColumnsToState = - indexPatternChanged || !isEqual(nextColumns, prev.current.recentlyUpdatedToColumns); + const { viewMode } = stateContainer.appState.getState(); + const changeViewMode = viewMode !== getValidViewMode({ viewMode, isEsqlMode: true }); - const changeViewMode = viewMode !== getValidViewMode({ viewMode, isEsqlMode: true }); + if (indexPatternChanged) { + stateContainer.internalState.transitions.setResetDefaultProfileState({ + columns: true, + rowHeight: true, + }); + } else if (allColumnsChanged) { + stateContainer.internalState.transitions.setResetDefaultProfileState({ + columns: true, + rowHeight: false, + }); + } - if (!indexPatternChanged && !addColumnsToState && !changeViewMode) { - sendComplete(); - return; - } + prev.current.allColumns = nextAllColumns; - prev.current.query = query.esql; - prev.current.recentlyUpdatedToColumns = nextColumns; + if (indexPatternChanged || changeDefaultColumns || changeViewMode) { + prev.current.query = nextQuery.esql; + prev.current.defaultColumns = nextDefaultColumns; // just change URL state if necessary - if (addColumnsToState || changeViewMode) { + if (changeDefaultColumns || changeViewMode) { const nextState = { - ...(addColumnsToState && { columns: nextColumns }), + ...(changeDefaultColumns && { columns: nextDefaultColumns }), ...(changeViewMode && { viewMode: undefined }), }; + await stateContainer.appState.replaceUrlState(nextState); } - - sendComplete(); - } else { - // cleanup for a "regular" query - cleanup(); } + + stateContainer.dataState.data$.documents$.next({ + ...next, + fetchStatus: FetchStatus.COMPLETE, + }); }) ) .subscribe(); + return () => { // cleanup for e.g. when savedSearch is switched cleanup(); diff --git a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts index 75ae6208be871..ffc566d10f316 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts @@ -8,39 +8,55 @@ import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import { + createKbnUrlStateStorage, + IKbnUrlStateStorage, + withNotifyOnErrors, +} from '@kbn/kibana-utils-plugin/public'; import type { Filter } from '@kbn/es-query'; import { History } from 'history'; -import { savedSearchMock } from '../../../__mocks__/saved_search'; import { discoverServiceMock } from '../../../__mocks__/services'; -import { - DiscoverAppStateContainer, - getDiscoverAppStateContainer, - isEqualState, -} from './discover_app_state_container'; +import { getDiscoverAppStateContainer, isEqualState } from './discover_app_state_container'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/common'; import { createDataViewDataSource } from '../../../../common/data_sources'; +import { getInternalStateContainer } from './discover_internal_state_container'; +import { + DiscoverSavedSearchContainer, + getSavedSearchContainer, +} from './discover_saved_search_container'; +import { getDiscoverGlobalStateContainer } from './discover_global_state_container'; let history: History; -let state: DiscoverAppStateContainer; +let stateStorage: IKbnUrlStateStorage; +let internalState: ReturnType; +let savedSearchState: DiscoverSavedSearchContainer; describe('Test discover app state container', () => { beforeEach(async () => { const storeInSessionStorage = discoverServiceMock.uiSettings.get('state:storeInSessionStorage'); const toasts = discoverServiceMock.core.notifications.toasts; - const stateStorage = createKbnUrlStateStorage({ + stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, history, ...(toasts && withNotifyOnErrors(toasts)), }); - state = getDiscoverAppStateContainer({ - stateStorage, - savedSearch: savedSearchMock, + internalState = getInternalStateContainer(); + savedSearchState = getSavedSearchContainer({ services: discoverServiceMock, + globalStateContainer: getDiscoverGlobalStateContainer(stateStorage), }); }); + const getStateContainer = () => + getDiscoverAppStateContainer({ + stateStorage, + internalStateContainer: internalState, + savedSearchContainer: savedSearchState, + services: discoverServiceMock, + }); + test('hasChanged returns whether the current state has changed', async () => { + const state = getStateContainer(); state.set({ dataSource: createDataViewDataSource({ dataViewId: 'modified' }), }); @@ -50,6 +66,7 @@ describe('Test discover app state container', () => { }); test('getPrevious returns the state before the current', async () => { + const state = getStateContainer(); state.set({ dataSource: createDataViewDataSource({ dataViewId: 'first' }), }); @@ -110,6 +127,7 @@ describe('Test discover app state container', () => { } as SavedSearch; test('should return correct output', () => { + const state = getStateContainer(); const appState = state.getAppStateFromSavedSearch(localSavedSearchMock); expect(appState).toMatchObject( expect.objectContaining({ @@ -133,6 +151,7 @@ describe('Test discover app state container', () => { }); test('should return default query if query is undefined', () => { + const state = getStateContainer(); discoverServiceMock.data.query.queryString.getDefaultQuery = jest .fn() .mockReturnValue(defaultQuery); @@ -233,6 +252,7 @@ describe('Test discover app state container', () => { }); test('should automatically set ES|QL data source when query is ES|QL', () => { + const state = getStateContainer(); state.update({ dataSource: createDataViewDataSource({ dataViewId: 'test' }), }); @@ -244,4 +264,70 @@ describe('Test discover app state container', () => { }); expect(state.get().dataSource?.type).toBe('esql'); }); + + describe('initAndSync', () => { + it('should call setResetDefaultProfileState correctly with no initial state', () => { + const state = getStateContainer(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + state.initAndSync(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: true, + rowHeight: true, + }); + }); + + it('should call setResetDefaultProfileState correctly with initial columns', () => { + const stateStorageGetSpy = jest.spyOn(stateStorage, 'get'); + stateStorageGetSpy.mockReturnValue({ columns: ['test'] }); + const state = getStateContainer(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + state.initAndSync(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: true, + }); + }); + + it('should call setResetDefaultProfileState correctly with initial rowHeight', () => { + const stateStorageGetSpy = jest.spyOn(stateStorage, 'get'); + stateStorageGetSpy.mockReturnValue({ rowHeight: 5 }); + const state = getStateContainer(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + state.initAndSync(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: true, + rowHeight: false, + }); + }); + + it('should call setResetDefaultProfileState correctly with saved search', () => { + const stateStorageGetSpy = jest.spyOn(stateStorage, 'get'); + stateStorageGetSpy.mockReturnValue({ columns: ['test'], rowHeight: 5 }); + const savedSearchGetSpy = jest.spyOn(savedSearchState, 'getState'); + savedSearchGetSpy.mockReturnValue({ + id: 'test', + searchSource: createSearchSourceMock(), + managed: false, + }); + const state = getStateContainer(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + state.initAndSync(); + expect(internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + }); + }); }); diff --git a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts index 5ec9ca4d7215c..f364530faba5e 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts @@ -38,6 +38,8 @@ import { DiscoverDataSource, isDataSourceType, } from '../../../../common/data_sources'; +import type { DiscoverInternalStateContainer } from './discover_internal_state_container'; +import type { DiscoverSavedSearchContainer } from './discover_saved_search_container'; export const APP_STATE_URL_KEY = '_a'; export interface DiscoverAppStateContainer extends ReduxLikeStateContainer { @@ -54,10 +56,9 @@ export interface DiscoverAppStateContainer extends ReduxLikeStateContainer boolean; /** - * Initializes the state by the given saved search and starts syncing the state with the URL - * @param currentSavedSearch + * Initializes the app state and starts syncing it with the URL */ - initAndSync: (currentSavedSearch: SavedSearch) => () => void; + initAndSync: () => () => void; /** * Replaces the current state in URL with the given state * @param newState @@ -82,11 +83,10 @@ export interface DiscoverAppStateContainer extends ReduxLikeStateContainer void; - /* * Get updated AppState when given a saved search * - * */ + */ getAppStateFromSavedSearch: (newSavedSearch: SavedSearch) => DiscoverAppState; } @@ -157,6 +157,17 @@ export interface DiscoverAppState { breakdownField?: string; } +export interface AppStateUrl extends Omit { + /** + * Necessary to take care of legacy links [fieldName,direction] + */ + sort?: string[][] | [string, string]; + /** + * Legacy data view ID prop + */ + index?: string; +} + export const { Provider: DiscoverAppStateProvider, useSelector: useAppStateSelector } = createStateContainerReactHelpers>(); @@ -168,14 +179,20 @@ export const { Provider: DiscoverAppStateProvider, useSelector: useAppStateSelec */ export const getDiscoverAppStateContainer = ({ stateStorage, - savedSearch, + internalStateContainer, + savedSearchContainer, services, }: { stateStorage: IKbnUrlStateStorage; - savedSearch: SavedSearch; + internalStateContainer: DiscoverInternalStateContainer; + savedSearchContainer: DiscoverSavedSearchContainer; services: DiscoverServices; }): DiscoverAppStateContainer => { - let initialState = getInitialState(stateStorage, savedSearch, services); + let initialState = getInitialState( + getCurrentUrlState(stateStorage, services), + savedSearchContainer.getState(), + services + ); let previousState = initialState; const appStateContainer = createStateContainer(initialState); @@ -234,9 +251,20 @@ export const getDiscoverAppStateContainer = ({ }); }; - const initializeAndSync = (currentSavedSearch: SavedSearch) => { + const initializeAndSync = () => { + const currentSavedSearch = savedSearchContainer.getState(); + addLog('[appState] initialize state and sync with URL', currentSavedSearch); + if (!currentSavedSearch.id) { + const { columns, rowHeight } = getCurrentUrlState(stateStorage, services); + + internalStateContainer.transitions.setResetDefaultProfileState({ + columns: columns === undefined, + rowHeight: rowHeight === undefined, + }); + } + const { data } = services; const savedSearchDataView = currentSavedSearch.searchSource.getField('index'); const appState = enhancedAppContainer.getState(); @@ -314,34 +342,24 @@ export const getDiscoverAppStateContainer = ({ }; }; -export interface AppStateUrl extends Omit { - /** - * Necessary to take care of legacy links [fieldName,direction] - */ - sort?: string[][] | [string, string]; - /** - * Legacy data view ID prop - */ - index?: string; +function getCurrentUrlState(stateStorage: IKbnUrlStateStorage, services: DiscoverServices) { + return cleanupUrlState( + stateStorage.get(APP_STATE_URL_KEY) ?? {}, + services.uiSettings + ); } export function getInitialState( - stateStorage: IKbnUrlStateStorage | undefined, + initialUrlState: DiscoverAppState | undefined, savedSearch: SavedSearch, services: DiscoverServices ) { - const appStateFromUrl = stateStorage?.get(APP_STATE_URL_KEY); const defaultAppState = getStateDefaults({ savedSearch, services, }); return handleSourceColumnState( - appStateFromUrl == null - ? defaultAppState - : { - ...defaultAppState, - ...cleanupUrlState(appStateFromUrl, services.uiSettings), - }, + initialUrlState === undefined ? defaultAppState : { ...defaultAppState, ...initialUrlState }, services.uiSettings ); } diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts index 05668e0406f9c..1c300aaf7cc15 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts @@ -165,4 +165,62 @@ describe('test getDataStateContainer', () => { dataState.refetch$.next('fetch_more'); }); + + it('should update app state from default profile state', async () => { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + const dataState = stateContainer.dataState; + const dataUnsub = dataState.subscribe(); + const appUnsub = stateContainer.appState.initAndSync(); + discoverServiceMock.profilesManager.resolveDataSourceProfile({}); + stateContainer.actions.setDataView(dataViewMock); + stateContainer.internalState.transitions.setResetDefaultProfileState({ + columns: true, + rowHeight: true, + }); + dataState.data$.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + result: 0, + }); + dataState.refetch$.next(undefined); + await waitFor(() => { + expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE); + }); + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + expect(stateContainer.appState.get().columns).toEqual(['message', 'extension']); + expect(stateContainer.appState.get().rowHeight).toEqual(3); + dataUnsub(); + appUnsub(); + }); + + it('should not update app state from default profile state', async () => { + const stateContainer = getDiscoverStateMock({ isTimeBased: true }); + const dataState = stateContainer.dataState; + const dataUnsub = dataState.subscribe(); + const appUnsub = stateContainer.appState.initAndSync(); + discoverServiceMock.profilesManager.resolveDataSourceProfile({}); + stateContainer.actions.setDataView(dataViewMock); + stateContainer.internalState.transitions.setResetDefaultProfileState({ + columns: false, + rowHeight: false, + }); + dataState.data$.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + result: 0, + }); + dataState.refetch$.next(undefined); + await waitFor(() => { + expect(dataState.data$.main$.value.fetchStatus).toBe(FetchStatus.COMPLETE); + }); + expect(stateContainer.internalState.get().resetDefaultProfileState).toEqual({ + columns: false, + rowHeight: false, + }); + expect(stateContainer.appState.get().columns).toEqual(['default_column']); + expect(stateContainer.appState.get().rowHeight).toBeUndefined(); + dataUnsub(); + appUnsub(); + }); }); diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index d467d965f012d..6e34982dc91ae 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -10,24 +10,29 @@ import { BehaviorSubject, filter, map, mergeMap, Observable, share, Subject, tap import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public'; import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { DataView } from '@kbn/data-views-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/common'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import type { SearchResponseWarning } from '@kbn/search-response-warnings'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING } from '@kbn/discover-utils'; +import { + DEFAULT_COLUMNS_SETTING, + SEARCH_FIELDS_FROM_SOURCE, + SEARCH_ON_PAGE_LOAD_SETTING, +} from '@kbn/discover-utils'; import { getEsqlDataView } from './utils/get_esql_data_view'; -import { DiscoverAppState } from './discover_app_state_container'; -import { DiscoverServices } from '../../../build_services'; -import { DiscoverSearchSessionManager } from './discover_search_session'; +import type { DiscoverAppStateContainer } from './discover_app_state_container'; +import type { DiscoverServices } from '../../../build_services'; +import type { DiscoverSearchSessionManager } from './discover_search_session'; import { FetchStatus } from '../../types'; import { validateTimeRange } from './utils/validate_time_range'; import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all'; import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { getFetch$ } from '../data_fetching/get_fetch_observable'; -import { InternalState } from './discover_internal_state_container'; +import type { DiscoverInternalStateContainer } from './discover_internal_state_container'; +import { getDefaultProfileState } from './utils/get_default_profile_state'; export interface SavedSearchData { main$: DataMain$; @@ -138,15 +143,15 @@ export interface DiscoverDataStateContainer { export function getDataStateContainer({ services, searchSessionManager, - getAppState, - getInternalState, + appStateContainer, + internalStateContainer, getSavedSearch, setDataView, }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; - getAppState: () => DiscoverAppState; - getInternalState: () => InternalState; + appStateContainer: DiscoverAppStateContainer; + internalStateContainer: DiscoverInternalStateContainer; getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; }): DiscoverDataStateContainer { @@ -221,8 +226,8 @@ export function getDataStateContainer({ inspectorAdapters, searchSessionId, services, - getAppState, - getInternalState, + getAppState: appStateContainer.getState, + getInternalState: internalStateContainer.getState, savedSearch: getSavedSearch(), useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), }; @@ -232,34 +237,65 @@ export function getDataStateContainer({ if (options.fetchMore) { abortControllerFetchMore = new AbortController(); - const fetchMoreStartTime = window.performance.now(); + await fetchMoreDocuments(dataSubjects, { abortController: abortControllerFetchMore, ...commonFetchDeps, }); + const fetchMoreDuration = window.performance.now() - fetchMoreStartTime; reportPerformanceMetricEvent(services.analytics, { eventName: 'discoverFetchMore', duration: fetchMoreDuration, }); + return; } await profilesManager.resolveDataSourceProfile({ - dataSource: getAppState().dataSource, + dataSource: appStateContainer.getState().dataSource, dataView: getSavedSearch().searchSource.getField('index'), - query: getAppState().query, + query: appStateContainer.getState().query, }); abortController = new AbortController(); const prevAutoRefreshDone = autoRefreshDone; - const fetchAllStartTime = window.performance.now(); - await fetchAll(dataSubjects, options.reset, { - abortController, - ...commonFetchDeps, - }); + + await fetchAll( + dataSubjects, + options.reset, + { + abortController, + ...commonFetchDeps, + }, + async () => { + const { resetDefaultProfileState, dataView } = internalStateContainer.getState(); + const { esqlQueryColumns } = dataSubjects.documents$.getValue(); + const defaultColumns = uiSettings.get(DEFAULT_COLUMNS_SETTING, []); + + if (dataView) { + const stateUpdate = getDefaultProfileState({ + profilesManager, + resetDefaultProfileState, + defaultColumns, + dataView, + esqlQueryColumns, + }); + + if (stateUpdate) { + await appStateContainer.replaceUrlState(stateUpdate); + } + } + + internalStateContainer.transitions.setResetDefaultProfileState({ + columns: false, + rowHeight: false, + }); + } + ); + const fetchAllDuration = window.performance.now() - fetchAllStartTime; reportPerformanceMetricEvent(services.analytics, { eventName: 'discoverFetchAll', @@ -286,7 +322,7 @@ export function getDataStateContainer({ } const fetchQuery = async (resetQuery?: boolean) => { - const query = getAppState().query; + const query = appStateContainer.getState().query; const currentDataView = getSavedSearch().searchSource.getField('index'); if (isOfAggregateQueryType(query)) { @@ -301,6 +337,7 @@ export function getDataStateContainer({ } else { refetch$.next(undefined); } + return refetch$; }; diff --git a/src/plugins/discover/public/application/main/state_management/discover_internal_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_internal_state_container.ts index 4ebbe94832d0c..cb5a9d66d7f89 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_internal_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_internal_state_container.ts @@ -24,6 +24,7 @@ export interface InternalState { expandedDoc: DataTableRecord | undefined; customFilters: Filter[]; overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saved search saving + resetDefaultProfileState: { columns: boolean; rowHeight: boolean }; } export interface InternalStateTransitions { @@ -48,6 +49,9 @@ export interface InternalStateTransitions { overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined ) => InternalState; resetOnSavedSearchChange: (state: InternalState) => () => InternalState; + setResetDefaultProfileState: ( + state: InternalState + ) => (resetDefaultProfileState: InternalState['resetDefaultProfileState']) => InternalState; } export type DiscoverInternalStateContainer = ReduxLikeStateContainer< @@ -68,6 +72,7 @@ export function getInternalStateContainer() { expandedDoc: undefined, customFilters: [], overriddenVisContextAfterInvalidation: undefined, + resetDefaultProfileState: { columns: false, rowHeight: false }, }, { setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({ @@ -134,6 +139,12 @@ export function getInternalStateContainer() { overriddenVisContextAfterInvalidation: undefined, expandedDoc: undefined, }), + setResetDefaultProfileState: + (prevState: InternalState) => + (resetDefaultProfileState: InternalState['resetDefaultProfileState']) => ({ + ...prevState, + resetDefaultProfileState, + }), }, {}, { freeze: (state) => state } diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index 169e596a2cf93..a8c8ce3419eb8 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -256,20 +256,21 @@ export function getDiscoverStateContainer({ globalStateContainer, }); + /** + * Internal State Container, state that's not persisted and not part of the URL + */ + const internalStateContainer = getInternalStateContainer(); + /** * App State Container, synced with the _a part URL */ const appStateContainer = getDiscoverAppStateContainer({ stateStorage, - savedSearch: savedSearchContainer.getState(), + internalStateContainer, + savedSearchContainer, services, }); - /** - * Internal State Container, state that's not persisted and not part of the URL - */ - const internalStateContainer = getInternalStateContainer(); - const pauseAutoRefreshInterval = async (dataView: DataView) => { if (dataView && (!dataView.isTimeBased() || dataView.type === DataViewType.ROLLUP)) { const state = globalStateContainer.get(); @@ -281,6 +282,7 @@ export function getDiscoverStateContainer({ } } }; + const setDataView = (dataView: DataView) => { internalStateContainer.transitions.setDataView(dataView); pauseAutoRefreshInterval(dataView); @@ -290,8 +292,8 @@ export function getDiscoverStateContainer({ const dataStateContainer = getDataStateContainer({ services, searchSessionManager, - getAppState: appStateContainer.getState, - getInternalState: internalStateContainer.getState, + appStateContainer, + internalStateContainer, getSavedSearch: savedSearchContainer.getState, setDataView, }); @@ -403,9 +405,8 @@ export function getDiscoverStateContainer({ }); // initialize app state container, syncing with _g and _a part of the URL - const appStateInitAndSyncUnsubscribe = appStateContainer.initAndSync( - savedSearchContainer.getState() - ); + const appStateInitAndSyncUnsubscribe = appStateContainer.initAndSync(); + // subscribing to state changes of appStateContainer, triggering data fetching const appStateUnsubscribe = appStateContainer.subscribe( buildStateSubscribe({ @@ -417,6 +418,7 @@ export function getDiscoverStateContainer({ setDataView, }) ); + // start subscribing to dataStateContainer, triggering data fetching const unsubscribeData = dataStateContainer.subscribe(); @@ -467,6 +469,7 @@ export function getDiscoverStateContainer({ await onChangeDataView(newDataView); return newDataView; }; + /** * Triggered when a user submits a query in the search bar */ @@ -492,6 +495,7 @@ export function getDiscoverStateContainer({ appState: appStateContainer, }); }; + /** * Undo all changes to the current saved search */ @@ -518,6 +522,7 @@ export function getDiscoverStateContainer({ await appStateContainer.replaceUrlState(newAppState); return nextSavedSearch; }; + const fetchData = (initial: boolean = false) => { addLog('fetchData', { initial }); if (!initial || dataStateContainer.getInitialFetchStatus() === FetchStatus.LOADING) { diff --git a/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts b/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts index 4e486e588b8eb..59a5010e178f8 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts @@ -31,6 +31,7 @@ const setupTestParams = (dataView: DataView | undefined) => { discoverState.appState.update = jest.fn(); discoverState.internalState.transitions = { setIsDataViewLoading: jest.fn(), + setResetDefaultProfileState: jest.fn(), } as unknown as Readonly>; return { services, @@ -71,4 +72,14 @@ describe('changeDataView', () => { expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true); expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(2, false); }); + + it('should call setResetDefaultProfileState correctly when switching data view', async () => { + const params = setupTestParams(dataViewComplexMock); + expect(params.internalState.transitions.setResetDefaultProfileState).not.toHaveBeenCalled(); + await changeDataView(dataViewComplexMock.id!, params); + expect(params.internalState.transitions.setResetDefaultProfileState).toHaveBeenCalledWith({ + columns: true, + rowHeight: true, + }); + }); }); diff --git a/src/plugins/discover/public/application/main/state_management/utils/change_data_view.ts b/src/plugins/discover/public/application/main/state_management/utils/change_data_view.ts index 65e029260130c..df48b56922ac0 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/change_data_view.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/change_data_view.ts @@ -35,10 +35,12 @@ export async function changeDataView( } ) { addLog('[ui] changeDataView', { id }); + const { dataViews, uiSettings } = services; const dataView = internalState.getState().dataView; const state = appState.getState(); let nextDataView: DataView | null = null; + internalState.transitions.setIsDataViewLoading(true); try { @@ -60,9 +62,12 @@ export async function changeDataView( ); appState.update(nextAppState); + if (internalState.getState().expandedDoc) { internalState.transitions.setExpandedDoc(undefined); } } + internalState.transitions.setIsDataViewLoading(false); + internalState.transitions.setResetDefaultProfileState({ columns: true, rowHeight: true }); } diff --git a/src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts b/src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts new file mode 100644 index 0000000000000..e36a753bd4c25 --- /dev/null +++ b/src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.test.ts @@ -0,0 +1,99 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { fieldList } from '@kbn/data-views-plugin/common'; +import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { createContextAwarenessMocks } from '../../../../context_awareness/__mocks__'; +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { getDefaultProfileState } from './get_default_profile_state'; + +const emptyDataView = buildDataViewMock({ + name: 'emptyDataView', + fields: fieldList(), +}); +const { profilesManagerMock } = createContextAwarenessMocks(); + +profilesManagerMock.resolveDataSourceProfile({}); + +describe('getDefaultProfileState', () => { + it('should return expected columns', () => { + let appState = getDefaultProfileState({ + profilesManager: profilesManagerMock, + resetDefaultProfileState: { + columns: true, + rowHeight: false, + }, + defaultColumns: ['messsage', 'bytes'], + dataView: dataViewWithTimefieldMock, + esqlQueryColumns: undefined, + }); + expect(appState).toEqual({ + columns: ['message', 'extension', 'bytes'], + grid: { + columns: { + extension: { + width: 200, + }, + message: { + width: 100, + }, + }, + }, + }); + appState = getDefaultProfileState({ + profilesManager: profilesManagerMock, + resetDefaultProfileState: { + columns: true, + rowHeight: false, + }, + defaultColumns: ['messsage', 'bytes'], + dataView: emptyDataView, + esqlQueryColumns: [{ id: '1', name: 'foo', meta: { type: 'string' } }], + }); + expect(appState).toEqual({ + columns: ['foo'], + grid: { + columns: { + foo: { + width: 300, + }, + }, + }, + }); + }); + + it('should return expected rowHeight', () => { + const appState = getDefaultProfileState({ + profilesManager: profilesManagerMock, + resetDefaultProfileState: { + columns: false, + rowHeight: true, + }, + defaultColumns: [], + dataView: dataViewWithTimefieldMock, + esqlQueryColumns: undefined, + }); + expect(appState).toEqual({ + rowHeight: 3, + }); + }); + + it('should return undefined', () => { + const appState = getDefaultProfileState({ + profilesManager: profilesManagerMock, + resetDefaultProfileState: { + columns: false, + rowHeight: false, + }, + defaultColumns: [], + dataView: dataViewWithTimefieldMock, + esqlQueryColumns: undefined, + }); + expect(appState).toEqual(undefined); + }); +}); diff --git a/src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.ts b/src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.ts new file mode 100644 index 0000000000000..b1bc2bc3e3f92 --- /dev/null +++ b/src/plugins/discover/public/application/main/state_management/utils/get_default_profile_state.ts @@ -0,0 +1,81 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; +import { uniqBy } from 'lodash'; +import type { DiscoverAppState } from '../discover_app_state_container'; +import { + DefaultAppStateColumn, + getMergedAccessor, + ProfilesManager, +} from '../../../../context_awareness'; +import type { InternalState } from '../discover_internal_state_container'; +import type { DataDocumentsMsg } from '../discover_data_state_container'; + +export const getDefaultProfileState = ({ + profilesManager, + resetDefaultProfileState, + defaultColumns, + dataView, + esqlQueryColumns, +}: { + profilesManager: ProfilesManager; + resetDefaultProfileState: InternalState['resetDefaultProfileState']; + defaultColumns: string[]; + dataView: DataView; + esqlQueryColumns: DataDocumentsMsg['esqlQueryColumns']; +}) => { + const stateUpdate: DiscoverAppState = {}; + const defaultState = getDefaultState(profilesManager, dataView); + + if (resetDefaultProfileState.columns) { + const mappedDefaultColumns = defaultColumns.map((name) => ({ name })); + const isValidColumn = getIsValidColumn(dataView, esqlQueryColumns); + const validColumns = uniqBy( + defaultState.columns?.concat(mappedDefaultColumns).filter(isValidColumn), + 'name' + ); + + if (validColumns?.length) { + const columns = validColumns.reduce( + (acc, { name, width }) => (width ? { ...acc, [name]: { width } } : acc), + undefined + ); + + stateUpdate.grid = columns ? { columns } : undefined; + stateUpdate.columns = validColumns.map(({ name }) => name); + } + } + + if (resetDefaultProfileState.rowHeight && defaultState.rowHeight !== undefined) { + stateUpdate.rowHeight = defaultState.rowHeight; + } + + return Object.keys(stateUpdate).length ? stateUpdate : undefined; +}; + +const getDefaultState = (profilesManager: ProfilesManager, dataView: DataView) => { + const getDefaultAppState = getMergedAccessor( + profilesManager.getProfiles(), + 'getDefaultAppState', + () => ({}) + ); + + return getDefaultAppState({ dataView }); +}; + +const getIsValidColumn = + (dataView: DataView, esqlQueryColumns: DataDocumentsMsg['esqlQueryColumns']) => + (column: DefaultAppStateColumn) => { + const isValid = esqlQueryColumns + ? esqlQueryColumns.some((esqlColumn) => esqlColumn.name === column.name) + : dataView.fields.getByName(column.name); + + return Boolean(isValid); + }; diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.tsx b/src/plugins/discover/public/context_awareness/__mocks__/index.tsx index 9e94c4b9b4857..5d55dfa306b8e 100644 --- a/src/plugins/discover/public/context_awareness/__mocks__/index.tsx +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.tsx @@ -49,6 +49,23 @@ export const createContextAwarenessMocks = ({ ...prev(), rootProfile: () => <>data-source-profile, })), + getDefaultAppState: jest.fn(() => () => ({ + columns: [ + { + name: 'message', + width: 100, + }, + { + name: 'extension', + width: 200, + }, + { + name: 'foo', + width: 300, + }, + ], + rowHeight: 3, + })), }, resolve: jest.fn(() => ({ isMatch: true, diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx index f9cba1592dc30..911e69e6d0d33 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx @@ -71,6 +71,22 @@ export const exampleDataSourceProfileProvider: DataSourceProfileProvider = { }, }; }, + getDefaultAppState: () => () => ({ + columns: [ + { + name: '@timestamp', + width: 212, + }, + { + name: 'log.level', + width: 150, + }, + { + name: 'message', + }, + ], + rowHeight: 5, + }), }, resolve: (params) => { let indexPattern: string | undefined; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.test.ts new file mode 100644 index 0000000000000..0bde960cf0020 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.test.ts @@ -0,0 +1,34 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { createContextAwarenessMocks } from '../__mocks__'; +import { extendProfileProvider } from './extend_profile_provider'; + +const { dataSourceProfileProviderMock } = createContextAwarenessMocks(); + +describe('extendProfileProvider', () => { + it('should merge profiles and overwrite other properties', () => { + const resolve = jest.fn(); + const getDefaultAppState = jest.fn(); + const extendedProfileProvider = extendProfileProvider(dataSourceProfileProviderMock, { + profileId: 'extended-profile', + profile: { getDefaultAppState }, + resolve, + }); + + expect(extendedProfileProvider).toEqual({ + ...dataSourceProfileProviderMock, + profileId: 'extended-profile', + profile: { + ...dataSourceProfileProviderMock.profile, + getDefaultAppState, + }, + resolve, + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.ts b/src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.ts new file mode 100644 index 0000000000000..e0165a4a101cd --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/extend_profile_provider.ts @@ -0,0 +1,21 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { BaseProfileProvider } from '../profile_service'; + +export const extendProfileProvider = >( + baseProvider: TProvider, + extension: Partial & Pick +): TProvider => ({ + ...baseProvider, + ...extension, + profile: { + ...baseProvider.profile, + ...extension.profile, + }, +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.test.ts new file mode 100644 index 0000000000000..6d57c342aa41e --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.test.ts @@ -0,0 +1,40 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { createDataViewDataSource, createEsqlDataSource } from '../../../common/data_sources'; +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; +import { extractIndexPatternFrom } from './extract_index_pattern_from'; + +describe('extractIndexPatternFrom', () => { + it('should return index pattern from data view', () => { + const indexPattern = extractIndexPatternFrom({ + dataSource: createDataViewDataSource({ dataViewId: dataViewWithTimefieldMock.id! }), + dataView: dataViewWithTimefieldMock, + }); + expect(indexPattern).toBe(dataViewWithTimefieldMock.getIndexPattern()); + }); + + it('should return index pattern from ES|QL query', () => { + const indexPattern = extractIndexPatternFrom({ + dataSource: createEsqlDataSource(), + query: { esql: 'FROM index-pattern' }, + }); + expect(indexPattern).toBe('index-pattern'); + }); + + it('should return null if no data view or ES|QL query', () => { + let indexPattern = extractIndexPatternFrom({ + dataSource: createDataViewDataSource({ dataViewId: dataViewWithTimefieldMock.id! }), + }); + expect(indexPattern).toBeNull(); + indexPattern = extractIndexPatternFrom({ + dataSource: createEsqlDataSource(), + }); + expect(indexPattern).toBeNull(); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.ts b/src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.ts new file mode 100644 index 0000000000000..63903953f732d --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/extract_index_pattern_from.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { isDataViewSource, isEsqlSource } from '../../../common/data_sources'; +import type { DataSourceProfileProviderParams } from '../profiles'; + +export const extractIndexPatternFrom = ({ + dataSource, + dataView, + query, +}: Pick) => { + if (isEsqlSource(dataSource) && isOfAggregateQueryType(query)) { + return getIndexPatternFromESQLQuery(query.esql); + } else if (isDataViewSource(dataSource) && dataView) { + return dataView.getIndexPattern(); + } + + return null; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/get_default_app_state.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/get_default_app_state.ts new file mode 100644 index 0000000000000..b8d24fed9ae56 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/get_default_app_state.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import type { DataSourceProfileProvider } from '../../../profiles'; +import { DefaultAppStateColumn } from '../../../types'; + +export const createGetDefaultAppState = ({ + defaultColumns, +}: { + defaultColumns?: DefaultAppStateColumn[]; +}): DataSourceProfileProvider['profile']['getDefaultAppState'] => { + return (prev) => (params) => { + const appState = { ...prev(params) }; + + if (defaultColumns) { + appState.columns = []; + + if (params.dataView.isTimeBased()) { + appState.columns.push({ name: params.dataView.timeFieldName, width: 212 }); + } + + appState.columns.push(...defaultColumns); + } + + return appState; + }; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/index.ts index 12522bbf915c6..a1cefc04aa486 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/index.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/accessors/index.ts @@ -7,4 +7,5 @@ */ export { getRowIndicatorProvider } from './get_row_indicator_provider'; +export { createGetDefaultAppState } from './get_default_app_state'; export { getCellRenderers } from './get_cell_renderers'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/consts.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/consts.ts new file mode 100644 index 0000000000000..c7e2536751a98 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/consts.ts @@ -0,0 +1,14 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { DefaultAppStateColumn } from '../../types'; + +export const LOG_LEVEL_COLUMN: DefaultAppStateColumn = { name: 'log.level', width: 150 }; +export const MESSAGE_COLUMN: DefaultAppStateColumn = { name: 'message' }; +export const CLIENT_IP_COLUMN: DefaultAppStateColumn = { name: 'client.ip', width: 150 }; +export const HOST_NAME_COLUMN: DefaultAppStateColumn = { name: 'host.name', width: 250 }; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/create_profile_providers.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/create_profile_providers.ts new file mode 100644 index 0000000000000..6fbc5b60a0ea5 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/create_profile_providers.ts @@ -0,0 +1,34 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { ProfileProviderServices } from '../profile_provider_services'; +import { createLogsDataSourceProfileProvider } from './profile'; +import { + createApacheErrorLogsDataSourceProfileProvider, + createAwsS3accessLogsDataSourceProfileProvider, + createKubernetesContainerLogsDataSourceProfileProvider, + createNginxAccessLogsDataSourceProfileProvider, + createNginxErrorLogsDataSourceProfileProvider, + createSystemLogsDataSourceProfileProvider, + createWindowsLogsDataSourceProfileProvider, +} from './sub_profiles'; + +export const createLogsDataSourceProfileProviders = (providerServices: ProfileProviderServices) => { + const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(providerServices); + + return [ + createSystemLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + createKubernetesContainerLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + createWindowsLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + createAwsS3accessLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + createNginxErrorLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + createNginxAccessLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + createApacheErrorLogsDataSourceProfileProvider(logsDataSourceProfileProvider), + logsDataSourceProfileProvider, + ]; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/index.ts index f7d780da6ef02..0f1024cf2b2b8 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/index.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { createLogsDataSourceProfileProvider } from './profile'; +export { createLogsDataSourceProfileProviders } from './create_profile_providers'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts index 17f53f30f397b..7e1a6874466ec 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts @@ -6,16 +6,10 @@ * Side Public License, v 1. */ -import { isOfAggregateQueryType } from '@kbn/es-query'; -import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; -import { isDataViewSource, isEsqlSource } from '../../../../common/data_sources'; -import { - DataSourceCategory, - DataSourceProfileProvider, - DataSourceProfileProviderParams, -} from '../../profiles'; +import { DataSourceCategory, DataSourceProfileProvider } from '../../profiles'; import { ProfileProviderServices } from '../profile_provider_services'; import { getRowIndicatorProvider } from './accessors'; +import { extractIndexPatternFrom } from '../extract_index_pattern_from'; import { getCellRenderers } from './accessors'; export const createLogsDataSourceProfileProvider = ( @@ -39,17 +33,3 @@ export const createLogsDataSourceProfileProvider = ( }; }, }); - -const extractIndexPatternFrom = ({ - dataSource, - dataView, - query, -}: DataSourceProfileProviderParams) => { - if (isEsqlSource(dataSource) && isOfAggregateQueryType(query)) { - return getIndexPatternFromESQLQuery(query.esql); - } else if (isDataViewSource(dataSource) && dataView) { - return dataView.getIndexPattern(); - } - - return null; -}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.test.ts new file mode 100644 index 0000000000000..adc84f24949b1 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.test.ts @@ -0,0 +1,53 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createApacheErrorLogsDataSourceProfileProvider } from './apache_error_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createApacheErrorLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createApacheErrorLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-apache.error-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-apache.access-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'log.level', width: 150 }, + { name: 'client.ip', width: 150 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.ts new file mode 100644 index 0000000000000..8ed7f5c406395 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/apache_error_logs.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { CLIENT_IP_COLUMN, LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createApacheErrorLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'apache-error-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [LOG_LEVEL_COLUMN, CLIENT_IP_COLUMN, MESSAGE_COLUMN], + }), + }, + resolve: createResolve('logs-apache.error'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.test.ts new file mode 100644 index 0000000000000..9d1cd1d6c166f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.test.ts @@ -0,0 +1,55 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createAwsS3accessLogsDataSourceProfileProvider } from './aws_s3access_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createAwsS3accessLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createAwsS3accessLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-aws.s3access-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-aws.s3noaccess-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'aws.s3.bucket.name', width: 200 }, + { name: 'aws.s3.object.key', width: 200 }, + { name: 'aws.s3access.operation', width: 200 }, + { name: 'client.ip', width: 150 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.ts new file mode 100644 index 0000000000000..d517f670d074e --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/aws_s3access_logs.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { CLIENT_IP_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createAwsS3accessLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'aws-s3access-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [ + { name: 'aws.s3.bucket.name', width: 200 }, + { name: 'aws.s3.object.key', width: 200 }, + { name: 'aws.s3access.operation', width: 200 }, + CLIENT_IP_COLUMN, + MESSAGE_COLUMN, + ], + }), + }, + resolve: createResolve('logs-aws.s3access'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/create_resolve.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/create_resolve.ts new file mode 100644 index 0000000000000..98be1f003db7c --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/create_resolve.ts @@ -0,0 +1,30 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { createRegExpPatternFrom, testPatternAgainstAllowedList } from '@kbn/data-view-utils'; +import { DataSourceCategory, DataSourceProfileProvider } from '../../../profiles'; +import { extractIndexPatternFrom } from '../../extract_index_pattern_from'; + +export const createResolve = (baseIndexPattern: string): DataSourceProfileProvider['resolve'] => { + const testIndexPattern = testPatternAgainstAllowedList([ + createRegExpPatternFrom(baseIndexPattern), + ]); + + return (params) => { + const indexPattern = extractIndexPatternFrom(params); + + if (!indexPattern || !testIndexPattern(indexPattern)) { + return { isMatch: false }; + } + + return { + isMatch: true, + context: { category: DataSourceCategory.Logs }, + }; + }; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/index.ts new file mode 100644 index 0000000000000..3c1f5cfa114b1 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/index.ts @@ -0,0 +1,15 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export { createApacheErrorLogsDataSourceProfileProvider } from './apache_error_logs'; +export { createAwsS3accessLogsDataSourceProfileProvider } from './aws_s3access_logs'; +export { createKubernetesContainerLogsDataSourceProfileProvider } from './kubernetes_container_logs'; +export { createNginxAccessLogsDataSourceProfileProvider } from './nginx_access_logs'; +export { createNginxErrorLogsDataSourceProfileProvider } from './nginx_error_logs'; +export { createSystemLogsDataSourceProfileProvider } from './system_logs'; +export { createWindowsLogsDataSourceProfileProvider } from './windows_logs'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.test.ts new file mode 100644 index 0000000000000..dcad9eea0f7c8 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.test.ts @@ -0,0 +1,55 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createKubernetesContainerLogsDataSourceProfileProvider } from './kubernetes_container_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createKubernetesContainerLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createKubernetesContainerLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-kubernetes.container_logs-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-kubernetes.access_logs-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'log.level', width: 150 }, + { name: 'kubernetes.pod.name', width: 200 }, + { name: 'kubernetes.namespace', width: 200 }, + { name: 'orchestrator.cluster.name', width: 200 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.ts new file mode 100644 index 0000000000000..506d2d924d105 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/kubernetes_container_logs.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createKubernetesContainerLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'kubernetes-container-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [ + LOG_LEVEL_COLUMN, + { name: 'kubernetes.pod.name', width: 200 }, + { name: 'kubernetes.namespace', width: 200 }, + { name: 'orchestrator.cluster.name', width: 200 }, + MESSAGE_COLUMN, + ], + }), + }, + resolve: createResolve('logs-kubernetes.container_logs'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.test.ts new file mode 100644 index 0000000000000..6c04777873dce --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.test.ts @@ -0,0 +1,55 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createNginxAccessLogsDataSourceProfileProvider } from './nginx_access_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createNginxAccessLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createNginxAccessLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-nginx.access-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-nginx.error-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'url.path', width: 150 }, + { name: 'http.response.status_code', width: 200 }, + { name: 'client.ip', width: 150 }, + { name: 'host.name', width: 250 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.ts new file mode 100644 index 0000000000000..874bf561bdb4f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_access_logs.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { CLIENT_IP_COLUMN, HOST_NAME_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createNginxAccessLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'nginx-access-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [ + { name: 'url.path', width: 150 }, + { name: 'http.response.status_code', width: 200 }, + CLIENT_IP_COLUMN, + HOST_NAME_COLUMN, + MESSAGE_COLUMN, + ], + }), + }, + resolve: createResolve('logs-nginx.access'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.test.ts new file mode 100644 index 0000000000000..3959b27eb96ed --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.test.ts @@ -0,0 +1,52 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createNginxErrorLogsDataSourceProfileProvider } from './nginx_error_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createNginxErrorLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createNginxErrorLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-nginx.error-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-nginx.access-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'log.level', width: 150 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.ts new file mode 100644 index 0000000000000..9123d00f6b948 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/nginx_error_logs.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createNginxErrorLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'nginx-error-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [LOG_LEVEL_COLUMN, MESSAGE_COLUMN], + }), + }, + resolve: createResolve('logs-nginx.error'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.test.ts new file mode 100644 index 0000000000000..ee34cf12f2047 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.test.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createSystemLogsDataSourceProfileProvider } from './system_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createSystemLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createSystemLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-system.syslog-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-notsystem.syslog-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'log.level', width: 150 }, + { name: 'process.name', width: 150 }, + { name: 'host.name', width: 250 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.ts new file mode 100644 index 0000000000000..8b7fca5fdae13 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/system_logs.ts @@ -0,0 +1,31 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { HOST_NAME_COLUMN, LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createSystemLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'system-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [ + LOG_LEVEL_COLUMN, + { name: 'process.name', width: 150 }, + HOST_NAME_COLUMN, + MESSAGE_COLUMN, + ], + }), + }, + resolve: createResolve('logs-system'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.test.ts new file mode 100644 index 0000000000000..b6ad5234ab9d0 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.test.ts @@ -0,0 +1,53 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { createEsqlDataSource } from '../../../../../common/data_sources'; +import { DataSourceCategory, RootContext, SolutionType } from '../../../profiles'; +import { createContextAwarenessMocks } from '../../../__mocks__'; +import { createLogsDataSourceProfileProvider } from '../profile'; +import { createWindowsLogsDataSourceProfileProvider } from './windows_logs'; + +const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default }; +const { profileProviderServices } = createContextAwarenessMocks(); +const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(profileProviderServices); +const dataSourceProfileProvider = createWindowsLogsDataSourceProfileProvider( + logsDataSourceProfileProvider +); + +describe('createWindowsLogsDataSourceProfileProvider', () => { + it('should match a valid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-windows.powershell-*' }, + }); + expect(result).toEqual({ isMatch: true, context: { category: DataSourceCategory.Logs } }); + }); + + it('should not match an invalid index pattern', async () => { + const result = await dataSourceProfileProvider.resolve({ + rootContext: ROOT_CONTEXT, + dataSource: createEsqlDataSource(), + query: { esql: 'FROM logs-notwindows.powershell-*' }, + }); + expect(result).toEqual({ isMatch: false }); + }); + + it('should return default app state', () => { + const getDefaultAppState = dataSourceProfileProvider.profile.getDefaultAppState?.(() => ({})); + expect(getDefaultAppState?.({ dataView: dataViewWithTimefieldMock })).toEqual({ + columns: [ + { name: 'timestamp', width: 212 }, + { name: 'log.level', width: 150 }, + { name: 'host.name', width: 250 }, + { name: 'message' }, + ], + }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.ts new file mode 100644 index 0000000000000..0d2e4c0e1652b --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/sub_profiles/windows_logs.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { DataSourceProfileProvider } from '../../../profiles'; +import { extendProfileProvider } from '../../extend_profile_provider'; +import { createGetDefaultAppState } from '../accessors'; +import { HOST_NAME_COLUMN, LOG_LEVEL_COLUMN, MESSAGE_COLUMN } from '../consts'; +import { createResolve } from './create_resolve'; + +export const createWindowsLogsDataSourceProfileProvider = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'windows-logs-data-source', + profile: { + getDefaultAppState: createGetDefaultAppState({ + defaultColumns: [LOG_LEVEL_COLUMN, HOST_NAME_COLUMN, MESSAGE_COLUMN], + }), + }, + resolve: createResolve('logs-windows'), + }); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts index 1f4f2fbb7d931..22ed673cd9558 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -10,15 +10,19 @@ import { uniq } from 'lodash'; import type { DataSourceProfileService, DocumentProfileService, + RootProfileProvider, RootProfileService, } from '../profiles'; import type { BaseProfileProvider, BaseProfileService } from '../profile_service'; import { exampleDataSourceProfileProvider } from './example_data_source_profile'; import { exampleDocumentProfileProvider } from './example_document_profile'; import { exampleRootProfileProvider } from './example_root_pofile'; -import { createLogsDataSourceProfileProvider } from './logs_data_source_profile'; +import { createLogsDataSourceProfileProviders } from './logs_data_source_profile'; import { createLogDocumentProfileProvider } from './log_document_profile'; -import { createProfileProviderServices } from './profile_provider_services'; +import { + createProfileProviderServices, + ProfileProviderServices, +} from './profile_provider_services'; export const registerProfileProviders = ({ rootProfileService, @@ -32,35 +36,31 @@ export const registerProfileProviders = ({ experimentalProfileIds: string[]; }) => { const providerServices = createProfileProviderServices(); - const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(providerServices); - const logsDocumentProfileProvider = createLogDocumentProfileProvider(providerServices); - const rootProfileProviders = [exampleRootProfileProvider]; - const dataSourceProfileProviders = [ - exampleDataSourceProfileProvider, - logsDataSourceProfileProvider, - ]; - const documentProfileProviders = [exampleDocumentProfileProvider, logsDocumentProfileProvider]; + const rootProfileProviders = createRootProfileProviders(providerServices); + const dataSourceProfileProviders = createDataSourceProfileProviders(providerServices); + const documentProfileProviders = createDocumentProfileProviders(providerServices); const enabledProfileIds = uniq([ - logsDataSourceProfileProvider.profileId, - logsDocumentProfileProvider.profileId, + ...extractProfileIds(rootProfileProviders), + ...extractProfileIds(dataSourceProfileProviders), + ...extractProfileIds(documentProfileProviders), ...experimentalProfileIds, ]); registerEnabledProfileProviders({ profileService: rootProfileService, - availableProviders: rootProfileProviders, + availableProviders: [exampleRootProfileProvider, ...rootProfileProviders], enabledProfileIds, }); registerEnabledProfileProviders({ profileService: dataSourceProfileService, - availableProviders: dataSourceProfileProviders, + availableProviders: [exampleDataSourceProfileProvider, ...dataSourceProfileProviders], enabledProfileIds, }); registerEnabledProfileProviders({ profileService: documentProfileService, - availableProviders: documentProfileProviders, + availableProviders: [exampleDocumentProfileProvider, ...documentProfileProviders], enabledProfileIds, }); }; @@ -77,9 +77,23 @@ export const registerEnabledProfileProviders = < availableProviders: TProvider[]; enabledProfileIds: string[]; }) => { - for (const profile of availableProviders) { - if (enabledProfileIds.includes(profile.profileId)) { - profileService.registerProvider(profile); + for (const provider of availableProviders) { + if (enabledProfileIds.includes(provider.profileId)) { + profileService.registerProvider(provider); } } }; + +const extractProfileIds = (providers: Array>) => + providers.map(({ profileId }) => profileId); + +const createRootProfileProviders = (_providerServices: ProfileProviderServices) => + [] as RootProfileProvider[]; + +const createDataSourceProfileProviders = (providerServices: ProfileProviderServices) => [ + ...createLogsDataSourceProfileProviders(providerServices), +]; + +const createDocumentProfileProviders = (providerServices: ProfileProviderServices) => [ + createLogDocumentProfileProvider(providerServices), +]; diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts index f2555eae52aad..54a29704c91b9 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -17,7 +17,7 @@ export enum DocumentType { Default = 'default', } -export type DocumentProfile = Omit; +export type DocumentProfile = Pick; export interface DocumentProfileProviderParams { rootContext: RootContext; diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 19e51decc62f6..38c7116a765b1 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -24,9 +24,24 @@ export interface RowIndicatorExtensionParams { dataView: DataView; } +export interface DefaultAppStateColumn { + name: string; + width?: number; +} + +export interface DefaultAppStateExtensionParams { + dataView: DataView; +} + +export interface DefaultAppStateExtension { + columns?: DefaultAppStateColumn[]; + rowHeight?: number; +} + export interface Profile { getCellRenderers: () => CustomCellRenderer; getDocViewer: (params: DocViewerExtensionParams) => DocViewerExtension; + getDefaultAppState: (params: DefaultAppStateExtensionParams) => DefaultAppStateExtension; getRowIndicatorProvider: ( params: RowIndicatorExtensionParams ) => UnifiedDataTableProps['getRowIndicator'] | undefined; diff --git a/test/functional/apps/discover/context_awareness/_data_source_profile.ts b/test/functional/apps/discover/context_awareness/_data_source_profile.ts index 594e6dee5dd78..d42a4e8b9c4c6 100644 --- a/test/functional/apps/discover/context_awareness/_data_source_profile.ts +++ b/test/functional/apps/discover/context_awareness/_data_source_profile.ts @@ -24,8 +24,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -43,8 +43,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-logs | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -66,8 +66,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); @@ -82,8 +82,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-logs | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); @@ -98,7 +98,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { describe('cell renderers', () => { it('should render custom @timestamp but not custom log.level', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -112,7 +114,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render custom @timestamp and custom log.level', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs'); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -130,7 +134,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('doc viewer extension', () => { it('should not render custom doc viewer view', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); @@ -141,7 +147,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render custom doc viewer view', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); diff --git a/test/functional/apps/discover/context_awareness/_root_profile.ts b/test/functional/apps/discover/context_awareness/_root_profile.ts index c0bb4885699f8..bf4fee50704f7 100644 --- a/test/functional/apps/discover/context_awareness/_root_profile.ts +++ b/test/functional/apps/discover/context_awareness/_root_profile.ts @@ -23,8 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); @@ -38,7 +38,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { describe('cell renderers', () => { it('should render custom @timestamp', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await PageObjects.discover.waitUntilSearchingHasFinished(); const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts b/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts index eadb9db7fd708..00531b80f4a73 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts @@ -35,8 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null', }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); @@ -55,8 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { esql: 'from my-example* | sort @timestamp desc | where `log.level` is not null', }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); @@ -68,7 +68,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { it('should render log.level badge cell', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs,logstash*'); await queryBar.setQuery('log.level:*'); await queryBar.submitQuery(); @@ -83,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it("should not render log.level badge cell if it's not a logs data source", async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await queryBar.setQuery('log.level:*'); await queryBar.submitQuery(); diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_default_app_state.ts b/test/functional/apps/discover/context_awareness/extensions/_get_default_app_state.ts new file mode 100644 index 0000000000000..4991aa5f36ee1 --- /dev/null +++ b/test/functional/apps/discover/context_awareness/extensions/_get_default_app_state.ts @@ -0,0 +1,206 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import kbnRison from '@kbn/rison'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'discover', 'unifiedFieldList']); + const dataViews = getService('dataViews'); + const dataGrid = getService('dataGrid'); + const queryBar = getService('queryBar'); + const monacoEditor = getService('monacoEditor'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + describe('extension getDefaultAppState', () => { + afterEach(async () => { + await kibanaServer.uiSettings.unset('defaultColumns'); + }); + + describe('ES|QL mode', () => { + it('should render default columns and row height', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-logs', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + const rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should render default columns and row height when switching index patterns', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-*', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + let rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(3); + await monacoEditor.setCodeEditorValue('from my-example-logs'); + await queryBar.clickQuerySubmitButton(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should reset default columns and row height when clicking "New"', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-logs', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level'); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('message'); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + await dataGrid.changeRowHeightValue('Single'); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Single'); + await testSubjects.click('discoverNewButton'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should merge and dedup configured default columns with default profile columns', async () => { + await kibanaServer.uiSettings.update({ + defaultColumns: ['bad_column', 'data_stream.type', 'message'], + }); + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-logs', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']); + }); + }); + + describe('data view mode', () => { + it('should render default columns and row height', async () => { + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + const rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should render default columns and row height when switching data views', async () => { + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-*'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + let rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(3); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should reset default columns and row height when clicking "New"', async () => { + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level'); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('message'); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + await dataGrid.changeRowHeightValue('Single'); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Single'); + await testSubjects.click('discoverNewButton'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should merge and dedup configured default columns with default profile columns', async () => { + await kibanaServer.uiSettings.update({ + defaultColumns: ['bad_column', 'data_stream.type', 'message'], + }); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']); + }); + }); + }); +} diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_doc_viewer.ts b/test/functional/apps/discover/context_awareness/extensions/_get_doc_viewer.ts index 7f60f92cf6191..e2c91143d53f7 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_doc_viewer.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_doc_viewer.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-logs | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); @@ -38,8 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-metrics | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); @@ -50,7 +50,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { it('should render logs overview tab for logs data source', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); @@ -61,7 +63,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not render logs overview tab for non-logs data source', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-metrics'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts b/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts index 72dafc0f46e7f..8efa852cbfb2a 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_row_indicator_provider.ts @@ -31,8 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from logstash* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -51,8 +51,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); // my-example* has a log.level field, but it's not matching the logs profile, so the color indicator should not be rendered @@ -67,8 +67,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null', }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); // in this case it's matching the logs data source profile and has a log.level field, so the color indicator should be rendered diff --git a/test/functional/apps/discover/context_awareness/index.ts b/test/functional/apps/discover/context_awareness/index.ts index 3fdd8ab799266..82f03e7f54bbc 100644 --- a/test/functional/apps/discover/context_awareness/index.ts +++ b/test/functional/apps/discover/context_awareness/index.ts @@ -38,5 +38,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./extensions/_get_row_indicator_provider')); loadTestFile(require.resolve('./extensions/_get_doc_viewer')); loadTestFile(require.resolve('./extensions/_get_cell_renderers')); + loadTestFile(require.resolve('./extensions/_get_default_app_state')); }); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index d659ed49a7971..5014846a67407 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -469,6 +469,13 @@ export class DataGridService extends FtrService { return value; } + public async getCustomRowHeightNumber(scope: 'row' | 'header' = 'row') { + const input = await this.testSubjects.find( + `unifiedDataTable${scope === 'header' ? 'Header' : ''}RowHeightSettings_lineCountNumber` + ); + return Number(await input.getAttribute('value')); + } + public async changeRowHeightValue(newValue: string) { const buttonGroup = await this.testSubjects.find( 'unifiedDataTableRowHeightSettings_rowHeightButtonGroup' diff --git a/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx b/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx index f317f6ded4b15..e9d06cd157c25 100644 --- a/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx @@ -125,7 +125,7 @@ export const useDiscoverInTimelineActions = ( newSavedSearchId ); const savedSearchState = savedSearch ? getAppStateFromSavedSearch(savedSearch) : null; - discoverStateContainer.current?.appState.initAndSync(savedSearch); + discoverStateContainer.current?.appState.initAndSync(); await discoverStateContainer.current?.appState.replaceUrlState( savedSearchState?.appState ?? {} ); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_data_source_profile.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_data_source_profile.ts index 61a20883bbad8..e03a6d2e081a4 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_data_source_profile.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_data_source_profile.ts @@ -33,8 +33,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -50,8 +50,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-logs | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -71,8 +71,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); @@ -87,8 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-logs | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); @@ -103,7 +103,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { describe('cell renderers', () => { it('should not render custom @timestamp or log.level', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -115,7 +117,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not render custom @timestamp but should render custom log.level', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs'); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); @@ -131,7 +135,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('doc viewer extension', () => { it('should not render custom doc viewer view', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); @@ -142,7 +148,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render custom doc viewer view', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle({ rowIndex: 0 }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts index 8ad7d97b13da6..e7eb75384d67c 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts @@ -16,8 +16,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('root profile', () => { before(async () => { - await PageObjects.svlCommonPage.loginAsViewer(); + await PageObjects.svlCommonPage.loginAsAdmin(); }); + describe('ES|QL mode', () => { describe('cell renderers', () => { it('should not render custom @timestamp', async () => { @@ -25,8 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); @@ -38,7 +39,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { describe('cell renderers', () => { it('should not render custom @timestamp', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await PageObjects.discover.waitUntilSearchingHasFinished(); const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts index 2a81561199f47..7bce934099e18 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'discover', 'unifiedFieldList']); + const PageObjects = getPageObjects(['common', 'discover', 'unifiedFieldList', 'svlCommonPage']); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const dataGrid = getService('dataGrid'); @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('extension getCellRenderers', () => { before(async () => { + await PageObjects.svlCommonPage.loginAsAdmin(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); }); @@ -34,8 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null', }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); @@ -54,8 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { esql: 'from my-example* | sort @timestamp desc | where `log.level` is not null', }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); @@ -67,7 +68,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { it('should render log.level badge cell', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs,logstash*'); await queryBar.setQuery('log.level:*'); await queryBar.submitQuery(); @@ -82,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it("should not render log.level badge cell if it's not a logs data source", async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-*'); await queryBar.setQuery('log.level:*'); await queryBar.submitQuery(); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_default_app_state.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_default_app_state.ts new file mode 100644 index 0000000000000..2808686ab0b09 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_default_app_state.ts @@ -0,0 +1,209 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import kbnRison from '@kbn/rison'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'discover', 'svlCommonPage', 'unifiedFieldList']); + const dataViews = getService('dataViews'); + const dataGrid = getService('dataGrid'); + const queryBar = getService('queryBar'); + const monacoEditor = getService('monacoEditor'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + describe('extension getDefaultAppState', () => { + before(async () => { + await PageObjects.svlCommonPage.loginAsAdmin(); + }); + + afterEach(async () => { + await kibanaServer.uiSettings.unset('defaultColumns'); + }); + + describe('ES|QL mode', () => { + it('should render default columns and row height', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-logs', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + const rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should render default columns and row height when switching index patterns', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-*', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + let rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(3); + await monacoEditor.setCodeEditorValue('from my-example-logs'); + await queryBar.clickQuerySubmitButton(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should reset default columns and row height when clicking "New"', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-logs', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level'); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('message'); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + await dataGrid.changeRowHeightValue('Single'); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Single'); + await testSubjects.click('discoverNewButton'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should merge and dedup configured default columns with default profile columns', async () => { + await kibanaServer.uiSettings.update({ + defaultColumns: ['bad_column', 'data_stream.type', 'message'], + }); + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { + esql: 'from my-example-logs', + }, + }); + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']); + }); + }); + + describe('data view mode', () => { + it('should render default columns and row height', async () => { + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + const rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should render default columns and row height when switching data views', async () => { + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-*'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + let rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(3); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should reset default columns and row height when clicking "New"', async () => { + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('log.level'); + await PageObjects.unifiedFieldList.clickFieldListItemRemove('message'); + let columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'Document']); + await dataGrid.clickGridSettings(); + await dataGrid.changeRowHeightValue('Single'); + let rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Single'); + await testSubjects.click('discoverNewButton'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message']); + await dataGrid.clickGridSettings(); + rowHeightValue = await dataGrid.getCurrentRowHeightValue(); + expect(rowHeightValue).to.be('Custom'); + const rowHeightNumber = await dataGrid.getCustomRowHeightNumber(); + expect(rowHeightNumber).to.be(5); + }); + + it('should merge and dedup configured default columns with default profile columns', async () => { + await kibanaServer.uiSettings.update({ + defaultColumns: ['bad_column', 'data_stream.type', 'message'], + }); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const columns = await PageObjects.discover.getColumnHeaders(); + expect(columns).to.eql(['@timestamp', 'log.level', 'message', 'data_stream.type']); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_doc_viewer.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_doc_viewer.ts index 61a9684481ee2..52b514a6673b6 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_doc_viewer.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_doc_viewer.ts @@ -18,14 +18,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await PageObjects.svlCommonPage.loginAsAdmin(); }); + describe('ES|QL mode', () => { it('should render logs overview tab for logs data source', async () => { const state = kbnRison.encode({ dataSource: { type: 'esql' }, query: { esql: 'from my-example-logs | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); @@ -40,8 +41,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example-metrics | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); @@ -52,7 +53,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data view mode', () => { it('should render logs overview tab for logs data source', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-logs'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); @@ -63,7 +66,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not render logs overview tab for non-logs data source', async () => { - await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); await dataViews.switchTo('my-example-metrics'); await PageObjects.discover.waitUntilSearchingHasFinished(); await dataGrid.clickRowToggle(); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_indicator_provider.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_indicator_provider.ts index 27c27360a28b7..c7b402665e689 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_indicator_provider.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_indicator_provider.ts @@ -37,8 +37,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from logstash* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -57,8 +57,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dataSource: { type: 'esql' }, query: { esql: 'from my-example* | sort @timestamp desc' }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); // my-example* has a log.level field, but it's not matching the logs profile, so the color indicator should not be rendered @@ -73,8 +73,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { esql: 'from my-example-logs,logstash* | sort @timestamp desc | where `log.level` is not null', }, }); - await PageObjects.common.navigateToApp('discover', { - hash: `/?_a=${state}`, + await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, }); await PageObjects.discover.waitUntilSearchingHasFinished(); // in this case it's matching the logs data source profile and has a log.level field, so the color indicator should be rendered diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts index 298dad659cf43..e8c8f1234aab5 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts @@ -40,5 +40,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./extensions/_get_row_indicator_provider')); loadTestFile(require.resolve('./extensions/_get_doc_viewer')); loadTestFile(require.resolve('./extensions/_get_cell_renderers')); + loadTestFile(require.resolve('./extensions/_get_default_app_state')); }); } From a99a493bf12cd68991baa5cabc89e03c64af8579 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 2 Aug 2024 17:24:07 +0200 Subject: [PATCH 02/13] [Infra] Move data fetching core to apm data access (#189654) part of [#188752](https://github.com/elastic/kibana/issues/188752) ## Summary In order to leverage the same performance benefits from `apm` plugin in services created in `apm-data-access`, we need to move the `APMEvenClient` and `get_document_sources` over to `apm-data-access` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../apm/common/data_source.ts | 23 +- .../apm/common/document_type.ts | 23 +- .../apm/common/rollup.ts | 7 +- .../apm/common/time_range_metadata.ts | 7 +- .../apm/common/utils/get_bucket_size/index.ts | 23 +- ...e_preferred_data_source_and_bucket_size.ts | 2 +- .../create_apm_event_client/index.ts | 327 +---------------- .../create_internal_es_client/index.ts | 14 +- .../helpers/get_apm_data_access_services.ts | 21 ++ .../lib/helpers/get_apm_event_client.ts | 2 + ...et_is_using_service_destination_metrics.ts | 26 +- .../server/lib/helpers/transactions/index.ts | 21 +- .../routes/time_range_metadata/route.ts | 6 +- .../apm/server/utils/with_apm_span.ts | 20 +- .../observability_solution/apm/tsconfig.json | 2 - .../apm_data_access/common/data_source.ts | 29 ++ .../apm_data_access/common/document_type.ts | 25 ++ .../apm_data_access/common/index.ts | 14 + .../apm_data_access/common/rollup.ts | 13 + .../common/time_range_metadata.ts | 13 + .../get_bucket_size/calculate_auto.test.ts | 0 .../utils/get_bucket_size/calculate_auto.ts} | 37 +- .../get_bucket_size/get_bucket_size.test.ts | 0 .../common/utils/get_bucket_size/index.ts | 29 ++ ...ferred_bucket_size_and_data_source.test.ts | 0 ...t_preferred_bucket_size_and_data_source.ts | 0 .../apm_data_access/server/index.ts | 13 +- .../create_es_client/call_async_with_debug.ts | 6 +- .../cancel_es_request_on_abort.ts | 0 .../get_request_base.test.ts | 4 +- .../get_request_base.ts | 6 +- .../create_apm_event_client/index.test.ts | 0 .../create_apm_event_client/index.ts | 333 ++++++++++++++++++ .../helpers/create_es_client/document_type.ts | 4 +- .../server/lib/helpers/index.ts | 23 ++ ...et_is_using_service_destination_metrics.ts | 31 ++ .../server/lib/helpers/transactions/index.ts | 45 +++ .../apm_data_access/server/plugin.ts | 11 +- .../get_document_sources.ts | 17 +- .../services/get_document_sources/index.ts | 29 ++ .../server/services/get_services.ts | 19 + .../apm_data_access/server/types.ts | 12 + .../apm_data_access/server/utils.ts | 16 + .../server/utils/with_apm_span.ts | 25 ++ .../apm_data_access/tsconfig.json | 10 +- 45 files changed, 785 insertions(+), 503 deletions(-) create mode 100644 x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_data_access_services.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/common/data_source.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/common/document_type.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/common/rollup.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/common/time_range_metadata.ts rename x-pack/plugins/observability_solution/{apm => apm_data_access}/common/utils/get_bucket_size/calculate_auto.test.ts (100%) rename x-pack/plugins/observability_solution/{apm/common/utils/get_bucket_size/calculate_auto.js => apm_data_access/common/utils/get_bucket_size/calculate_auto.ts} (60%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/common/utils/get_bucket_size/get_bucket_size.test.ts (100%) create mode 100644 x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/index.ts rename x-pack/plugins/observability_solution/{apm => apm_data_access}/common/utils/get_preferred_bucket_size_and_data_source.test.ts (100%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/common/utils/get_preferred_bucket_size_and_data_source.ts (100%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/server/lib/helpers/create_es_client/call_async_with_debug.ts (91%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts (100%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts (91%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts (89%) rename x-pack/plugins/observability_solution/{apm => apm_data_access}/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts (100%) create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts rename x-pack/plugins/observability_solution/{apm => apm_data_access}/server/lib/helpers/create_es_client/document_type.ts (95%) create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/index.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/transactions/index.ts rename x-pack/plugins/observability_solution/{apm/server/lib/helpers => apm_data_access/server/services/get_document_sources}/get_document_sources.ts (92%) create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/index.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/services/get_services.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/utils.ts create mode 100644 x-pack/plugins/observability_solution/apm_data_access/server/utils/with_apm_span.ts diff --git a/x-pack/plugins/observability_solution/apm/common/data_source.ts b/x-pack/plugins/observability_solution/apm/common/data_source.ts index 217862e03e415..8a54757c17985 100644 --- a/x-pack/plugins/observability_solution/apm/common/data_source.ts +++ b/x-pack/plugins/observability_solution/apm/common/data_source.ts @@ -5,25 +5,4 @@ * 2.0. */ -import { ApmDocumentType } from './document_type'; -import { RollupInterval } from './rollup'; - -type AnyApmDocumentType = - | ApmDocumentType.ServiceTransactionMetric - | ApmDocumentType.TransactionMetric - | ApmDocumentType.TransactionEvent - | ApmDocumentType.ServiceDestinationMetric - | ApmDocumentType.ServiceSummaryMetric - | ApmDocumentType.ErrorEvent - | ApmDocumentType.SpanEvent; - -export interface ApmDataSource { - rollupInterval: RollupInterval; - documentType: TDocumentType; -} - -export type ApmDataSourceWithSummary = - ApmDataSource & { - hasDurationSummaryField: boolean; - hasDocs: boolean; - }; +export type { ApmDataSource, ApmDataSourceWithSummary } from '@kbn/apm-data-access-plugin/common'; diff --git a/x-pack/plugins/observability_solution/apm/common/document_type.ts b/x-pack/plugins/observability_solution/apm/common/document_type.ts index e8a29e8d08c43..6e9341a6d45ec 100644 --- a/x-pack/plugins/observability_solution/apm/common/document_type.ts +++ b/x-pack/plugins/observability_solution/apm/common/document_type.ts @@ -5,21 +5,8 @@ * 2.0. */ -export enum ApmDocumentType { - TransactionMetric = 'transactionMetric', - ServiceTransactionMetric = 'serviceTransactionMetric', - TransactionEvent = 'transactionEvent', - ServiceDestinationMetric = 'serviceDestinationMetric', - ServiceSummaryMetric = 'serviceSummaryMetric', - ErrorEvent = 'error', - SpanEvent = 'span', -} - -export type ApmServiceTransactionDocumentType = - | ApmDocumentType.ServiceTransactionMetric - | ApmDocumentType.TransactionMetric - | ApmDocumentType.TransactionEvent; - -export type ApmTransactionDocumentType = - | ApmDocumentType.TransactionMetric - | ApmDocumentType.TransactionEvent; +export { + ApmDocumentType, + type ApmServiceTransactionDocumentType, + type ApmTransactionDocumentType, +} from '@kbn/apm-data-access-plugin/common'; diff --git a/x-pack/plugins/observability_solution/apm/common/rollup.ts b/x-pack/plugins/observability_solution/apm/common/rollup.ts index 500337e3fc06b..d3bab49a01377 100644 --- a/x-pack/plugins/observability_solution/apm/common/rollup.ts +++ b/x-pack/plugins/observability_solution/apm/common/rollup.ts @@ -5,9 +5,4 @@ * 2.0. */ -export enum RollupInterval { - OneMinute = '1m', - TenMinutes = '10m', - SixtyMinutes = '60m', - None = 'none', -} +export { RollupInterval } from '@kbn/apm-data-access-plugin/common'; diff --git a/x-pack/plugins/observability_solution/apm/common/time_range_metadata.ts b/x-pack/plugins/observability_solution/apm/common/time_range_metadata.ts index f13ab5a89d6d1..7aa9d80a8906f 100644 --- a/x-pack/plugins/observability_solution/apm/common/time_range_metadata.ts +++ b/x-pack/plugins/observability_solution/apm/common/time_range_metadata.ts @@ -5,9 +5,4 @@ * 2.0. */ -import { ApmDataSource } from './data_source'; - -export interface TimeRangeMetadata { - isUsingServiceDestinationMetrics: boolean; - sources: Array; -} +export type { TimeRangeMetadata } from '@kbn/apm-data-access-plugin/common'; diff --git a/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/index.ts b/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/index.ts index a2946137cf911..837b0295fa22b 100644 --- a/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/index.ts +++ b/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/index.ts @@ -5,25 +5,4 @@ * 2.0. */ -import moment from 'moment'; -import { calculateAuto } from './calculate_auto'; - -export function getBucketSize({ - start, - end, - numBuckets = 50, - minBucketSize, -}: { - start: number; - end: number; - numBuckets?: number; - minBucketSize?: number; -}) { - const duration = moment.duration(end - start, 'ms'); - const bucketSize = Math.max( - calculateAuto.near(numBuckets, duration)?.asSeconds() ?? 0, - minBucketSize || 1 - ); - - return { bucketSize, intervalString: `${bucketSize}s` }; -} +export { getBucketSize } from '@kbn/apm-data-access-plugin/common'; diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts b/x-pack/plugins/observability_solution/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts index b8dfa5682a340..cf27bce80de40 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts +++ b/x-pack/plugins/observability_solution/apm/public/hooks/use_preferred_data_source_and_bucket_size.ts @@ -6,10 +6,10 @@ */ import { useMemo } from 'react'; +import { getPreferredBucketSizeAndDataSource } from '@kbn/apm-data-access-plugin/common'; import { ApmDataSourceWithSummary } from '../../common/data_source'; import { ApmDocumentType } from '../../common/document_type'; import { getBucketSize } from '../../common/utils/get_bucket_size'; -import { getPreferredBucketSizeAndDataSource } from '../../common/utils/get_preferred_bucket_size_and_data_source'; import { useTimeRangeMetadata } from '../context/time_range_metadata/use_time_range_metadata_context'; /** diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 1d6592814526c..0ff0db5b36989 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -4,329 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { - EqlSearchRequest, - FieldCapsRequest, - FieldCapsResponse, - MsearchMultisearchBody, - MsearchMultisearchHeader, - TermsEnumRequest, - TermsEnumResponse, -} from '@elastic/elasticsearch/lib/api/types'; -import { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; -import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { unwrapEsResponse } from '@kbn/observability-plugin/server'; -import { compact, omit } from 'lodash'; -import { ValuesType } from 'utility-types'; -import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; -import { ApmDataSource } from '../../../../../common/data_source'; -import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { Metric } from '../../../../../typings/es_schemas/ui/metric'; -import { Span } from '../../../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { Event } from '../../../../../typings/es_schemas/ui/event'; -import { withApmSpan } from '../../../../utils/with_apm_span'; -import { callAsyncWithDebug, getDebugBody, getDebugTitle } from '../call_async_with_debug'; -import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; -import { ProcessorEventOfDocumentType } from '../document_type'; -import { getRequestBase, processorEventsToIndex } from './get_request_base'; - -export type APMEventESSearchRequest = Omit & { - apm: { - includeLegacyData?: boolean; - } & ({ events: ProcessorEvent[] } | { sources: ApmDataSource[] }); - body: { - size: number; - track_total_hits: boolean | number; - }; -}; - -export type APMLogEventESSearchRequest = Omit & { - body: { - size: number; - track_total_hits: boolean | number; - }; -}; - -type APMEventWrapper = Omit & { - apm: { events: ProcessorEvent[] }; -}; - -type APMEventTermsEnumRequest = APMEventWrapper; -type APMEventEqlSearchRequest = APMEventWrapper; -type APMEventFieldCapsRequest = APMEventWrapper; - -type TypeOfProcessorEvent = { - [ProcessorEvent.error]: APMError; - [ProcessorEvent.transaction]: Transaction; - [ProcessorEvent.span]: Span; - [ProcessorEvent.metric]: Metric; -}[T]; - -type TypedLogEventSearchResponse = - InferSearchResponseOf; - -type TypedSearchResponse = InferSearchResponseOf< - TypeOfProcessorEvent< - TParams['apm'] extends { events: ProcessorEvent[] } - ? ValuesType - : TParams['apm'] extends { sources: ApmDataSource[] } - ? ProcessorEventOfDocumentType['documentType']> - : never - >, - TParams ->; - -interface TypedMSearchResponse { - responses: Array>; -} - -export interface APMEventClientConfig { - esClient: ElasticsearchClient; - debug: boolean; - request: KibanaRequest; - indices: APMIndices; - options: { - includeFrozen: boolean; - }; -} - -export class APMEventClient { - private readonly esClient: ElasticsearchClient; - private readonly debug: boolean; - private readonly request: KibanaRequest; - public readonly indices: APMIndices; - private readonly includeFrozen: boolean; - - constructor(config: APMEventClientConfig) { - this.esClient = config.esClient; - this.debug = config.debug; - this.request = config.request; - this.indices = config.indices; - this.includeFrozen = config.options.includeFrozen; - } - - private callAsyncWithDebug({ - requestType, - params, - cb, - operationName, - }: { - requestType: string; - params: Record; - cb: (requestOpts: { signal: AbortSignal; meta: true }) => Promise; - operationName: string; - }): Promise { - return callAsyncWithDebug({ - getDebugMessage: () => ({ - body: getDebugBody({ - params, - requestType, - operationName, - }), - title: getDebugTitle(this.request), - }), - isCalledWithInternalUser: false, - debug: this.debug, - request: this.request, - operationName, - requestParams: params, - cb: () => { - const controller = new AbortController(); - - const promise = withApmSpan(operationName, () => { - return cancelEsRequestOnAbort( - cb({ signal: controller.signal, meta: true }), - this.request, - controller - ); - }); - - return unwrapEsResponse(promise); - }, - }); - } - - async search( - operationName: string, - params: TParams - ): Promise> { - const { index, filters } = getRequestBase({ - apm: params.apm, - indices: this.indices, - }); - - const searchParams = { - ...omit(params, 'apm', 'body'), - index, - body: { - ...params.body, - query: { - bool: { - filter: filters, - must: compact([params.body.query]), - }, - }, - }, - ...(this.includeFrozen ? { ignore_throttled: false } : {}), - ignore_unavailable: true, - preference: 'any', - expand_wildcards: ['open' as const, 'hidden' as const], - }; - - return this.callAsyncWithDebug({ - cb: (opts) => - this.esClient.search(searchParams, opts) as unknown as Promise<{ - body: TypedSearchResponse; - }>, - operationName, - params: searchParams, - requestType: 'search', - }); - } - - async logEventSearch( - operationName: string, - params: TParams - ): Promise> { - // Reusing indices configured for errors since both events and errors are stored as logs. - const index = processorEventsToIndex([ProcessorEvent.error], this.indices); - - const searchParams = { - ...omit(params, 'body'), - index, - body: { - ...params.body, - query: { - bool: { - must: compact([params.body.query]), - }, - }, - }, - ...(this.includeFrozen ? { ignore_throttled: false } : {}), - ignore_unavailable: true, - preference: 'any', - expand_wildcards: ['open' as const, 'hidden' as const], - }; - - return this.callAsyncWithDebug({ - cb: (opts) => - this.esClient.search(searchParams, opts) as unknown as Promise<{ - body: TypedLogEventSearchResponse; - }>, - operationName, - params: searchParams, - requestType: 'search', - }); - } - - async msearch( - operationName: string, - ...allParams: TParams[] - ): Promise> { - const searches = allParams - .map((params) => { - const { index, filters } = getRequestBase({ - apm: params.apm, - indices: this.indices, - }); - - const searchParams: [MsearchMultisearchHeader, MsearchMultisearchBody] = [ - { - index, - preference: 'any', - ...(this.includeFrozen ? { ignore_throttled: false } : {}), - ignore_unavailable: true, - expand_wildcards: ['open' as const, 'hidden' as const], - }, - { - ...omit(params, 'apm', 'body'), - ...params.body, - query: { - bool: { - filter: compact([params.body.query, ...filters]), - }, - }, - }, - ]; - - return searchParams; - }) - .flat(); - - return this.callAsyncWithDebug({ - cb: (opts) => - this.esClient.msearch( - { - searches, - }, - opts - ) as unknown as Promise<{ - body: TypedMSearchResponse; - }>, - operationName, - params: searches, - requestType: 'msearch', - }); - } - - async eqlSearch(operationName: string, params: APMEventEqlSearchRequest) { - const index = processorEventsToIndex(params.apm.events, this.indices); - - const requestParams = { - ...omit(params, 'apm'), - index, - }; - - return this.callAsyncWithDebug({ - operationName, - requestType: 'eql_search', - params: requestParams, - cb: (opts) => this.esClient.eql.search(requestParams, opts), - }); - } - - async fieldCaps( - operationName: string, - params: APMEventFieldCapsRequest - ): Promise { - const index = processorEventsToIndex(params.apm.events, this.indices); - - const requestParams = { - ...omit(params, 'apm'), - index, - }; - - return this.callAsyncWithDebug({ - operationName, - requestType: '_field_caps', - params: requestParams, - cb: (opts) => this.esClient.fieldCaps(requestParams, opts), - }); - } - - async termsEnum( - operationName: string, - params: APMEventTermsEnumRequest - ): Promise { - const index = processorEventsToIndex(params.apm.events, this.indices); - - const requestParams = { - ...omit(params, 'apm'), - index: index.join(','), - }; - - return this.callAsyncWithDebug({ - operationName, - requestType: '_terms_enum', - params: requestParams, - cb: (opts) => this.esClient.termsEnum(requestParams, opts), - }); - } - - getIndicesFromProcessorEvent(processorEvent: ProcessorEvent) { - return processorEventsToIndex([processorEvent], this.indices); - } -} +export { APMEventClient, type APMEventESSearchRequest } from '@kbn/apm-data-access-plugin/server'; diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index a0f5a6dfbf319..272f482cdc8eb 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -9,9 +9,16 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { unwrapEsResponse } from '@kbn/observability-plugin/server'; import type { ESSearchResponse, ESSearchRequest } from '@kbn/es-types'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { APMRouteHandlerResources } from '../../../../routes/apm_routes/register_apm_server_routes'; -import { callAsyncWithDebug, getDebugBody, getDebugTitle } from '../call_async_with_debug'; -import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; +import { + callAsyncWithDebug, + getDebugBody, + getDebugTitle, + cancelEsRequestOnAbort, +} from '@kbn/apm-data-access-plugin/server/utils'; +import { + type APMRouteHandlerResources, + inspectableEsQueriesMap, +} from '../../../../routes/apm_routes/register_apm_server_routes'; export type APMIndexDocumentParams = estypes.IndexRequest; @@ -71,6 +78,7 @@ export async function createInternalESClient({ request, requestParams: params, operationName, + inspectableEsQueriesMap, }); } diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_data_access_services.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_data_access_services.ts new file mode 100644 index 0000000000000..176507e6e3456 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_data_access_services.ts @@ -0,0 +1,21 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApmDataAccessServices, APMEventClient } from '@kbn/apm-data-access-plugin/server'; +import { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/register_apm_server_routes'; + +export async function getApmDataAccessServices({ + apmEventClient, + plugins, +}: { + apmEventClient: APMEventClient; +} & Pick): Promise { + const { apmDataAccess } = plugins; + return apmDataAccess.setup.getServices({ + apmEventClient, + }); +} diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts index b756876eb3212..8f21bf8f1c691 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts @@ -9,6 +9,7 @@ import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { APMEventClient } from './create_es_client/create_apm_event_client'; import { withApmSpan } from '../../utils/with_apm_span'; import { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/register_apm_server_routes'; +import { inspectableEsQueriesMap } from '../../routes/apm_routes/register_apm_server_routes'; export async function getApmEventClient({ context, @@ -35,6 +36,7 @@ export async function getApmEventClient({ indices, options: { includeFrozen, + inspectableEsQueriesMap, }, }); }); diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts index 7e53735bafabb..07ec546196707 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts @@ -6,11 +6,10 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { kqlQuery, rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { getDocumentTypeFilterForServiceDestinationStatistics } from '@kbn/apm-data-access-plugin/server/utils'; import { - METRICSET_NAME, - METRICSET_INTERVAL, SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, SPAN_DURATION, @@ -24,25 +23,6 @@ export function getProcessorEventForServiceDestinationStatistics( return searchServiceDestinationMetrics ? ProcessorEvent.metric : ProcessorEvent.span; } -export function getDocumentTypeFilterForServiceDestinationStatistics( - searchServiceDestinationMetrics: boolean -) { - return searchServiceDestinationMetrics - ? [ - { - bool: { - filter: termQuery(METRICSET_NAME, 'service_destination'), - must_not: { - terms: { - [METRICSET_INTERVAL]: ['10m', '60m'], - }, - }, - }, - }, - ] - : []; -} - export function getLatencyFieldForServiceDestinationStatistics( searchServiceDestinationMetrics: boolean ) { @@ -117,3 +97,5 @@ export async function getIsUsingServiceDestinationMetrics({ anyServiceDestinationMetricsCount > 0 && serviceDestinationMetricsWithoutSpanNameCount === 0 ); } + +export { getDocumentTypeFilterForServiceDestinationStatistics }; diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/transactions/index.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/transactions/index.ts index f1aa16f4e4f37..8bf8c0cb74a70 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/transactions/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/transactions/index.ts @@ -14,14 +14,14 @@ import { TRANSACTION_DURATION_HISTOGRAM, TRANSACTION_ROOT, PARENT_ID, - METRICSET_INTERVAL, - METRICSET_NAME, TRANSACTION_DURATION_SUMMARY, } from '../../../../common/es_fields/apm'; import { APMConfig } from '../../..'; import { APMEventClient } from '../create_es_client/create_apm_event_client'; import { ApmDocumentType } from '../../../../common/document_type'; +export { getBackwardCompatibleDocumentTypeFilter } from '@kbn/apm-data-access-plugin/server/utils'; + export async function getHasTransactionsEvents({ start, end, @@ -125,23 +125,6 @@ export function getDurationFieldForTransactions( return TRANSACTION_DURATION; } -// The function returns Document type filter for 1m Transaction Metrics -export function getBackwardCompatibleDocumentTypeFilter(searchAggregatedTransactions: boolean) { - return searchAggregatedTransactions - ? [ - { - bool: { - filter: [{ exists: { field: TRANSACTION_DURATION_HISTOGRAM } }], - must_not: [ - { terms: { [METRICSET_INTERVAL]: ['10m', '60m'] } }, - { term: { [METRICSET_NAME]: 'service_transaction' } }, - ], - }, - }, - ] - : []; -} - export function getProcessorEventForTransactions( searchAggregatedTransactions: boolean ): ProcessorEvent.metric | ProcessorEvent.transaction { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/time_range_metadata/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/time_range_metadata/route.ts index eaff14fd7c647..bf91af259249e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/time_range_metadata/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/time_range_metadata/route.ts @@ -8,10 +8,10 @@ import { toBooleanRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { TimeRangeMetadata } from '../../../common/time_range_metadata'; import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; -import { getDocumentSources } from '../../lib/helpers/get_document_sources'; import { getIsUsingServiceDestinationMetrics } from '../../lib/helpers/spans/get_is_using_service_destination_metrics'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { kueryRt, rangeRt } from '../default_api_types'; +import { getApmDataAccessServices } from '../../lib/helpers/get_apm_data_access_services'; export const timeRangeMetadataRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/time_range_metadata', @@ -31,6 +31,7 @@ export const timeRangeMetadataRoute = createApmServerRoute({ }, handler: async (resources): Promise => { const apmEventClient = await getApmEventClient(resources); + const apmDataAccessServices = await getApmDataAccessServices({ apmEventClient, ...resources }); const { query: { @@ -51,8 +52,7 @@ export const timeRangeMetadataRoute = createApmServerRoute({ end, kuery, }), - getDocumentSources({ - apmEventClient, + apmDataAccessServices.getDocumentSources({ start, end, kuery, diff --git a/x-pack/plugins/observability_solution/apm/server/utils/with_apm_span.ts b/x-pack/plugins/observability_solution/apm/server/utils/with_apm_span.ts index 1343970f04a3f..f852c8cc102b0 100644 --- a/x-pack/plugins/observability_solution/apm/server/utils/with_apm_span.ts +++ b/x-pack/plugins/observability_solution/apm/server/utils/with_apm_span.ts @@ -4,22 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { withSpan, SpanOptions, parseSpanOptions } from '@kbn/apm-utils'; - -export function withApmSpan( - optionsOrName: SpanOptions | string, - cb: () => Promise -): Promise { - const options = parseSpanOptions(optionsOrName); - - const optionsWithDefaults = { - ...(options.intercept ? {} : { type: 'plugin:apm' }), - ...options, - labels: { - plugin: 'apm', - ...options.labels, - }, - }; - - return withSpan(optionsWithDefaults, cb); -} +export { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils'; diff --git a/x-pack/plugins/observability_solution/apm/tsconfig.json b/x-pack/plugins/observability_solution/apm/tsconfig.json index fcc6346802830..ec8a63ea1fb65 100644 --- a/x-pack/plugins/observability_solution/apm/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm/tsconfig.json @@ -64,13 +64,11 @@ "@kbn/rison", "@kbn/config-schema", "@kbn/repo-info", - "@kbn/apm-utils", "@kbn/apm-data-view", "@kbn/logging", "@kbn/std", "@kbn/core-saved-objects-api-server-mocks", "@kbn/field-types", - "@kbn/core-http-server-mocks", "@kbn/babel-register", "@kbn/core-saved-objects-migration-server-internal", "@kbn/core-elasticsearch-server", diff --git a/x-pack/plugins/observability_solution/apm_data_access/common/data_source.ts b/x-pack/plugins/observability_solution/apm_data_access/common/data_source.ts new file mode 100644 index 0000000000000..b94af60d802b4 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/common/data_source.ts @@ -0,0 +1,29 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ApmDocumentType } from './document_type'; +import type { RollupInterval } from './rollup'; + +type AnyApmDocumentType = + | ApmDocumentType.ServiceTransactionMetric + | ApmDocumentType.TransactionMetric + | ApmDocumentType.TransactionEvent + | ApmDocumentType.ServiceDestinationMetric + | ApmDocumentType.ServiceSummaryMetric + | ApmDocumentType.ErrorEvent + | ApmDocumentType.SpanEvent; + +export interface ApmDataSource { + rollupInterval: RollupInterval; + documentType: TDocumentType; +} + +export type ApmDataSourceWithSummary = + ApmDataSource & { + hasDurationSummaryField: boolean; + hasDocs: boolean; + }; diff --git a/x-pack/plugins/observability_solution/apm_data_access/common/document_type.ts b/x-pack/plugins/observability_solution/apm_data_access/common/document_type.ts new file mode 100644 index 0000000000000..e8a29e8d08c43 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/common/document_type.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum ApmDocumentType { + TransactionMetric = 'transactionMetric', + ServiceTransactionMetric = 'serviceTransactionMetric', + TransactionEvent = 'transactionEvent', + ServiceDestinationMetric = 'serviceDestinationMetric', + ServiceSummaryMetric = 'serviceSummaryMetric', + ErrorEvent = 'error', + SpanEvent = 'span', +} + +export type ApmServiceTransactionDocumentType = + | ApmDocumentType.ServiceTransactionMetric + | ApmDocumentType.TransactionMetric + | ApmDocumentType.TransactionEvent; + +export type ApmTransactionDocumentType = + | ApmDocumentType.TransactionMetric + | ApmDocumentType.TransactionEvent; diff --git a/x-pack/plugins/observability_solution/apm_data_access/common/index.ts b/x-pack/plugins/observability_solution/apm_data_access/common/index.ts index 19d4963c3cec5..df61d8d9c3702 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/common/index.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/common/index.ts @@ -7,3 +7,17 @@ export const PLUGIN_ID = 'apmDataAccess'; export const PLUGIN_NAME = 'apmDataAccess'; + +export type { ApmDataSource, ApmDataSourceWithSummary } from './data_source'; +export { + ApmDocumentType, + type ApmServiceTransactionDocumentType, + type ApmTransactionDocumentType, +} from './document_type'; + +export type { TimeRangeMetadata } from './time_range_metadata'; + +export { getPreferredBucketSizeAndDataSource } from './utils/get_preferred_bucket_size_and_data_source'; +export { getBucketSize } from './utils/get_bucket_size'; + +export { RollupInterval } from './rollup'; diff --git a/x-pack/plugins/observability_solution/apm_data_access/common/rollup.ts b/x-pack/plugins/observability_solution/apm_data_access/common/rollup.ts new file mode 100644 index 0000000000000..500337e3fc06b --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/common/rollup.ts @@ -0,0 +1,13 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum RollupInterval { + OneMinute = '1m', + TenMinutes = '10m', + SixtyMinutes = '60m', + None = 'none', +} diff --git a/x-pack/plugins/observability_solution/apm_data_access/common/time_range_metadata.ts b/x-pack/plugins/observability_solution/apm_data_access/common/time_range_metadata.ts new file mode 100644 index 0000000000000..f13ab5a89d6d1 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/common/time_range_metadata.ts @@ -0,0 +1,13 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApmDataSource } from './data_source'; + +export interface TimeRangeMetadata { + isUsingServiceDestinationMetrics: boolean; + sources: Array; +} diff --git a/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/calculate_auto.test.ts b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/calculate_auto.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/calculate_auto.test.ts rename to x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/calculate_auto.test.ts diff --git a/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/calculate_auto.js b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/calculate_auto.ts similarity index 60% rename from x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/calculate_auto.js rename to x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/calculate_auto.ts index bd4d6e51ccc0d..720a924dddcb5 100644 --- a/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/calculate_auto.js +++ b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/calculate_auto.ts @@ -5,10 +5,12 @@ * 2.0. */ -import moment from 'moment'; +import moment, { Duration } from 'moment'; const d = moment.duration; -const roundingRules = [ +type RoundingRule = [Duration, Duration]; + +const roundingRules: RoundingRule[] = [ [d(500, 'ms'), d(100, 'ms')], [d(5, 'second'), d(1, 'second')], [d(7.5, 'second'), d(5, 'second')], @@ -24,19 +26,21 @@ const roundingRules = [ [d(1, 'week'), d(1, 'd')], [d(3, 'week'), d(1, 'week')], [d(1, 'year'), d(1, 'month')], - [Infinity, d(1, 'year')], + [d(Infinity, 'year'), d(1, 'year')], ]; -const revRoundingRules = roundingRules.slice(0).reverse(); +const revRoundingRules = [...roundingRules].reverse(); + +type CheckFunction = (bound: Duration, interval: Duration, target: number) => Duration | null; -function find(rules, check, last) { - function pick(buckets, duration) { - const target = duration / buckets; - let lastResp = null; +function find(rules: RoundingRule[], check: CheckFunction, last?: boolean) { + function pick(buckets: number, duration: Duration): Duration | null { + const target = duration.asMilliseconds() / buckets; + let lastResp: Duration | null = null; for (let i = 0; i < rules.length; i++) { - const rule = rules[i]; - const resp = check(rule[0], rule[1], target); + const [bound, interval] = rules[i]; + const resp = check(bound, interval, target); if (resp == null) { if (!last) continue; @@ -53,9 +57,9 @@ function find(rules, check, last) { return moment.duration(ms, 'ms'); } - return (buckets, duration) => { + return (buckets: number, duration: Duration): Duration | undefined => { const interval = pick(buckets, duration); - if (interval) return moment.duration(interval._data); + if (interval) return moment.duration(interval); }; } @@ -63,16 +67,19 @@ export const calculateAuto = { near: find( revRoundingRules, function near(bound, interval, target) { - if (bound > target) return interval; + if (bound.asMilliseconds() > target) return interval; + return null; }, true ), lessThan: find(revRoundingRules, function lessThan(_bound, interval, target) { - if (interval < target) return interval; + if (interval.asMilliseconds() < target) return interval; + return null; }), atLeast: find(revRoundingRules, function atLeast(_bound, interval, target) { - if (interval <= target) return interval; + if (interval.asMilliseconds() <= target) return interval; + return null; }), }; diff --git a/x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/get_bucket_size.test.ts b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/get_bucket_size.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/apm/common/utils/get_bucket_size/get_bucket_size.test.ts rename to x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/get_bucket_size.test.ts diff --git a/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/index.ts b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/index.ts new file mode 100644 index 0000000000000..a2946137cf911 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_bucket_size/index.ts @@ -0,0 +1,29 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { calculateAuto } from './calculate_auto'; + +export function getBucketSize({ + start, + end, + numBuckets = 50, + minBucketSize, +}: { + start: number; + end: number; + numBuckets?: number; + minBucketSize?: number; +}) { + const duration = moment.duration(end - start, 'ms'); + const bucketSize = Math.max( + calculateAuto.near(numBuckets, duration)?.asSeconds() ?? 0, + minBucketSize || 1 + ); + + return { bucketSize, intervalString: `${bucketSize}s` }; +} diff --git a/x-pack/plugins/observability_solution/apm/common/utils/get_preferred_bucket_size_and_data_source.test.ts b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_preferred_bucket_size_and_data_source.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/apm/common/utils/get_preferred_bucket_size_and_data_source.test.ts rename to x-pack/plugins/observability_solution/apm_data_access/common/utils/get_preferred_bucket_size_and_data_source.test.ts diff --git a/x-pack/plugins/observability_solution/apm/common/utils/get_preferred_bucket_size_and_data_source.ts b/x-pack/plugins/observability_solution/apm_data_access/common/utils/get_preferred_bucket_size_and_data_source.ts similarity index 100% rename from x-pack/plugins/observability_solution/apm/common/utils/get_preferred_bucket_size_and_data_source.ts rename to x-pack/plugins/observability_solution/apm_data_access/common/utils/get_preferred_bucket_size_and_data_source.ts diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/index.ts index f322ff2eb910d..9dfcc5a454cc5 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/index.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/index.ts @@ -82,4 +82,15 @@ export async function plugin(initializerContext: PluginInitializerContext) { return new ApmDataAccessPlugin(initializerContext); } -export type { ApmDataAccessPluginSetup, ApmDataAccessPluginStart } from './types'; +export type { + ApmDataAccessPluginSetup, + ApmDataAccessPluginStart, + ApmDataAccessServices, + ApmDataAccessServicesParams, + APMEventClientConfig, + APMEventESSearchRequest, + APMLogEventESSearchRequest, + DocumentSourcesRequest, +} from './types'; + +export { APMEventClient } from './lib/helpers'; diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/call_async_with_debug.ts similarity index 91% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/call_async_with_debug.ts index f1899b8f4c2db..9fbd6eb4cefa5 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -8,11 +8,11 @@ /* eslint-disable no-console */ import chalk from 'chalk'; -import { KibanaRequest } from '@kbn/core/server'; +import type { KibanaRequest } from '@kbn/core/server'; import { RequestStatus } from '@kbn/inspector-plugin/common'; import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server'; import { getInspectResponse } from '@kbn/observability-shared-plugin/common'; -import { inspectableEsQueriesMap } from '../../../routes/apm_routes/register_apm_server_routes'; +import type { InspectResponse } from '@kbn/observability-plugin/typings/common'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); @@ -26,6 +26,7 @@ export async function callAsyncWithDebug({ requestParams, operationName, isCalledWithInternalUser, + inspectableEsQueriesMap = new WeakMap(), }: { cb: () => Promise; getDebugMessage: () => { body: string; title: string }; @@ -34,6 +35,7 @@ export async function callAsyncWithDebug({ requestParams: Record; operationName: string; isCalledWithInternalUser: boolean; // only allow inspection of queries that were retrieved with credentials of the end user + inspectableEsQueriesMap?: WeakMap; }): Promise { if (!debug) { return cb(); diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts similarity index 100% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/cancel_es_request_on_abort.ts diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts similarity index 91% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts index 253a0993d1a8c..09fca8ab2331b 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { APMEventESSearchRequest } from '.'; -import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; +import type { APMIndices } from '../../../..'; +import type { APMEventESSearchRequest } from '.'; import { getRequestBase } from './get_request_base'; describe('getRequestBase', () => { diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts similarity index 89% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts index 8a305c9601de5..54cd8e9eeb9a8 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/get_request_base.ts @@ -6,12 +6,12 @@ */ import type { ESFilter } from '@kbn/es-types'; -import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { uniq } from 'lodash'; -import { ApmDataSource } from '../../../../../common/data_source'; -import { PROCESSOR_EVENT } from '../../../../../common/es_fields/apm'; +import { PROCESSOR_EVENT } from '@kbn/apm-types/es_fields'; +import type { APMIndices } from '../../../..'; import { getConfigForDocumentType, getProcessorEventForDocumentType } from '../document_type'; +import type { ApmDataSource } from '../../../../../common/data_source'; const processorEventIndexMap = { [ProcessorEvent.transaction]: 'transaction', diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts new file mode 100644 index 0000000000000..3c195b752c854 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -0,0 +1,333 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + EqlSearchRequest, + FieldCapsRequest, + FieldCapsResponse, + MsearchMultisearchBody, + MsearchMultisearchHeader, + TermsEnumRequest, + TermsEnumResponse, +} from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; +import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unwrapEsResponse } from '@kbn/observability-plugin/server'; +import { compact, omit } from 'lodash'; +import { ValuesType } from 'utility-types'; +import type { APMError, Metric, Span, Transaction, Event } from '@kbn/apm-types/es_schemas_ui'; +import type { InspectResponse } from '@kbn/observability-plugin/typings/common'; +import { withApmSpan } from '../../../../utils'; +import type { ApmDataSource } from '../../../../../common/data_source'; +import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort'; +import { callAsyncWithDebug, getDebugBody, getDebugTitle } from '../call_async_with_debug'; +import type { ProcessorEventOfDocumentType } from '../document_type'; +import type { APMIndices } from '../../../..'; +import { getRequestBase, processorEventsToIndex } from './get_request_base'; + +export type APMEventESSearchRequest = Omit & { + apm: { + includeLegacyData?: boolean; + } & ({ events: ProcessorEvent[] } | { sources: ApmDataSource[] }); + body: { + size: number; + track_total_hits: boolean | number; + }; +}; + +export type APMLogEventESSearchRequest = Omit & { + body: { + size: number; + track_total_hits: boolean | number; + }; +}; + +type APMEventWrapper = Omit & { + apm: { events: ProcessorEvent[] }; +}; + +type APMEventTermsEnumRequest = APMEventWrapper; +type APMEventEqlSearchRequest = APMEventWrapper; +type APMEventFieldCapsRequest = APMEventWrapper; + +type TypeOfProcessorEvent = { + [ProcessorEvent.error]: APMError; + [ProcessorEvent.transaction]: Transaction; + [ProcessorEvent.span]: Span; + [ProcessorEvent.metric]: Metric; +}[T]; + +type TypedLogEventSearchResponse = + InferSearchResponseOf; + +type TypedSearchResponse = InferSearchResponseOf< + TypeOfProcessorEvent< + TParams['apm'] extends { events: ProcessorEvent[] } + ? ValuesType + : TParams['apm'] extends { sources: ApmDataSource[] } + ? ProcessorEventOfDocumentType['documentType']> + : never + >, + TParams +>; + +interface TypedMSearchResponse { + responses: Array>; +} + +export interface APMEventClientConfig { + esClient: ElasticsearchClient; + debug: boolean; + request: KibanaRequest; + indices: APMIndices; + options: { + includeFrozen: boolean; + inspectableEsQueriesMap?: WeakMap; + }; +} + +export class APMEventClient { + private readonly esClient: ElasticsearchClient; + private readonly debug: boolean; + private readonly request: KibanaRequest; + public readonly indices: APMIndices; + private readonly includeFrozen: boolean; + private readonly inspectableEsQueriesMap?: WeakMap; + + constructor(config: APMEventClientConfig) { + this.esClient = config.esClient; + this.debug = config.debug; + this.request = config.request; + this.indices = config.indices; + this.includeFrozen = config.options.includeFrozen; + this.inspectableEsQueriesMap = config.options.inspectableEsQueriesMap; + } + + private callAsyncWithDebug({ + requestType, + params, + cb, + operationName, + }: { + requestType: string; + params: Record; + cb: (requestOpts: { signal: AbortSignal; meta: true }) => Promise; + operationName: string; + }): Promise { + return callAsyncWithDebug({ + getDebugMessage: () => ({ + body: getDebugBody({ + params, + requestType, + operationName, + }), + title: getDebugTitle(this.request), + }), + isCalledWithInternalUser: false, + debug: this.debug, + request: this.request, + operationName, + requestParams: params, + inspectableEsQueriesMap: this.inspectableEsQueriesMap, + cb: () => { + const controller = new AbortController(); + + const promise = withApmSpan(operationName, () => { + return cancelEsRequestOnAbort( + cb({ signal: controller.signal, meta: true }), + this.request, + controller + ); + }); + + return unwrapEsResponse(promise); + }, + }); + } + + async search( + operationName: string, + params: TParams + ): Promise> { + const { index, filters } = getRequestBase({ + apm: params.apm, + indices: this.indices, + }); + + const searchParams = { + ...omit(params, 'apm', 'body'), + index, + body: { + ...params.body, + query: { + bool: { + filter: filters, + must: compact([params.body.query]), + }, + }, + }, + ...(this.includeFrozen ? { ignore_throttled: false } : {}), + ignore_unavailable: true, + preference: 'any', + expand_wildcards: ['open' as const, 'hidden' as const], + }; + + return this.callAsyncWithDebug({ + cb: (opts) => + this.esClient.search(searchParams, opts) as unknown as Promise<{ + body: TypedSearchResponse; + }>, + operationName, + params: searchParams, + requestType: 'search', + }); + } + + async logEventSearch( + operationName: string, + params: TParams + ): Promise> { + // Reusing indices configured for errors since both events and errors are stored as logs. + const index = processorEventsToIndex([ProcessorEvent.error], this.indices); + + const searchParams = { + ...omit(params, 'body'), + index, + body: { + ...params.body, + query: { + bool: { + must: compact([params.body.query]), + }, + }, + }, + ...(this.includeFrozen ? { ignore_throttled: false } : {}), + ignore_unavailable: true, + preference: 'any', + expand_wildcards: ['open' as const, 'hidden' as const], + }; + + return this.callAsyncWithDebug({ + cb: (opts) => + this.esClient.search(searchParams, opts) as unknown as Promise<{ + body: TypedLogEventSearchResponse; + }>, + operationName, + params: searchParams, + requestType: 'search', + }); + } + + async msearch( + operationName: string, + ...allParams: TParams[] + ): Promise> { + const searches = allParams + .map((params) => { + const { index, filters } = getRequestBase({ + apm: params.apm, + indices: this.indices, + }); + + const searchParams: [MsearchMultisearchHeader, MsearchMultisearchBody] = [ + { + index, + preference: 'any', + ...(this.includeFrozen ? { ignore_throttled: false } : {}), + ignore_unavailable: true, + expand_wildcards: ['open' as const, 'hidden' as const], + }, + { + ...omit(params, 'apm', 'body'), + ...params.body, + query: { + bool: { + filter: compact([params.body.query, ...filters]), + }, + }, + }, + ]; + + return searchParams; + }) + .flat(); + + return this.callAsyncWithDebug({ + cb: (opts) => + this.esClient.msearch( + { + searches, + }, + opts + ) as unknown as Promise<{ + body: TypedMSearchResponse; + }>, + operationName, + params: searches, + requestType: 'msearch', + }); + } + + async eqlSearch(operationName: string, params: APMEventEqlSearchRequest) { + const index = processorEventsToIndex(params.apm.events, this.indices); + + const requestParams = { + ...omit(params, 'apm'), + index, + }; + + return this.callAsyncWithDebug({ + operationName, + requestType: 'eql_search', + params: requestParams, + cb: (opts) => this.esClient.eql.search(requestParams, opts), + }); + } + + async fieldCaps( + operationName: string, + params: APMEventFieldCapsRequest + ): Promise { + const index = processorEventsToIndex(params.apm.events, this.indices); + + const requestParams = { + ...omit(params, 'apm'), + index, + }; + + return this.callAsyncWithDebug({ + operationName, + requestType: '_field_caps', + params: requestParams, + cb: (opts) => this.esClient.fieldCaps(requestParams, opts), + }); + } + + async termsEnum( + operationName: string, + params: APMEventTermsEnumRequest + ): Promise { + const index = processorEventsToIndex(params.apm.events, this.indices); + + const requestParams = { + ...omit(params, 'apm'), + index: index.join(','), + }; + + return this.callAsyncWithDebug({ + operationName, + requestType: '_terms_enum', + params: requestParams, + cb: (opts) => this.esClient.termsEnum(requestParams, opts), + }); + } + + getIndicesFromProcessorEvent(processorEvent: ProcessorEvent) { + return processorEventsToIndex([processorEvent], this.indices); + } +} diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/document_type.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/document_type.ts similarity index 95% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/document_type.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/document_type.ts index 8165c7329b6b1..c142fa932ff42 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/create_es_client/document_type.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/document_type.ts @@ -6,10 +6,10 @@ */ import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { termQuery } from '@kbn/observability-plugin/server'; +import { METRICSET_INTERVAL, METRICSET_NAME } from '@kbn/apm-types/es_fields'; import { ApmDocumentType } from '../../../../common/document_type'; -import { METRICSET_INTERVAL, METRICSET_NAME } from '../../../../common/es_fields/apm'; import { RollupInterval } from '../../../../common/rollup'; -import { termQuery } from '../../../../common/utils/term_query'; import { getDocumentTypeFilterForServiceDestinationStatistics } from '../spans/get_is_using_service_destination_metrics'; import { getBackwardCompatibleDocumentTypeFilter } from '../transactions'; diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/index.ts new file mode 100644 index 0000000000000..30a2ff30d98ee --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/index.ts @@ -0,0 +1,23 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getDocumentTypeFilterForServiceDestinationStatistics } from './spans/get_is_using_service_destination_metrics'; +export { getBackwardCompatibleDocumentTypeFilter } from './transactions'; +export { + APMEventClient, + type APMEventESSearchRequest, + type APMEventClientConfig, + type APMLogEventESSearchRequest, +} from './create_es_client/create_apm_event_client'; + +export { + callAsyncWithDebug, + getDebugBody, + getDebugTitle, +} from './create_es_client/call_async_with_debug'; + +export { cancelEsRequestOnAbort } from './create_es_client/cancel_es_request_on_abort'; diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts new file mode 100644 index 0000000000000..de895259edecb --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/spans/get_is_using_service_destination_metrics.ts @@ -0,0 +1,31 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { termQuery, termsQuery } from '@kbn/observability-plugin/server'; +import { METRICSET_NAME, METRICSET_INTERVAL } from '@kbn/apm-types/es_fields'; +import { RollupInterval } from '../../../../common/rollup'; + +export function getDocumentTypeFilterForServiceDestinationStatistics( + searchServiceDestinationMetrics: boolean +) { + return searchServiceDestinationMetrics + ? [ + { + bool: { + filter: termQuery(METRICSET_NAME, 'service_destination'), + must_not: [ + ...termsQuery( + METRICSET_INTERVAL, + RollupInterval.TenMinutes, + RollupInterval.SixtyMinutes + ), + ], + }, + }, + ] + : []; +} diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/transactions/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/transactions/index.ts new file mode 100644 index 0000000000000..c93d549e2b1dd --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/transactions/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + TRANSACTION_DURATION_HISTOGRAM, + METRICSET_INTERVAL, + METRICSET_NAME, + TRANSACTION_DURATION_SUMMARY, +} from '@kbn/apm-types/es_fields'; +import { existsQuery, termQuery, termsQuery } from '@kbn/observability-plugin/server'; +import { RollupInterval } from '../../../../common/rollup'; + +// The function returns Document type filter for 1m Transaction Metrics +export function getBackwardCompatibleDocumentTypeFilter(searchAggregatedTransactions: boolean) { + return searchAggregatedTransactions + ? [ + { + bool: { + filter: [...existsQuery(TRANSACTION_DURATION_HISTOGRAM)], + must_not: [ + ...termsQuery( + METRICSET_INTERVAL, + RollupInterval.TenMinutes, + RollupInterval.SixtyMinutes + ), + ...termQuery(METRICSET_NAME, 'service_transaction'), + ], + }, + }, + ] + : []; +} + +export function isDurationSummaryNotSupportedFilter(): QueryDslQueryContainer { + return { + bool: { + must_not: [...existsQuery(TRANSACTION_DURATION_SUMMARY)], + }, + }; +} diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/plugin.ts b/x-pack/plugins/observability_solution/apm_data_access/server/plugin.ts index bba13bc6fea36..71b878794180f 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/plugin.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/plugin.ts @@ -19,6 +19,7 @@ import { apmIndicesSavedObjectDefinition, getApmIndicesSavedObject, } from './saved_objects/apm_indices'; +import { getServices } from './services/get_services'; export class ApmDataAccessPlugin implements Plugin @@ -32,16 +33,18 @@ export class ApmDataAccessPlugin const apmDataAccessConfig = this.initContext.config.get(); const apmIndicesFromConfigFile = apmDataAccessConfig.indices; + const getApmIndices = async (savedObjectsClient: SavedObjectsClientContract) => { + const apmIndicesFromSavedObject = await getApmIndicesSavedObject(savedObjectsClient); + return { ...apmIndicesFromConfigFile, ...apmIndicesFromSavedObject }; + }; // register saved object core.savedObjects.registerType(apmIndicesSavedObjectDefinition); // expose return { apmIndicesFromConfigFile, - getApmIndices: async (savedObjectsClient: SavedObjectsClientContract) => { - const apmIndicesFromSavedObject = await getApmIndicesSavedObject(savedObjectsClient); - return { ...apmIndicesFromConfigFile, ...apmIndicesFromSavedObject }; - }, + getApmIndices, + getServices, }; } diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_document_sources.ts b/x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/get_document_sources.ts similarity index 92% rename from x-pack/plugins/observability_solution/apm/server/lib/helpers/get_document_sources.ts rename to x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/get_document_sources.ts index ca049603b5c52..3e1c9fcbb1c78 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_document_sources.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/get_document_sources.ts @@ -6,18 +6,27 @@ */ import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ApmDocumentType } from '../../../common/document_type'; import { RollupInterval } from '../../../common/rollup'; -import { APMEventClient } from './create_es_client/create_apm_event_client'; -import { getConfigForDocumentType } from './create_es_client/document_type'; import { TimeRangeMetadata } from '../../../common/time_range_metadata'; -import { isDurationSummaryNotSupportedFilter } from './transactions'; +import { isDurationSummaryNotSupportedFilter } from '../../lib/helpers/transactions'; +import { ApmDocumentType } from '../../../common/document_type'; +import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; +import { getConfigForDocumentType } from '../../lib/helpers/create_es_client/document_type'; const QUERY_INDEX = { DOCUMENT_TYPE: 0, DURATION_SUMMARY_NOT_SUPPORTED: 1, } as const; +export interface DocumentSourcesRequest { + apmEventClient: APMEventClient; + start: number; + end: number; + kuery: string; + enableServiceTransactionMetrics: boolean; + enableContinuousRollups: boolean; +} + const getRequest = ({ documentType, rollupInterval, diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/index.ts new file mode 100644 index 0000000000000..e8bee4e431dc3 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/services/get_document_sources/index.ts @@ -0,0 +1,29 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ApmDataAccessServicesParams } from '../get_services'; +import { getDocumentSources, type DocumentSourcesRequest } from './get_document_sources'; + +export function createGetDocumentSources({ apmEventClient }: ApmDataAccessServicesParams) { + return async ({ + enableContinuousRollups, + enableServiceTransactionMetrics, + end, + kuery, + start, + }: Omit) => { + return getDocumentSources({ + apmEventClient, + enableContinuousRollups, + enableServiceTransactionMetrics, + end, + kuery, + start, + }); + }; +} + +export { getDocumentSources, type DocumentSourcesRequest }; diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/services/get_services.ts b/x-pack/plugins/observability_solution/apm_data_access/server/services/get_services.ts new file mode 100644 index 0000000000000..edcea39884d93 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/services/get_services.ts @@ -0,0 +1,19 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APMEventClient } from '../lib/helpers/create_es_client/create_apm_event_client'; +import { createGetDocumentSources } from './get_document_sources'; + +export interface ApmDataAccessServicesParams { + apmEventClient: APMEventClient; +} + +export function getServices(params: ApmDataAccessServicesParams) { + return { + getDocumentSources: createGetDocumentSources(params), + }; +} diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/types.ts b/x-pack/plugins/observability_solution/apm_data_access/server/types.ts index 39c21c8aa0016..c8f9b38a83874 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/types.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/types.ts @@ -7,10 +7,22 @@ import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { APMIndices } from '.'; +import { getServices } from './services/get_services'; export interface ApmDataAccessPluginSetup { apmIndicesFromConfigFile: APMIndices; getApmIndices: (soClient: SavedObjectsClientContract) => Promise; + getServices: typeof getServices; } + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ApmDataAccessPluginStart {} + +export type ApmDataAccessServices = ReturnType; +export type { ApmDataAccessServicesParams } from './services/get_services'; +export type { DocumentSourcesRequest } from './services/get_document_sources'; +export type { + APMEventClientConfig, + APMEventESSearchRequest, + APMLogEventESSearchRequest, +} from './lib/helpers'; diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts b/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts new file mode 100644 index 0000000000000..b1e768edf3733 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { + getDocumentTypeFilterForServiceDestinationStatistics, + getBackwardCompatibleDocumentTypeFilter, + callAsyncWithDebug, + cancelEsRequestOnAbort, + getDebugBody, + getDebugTitle, +} from './lib/helpers'; + +export { withApmSpan } from './utils/with_apm_span'; diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/utils/with_apm_span.ts b/x-pack/plugins/observability_solution/apm_data_access/server/utils/with_apm_span.ts new file mode 100644 index 0000000000000..1343970f04a3f --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/utils/with_apm_span.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { withSpan, SpanOptions, parseSpanOptions } from '@kbn/apm-utils'; + +export function withApmSpan( + optionsOrName: SpanOptions | string, + cb: () => Promise +): Promise { + const options = parseSpanOptions(optionsOrName); + + const optionsWithDefaults = { + ...(options.intercept ? {} : { type: 'plugin:apm' }), + ...options, + labels: { + plugin: 'apm', + ...options.labels, + }, + }; + + return withSpan(optionsWithDefaults, cb); +} diff --git a/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json b/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json index faa5185404fd0..cdcfd3ec3d023 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json @@ -9,6 +9,14 @@ "@kbn/config-schema", "@kbn/core", "@kbn/i18n", - "@kbn/core-saved-objects-api-server" + "@kbn/core-saved-objects-api-server", + "@kbn/data-plugin", + "@kbn/inspector-plugin", + "@kbn/observability-plugin", + "@kbn/observability-shared-plugin", + "@kbn/es-types", + "@kbn/apm-types", + "@kbn/core-http-server-mocks", + "@kbn/apm-utils" ] } From 004ba7ac5471009c5051892a3d7c7561f9300c29 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 2 Aug 2024 17:25:10 +0200 Subject: [PATCH 03/13] Make ELU history route `internal` (#189786) --- .../core-metrics-server-internal/src/routes/elu_history.ts | 5 +++-- src/core/server/integration_tests/metrics/elu_load.test.ts | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/metrics/core-metrics-server-internal/src/routes/elu_history.ts b/packages/core/metrics/core-metrics-server-internal/src/routes/elu_history.ts index 4422bdbb88117..c7041427950d6 100644 --- a/packages/core/metrics/core-metrics-server-internal/src/routes/elu_history.ts +++ b/packages/core/metrics/core-metrics-server-internal/src/routes/elu_history.ts @@ -44,7 +44,8 @@ export function registerEluHistoryRoute(router: IRouter, metrics$: Observable { diff --git a/src/core/server/integration_tests/metrics/elu_load.test.ts b/src/core/server/integration_tests/metrics/elu_load.test.ts index a36017c9dcda2..8c4299fd5fa5d 100644 --- a/src/core/server/integration_tests/metrics/elu_load.test.ts +++ b/src/core/server/integration_tests/metrics/elu_load.test.ts @@ -52,7 +52,10 @@ describe('GET /api/_elu_load', () => { }); it('gets ELU load average', async () => { - const { body } = await supertest(listener).get('/api/_elu_history').expect(200); + const { body } = await supertest(listener) + .get('/api/_elu_history') + .query({ apiVersion: '1', elasticInternalOrigin: 'true' }) + .expect(200); expect(body).toEqual({ history: { short: expect.any(Number), From b809a9a3c7c01d9599c08a07b12fc7a65de330c0 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 2 Aug 2024 10:34:16 -0500 Subject: [PATCH 04/13] [project-deploy] Remove observability deploy (#189762) This is managed out of band. --- .buildkite/scripts/steps/serverless/deploy.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.buildkite/scripts/steps/serverless/deploy.sh b/.buildkite/scripts/steps/serverless/deploy.sh index 325aadf187b5b..d30723393dacd 100644 --- a/.buildkite/scripts/steps/serverless/deploy.sh +++ b/.buildkite/scripts/steps/serverless/deploy.sh @@ -163,8 +163,7 @@ if is_pr_with_label "ci:project-deploy-observability" ; then # Only deploy observability if the PR is targeting main if [[ "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" == "main" ]]; then create_github_issue_oblt_test_environments - echo "--- Deploy observability with Kibana CI" - deploy "observability" + buildkite-agent annotate --context obl-test-info --style info 'See linked [Deploy Serverless Kibana] issue in pull request for project deployment information' fi fi is_pr_with_label "ci:project-deploy-security" && deploy "security" From 3c431ffd7ef1d6fca3cf8186f6daa1c25fdef5d8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Sat, 3 Aug 2024 01:38:28 +1000 Subject: [PATCH 05/13] skip failing test suite (#189805) --- .../test/fleet_api_integration/apis/space_awareness/actions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts index 1b9e193bb622a..efd73ddb54b0f 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts @@ -19,7 +19,8 @@ export default function (providerContext: FtrProviderContext) { const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - describe('actions', async function () { + // Failing: See https://github.com/elastic/kibana/issues/189805 + describe.skip('actions', async function () { skipIfNoDockerRegistry(providerContext); const apiClient = new SpaceTestApiClient(supertest); From bc79b994d1f26c13f09cfe538476b1ad59d51efe Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Fri, 2 Aug 2024 16:58:32 +0100 Subject: [PATCH 06/13] [Entity Analytics] Add return types to all our routes (#189726) ## Summary Add return types to all of our route handlers. Before we did things like this in _some_ APIs: ``` const resBody: CreateAssetCriticalityRecordResponse = { blah : 'blah'}; ``` And I have moved to this style: ``` async ( context, request, response ): Promise> => { ``` This keeps the API docs in sync, I saw that this is how they do it in the elastic assistant plugin and liked it. I think it is clearer to have this stuff in the route definition, near the URL and request validation. --------- Co-authored-by: Elastic Machine --- .../get_asset_criticality_privileges.gen.ts | 33 +---- ...t_asset_criticality_privileges.schema.yaml | 43 +------ .../api/entity_analytics/common/common.gen.ts | 26 ++++ .../common/common.schema.yaml | 36 ++++++ .../common/api/entity_analytics/index.ts | 1 + .../get_risk_engine_privileges.gen.ts | 22 ++++ .../get_risk_engine_privileges.schema.yaml | 26 ++++ .../api/entity_analytics/risk_engine/index.ts | 1 + .../risk_engine/privileges.test.ts | 2 +- .../risk_engine/privileges.ts | 2 +- .../public/entity_analytics/api/api.ts | 2 +- .../asset_criticality/routes/bulk_upload.ts | 18 ++- .../asset_criticality/routes/get.ts | 13 +- .../asset_criticality/routes/list.ts | 10 +- .../asset_criticality/routes/privileges.ts | 9 +- .../asset_criticality/routes/status.ts | 15 ++- .../asset_criticality/routes/upload_csv.ts | 8 +- .../asset_criticality/routes/upsert.ts | 13 +- .../risk_engine/routes/disable.ts | 86 +++++++------ .../risk_engine/routes/enable.ts | 81 ++++++------ .../risk_engine/routes/init.ts | 120 +++++++++--------- .../risk_engine/routes/privileges.ts | 60 +++++---- .../risk_engine/routes/settings.ts | 71 ++++++----- .../risk_engine/routes/status.ts | 54 ++++---- .../risk_score/routes/entity_calculation.ts | 4 +- .../risk_score/routes/preview.ts | 5 +- .../services/security_solution_api.gen.ts | 14 ++ 27 files changed, 454 insertions(+), 321 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.schema.yaml diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.gen.ts index 3d828e0e38a7a..8f53ab8c558c3 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.gen.ts @@ -14,30 +14,11 @@ * version: 1 */ -import { z } from 'zod'; +import type { z } from 'zod'; -export type EntityAnalyticsPrivileges = z.infer; -export const EntityAnalyticsPrivileges = z.object({ - has_all_required: z.boolean(), - has_read_permissions: z.boolean().optional(), - has_write_permissions: z.boolean().optional(), - privileges: z.object({ - elasticsearch: z.object({ - cluster: z - .object({ - manage_index_templates: z.boolean().optional(), - manage_transform: z.boolean().optional(), - }) - .optional(), - index: z - .object({}) - .catchall( - z.object({ - read: z.boolean().optional(), - write: z.boolean().optional(), - }) - ) - .optional(), - }), - }), -}); +import { EntityAnalyticsPrivileges } from '../common/common.gen'; + +export type AssetCriticalityGetPrivilegesResponse = z.infer< + typeof AssetCriticalityGetPrivilegesResponse +>; +export const AssetCriticalityGetPrivilegesResponse = EntityAnalyticsPrivileges; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.schema.yaml index 548237265d0fb..267665613b7c7 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/asset_criticality/get_asset_criticality_privileges.schema.yaml @@ -7,6 +7,7 @@ paths: get: x-labels: [ess, serverless] x-internal: true + x-codegen-enabled: true operationId: AssetCriticalityGetPrivileges summary: Get Asset Criticality Privileges responses: @@ -15,49 +16,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityAnalyticsPrivileges' + $ref: '../common/common.schema.yaml#/components/schemas/EntityAnalyticsPrivileges' example: elasticsearch: index: '.asset-criticality.asset-criticality-*': read: true write: false - has_all_required: false -components: - schemas: - EntityAnalyticsPrivileges: - type: object - properties: - has_all_required: - type: boolean - has_read_permissions: - type: boolean - has_write_permissions: - type: boolean - privileges: - type: object - properties: - elasticsearch: - type: object - properties: - cluster: - type: object - properties: - manage_index_templates: - type: boolean - manage_transform: - type: boolean - index: - type: object - additionalProperties: - type: object - properties: - read: - type: boolean - write: - type: boolean - required: - - elasticsearch - required: - - has_all_required - - privileges + has_all_required: false \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts index 5b3538917f78c..8e6f3841b8f6d 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts @@ -18,6 +18,32 @@ import { z } from 'zod'; import { AssetCriticalityLevel } from '../asset_criticality/common.gen'; +export type EntityAnalyticsPrivileges = z.infer; +export const EntityAnalyticsPrivileges = z.object({ + has_all_required: z.boolean(), + has_read_permissions: z.boolean().optional(), + has_write_permissions: z.boolean().optional(), + privileges: z.object({ + elasticsearch: z.object({ + cluster: z + .object({ + manage_index_templates: z.boolean().optional(), + manage_transform: z.boolean().optional(), + }) + .optional(), + index: z + .object({}) + .catchall( + z.object({ + read: z.boolean().optional(), + write: z.boolean().optional(), + }) + ) + .optional(), + }), + }), +}); + export type EntityAfterKey = z.infer; export const EntityAfterKey = z.object({}).catchall(z.string()); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml index 63aa739d2133d..67428b261a0f9 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml @@ -6,6 +6,42 @@ info: paths: {} components: schemas: + EntityAnalyticsPrivileges: + type: object + properties: + has_all_required: + type: boolean + has_read_permissions: + type: boolean + has_write_permissions: + type: boolean + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + type: object + properties: + manage_index_templates: + type: boolean + manage_transform: + type: boolean + index: + type: object + additionalProperties: + type: object + properties: + read: + type: boolean + write: + type: boolean + required: + - elasticsearch + required: + - has_all_required + - privileges EntityAfterKey: type: object additionalProperties: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/index.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/index.ts index afb71bbd5bb17..9d3c3a29bdebf 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/index.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/index.ts @@ -8,3 +8,4 @@ export * from './asset_criticality'; export * from './risk_engine'; export * from './risk_score'; +export { EntityAnalyticsPrivileges } from './common'; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.gen.ts new file mode 100644 index 0000000000000..db07db331e477 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.gen.ts @@ -0,0 +1,22 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Get Risk Engine Privileges Schema + * version: 1 + */ + +import type { z } from 'zod'; + +import { EntityAnalyticsPrivileges } from '../common/common.gen'; + +export type RiskEngineGetPrivilegesResponse = z.infer; +export const RiskEngineGetPrivilegesResponse = EntityAnalyticsPrivileges; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.schema.yaml new file mode 100644 index 0000000000000..0fcaf08f10c16 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/get_risk_engine_privileges.schema.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + title: Get Risk Engine Privileges Schema + version: '1' +paths: + /internal/risk_engine/privileges: + get: + x-labels: [ess, serverless] + x-internal: true + x-codegen-enabled: true + operationId: RiskEngineGetPrivileges + summary: Get Risk Engine Privileges + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../common/common.schema.yaml#/components/schemas/EntityAnalyticsPrivileges' + example: + elasticsearch: + index: + 'risk-score.risk-score-*': + read: true + write: false + has_all_required: false \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts index 97f11da2ef090..94d587cd2bfc7 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts @@ -14,3 +14,4 @@ export * from './engine_status_route.gen'; export * from './calculation_route.gen'; export * from './preview_route.gen'; export * from './entity_calculation_route.gen'; +export * from './get_risk_engine_privileges.gen'; diff --git a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.test.ts b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.test.ts index e0111b3d67871..caf7b640582a6 100644 --- a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.test.ts +++ b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { EntityAnalyticsPrivileges } from '../../api/entity_analytics/asset_criticality/get_asset_criticality_privileges.gen'; +import type { EntityAnalyticsPrivileges } from '../../api/entity_analytics'; import { getMissingRiskEnginePrivileges } from './privileges'; describe('getMissingRiskEnginePrivileges', () => { diff --git a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.ts b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.ts index b0bbc39609b3b..b03b9e2921325 100644 --- a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.ts +++ b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/privileges.ts @@ -6,7 +6,7 @@ */ import type { NonEmptyArray } from 'fp-ts/NonEmptyArray'; -import type { EntityAnalyticsPrivileges } from '../../api/entity_analytics/asset_criticality/get_asset_criticality_privileges.gen'; +import type { EntityAnalyticsPrivileges } from '../../api/entity_analytics'; import type { RiskEngineIndexPrivilege } from './constants'; import { RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 500c327d86b0c..9351e34ab4b5b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -22,7 +22,7 @@ import type { import type { AssetCriticalityRecord, EntityAnalyticsPrivileges, -} from '../../../common/api/entity_analytics/asset_criticality'; +} from '../../../common/api/entity_analytics'; import type { RiskScoreEntity } from '../../../common/search_strategy'; import { RISK_ENGINE_STATUS_URL, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts index 822c8a644d9b3..960f6c87be283 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/bulk_upload.ts @@ -4,13 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { Readable } from 'node:stream'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { BulkUpsertAssetCriticalityRecordsResponse } from '../../../../../common/api/entity_analytics'; -import { BulkUpsertAssetCriticalityRecordsRequestBody } from '../../../../../common/api/entity_analytics'; +import { + BulkUpsertAssetCriticalityRecordsRequestBody, + type BulkUpsertAssetCriticalityRecordsResponse, +} from '../../../../../common/api/entity_analytics'; import type { ConfigType } from '../../../../config'; import { ASSET_CRITICALITY_PUBLIC_BULK_UPLOAD_URL, @@ -46,7 +48,11 @@ export const assetCriticalityPublicBulkUploadRoute = ( }, }, }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const { errorRetries, maxBulkRequestBodySizeBytes } = config.entityAnalytics.assetCriticality.csvUpload; const { records } = request.body; @@ -90,9 +96,7 @@ export const assetCriticalityPublicBulkUploadRoute = ( () => `Asset criticality Bulk upload completed in ${tookMs}ms ${JSON.stringify(stats)}` ); - const resBody: BulkUpsertAssetCriticalityRecordsResponse = { errors, stats }; - - return response.ok({ body: resBody }); + return response.ok({ body: { errors, stats } }); } catch (e) { logger.error(`Error during asset criticality bulk upload: ${e}`); const error = transformError(e); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts index 99f7d3ff97ae4..ed63f6207fec1 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/get.ts @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import { GetAssetCriticalityRecordRequestQuery } from '../../../../../common/api/entity_analytics'; +import { + GetAssetCriticalityRecordRequestQuery, + type GetAssetCriticalityRecordResponse, +} from '../../../../../common/api/entity_analytics'; import { ASSET_CRITICALITY_PUBLIC_URL, APP_ID, @@ -42,7 +45,11 @@ export const assetCriticalityPublicGetRoute = ( }, }, }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const siemResponse = buildSiemResponse(response); try { await assertAdvancedSettingsEnabled(await context.core, ENABLE_ASSET_CRITICALITY_SETTING); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts index 711426e4df510..64bbca127ed77 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/list.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; @@ -43,7 +43,11 @@ export const assetCriticalityPublicListRoute = ( }, }, }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const siemResponse = buildSiemResponse(response); try { await assertAdvancedSettingsEnabled(await context.core, ENABLE_ASSET_CRITICALITY_SETTING); @@ -81,7 +85,7 @@ export const assetCriticalityPublicListRoute = ( }, }); - const body: FindAssetCriticalityRecordsResponse = { + const body = { records, total, page, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/privileges.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/privileges.ts index a3b4c48d828df..7f6b80dd92909 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/privileges.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/privileges.ts @@ -4,9 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { AssetCriticalityGetPrivilegesResponse } from '../../../../../common/api/entity_analytics'; import { ASSET_CRITICALITY_INTERNAL_PRIVILEGES_URL, APP_ID, @@ -38,7 +39,11 @@ export const assetCriticalityInternalPrivilegesRoute = ( version: API_VERSIONS.internal.v1, validate: false, }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const siemResponse = buildSiemResponse(response); try { await assertAdvancedSettingsEnabled(await context.core, ENABLE_ASSET_CRITICALITY_SETTING); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts index 9d77817a20d98..a0070503a3f8c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/status.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { GetAssetCriticalityStatusResponse } from '../../../../../common/api/entity_analytics'; @@ -34,7 +34,11 @@ export const assetCriticalityInternalStatusRoute = ( }) .addVersion( { version: API_VERSIONS.internal.v1, validate: {} }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const siemResponse = buildSiemResponse(response); try { await assertAdvancedSettingsEnabled(await context.core, ENABLE_ASSET_CRITICALITY_SETTING); @@ -55,11 +59,10 @@ export const assetCriticalityInternalStatusRoute = ( }, }); - const body: GetAssetCriticalityStatusResponse = { - asset_criticality_resources_installed: result.isAssetCriticalityResourcesInstalled, - }; return response.ok({ - body, + body: { + asset_criticality_resources_installed: result.isAssetCriticalityResourcesInstalled, + }, }); } catch (e) { const error = transformError(e); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts index 6f69695f20a74..cbe434ccb25cf 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upload_csv.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { schema } from '@kbn/config-schema'; import Papa from 'papaparse'; @@ -57,7 +57,11 @@ export const assetCriticalityPublicCSVUploadRoute = ( }, }, }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const { errorRetries, maxBulkRequestBodySizeBytes } = config.entityAnalytics.assetCriticality.csvUpload; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts index 02ff12b1b91d3..8feeb822bdddf 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/routes/upsert.ts @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import { CreateAssetCriticalityRecordRequestBody } from '../../../../../common/api/entity_analytics'; +import { + CreateAssetCriticalityRecordRequestBody, + type CreateAssetCriticalityRecordResponse, +} from '../../../../../common/api/entity_analytics'; import { ASSET_CRITICALITY_PUBLIC_URL, APP_ID, @@ -42,7 +45,11 @@ export const assetCriticalityPublicUpsertRoute = ( }, }, }, - async (context, request, response) => { + async ( + context, + request, + response + ): Promise> => { const siemResponse = buildSiemResponse(response); try { await assertAdvancedSettingsEnabled(await context.core, ENABLE_ASSET_CRITICALITY_SETTING); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts index df45eb4ddb934..59b4b4f77537e 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.ts @@ -7,6 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; import type { DisableRiskEngineResponse } from '../../../../../common/api/entity_analytics'; import { RISK_ENGINE_DISABLE_URL, APP_ID } from '../../../../../common/constants'; import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; @@ -29,59 +30,60 @@ export const riskEngineDisableRoute = ( }) .addVersion( { version: '1', validate: {} }, - withRiskEnginePrivilegeCheck(getStartServices, async (context, request, response) => { - const securitySolution = await context.securitySolution; + withRiskEnginePrivilegeCheck( + getStartServices, + async (context, request, response): Promise> => { + const securitySolution = await context.securitySolution; - securitySolution.getAuditLogger()?.log({ - message: 'User attempted to disable the risk engine.', - event: { - action: RiskEngineAuditActions.RISK_ENGINE_DISABLE, - category: AUDIT_CATEGORY.DATABASE, - type: AUDIT_TYPE.CHANGE, - outcome: AUDIT_OUTCOME.UNKNOWN, - }, - }); - - const siemResponse = buildSiemResponse(response); - const [_, { taskManager }] = await getStartServices(); - - const riskEngineClient = securitySolution.getRiskEngineDataClient(); - - if (!taskManager) { securitySolution.getAuditLogger()?.log({ - message: - 'User attempted to disable the risk engine, but the Kibana Task Manager was unavailable', + message: 'User attempted to disable the risk engine.', event: { action: RiskEngineAuditActions.RISK_ENGINE_DISABLE, category: AUDIT_CATEGORY.DATABASE, type: AUDIT_TYPE.CHANGE, - outcome: AUDIT_OUTCOME.FAILURE, - }, - error: { - message: - 'User attempted to disable the risk engine, but the Kibana Task Manager was unavailable', + outcome: AUDIT_OUTCOME.UNKNOWN, }, }); - return siemResponse.error({ - statusCode: 400, - body: TASK_MANAGER_UNAVAILABLE_ERROR, - }); - } + const siemResponse = buildSiemResponse(response); + const [_, { taskManager }] = await getStartServices(); - try { - await riskEngineClient.disableRiskEngine({ taskManager }); - const body: DisableRiskEngineResponse = { success: true }; - return response.ok({ body }); - } catch (e) { - const error = transformError(e); + const riskEngineClient = securitySolution.getRiskEngineDataClient(); - return siemResponse.error({ - statusCode: error.statusCode, - body: { message: error.message, full_error: JSON.stringify(e) }, - bypassErrorFormat: true, - }); + if (!taskManager) { + securitySolution.getAuditLogger()?.log({ + message: + 'User attempted to disable the risk engine, but the Kibana Task Manager was unavailable', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_DISABLE, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.CHANGE, + outcome: AUDIT_OUTCOME.FAILURE, + }, + error: { + message: + 'User attempted to disable the risk engine, but the Kibana Task Manager was unavailable', + }, + }); + + return siemResponse.error({ + statusCode: 400, + body: TASK_MANAGER_UNAVAILABLE_ERROR, + }); + } + + try { + await riskEngineClient.disableRiskEngine({ taskManager }); + return response.ok({ body: { success: true } }); + } catch (e) { + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } } - }) + ) ); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts index e537a49b498a8..24b3c3816440d 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.ts @@ -7,6 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; import type { EnableRiskEngineResponse } from '../../../../../common/api/entity_analytics'; import { RISK_ENGINE_ENABLE_URL, APP_ID } from '../../../../../common/constants'; import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations'; @@ -29,57 +30,59 @@ export const riskEngineEnableRoute = ( }) .addVersion( { version: '1', validate: {} }, - withRiskEnginePrivilegeCheck(getStartServices, async (context, request, response) => { - const securitySolution = await context.securitySolution; + withRiskEnginePrivilegeCheck( + getStartServices, + async (context, request, response): Promise> => { + const securitySolution = await context.securitySolution; - securitySolution.getAuditLogger()?.log({ - message: 'User attempted to enable the risk engine', - event: { - action: RiskEngineAuditActions.RISK_ENGINE_ENABLE, - category: AUDIT_CATEGORY.DATABASE, - type: AUDIT_TYPE.CHANGE, - outcome: AUDIT_OUTCOME.UNKNOWN, - }, - }); - - const siemResponse = buildSiemResponse(response); - const [_, { taskManager }] = await getStartServices(); - const riskEngineClient = securitySolution.getRiskEngineDataClient(); - if (!taskManager) { securitySolution.getAuditLogger()?.log({ - message: - 'User attempted to enable the risk engine, but the Kibana Task Manager was unavailable', + message: 'User attempted to enable the risk engine', event: { action: RiskEngineAuditActions.RISK_ENGINE_ENABLE, category: AUDIT_CATEGORY.DATABASE, type: AUDIT_TYPE.CHANGE, - outcome: AUDIT_OUTCOME.FAILURE, + outcome: AUDIT_OUTCOME.UNKNOWN, }, - error: { + }); + + const siemResponse = buildSiemResponse(response); + const [_, { taskManager }] = await getStartServices(); + const riskEngineClient = securitySolution.getRiskEngineDataClient(); + if (!taskManager) { + securitySolution.getAuditLogger()?.log({ message: 'User attempted to enable the risk engine, but the Kibana Task Manager was unavailable', - }, - }); + event: { + action: RiskEngineAuditActions.RISK_ENGINE_ENABLE, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.CHANGE, + outcome: AUDIT_OUTCOME.FAILURE, + }, + error: { + message: + 'User attempted to enable the risk engine, but the Kibana Task Manager was unavailable', + }, + }); - return siemResponse.error({ - statusCode: 400, - body: TASK_MANAGER_UNAVAILABLE_ERROR, - }); - } + return siemResponse.error({ + statusCode: 400, + body: TASK_MANAGER_UNAVAILABLE_ERROR, + }); + } - try { - await riskEngineClient.enableRiskEngine({ taskManager }); - const body: EnableRiskEngineResponse = { success: true }; - return response.ok({ body }); - } catch (e) { - const error = transformError(e); + try { + await riskEngineClient.enableRiskEngine({ taskManager }); + return response.ok({ body: { success: true } }); + } catch (e) { + const error = transformError(e); - return siemResponse.error({ - statusCode: error.statusCode, - body: { message: error.message, full_error: JSON.stringify(e) }, - bypassErrorFormat: true, - }); + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } } - }) + ) ); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts index 160d040f6d9fc..4657d21cbcbe0 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/init.ts @@ -7,6 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; import type { InitRiskEngineResponse, InitRiskEngineResult, @@ -31,75 +32,78 @@ export const riskEngineInitRoute = ( }) .addVersion( { version: '1', validate: {} }, - withRiskEnginePrivilegeCheck(getStartServices, async (context, request, response) => { - const securitySolution = await context.securitySolution; + withRiskEnginePrivilegeCheck( + getStartServices, + async (context, request, response): Promise> => { + const securitySolution = await context.securitySolution; - securitySolution.getAuditLogger()?.log({ - message: 'User attempted to initialize the risk engine', - event: { - action: RiskEngineAuditActions.RISK_ENGINE_INIT, - category: AUDIT_CATEGORY.DATABASE, - type: AUDIT_TYPE.CHANGE, - outcome: AUDIT_OUTCOME.UNKNOWN, - }, - }); + securitySolution.getAuditLogger()?.log({ + message: 'User attempted to initialize the risk engine', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_INIT, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.CHANGE, + outcome: AUDIT_OUTCOME.UNKNOWN, + }, + }); - const siemResponse = buildSiemResponse(response); - const [_, { taskManager }] = await getStartServices(); - const riskEngineDataClient = securitySolution.getRiskEngineDataClient(); - const riskScoreDataClient = securitySolution.getRiskScoreDataClient(); - const spaceId = securitySolution.getSpaceId(); + const siemResponse = buildSiemResponse(response); + const [_, { taskManager }] = await getStartServices(); + const riskEngineDataClient = securitySolution.getRiskEngineDataClient(); + const riskScoreDataClient = securitySolution.getRiskScoreDataClient(); + const spaceId = securitySolution.getSpaceId(); - try { - if (!taskManager) { - return siemResponse.error({ - statusCode: 400, - body: TASK_MANAGER_UNAVAILABLE_ERROR, - }); - } + try { + if (!taskManager) { + return siemResponse.error({ + statusCode: 400, + body: TASK_MANAGER_UNAVAILABLE_ERROR, + }); + } - const initResult = await riskEngineDataClient.init({ - taskManager, - namespace: spaceId, - riskScoreDataClient, - }); - - const result: InitRiskEngineResult = { - risk_engine_enabled: initResult.riskEngineEnabled, - risk_engine_resources_installed: initResult.riskEngineResourcesInstalled, - risk_engine_configuration_created: initResult.riskEngineConfigurationCreated, - legacy_risk_engine_disabled: initResult.legacyRiskEngineDisabled, - errors: initResult.errors, - }; + const initResult = await riskEngineDataClient.init({ + taskManager, + namespace: spaceId, + riskScoreDataClient, + }); - const initResponse: InitRiskEngineResponse = { - result, - }; + const result: InitRiskEngineResult = { + risk_engine_enabled: initResult.riskEngineEnabled, + risk_engine_resources_installed: initResult.riskEngineResourcesInstalled, + risk_engine_configuration_created: initResult.riskEngineConfigurationCreated, + legacy_risk_engine_disabled: initResult.legacyRiskEngineDisabled, + errors: initResult.errors, + }; - if ( - !initResult.riskEngineEnabled || - !initResult.riskEngineResourcesInstalled || - !initResult.riskEngineConfigurationCreated - ) { - return siemResponse.error({ - statusCode: 400, + if ( + !initResult.riskEngineEnabled || + !initResult.riskEngineResourcesInstalled || + !initResult.riskEngineConfigurationCreated + ) { + return siemResponse.error({ + statusCode: 400, + body: { + message: result.errors.join('\n'), + full_error: result, + }, + bypassErrorFormat: true, + }); + } + return response.ok({ body: { - message: result.errors.join('\n'), - full_error: result, + result, }, + }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, bypassErrorFormat: true, }); } - return response.ok({ body: initResponse }); - } catch (e) { - const error = transformError(e); - - return siemResponse.error({ - statusCode: error.statusCode, - body: { message: error.message, full_error: JSON.stringify(e) }, - bypassErrorFormat: true, - }); } - }) + ) ); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/privileges.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/privileges.ts index 38b48aca7e5ab..f14e06fa72868 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/privileges.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/privileges.ts @@ -7,7 +7,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { EntityAnalyticsPrivileges } from '../../../../../common/api/entity_analytics'; +import type { IKibanaResponse } from '@kbn/core-http-server'; +import type { RiskEngineGetPrivilegesResponse } from '../../../../../common/api/entity_analytics'; import { RISK_ENGINE_PRIVILEGES_URL, APP_ID } from '../../../../../common/constants'; import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; import { RiskScoreAuditActions } from '../../risk_score/audit'; @@ -27,34 +28,41 @@ export const riskEnginePrivilegesRoute = ( tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], }, }) - .addVersion({ version: '1', validate: false }, async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - const [_, { security }] = await getStartServices(); - const securitySolution = await context.securitySolution; + .addVersion( + { version: '1', validate: false }, + async ( + context, + request, + response + ): Promise> => { + const siemResponse = buildSiemResponse(response); + const [_, { security }] = await getStartServices(); + const securitySolution = await context.securitySolution; - const body: EntityAnalyticsPrivileges = await getUserRiskEnginePrivileges(request, security); + const body = await getUserRiskEnginePrivileges(request, security); - securitySolution.getAuditLogger()?.log({ - message: 'User checked if they have the required privileges to configure the risk engine', - event: { - action: RiskScoreAuditActions.RISK_ENGINE_PRIVILEGES_GET, - category: AUDIT_CATEGORY.AUTHENTICATION, - type: AUDIT_TYPE.ACCESS, - outcome: AUDIT_OUTCOME.SUCCESS, - }, - }); - - try { - return response.ok({ - body, + securitySolution.getAuditLogger()?.log({ + message: 'User checked if they have the required privileges to configure the risk engine', + event: { + action: RiskScoreAuditActions.RISK_ENGINE_PRIVILEGES_GET, + category: AUDIT_CATEGORY.AUTHENTICATION, + type: AUDIT_TYPE.ACCESS, + outcome: AUDIT_OUTCOME.SUCCESS, + }, }); - } catch (e) { - const error = transformError(e); - return siemResponse.error({ - statusCode: error.statusCode, - body: error.message, - }); + try { + return response.ok({ + body, + }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } } - }); + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts index 1d39fbaf18420..e300f012b86cf 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts @@ -7,6 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; import type { ReadRiskEngineSettingsResponse } from '../../../../../common/api/entity_analytics/risk_engine'; import { RISK_ENGINE_SETTINGS_URL, APP_ID } from '../../../../../common/constants'; import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit'; @@ -22,41 +23,47 @@ export const riskEngineSettingsRoute = (router: EntityAnalyticsRoutesDeps['route tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], }, }) - .addVersion({ version: '1', validate: {} }, async (context, request, response) => { - const siemResponse = buildSiemResponse(response); + .addVersion( + { version: '1', validate: {} }, + async ( + context, + request, + response + ): Promise> => { + const siemResponse = buildSiemResponse(response); - const securitySolution = await context.securitySolution; - const riskEngineClient = securitySolution.getRiskEngineDataClient(); + const securitySolution = await context.securitySolution; + const riskEngineClient = securitySolution.getRiskEngineDataClient(); - try { - const result = await riskEngineClient.getConfiguration(); - securitySolution.getAuditLogger()?.log({ - message: 'User accessed risk engine configuration information', - event: { - action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURATION_GET, - category: AUDIT_CATEGORY.DATABASE, - type: AUDIT_TYPE.ACCESS, - outcome: AUDIT_OUTCOME.SUCCESS, - }, - }); + try { + const result = await riskEngineClient.getConfiguration(); + securitySolution.getAuditLogger()?.log({ + message: 'User accessed risk engine configuration information', + event: { + action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURATION_GET, + category: AUDIT_CATEGORY.DATABASE, + type: AUDIT_TYPE.ACCESS, + outcome: AUDIT_OUTCOME.SUCCESS, + }, + }); - if (!result) { - throw new Error('Unable to get risk engine configuration'); - } - const body: ReadRiskEngineSettingsResponse = { - range: result.range, - }; - return response.ok({ - body, - }); - } catch (e) { - const error = transformError(e); + if (!result) { + throw new Error('Unable to get risk engine configuration'); + } + return response.ok({ + body: { + range: result.range, + }, + }); + } catch (e) { + const error = transformError(e); - return siemResponse.error({ - statusCode: error.statusCode, - body: { message: error.message, full_error: JSON.stringify(e) }, - bypassErrorFormat: true, - }); + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } } - }); + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts index 00806bfd43720..b3d0cc4082446 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts @@ -7,6 +7,7 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse } from '@kbn/core-http-server'; import type { RiskEngineStatusResponse } from '../../../../../common/api/entity_analytics'; import { RISK_ENGINE_STATUS_URL, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -20,34 +21,37 @@ export const riskEngineStatusRoute = (router: EntityAnalyticsRoutesDeps['router' tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], }, }) - .addVersion({ version: '1', validate: {} }, async (context, request, response) => { - const siemResponse = buildSiemResponse(response); + .addVersion( + { version: '1', validate: {} }, + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); - const securitySolution = await context.securitySolution; - const riskEngineClient = securitySolution.getRiskEngineDataClient(); - const spaceId = securitySolution.getSpaceId(); + const securitySolution = await context.securitySolution; + const riskEngineClient = securitySolution.getRiskEngineDataClient(); + const spaceId = securitySolution.getSpaceId(); - try { - const { riskEngineStatus, legacyRiskEngineStatus, isMaxAmountOfRiskEnginesReached } = - await riskEngineClient.getStatus({ - namespace: spaceId, - }); - - const body: RiskEngineStatusResponse = { - risk_engine_status: riskEngineStatus, - legacy_risk_engine_status: legacyRiskEngineStatus, - is_max_amount_of_risk_engines_reached: isMaxAmountOfRiskEnginesReached, - }; + try { + const { riskEngineStatus, legacyRiskEngineStatus, isMaxAmountOfRiskEnginesReached } = + await riskEngineClient.getStatus({ + namespace: spaceId, + }); - return response.ok({ body }); - } catch (e) { - const error = transformError(e); + return response.ok({ + body: { + risk_engine_status: riskEngineStatus, + legacy_risk_engine_status: legacyRiskEngineStatus, + is_max_amount_of_risk_engines_reached: isMaxAmountOfRiskEnginesReached, + }, + }); + } catch (e) { + const error = transformError(e); - return siemResponse.error({ - statusCode: error.statusCode, - body: { message: error.message, full_error: JSON.stringify(e) }, - bypassErrorFormat: true, - }); + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } } - }); + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts index c72a1706f089e..4b1cf773a572b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.ts @@ -33,7 +33,7 @@ type Handler = ( context: SecuritySolutionRequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory -) => Promise; +) => Promise>; const handler: (logger: Logger) => Handler = (logger) => async (context, request, response) => { const securityContext = await context.securitySolution; @@ -101,7 +101,7 @@ const handler: (logger: Logger) => Handler = (logger) => async (context, request const filter = isEmpty(userFilter) ? [identifierFilter] : [userFilter, identifierFilter]; - const result: RiskScoresCalculationResponse = await riskScoreService.calculateAndPersistScores({ + const result = await riskScoreService.calculateAndPersistScores({ pageSize, identifierType, index, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts index 68e7f2fc50b74..ae265d415288a 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.ts @@ -5,10 +5,11 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { RiskScoresPreviewResponse } from '../../../../../common/api/entity_analytics'; import { RiskScoresPreviewRequest } from '../../../../../common/api/entity_analytics'; import { APP_ID, @@ -40,7 +41,7 @@ export const riskScorePreviewRoute = ( request: { body: buildRouteValidationWithZod(RiskScoresPreviewRequest) }, }, }, - async (context, request, response) => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); const securityContext = await context.securitySolution; const coreContext = await context.core; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 3877bb3faa6dd..7d545b6d9ebb2 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -122,6 +122,13 @@ after 30 days. It also deletes other artifacts specific to the migration impleme .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + assetCriticalityGetPrivileges() { + return supertest + .get('/internal/asset_criticality/privileges') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Create new detection rules in bulk. */ @@ -730,6 +737,13 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + riskEngineGetPrivileges() { + return supertest + .get('/internal/risk_engine/privileges') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, rulePreview(props: RulePreviewProps) { return supertest .post('/api/detection_engine/rules/preview') From a4443783404f88ee30d6a18086d6599ff02ccce8 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Fri, 2 Aug 2024 18:25:18 +0200 Subject: [PATCH 07/13] [Observability Onboarding] Change k8s quick start flow title (#189795) Changes the title for the k8s flow. --- .../quickstart_flows/kubernetes/index.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx index 41314e37460e8..75f1376b35f7c 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx @@ -15,6 +15,7 @@ import { EuiStepStatus, } from '@elastic/eui'; import useEvent from 'react-use/lib/useEvent'; +import { i18n } from '@kbn/i18n'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { EmptyPrompt } from '../shared/empty_prompt'; import { CommandSnippet } from './command_snippet'; @@ -37,7 +38,12 @@ export const KubernetesPanel: React.FC = () => { const steps = [ { - title: 'Install Elastic Agent on your host', + title: i18n.translate( + 'xpack.observability_onboarding.experimentalOnboardingFlow.kubernetes.installStepTitle', + { + defaultMessage: 'Install Elastic Agent on your Kubernetes cluster', + } + ), children: ( <> {status !== FETCH_STATUS.SUCCESS && ( @@ -60,7 +66,12 @@ export const KubernetesPanel: React.FC = () => { ), }, { - title: 'Monitor your Kubernetes cluster', + title: i18n.translate( + 'xpack.observability_onboarding.experimentalOnboardingFlow.kubernetes.monitorStepTitle', + { + defaultMessage: 'Monitor your Kubernetes cluster', + } + ), status: (isMonitoringStepActive ? 'current' : 'incomplete') as EuiStepStatus, children: isMonitoringStepActive && , }, From 73d85c38c4b26cec7469d7ff9067e37bce989c45 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 2 Aug 2024 11:27:53 -0500 Subject: [PATCH 08/13] [artifacts] Publish kibana-wolfi docker image (#189799) --- .buildkite/scripts/steps/artifacts/publish.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.buildkite/scripts/steps/artifacts/publish.sh b/.buildkite/scripts/steps/artifacts/publish.sh index a833f8c663eac..52e58ebb50136 100644 --- a/.buildkite/scripts/steps/artifacts/publish.sh +++ b/.buildkite/scripts/steps/artifacts/publish.sh @@ -23,6 +23,8 @@ download "kibana-$FULL_VERSION-docker-image-aarch64.tar.gz" download "kibana-cloud-$FULL_VERSION-docker-image.tar.gz" download "kibana-cloud-$FULL_VERSION-docker-image-aarch64.tar.gz" download "kibana-ubi-$FULL_VERSION-docker-image.tar.gz" +download "kibana-wolfi-$FULL_VERSION-docker-image.tar.gz" +download "kibana-wolfi-$FULL_VERSION-docker-image-aarch64.tar.gz" download "kibana-$FULL_VERSION-arm64.deb" download "kibana-$FULL_VERSION-amd64.deb" @@ -33,6 +35,7 @@ download "kibana-$FULL_VERSION-docker-build-context.tar.gz" download "kibana-cloud-$FULL_VERSION-docker-build-context.tar.gz" download "kibana-ironbank-$FULL_VERSION-docker-build-context.tar.gz" download "kibana-ubi-$FULL_VERSION-docker-build-context.tar.gz" +download "kibana-wolfi-$FULL_VERSION-docker-build-context.tar.gz" download "kibana-$FULL_VERSION-linux-aarch64.tar.gz" download "kibana-$FULL_VERSION-linux-x86_64.tar.gz" From e5cb696f4707635e6ee758dc2c2b5b3bca3460fb Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 2 Aug 2024 10:30:41 -0600 Subject: [PATCH 09/13] [Embeddable Rebuild] [Controls] Add `order` to control factory (#189670) Closes https://github.com/elastic/kibana/issues/189407 ## Summary This PR adds an `order` attribute to the control factory so that the ordering in the UI remains consistent - previously, the order was determined by the order the factories were registered in (which is no longer predictable now that the registration happens `async` - it's hard to repro, but there were times where something delayed the options list registration and it would appear at the end of my list). Adding and sorting the UI based on the `order` of the factory removes this uncertainty. ### Checklist - [x] [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 ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../data_control_editor.test.tsx | 58 ++++++++++++++++--- .../data_controls/data_control_editor.tsx | 33 +++++++++-- .../get_options_list_control_factory.tsx | 1 + .../public/react_controls/types.ts | 1 + 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/examples/controls_example/public/react_controls/data_controls/data_control_editor.test.tsx b/examples/controls_example/public/react_controls/data_controls/data_control_editor.test.tsx index 5cf4a86752240..d6c1ff9a1ae35 100644 --- a/examples/controls_example/public/react_controls/data_controls/data_control_editor.test.tsx +++ b/examples/controls_example/public/react_controls/data_controls/data_control_editor.test.tsx @@ -32,7 +32,7 @@ import { getMockedSearchControlFactory, } from './mocks/factory_mocks'; import { ControlFactory } from '../types'; -import { DataControlApi, DefaultDataControlState } from './types'; +import { DataControlApi, DataControlFactory, DefaultDataControlState } from './types'; const mockDataViews = dataViewPluginMocks.createStartContract(); const mockDataView = createStubDataView({ @@ -106,13 +106,13 @@ describe('Data control editor', () => { return controlEditor.getByTestId(testId).getAttribute('aria-pressed'); }; + const mockRegistry: { [key: string]: ControlFactory } = { + search: getMockedSearchControlFactory({ parentApi: controlGroupApi }), + optionsList: getMockedOptionsListControlFactory({ parentApi: controlGroupApi }), + rangeSlider: getMockedRangeSliderControlFactory({ parentApi: controlGroupApi }), + }; + beforeAll(() => { - const mockRegistry: { [key: string]: ControlFactory } = - { - search: getMockedSearchControlFactory({ parentApi: controlGroupApi }), - optionsList: getMockedOptionsListControlFactory({ parentApi: controlGroupApi }), - rangeSlider: getMockedRangeSliderControlFactory({ parentApi: controlGroupApi }), - }; (getAllControlTypes as jest.Mock).mockReturnValue(Object.keys(mockRegistry)); (getControlFactory as jest.Mock).mockImplementation((key) => mockRegistry[key]); }); @@ -133,6 +133,50 @@ describe('Data control editor', () => { expect(saveButton).toBeEnabled(); }); + test('CompatibleControlTypesComponent respects ordering', async () => { + const tempRegistry: { + [key: string]: ControlFactory; + } = { + ...mockRegistry, + alphabeticalFirstControl: { + type: 'alphabeticalFirst', + getIconType: () => 'lettering', + getDisplayName: () => 'Alphabetically first', + isFieldCompatible: () => true, + buildControl: jest.fn().mockReturnValue({ + api: controlGroupApi, + Component: <>Should be first alphabetically, + }), + } as DataControlFactory, + supremeControl: { + type: 'supremeControl', + order: 100, // force it first despite alphabetical ordering + getIconType: () => 'starFilled', + getDisplayName: () => 'Supreme leader', + isFieldCompatible: () => true, + buildControl: jest.fn().mockReturnValue({ + api: controlGroupApi, + Component: <>This control is forced first via the factory order, + }), + } as DataControlFactory, + }; + (getAllControlTypes as jest.Mock).mockReturnValue(Object.keys(tempRegistry)); + (getControlFactory as jest.Mock).mockImplementation((key) => tempRegistry[key]); + + const controlEditor = await mountComponent({}); + const menu = controlEditor.getByTestId('controlTypeMenu'); + expect(menu.children.length).toEqual(5); + expect(menu.children[0].textContent).toEqual('Supreme leader'); // forced first - ignore alphabetical sorting + // the rest should be alphabetically sorted + expect(menu.children[1].textContent).toEqual('Alphabetically first'); + expect(menu.children[2].textContent).toEqual('Options list'); + expect(menu.children[3].textContent).toEqual('Range slider'); + expect(menu.children[4].textContent).toEqual('Search'); + + (getAllControlTypes as jest.Mock).mockReturnValue(Object.keys(mockRegistry)); + (getControlFactory as jest.Mock).mockImplementation((key) => mockRegistry[key]); + }); + test('selecting a keyword field - can only create an options list control', async () => { const controlEditor = await mountComponent({}); await selectField(controlEditor, 'machine.os.raw'); diff --git a/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx b/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx index 53a25073375bf..bccf64bf0745a 100644 --- a/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx +++ b/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx @@ -80,9 +80,18 @@ const CompatibleControlTypesComponent = ({ const dataControlFactories = useMemo(() => { return getAllControlTypes() .map((type) => getControlFactory(type)) - .filter((factory) => { - return isDataControlFactory(factory); - }); + .filter((factory) => isDataControlFactory(factory)) + .sort( + ( + { order: orderA = 0, getDisplayName: getDisplayNameA }, + { order: orderB = 0, getDisplayName: getDisplayNameB } + ) => { + const orderComparison = orderB - orderA; // sort descending by order + return orderComparison === 0 + ? getDisplayNameA().localeCompare(getDisplayNameB()) // if equal order, compare display names + : orderComparison; + } + ); }, []); return ( @@ -283,8 +292,23 @@ export const DataControlEditor = { setEditorState({ ...editorState, fieldName: field.name }); - setSelectedControlType(fieldRegistry?.[field.name]?.compatibleControlTypes[0]); + /** + * make sure that the new field is compatible with the selected control type and, if it's not, + * reset the selected control type to the **first** compatible control type + */ + const newCompatibleControlTypes = + fieldRegistry?.[field.name]?.compatibleControlTypes ?? []; + if ( + !selectedControlType || + !newCompatibleControlTypes.includes(selectedControlType!) + ) { + setSelectedControlType(newCompatibleControlTypes[0]); + } + + /** + * set the control title (i.e. the one set by the user) + default title (i.e. the field display name) + */ const newDefaultTitle = field.displayName ?? field.name; setDefaultPanelTitle(newDefaultTitle); const currentTitle = editorState.title; @@ -365,7 +389,6 @@ export const DataControlEditor = {CustomSettingsComponent} - {/* {!editorConfig?.hideAdditionalSettings ? CustomSettingsComponent : null} */} {initialState.controlId && ( <> diff --git a/examples/controls_example/public/react_controls/data_controls/options_list_control/get_options_list_control_factory.tsx b/examples/controls_example/public/react_controls/data_controls/options_list_control/get_options_list_control_factory.tsx index 22927cadf3cb1..0e0d28cd96a33 100644 --- a/examples/controls_example/public/react_controls/data_controls/options_list_control/get_options_list_control_factory.tsx +++ b/examples/controls_example/public/react_controls/data_controls/options_list_control/get_options_list_control_factory.tsx @@ -44,6 +44,7 @@ export const getOptionsListControlFactory = ( ): DataControlFactory => { return { type: OPTIONS_LIST_CONTROL_TYPE, + order: 3, // should always be first, since this is the most popular control getIconType: () => 'editorChecklist', getDisplayName: OptionsListStrings.control.getDisplayName, isFieldCompatible: (field) => { diff --git a/examples/controls_example/public/react_controls/types.ts b/examples/controls_example/public/react_controls/types.ts index a9a7bd2e2c1a7..c16333268441e 100644 --- a/examples/controls_example/public/react_controls/types.ts +++ b/examples/controls_example/public/react_controls/types.ts @@ -75,6 +75,7 @@ export interface ControlFactory< ControlApi extends DefaultControlApi = DefaultControlApi > { type: string; + order?: number; getIconType: () => string; getDisplayName: () => string; buildControl: ( From f941ba4d6b60cee41c418140e21b677ca0972c71 Mon Sep 17 00:00:00 2001 From: Brad White Date: Fri, 2 Aug 2024 10:35:56 -0600 Subject: [PATCH 10/13] [CI] Add suggestion from #189316 (#189765) ## Summary This was just a suggested change from #189316 which didn't get addressed because of auto merge. --- .buildkite/scripts/common/util.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/common/util.sh b/.buildkite/scripts/common/util.sh index 445871e6420bd..d50fafad6967b 100755 --- a/.buildkite/scripts/common/util.sh +++ b/.buildkite/scripts/common/util.sh @@ -36,7 +36,7 @@ check_for_changed_files() { GIT_CHANGES="$(git status --porcelain -- . ':!:.bazelrc' ':!:config/node.options' ':!config/kibana.yml')" if [ "$GIT_CHANGES" ]; then - if ! is_auto_commit_disabled && [[ "$SHOULD_AUTO_COMMIT_CHANGES" == "true" && "${BUILDKITE_PULL_REQUEST:-}" != "" && "${BUILDKITE_PULL_REQUEST}" != "false" ]]; then + if ! is_auto_commit_disabled && [[ "$SHOULD_AUTO_COMMIT_CHANGES" == "true" && "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then NEW_COMMIT_MESSAGE="[CI] Auto-commit changed files from '$1'" PREVIOUS_COMMIT_MESSAGE="$(git log -1 --pretty=%B)" From 964fb66a2590a531d7f064f8de82e41d232cd3bb Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:05:59 +0100 Subject: [PATCH 11/13] [Security Solution] Hide settings button when no privileges (#189075) ## Summary https://github.com/elastic/kibana/issues/189016 Step to verify: 1. Create a role with A user with security elastic `AI assistant` set to `none` and `AI assistant management` set to `ALL`. 2. Login with the role and visit stack management > ai assistant 3. The setting buttons should be hidden. Screenshot 2024-07-30 at 10 45 18 Before: before After: The buttons will be hidden when a user has no right to change the settings: Screenshot 2024-07-31 at 19 01 50 4 If landing directly on `/app/management/kibana/securityAiAssistantManagement`, it should be redirected to the home page. https://github.com/user-attachments/assets/2ca12f8e-d778-4732-be85-bb4779c9e7a7 ### Checklist Delete any items that are not applicable to this PR. - [x] 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/packages/kbn-i18n/README.md) - [x] [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Pedro Jaramillo --- packages/kbn-doc-links/src/get_doc_links.ts | 2 + packages/kbn-doc-links/src/types.ts | 2 + .../selection/public/app_context.tsx | 3 + .../selection/public/index.ts | 5 +- .../management_section/mount_section.tsx | 12 +- .../selection/public/plugin.ts | 13 +- .../ai_assistant_selection_page.test.tsx | 150 ++++++++++++++++++ .../ai_assistant_selection_page.tsx | 80 ++++++---- .../selection/tsconfig.json | 4 +- .../management_settings.test.tsx | 103 ++++++++++++ .../stack_management/management_settings.tsx | 14 ++ 11 files changed, 348 insertions(+), 40 deletions(-) create mode 100644 src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.test.tsx create mode 100644 x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 25c91238a7b6d..dec15cf568e98 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -496,6 +496,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D assetCriticality: `${SECURITY_SOLUTION_DOCS}asset-criticality.html`, }, detectionEngineOverview: `${SECURITY_SOLUTION_DOCS}detection-engine-overview.html`, + aiAssistant: `${SECURITY_SOLUTION_DOCS}security-assistant.html`, }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, @@ -618,6 +619,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D sloBurnRateRule: isServerless ? `${SERVERLESS_OBSERVABILITY_DOCS}create-slo-burn-rate-alert-rule` : `${OBSERVABILITY_DOCS}slo-burn-rate-alert.html`, + aiAssistant: `${OBSERVABILITY_DOCS}obs-ai-assistant.html`, }, alerting: { guide: isServerless diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index d271ed918327a..b92c59a624880 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -336,6 +336,7 @@ export interface DocLinks { readonly configureAlertSuppression: string; }; readonly securitySolution: { + readonly aiAssistant: string; readonly artifactControl: string; readonly avcResults: string; readonly trustedApps: string; @@ -441,6 +442,7 @@ export interface DocLinks { syntheticsProjectMonitors: string; syntheticsMigrateFromIntegration: string; sloBurnRateRule: string; + aiAssistant: string; }>; readonly alerting: Readonly<{ guide: string; diff --git a/src/plugins/ai_assistant_management/selection/public/app_context.tsx b/src/plugins/ai_assistant_management/selection/public/app_context.tsx index 9f7998b36800d..b8e27c9bc34bf 100644 --- a/src/plugins/ai_assistant_management/selection/public/app_context.tsx +++ b/src/plugins/ai_assistant_management/selection/public/app_context.tsx @@ -9,6 +9,7 @@ import React, { createContext, useContext } from 'react'; import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; import type { CoreStart } from '@kbn/core/public'; +import type { BuildFlavor } from '@kbn/config'; import type { StartDependencies } from './plugin'; interface ContextValue extends StartDependencies { @@ -16,6 +17,8 @@ interface ContextValue extends StartDependencies { capabilities: CoreStart['application']['capabilities']; navigateToApp: CoreStart['application']['navigateToApp']; + kibanaBranch: string; + buildFlavor: BuildFlavor; } const AppContext = createContext(null as any); diff --git a/src/plugins/ai_assistant_management/selection/public/index.ts b/src/plugins/ai_assistant_management/selection/public/index.ts index 46a20ceb7ffc8..a68138b5eb139 100644 --- a/src/plugins/ai_assistant_management/selection/public/index.ts +++ b/src/plugins/ai_assistant_management/selection/public/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { PluginInitializer } from '@kbn/core/public'; +import type { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; import { AIAssistantManagementPlugin } from './plugin'; import type { @@ -24,4 +24,5 @@ export type { export const plugin: PluginInitializer< AIAssistantManagementSelectionPluginPublicSetup, AIAssistantManagementSelectionPluginPublicStart -> = () => new AIAssistantManagementPlugin(); +> = (initializerContext: PluginInitializerContext) => + new AIAssistantManagementPlugin(initializerContext); diff --git a/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx b/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx index a1bb4e19f2265..5dc59b979cca0 100644 --- a/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx +++ b/src/plugins/ai_assistant_management/selection/public/management_section/mount_section.tsx @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import type { CoreSetup } from '@kbn/core/public'; import { wrapWithTheme } from '@kbn/kibana-react-plugin/public'; import type { ManagementAppMountParams } from '@kbn/management-plugin/public'; +import type { BuildFlavor } from '@kbn/config'; import type { StartDependencies, AIAssistantManagementSelectionPluginPublicStart } from '../plugin'; import { aIAssistantManagementSelectionRouter } from '../routes/config'; import { RedirectToHomeIfUnauthorized } from '../routes/components/redirect_to_home_if_unauthorized'; @@ -22,9 +23,16 @@ import { AppContextProvider } from '../app_context'; interface MountParams { core: CoreSetup; mountParams: ManagementAppMountParams; + kibanaBranch: string; + buildFlavor: BuildFlavor; } -export const mountManagementSection = async ({ core, mountParams }: MountParams) => { +export const mountManagementSection = async ({ + core, + mountParams, + kibanaBranch, + buildFlavor, +}: MountParams) => { const [coreStart, startDeps] = await core.getStartServices(); const { element, history, setBreadcrumbs } = mountParams; const { theme$ } = core.theme; @@ -45,6 +53,8 @@ export const mountManagementSection = async ({ core, mountParams }: MountParams) capabilities: coreStart.application.capabilities, navigateToApp: coreStart.application.navigateToApp, setBreadcrumbs, + kibanaBranch, + buildFlavor, }} > diff --git a/src/plugins/ai_assistant_management/selection/public/plugin.ts b/src/plugins/ai_assistant_management/selection/public/plugin.ts index e24a82dbddf20..99b96b90a5b53 100644 --- a/src/plugins/ai_assistant_management/selection/public/plugin.ts +++ b/src/plugins/ai_assistant_management/selection/public/plugin.ts @@ -7,11 +7,12 @@ */ import { i18n } from '@kbn/i18n'; -import { type CoreSetup, Plugin, type CoreStart } from '@kbn/core/public'; +import { type CoreSetup, Plugin, type CoreStart, PluginInitializerContext } from '@kbn/core/public'; import type { ManagementSetup } from '@kbn/management-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import type { ServerlessPluginSetup } from '@kbn/serverless/public'; import { BehaviorSubject, Observable } from 'rxjs'; +import type { BuildFlavor } from '@kbn/config'; import { AIAssistantType } from '../common/ai_assistant_type'; import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; @@ -40,7 +41,13 @@ export class AIAssistantManagementPlugin StartDependencies > { - constructor() {} + private readonly kibanaBranch: string; + private readonly buildFlavor: BuildFlavor; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.kibanaBranch = this.initializerContext.env.packageInfo.branch; + this.buildFlavor = this.initializerContext.env.packageInfo.buildFlavor; + } public setup( core: CoreSetup, @@ -78,6 +85,8 @@ export class AIAssistantManagementPlugin return mountManagementSection({ core, mountParams, + kibanaBranch: this.kibanaBranch, + buildFlavor: this.buildFlavor, }); }, }); diff --git a/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.test.tsx b/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.test.tsx new file mode 100644 index 0000000000000..c926b83716302 --- /dev/null +++ b/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import type { CoreStart } from '@kbn/core/public'; +import { AiAssistantSelectionPage } from './ai_assistant_selection_page'; +import { useAppContext } from '../../app_context'; +import { I18nProvider } from '@kbn/i18n-react'; + +jest.mock('../../app_context'); + +describe('AiAssistantSelectionPage', () => { + const setBreadcrumbs = jest.fn(); + const navigateToApp = jest.fn(); + + const generateMockCapabilities = (hasPermission: boolean) => + ({ + observabilityAIAssistant: { show: hasPermission }, + securitySolutionAssistant: { 'ai-assistant': hasPermission }, + } as unknown as CoreStart['application']['capabilities']); + + const testCapabilities = generateMockCapabilities(true); + + const renderComponent = (capabilities: CoreStart['application']['capabilities']) => { + (useAppContext as jest.Mock).mockReturnValue({ + capabilities, + setBreadcrumbs, + navigateToApp, + kibanaBranch: 'main', + buildFlavor: 'ess', + }); + render(, { + wrapper: I18nProvider, + }); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('sets the breadcrumbs on mount', () => { + renderComponent(testCapabilities); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { + text: 'AI Assistant', + }, + ]); + }); + + it('renders the title and description', () => { + renderComponent(generateMockCapabilities(true)); + expect(screen.getByTestId('pluginsAiAssistantSelectionPageTitle')).toBeInTheDocument(); + expect(screen.getByTestId('pluginsAiAssistantSelectionPageDescription')).toBeInTheDocument(); + }); + + describe('Observability AI Assistant Card', () => { + describe('when the feature is disabled', () => { + it('displays the disabled callout', () => { + renderComponent(generateMockCapabilities(false)); + expect( + screen.getByTestId('pluginsAiAssistantSelectionPageObservabilityDocumentationCallout') + ).toBeInTheDocument(); + }); + }); + + describe('when the feature is enabled', () => { + it('does not display the disabled callout', () => { + renderComponent(testCapabilities); + expect( + screen.queryByTestId('pluginsAiAssistantSelectionPageObservabilityDocumentationCallout') + ).not.toBeInTheDocument(); + }); + + it('renders the manage settings button', () => { + renderComponent(testCapabilities); + expect(screen.getByTestId('pluginsAiAssistantSelectionPageButton')).toBeInTheDocument(); + }); + + it('navigates to the observability AI Assistant settings on button click', () => { + renderComponent(testCapabilities); + fireEvent.click(screen.getByTestId('pluginsAiAssistantSelectionPageButton')); + expect(navigateToApp).toHaveBeenCalledWith('management', { + path: 'kibana/observabilityAiAssistantManagement', + }); + }); + + it('renders the documentation links correctly', () => { + renderComponent(testCapabilities); + + expect( + screen.getByTestId('pluginsAiAssistantSelectionPageDocumentationLink') + ).toHaveAttribute( + 'href', + 'https://www.elastic.co/guide/en/observability/master/obs-ai-assistant.html' + ); + }); + }); + }); + + describe('Security AI Assistant Card', () => { + describe('when the feature is disabled', () => { + it('displays the disabled callout', () => { + renderComponent(generateMockCapabilities(false)); + expect( + screen.getByTestId('pluginsAiAssistantSelectionPageSecurityDocumentationCallout') + ).toBeInTheDocument(); + }); + }); + + describe('when the feature is enabled', () => { + it('does not display the disabled callout', () => { + renderComponent(testCapabilities); + expect( + screen.queryByTestId('pluginsAiAssistantSelectionPageSecurityDocumentationCallout') + ).not.toBeInTheDocument(); + }); + + it('renders the manage settings button', () => { + renderComponent(testCapabilities); + expect( + screen.getByTestId('pluginsAiAssistantSelectionSecurityPageButton') + ).toBeInTheDocument(); + }); + + it('navigates to the security AI Assistant settings on button click', () => { + renderComponent(testCapabilities); + fireEvent.click(screen.getByTestId('pluginsAiAssistantSelectionSecurityPageButton')); + expect(navigateToApp).toHaveBeenCalledWith('management', { + path: 'kibana/securityAiAssistantManagement', + }); + }); + + it('renders the documentation links correctly', () => { + renderComponent(testCapabilities); + + expect( + screen.getByTestId('securityAiAssistantSelectionPageDocumentationLink') + ).toHaveAttribute( + 'href', + 'https://www.elastic.co/guide/en/security/master/security-assistant.html' + ); + }); + }); + }); +}); diff --git a/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx b/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx index 3b50e26d4de08..feaecf237e587 100644 --- a/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx +++ b/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx @@ -21,12 +21,16 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { getDocLinks } from '@kbn/doc-links'; import { useAppContext } from '../../app_context'; export function AiAssistantSelectionPage() { - const { capabilities, setBreadcrumbs, navigateToApp } = useAppContext(); + const { capabilities, setBreadcrumbs, navigateToApp, buildFlavor, kibanaBranch } = + useAppContext(); const observabilityAIAssistantEnabled = capabilities.observabilityAIAssistant?.show; const securityAIAssistantEnabled = capabilities.securitySolutionAssistant?.['ai-assistant']; + const observabilityDoc = getDocLinks({ buildFlavor, kibanaBranch }).observability.aiAssistant; + const securityDoc = getDocLinks({ buildFlavor, kibanaBranch }).securitySolution.aiAssistant; useEffect(() => { setBreadcrumbs([ @@ -40,7 +44,7 @@ export function AiAssistantSelectionPage() { return ( <> - +

{i18n.translate( 'aiAssistantManagementSelection.aiAssistantSettingsPage.h2.aIAssistantLabel', @@ -53,7 +57,7 @@ export function AiAssistantSelectionPage() { - + {i18n.translate( 'aiAssistantManagementSelection.aiAssistantSettingsPage.descriptionTextLabel', { @@ -75,6 +79,7 @@ export function AiAssistantSelectionPage() { @@ -89,14 +95,14 @@ export function AiAssistantSelectionPage() {

{i18n.translate( 'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkLabel', @@ -107,20 +113,22 @@ export function AiAssistantSelectionPage() { }} />

- - navigateToApp('management', { - path: 'kibana/observabilityAiAssistantManagement', - }) - } - > - {i18n.translate( - 'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.manageSettingsButtonLabel', - { defaultMessage: 'Manage Settings' } - )} - + {observabilityAIAssistantEnabled && ( + + navigateToApp('management', { + path: 'kibana/observabilityAiAssistantManagement', + }) + } + > + {i18n.translate( + 'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.manageSettingsButtonLabel', + { defaultMessage: 'Manage Settings' } + )} + + )} } display="plain" @@ -143,14 +151,16 @@ export function AiAssistantSelectionPage() { Features.', + 'This feature is disabled. You can enable it from from Spaces > Features.', } )} size="s" + className="eui-displayInlineBlock" /> @@ -158,14 +168,14 @@ export function AiAssistantSelectionPage() {

{i18n.translate( 'aiAssistantManagementSelection.aiAssistantSettingsPage.securityAssistant.documentationLinkLabel', @@ -176,18 +186,20 @@ export function AiAssistantSelectionPage() { }} />

- - navigateToApp('management', { path: 'kibana/securityAiAssistantManagement' }) - } - > - {i18n.translate( - 'aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.manageSettingsButtonLabel', - { defaultMessage: 'Manage Settings' } - )} - + {securityAIAssistantEnabled && ( + + navigateToApp('management', { path: 'kibana/securityAiAssistantManagement' }) + } + > + {i18n.translate( + 'aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.manageSettingsButtonLabel', + { defaultMessage: 'Manage Settings' } + )} + + )} } display="plain" diff --git a/src/plugins/ai_assistant_management/selection/tsconfig.json b/src/plugins/ai_assistant_management/selection/tsconfig.json index a57f7dbc6c12d..6bd8efe0e80b8 100644 --- a/src/plugins/ai_assistant_management/selection/tsconfig.json +++ b/src/plugins/ai_assistant_management/selection/tsconfig.json @@ -16,7 +16,9 @@ "@kbn/serverless", "@kbn/config-schema", "@kbn/core-plugins-server", - "@kbn/features-plugin" + "@kbn/features-plugin", + "@kbn/config", + "@kbn/doc-links" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx new file mode 100644 index 0000000000000..0662ca042a522 --- /dev/null +++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { ManagementSettings } from './management_settings'; +import type { Conversation } from '@kbn/elastic-assistant'; +import { + useAssistantContext, + useFetchCurrentUserConversations, + WELCOME_CONVERSATION_TITLE, +} from '@kbn/elastic-assistant'; +import { useKibana } from '../../common/lib/kibana'; +import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation'; + +// Mock the necessary hooks and components +jest.mock('@kbn/elastic-assistant', () => ({ + useAssistantContext: jest.fn(), + useFetchCurrentUserConversations: jest.fn(), + mergeBaseWithPersistedConversations: jest.fn(), + WELCOME_CONVERSATION_TITLE: 'Welcome Conversation', +})); +jest.mock('@kbn/elastic-assistant/impl/assistant/settings/assistant_settings_management', () => ({ + AssistantSettingsManagement: jest.fn(() =>
), +})); +jest.mock('@kbn/elastic-assistant/impl/assistant/use_conversation', () => ({ + useConversation: jest.fn(), +})); +jest.mock('../../common/lib/kibana', () => ({ + useKibana: jest.fn(), +})); + +const useAssistantContextMock = useAssistantContext as jest.Mock; +const useFetchCurrentUserConversationsMock = useFetchCurrentUserConversations as jest.Mock; +const useKibanaMock = useKibana as jest.Mock; +const useConversationMock = useConversation as jest.Mock; + +describe('ManagementSettings', () => { + const baseConversations = { base: 'conversation' }; + const http = {}; + const getDefaultConversation = jest.fn(); + const navigateToApp = jest.fn(); + const mockConversations = { + [WELCOME_CONVERSATION_TITLE]: { title: WELCOME_CONVERSATION_TITLE }, + } as Record; + + const renderComponent = ({ + isAssistantEnabled = true, + conversations, + }: { + isAssistantEnabled?: boolean; + conversations: Record; + }) => { + useAssistantContextMock.mockReturnValue({ + baseConversations, + http, + assistantAvailability: { isAssistantEnabled }, + }); + + useFetchCurrentUserConversationsMock.mockReturnValue({ + data: conversations, + }); + + useKibanaMock.mockReturnValue({ + services: { + application: { + navigateToApp, + capabilities: { + securitySolutionAssistant: { 'ai-assistant': false }, + }, + }, + }, + }); + + useConversationMock.mockReturnValue({ + getDefaultConversation, + }); + + return render(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('navigates to home if securityAIAssistant is disabled', () => { + renderComponent({ + conversations: mockConversations, + }); + expect(navigateToApp).toHaveBeenCalledWith('home'); + }); + + it('renders AssistantSettingsManagement when conversations are available and securityAIAssistant is enabled', () => { + renderComponent({ + conversations: mockConversations, + }); + expect(screen.getByTestId('AssistantSettingsManagement')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx index 2855c6b6115d3..6a7478eca0df0 100644 --- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx +++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx @@ -16,6 +16,7 @@ import { } from '@kbn/elastic-assistant'; import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation'; import type { FetchConversationsResponse } from '@kbn/elastic-assistant/impl/assistant/api'; +import { useKibana } from '../../common/lib/kibana'; const defaultSelectedConversationId = WELCOME_CONVERSATION_TITLE; @@ -26,6 +27,15 @@ export const ManagementSettings = React.memo(() => { assistantAvailability: { isAssistantEnabled }, } = useAssistantContext(); + const { + application: { + navigateToApp, + capabilities: { + securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled }, + }, + }, + } = useKibana().services; + const onFetchedConversations = useCallback( (conversationsData: FetchConversationsResponse): Record => mergeBaseWithPersistedConversations(baseConversations, conversationsData), @@ -46,6 +56,10 @@ export const ManagementSettings = React.memo(() => { [conversations, getDefaultConversation] ); + if (!securityAIAssistantEnabled) { + navigateToApp('home'); + } + if (conversations) { return ; } From d4dc118ffac98ce132d9b4569fdf63ed0884383e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Fri, 2 Aug 2024 10:42:13 -0700 Subject: [PATCH 12/13] [Cloud Security] Fix Detection Rule flaky FTR (#189768) ## Summary It closes https://github.com/elastic/kibana/issues/172312 This PR fixed the flaky FTR test by adding a retry method to the title checking. --- .../pages/findings_alerts.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts index 1c345b68caf7b..51df1527493d6 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_alerts.ts @@ -195,7 +195,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.click('csp:toast-success-link'); await pageObjects.header.waitUntilLoadingHasFinished(); const rulePageTitle = await testSubjects.find('header-page-title'); - expect(await rulePageTitle.getVisibleText()).to.be(ruleName1); + // Rule page title is not immediately available, so we need to retry until it is + await retry.try(async () => { + expect(await rulePageTitle.getVisibleText()).to.be(ruleName1); + }); }); }); describe('Rule details', () => { From 7dfe90eeca4a267a79c234ac515986c3b105845d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 2 Aug 2024 20:04:23 +0200 Subject: [PATCH 13/13] [Move `@kbn/config-schema` to server] `@kbn/optimizer` (#189769) --- packages/kbn-optimizer/kibana.jsonc | 2 +- packages/kbn-plugin-helpers/kibana.jsonc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-optimizer/kibana.jsonc b/packages/kbn-optimizer/kibana.jsonc index 1e912e055844e..0e8097384c533 100644 --- a/packages/kbn-optimizer/kibana.jsonc +++ b/packages/kbn-optimizer/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-common", + "type": "shared-server", "id": "@kbn/optimizer", "devOnly": true, "owner": "@elastic/kibana-operations" diff --git a/packages/kbn-plugin-helpers/kibana.jsonc b/packages/kbn-plugin-helpers/kibana.jsonc index bee9b9486a644..bf41df21ee751 100644 --- a/packages/kbn-plugin-helpers/kibana.jsonc +++ b/packages/kbn-plugin-helpers/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-common", + "type": "shared-server", "id": "@kbn/plugin-helpers", "devOnly": true, "owner": "@elastic/kibana-operations"