diff --git a/.buildkite/scripts/steps/serverless/deploy.sh b/.buildkite/scripts/steps/serverless/deploy.sh index 325aadf187b5..d30723393dac 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" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d7055dbca4f4..f60424da1c1d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -392,8 +392,8 @@ src/plugins/embeddable @elastic/kibana-presentation x-pack/examples/embedded_lens_example @elastic/kibana-visualizations x-pack/plugins/encrypted_saved_objects @elastic/kibana-security x-pack/plugins/enterprise_search @elastic/search-kibana -x-pack/packages/kbn-entities-schema @elastic/obs-knowledge-team -x-pack/plugins/observability_solution/entity_manager @elastic/obs-knowledge-team +x-pack/packages/kbn-entities-schema @elastic/obs-entities +x-pack/plugins/observability_solution/entity_manager @elastic/obs-entities examples/error_boundary @elastic/appex-sharedux packages/kbn-es @elastic/kibana-operations packages/kbn-es-archiver @elastic/kibana-operations @elastic/appex-qa diff --git a/examples/controls_example/public/app/app.tsx b/examples/controls_example/public/app/app.tsx index 59d8271163b0..e940daec0dab 100644 --- a/examples/controls_example/public/app/app.tsx +++ b/examples/controls_example/public/app/app.tsx @@ -19,11 +19,10 @@ import { import React, { useState } from 'react'; import ReactDOM from 'react-dom'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; - import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { ControlsExampleStartDeps } from '../plugin'; import { ControlGroupRendererExamples } from './control_group_renderer_examples'; -import { ReactControlExample } from './react_control_example'; +import { ReactControlExample } from './react_control_example/react_control_example'; const CONTROLS_AS_A_BUILDING_BLOCK = 'controls_as_a_building_block'; const CONTROLS_REFACTOR_TEST = 'controls_refactor_test'; diff --git a/examples/controls_example/public/app/react_control_example.tsx b/examples/controls_example/public/app/react_control_example/react_control_example.tsx similarity index 73% rename from examples/controls_example/public/app/react_control_example.tsx rename to examples/controls_example/public/app/react_control_example/react_control_example.tsx index 8e24eb10cbab..c3420cf22b60 100644 --- a/examples/controls_example/public/app/react_control_example.tsx +++ b/examples/controls_example/public/app/react_control_example/react_control_example.tsx @@ -7,10 +7,12 @@ */ import React, { useEffect, useMemo, useState } from 'react'; -import { BehaviorSubject, combineLatest } from 'rxjs'; +import { BehaviorSubject, combineLatest, Subject } from 'rxjs'; import { + EuiBadge, EuiButton, + EuiButtonEmpty, EuiButtonGroup, EuiCallOut, EuiCodeBlock, @@ -18,6 +20,7 @@ import { EuiFlexItem, EuiSpacer, EuiSuperDatePicker, + EuiToolTip, OnTimeChangeProps, } from '@elastic/eui'; import { @@ -39,12 +42,19 @@ import { } from '@kbn/presentation-publishing'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import { ControlGroupApi } from '../react_controls/control_group/types'; -import { OPTIONS_LIST_CONTROL_TYPE } from '../react_controls/data_controls/options_list_control/constants'; -import { RANGE_SLIDER_CONTROL_TYPE } from '../react_controls/data_controls/range_slider/types'; -import { SEARCH_CONTROL_TYPE } from '../react_controls/data_controls/search_control/types'; -import { TIMESLIDER_CONTROL_TYPE } from '../react_controls/timeslider_control/types'; -import { openDataControlEditor } from '../react_controls/data_controls/open_data_control_editor'; +import { + clearControlGroupSerializedState, + getControlGroupSerializedState, + setControlGroupSerializedState, + WEB_LOGS_DATA_VIEW_ID, +} from './serialized_control_group_state'; +import { + clearControlGroupRuntimeState, + getControlGroupRuntimeState, + setControlGroupRuntimeState, +} from './runtime_control_group_state'; +import { ControlGroupApi } from '../../react_controls/control_group/types'; +import { openDataControlEditor } from '../../react_controls/data_controls/open_data_control_editor'; const toggleViewButtons = [ { @@ -59,67 +69,6 @@ const toggleViewButtons = [ }, ]; -const optionsListId = 'optionsList1'; -const searchControlId = 'searchControl1'; -const rangeSliderControlId = 'rangeSliderControl1'; -const timesliderControlId = 'timesliderControl1'; -const controlGroupPanels = { - [searchControlId]: { - type: SEARCH_CONTROL_TYPE, - order: 3, - grow: true, - width: 'medium', - explicitInput: { - id: searchControlId, - fieldName: 'message', - title: 'Message', - grow: true, - width: 'medium', - enhancements: {}, - }, - }, - [rangeSliderControlId]: { - type: RANGE_SLIDER_CONTROL_TYPE, - order: 1, - grow: true, - width: 'medium', - explicitInput: { - id: rangeSliderControlId, - fieldName: 'bytes', - title: 'Bytes', - grow: true, - width: 'medium', - enhancements: {}, - }, - }, - [timesliderControlId]: { - type: TIMESLIDER_CONTROL_TYPE, - order: 4, - grow: true, - width: 'medium', - explicitInput: { - id: timesliderControlId, - enhancements: {}, - }, - }, - [optionsListId]: { - type: OPTIONS_LIST_CONTROL_TYPE, - order: 2, - grow: true, - width: 'medium', - explicitInput: { - id: searchControlId, - fieldName: 'agent.keyword', - title: 'Agent', - grow: true, - width: 'medium', - enhancements: {}, - }, - }, -}; - -const WEB_LOGS_DATA_VIEW_ID = '90943e30-9a47-11e8-b64d-95841ca0b247'; - export const ReactControlExample = ({ core, dataViews: dataViewsService, @@ -151,6 +100,9 @@ export const ReactControlExample = ({ const viewMode$ = useMemo(() => { return new BehaviorSubject(ViewMode.EDIT as ViewModeType); }, []); + const saveNotification$ = useMemo(() => { + return new Subject(); + }, []); const [dataLoading, timeRange, viewMode] = useBatchedPublishingSubjects( dataLoading$, timeRange$, @@ -188,6 +140,7 @@ export const ReactControlExample = ({ return Promise.resolve(undefined); }, lastUsedDataViewId: new BehaviorSubject(WEB_LOGS_DATA_VIEW_ID), + saveNotification$, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -277,16 +230,57 @@ export const ReactControlExample = ({ }; }, [controlGroupFilters$, filters$, unifiedSearchFilters$]); + const [unsavedChanges, setUnsavedChanges] = useState(undefined); + useEffect(() => { + if (!controlGroupApi) { + return; + } + const subscription = controlGroupApi.unsavedChanges.subscribe((nextUnsavedChanges) => { + if (!nextUnsavedChanges) { + clearControlGroupRuntimeState(); + setUnsavedChanges(undefined); + return; + } + + setControlGroupRuntimeState(nextUnsavedChanges); + + // JSON.stringify removes keys where value is `undefined` + // switch `undefined` to `null` to see when value has been cleared + const replacer = (key: unknown, value: unknown) => + typeof value === 'undefined' ? null : value; + setUnsavedChanges(JSON.stringify(nextUnsavedChanges, replacer, ' ')); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [controlGroupApi]); + return ( <> {dataViewNotFound && ( - <> - -

{`Install "Sample web logs" to run example`}

-
- - + +

{`Install "Sample web logs" to run example`}

+
+ )} + {!dataViewNotFound && ( + + { + clearControlGroupSerializedState(); + clearControlGroupRuntimeState(); + window.location.reload(); + }} + > + Reset example + + )} + + + + {unsavedChanges !== undefined && viewMode === 'edit' && ( + <> + + {unsavedChanges}}> + Unsaved changes + + + + { + controlGroupApi?.resetUnsavedChanges(); + }} + > + Reset + + + + { + if (controlGroupApi) { + saveNotification$.next(); + setControlGroupSerializedState(await controlGroupApi.serializeState()); + } + }} + > + Save + + + + )} ({ ...dashboardApi, - getSerializedStateForChild: () => ({ - rawState: { - controlStyle: 'oneLine', - chainingSystem: 'HIERARCHICAL', - showApplySelections: false, - panelsJSON: JSON.stringify(controlGroupPanels), - ignoreParentSettingsJSON: - '{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}', - } as object, - references: [ - { - name: `controlGroup_${searchControlId}:${SEARCH_CONTROL_TYPE}DataView`, - type: 'index-pattern', - id: WEB_LOGS_DATA_VIEW_ID, - }, - { - name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL_TYPE}DataView`, - type: 'index-pattern', - id: WEB_LOGS_DATA_VIEW_ID, - }, - { - name: `controlGroup_${optionsListId}:optionsListControlDataView`, - type: 'index-pattern', - id: WEB_LOGS_DATA_VIEW_ID, - }, - ], - }), + getSerializedStateForChild: getControlGroupSerializedState, + getRuntimeStateForChild: getControlGroupRuntimeState, })} key={`control_group`} /> diff --git a/examples/controls_example/public/app/react_control_example/runtime_control_group_state.ts b/examples/controls_example/public/app/react_control_example/runtime_control_group_state.ts new file mode 100644 index 000000000000..ce4bf2e1c931 --- /dev/null +++ b/examples/controls_example/public/app/react_control_example/runtime_control_group_state.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 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 { ControlGroupRuntimeState } from '../../react_controls/control_group/types'; + +const RUNTIME_STATE_SESSION_STORAGE_KEY = + 'kibana.examples.controls.reactControlExample.controlGroupRuntimeState'; + +export function clearControlGroupRuntimeState() { + sessionStorage.removeItem(RUNTIME_STATE_SESSION_STORAGE_KEY); +} + +export function getControlGroupRuntimeState(): Partial { + const runtimeStateJSON = sessionStorage.getItem(RUNTIME_STATE_SESSION_STORAGE_KEY); + return runtimeStateJSON ? JSON.parse(runtimeStateJSON) : {}; +} + +export function setControlGroupRuntimeState(runtimeState: Partial) { + sessionStorage.setItem(RUNTIME_STATE_SESSION_STORAGE_KEY, JSON.stringify(runtimeState)); +} diff --git a/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts b/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts new file mode 100644 index 000000000000..23071623b7ac --- /dev/null +++ b/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { SerializedPanelState } from '@kbn/presentation-containers'; +import { ControlGroupSerializedState } from '../../react_controls/control_group/types'; +import { OPTIONS_LIST_CONTROL_TYPE } from '../../react_controls/data_controls/options_list_control/constants'; +import { RANGE_SLIDER_CONTROL_TYPE } from '../../react_controls/data_controls/range_slider/types'; +import { SEARCH_CONTROL_TYPE } from '../../react_controls/data_controls/search_control/types'; +import { TIMESLIDER_CONTROL_TYPE } from '../../react_controls/timeslider_control/types'; + +const SERIALIZED_STATE_SESSION_STORAGE_KEY = + 'kibana.examples.controls.reactControlExample.controlGroupSerializedState'; +export const WEB_LOGS_DATA_VIEW_ID = '90943e30-9a47-11e8-b64d-95841ca0b247'; + +export function clearControlGroupSerializedState() { + sessionStorage.removeItem(SERIALIZED_STATE_SESSION_STORAGE_KEY); +} + +export function getControlGroupSerializedState(): SerializedPanelState { + const serializedStateJSON = sessionStorage.getItem(SERIALIZED_STATE_SESSION_STORAGE_KEY); + return serializedStateJSON ? JSON.parse(serializedStateJSON) : initialSerializedControlGroupState; +} + +export function setControlGroupSerializedState( + serializedState: SerializedPanelState +) { + sessionStorage.setItem(SERIALIZED_STATE_SESSION_STORAGE_KEY, JSON.stringify(serializedState)); +} + +const optionsListId = 'optionsList1'; +const searchControlId = 'searchControl1'; +const rangeSliderControlId = 'rangeSliderControl1'; +const timesliderControlId = 'timesliderControl1'; +const controlGroupPanels = { + [searchControlId]: { + type: SEARCH_CONTROL_TYPE, + order: 3, + grow: true, + width: 'medium', + explicitInput: { + id: searchControlId, + fieldName: 'message', + title: 'Message', + grow: true, + width: 'medium', + enhancements: {}, + }, + }, + [rangeSliderControlId]: { + type: RANGE_SLIDER_CONTROL_TYPE, + order: 1, + grow: true, + width: 'medium', + explicitInput: { + id: rangeSliderControlId, + fieldName: 'bytes', + title: 'Bytes', + grow: true, + width: 'medium', + enhancements: {}, + }, + }, + [timesliderControlId]: { + type: TIMESLIDER_CONTROL_TYPE, + order: 4, + grow: true, + width: 'medium', + explicitInput: { + id: timesliderControlId, + title: 'Time slider', + enhancements: {}, + }, + }, + [optionsListId]: { + type: OPTIONS_LIST_CONTROL_TYPE, + order: 2, + grow: true, + width: 'medium', + explicitInput: { + id: searchControlId, + fieldName: 'agent.keyword', + title: 'Agent', + grow: true, + width: 'medium', + enhancements: {}, + }, + }, +}; + +const initialSerializedControlGroupState = { + rawState: { + controlStyle: 'oneLine', + chainingSystem: 'HIERARCHICAL', + showApplySelections: false, + panelsJSON: JSON.stringify(controlGroupPanels), + ignoreParentSettingsJSON: + '{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}', + } as object, + references: [ + { + name: `controlGroup_${searchControlId}:${SEARCH_CONTROL_TYPE}DataView`, + type: 'index-pattern', + id: WEB_LOGS_DATA_VIEW_ID, + }, + { + name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL_TYPE}DataView`, + type: 'index-pattern', + id: WEB_LOGS_DATA_VIEW_ID, + }, + { + name: `controlGroup_${optionsListId}:optionsListControlDataView`, + type: 'index-pattern', + id: WEB_LOGS_DATA_VIEW_ID, + }, + ], +}; diff --git a/examples/controls_example/public/react_controls/control_group/control_group_unsaved_changes_api.ts b/examples/controls_example/public/react_controls/control_group/control_group_unsaved_changes_api.ts new file mode 100644 index 000000000000..399fbf6c463c --- /dev/null +++ b/examples/controls_example/public/react_controls/control_group/control_group_unsaved_changes_api.ts @@ -0,0 +1,79 @@ +/* + * 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 { omit } from 'lodash'; +import { + childrenUnsavedChanges$, + initializeUnsavedChanges, + PresentationContainer, +} from '@kbn/presentation-containers'; +import { + apiPublishesUnsavedChanges, + PublishesUnsavedChanges, + StateComparators, +} from '@kbn/presentation-publishing'; +import { combineLatest, map } from 'rxjs'; +import { ControlsInOrder, getControlsInOrder } from './init_controls_manager'; +import { ControlGroupRuntimeState, ControlPanelsState } from './types'; + +export type ControlGroupComparatorState = Pick< + ControlGroupRuntimeState, + | 'autoApplySelections' + | 'chainingSystem' + | 'ignoreParentSettings' + | 'initialChildControlState' + | 'labelPosition' +> & { + controlsInOrder: ControlsInOrder; +}; + +export function initializeControlGroupUnsavedChanges( + children$: PresentationContainer['children$'], + comparators: StateComparators, + snapshotControlsRuntimeState: () => ControlPanelsState, + parentApi: unknown, + lastSavedRuntimeState: ControlGroupRuntimeState +) { + const controlGroupUnsavedChanges = initializeUnsavedChanges( + { + autoApplySelections: lastSavedRuntimeState.autoApplySelections, + chainingSystem: lastSavedRuntimeState.chainingSystem, + controlsInOrder: getControlsInOrder(lastSavedRuntimeState.initialChildControlState), + ignoreParentSettings: lastSavedRuntimeState.ignoreParentSettings, + initialChildControlState: lastSavedRuntimeState.initialChildControlState, + labelPosition: lastSavedRuntimeState.labelPosition, + }, + parentApi, + comparators + ); + + return { + api: { + unsavedChanges: combineLatest([ + controlGroupUnsavedChanges.api.unsavedChanges, + childrenUnsavedChanges$(children$), + ]).pipe( + map(([unsavedControlGroupState, unsavedControlsState]) => { + const unsavedChanges: Partial = unsavedControlGroupState + ? omit(unsavedControlGroupState, 'controlsInOrder') + : {}; + if (unsavedControlsState || unsavedControlGroupState?.controlsInOrder) { + unsavedChanges.initialChildControlState = snapshotControlsRuntimeState(); + } + return Object.keys(unsavedChanges).length ? unsavedChanges : undefined; + }) + ), + resetUnsavedChanges: () => { + controlGroupUnsavedChanges.api.resetUnsavedChanges(); + Object.values(children$.value).forEach((controlApi) => { + if (apiPublishesUnsavedChanges(controlApi)) controlApi.resetUnsavedChanges(); + }); + }, + } as PublishesUnsavedChanges, + }; +} diff --git a/examples/controls_example/public/react_controls/control_group/get_control_group_factory.tsx b/examples/controls_example/public/react_controls/control_group/get_control_group_factory.tsx index 64e452aae118..13e6456071a4 100644 --- a/examples/controls_example/public/react_controls/control_group/get_control_group_factory.tsx +++ b/examples/controls_example/public/react_controls/control_group/get_control_group_factory.tsx @@ -8,6 +8,7 @@ import React, { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; +import fastIsEqual from 'fast-deep-equal'; import { ControlGroupChainingSystem, ControlWidth, @@ -22,7 +23,10 @@ import { DataView } from '@kbn/data-views-plugin/common'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { combineCompatibleChildrenApis } from '@kbn/presentation-containers'; +import { + apiHasSaveNotification, + combineCompatibleChildrenApis, +} from '@kbn/presentation-containers'; import { apiPublishesDataViews, PublishesDataViews, @@ -32,14 +36,10 @@ import { chaining$, controlFetch$, controlGroupFetch$ } from './control_fetch'; import { initControlsManager } from './init_controls_manager'; import { openEditControlGroupFlyout } from './open_edit_control_group_flyout'; import { deserializeControlGroup } from './serialization_utils'; -import { - ControlGroupApi, - ControlGroupRuntimeState, - ControlGroupSerializedState, - ControlGroupUnsavedChanges, -} from './types'; +import { ControlGroupApi, ControlGroupRuntimeState, ControlGroupSerializedState } from './types'; import { ControlGroup } from './components/control_group'; import { initSelectionsManager } from './selections_manager'; +import { initializeControlGroupUnsavedChanges } from './control_group_unsaved_changes_api'; export const getControlGroupEmbeddableFactory = (services: { core: CoreStart; @@ -52,7 +52,14 @@ export const getControlGroupEmbeddableFactory = (services: { > = { type: CONTROL_GROUP_TYPE, deserializeState: (state) => deserializeControlGroup(state), - buildEmbeddable: async (initialState, buildApi, uuid, parentApi, setApi) => { + buildEmbeddable: async ( + initialRuntimeState, + buildApi, + uuid, + parentApi, + setApi, + lastSavedRuntimeState + ) => { const { initialChildControlState, defaultControlGrow, @@ -61,7 +68,7 @@ export const getControlGroupEmbeddableFactory = (services: { chainingSystem, autoApplySelections, ignoreParentSettings, - } = initialState; + } = initialRuntimeState; const autoApplySelections$ = new BehaviorSubject(autoApplySelections); const controlsManager = initControlsManager(initialChildControlState); @@ -88,20 +95,36 @@ export const getControlGroupEmbeddableFactory = (services: { /** TODO: Handle loading; loading should be true if any child is loading */ const dataLoading$ = new BehaviorSubject(false); - /** TODO: Handle unsaved changes - * - Each child has an unsaved changed behaviour subject it pushes to - * - The control group listens to all of them (anyChildHasUnsavedChanges) and publishes its - * own unsaved changes if either one of its children has unsaved changes **or** one of - * the control group settings changed. - * - Children should **not** publish unsaved changes based on their output filters or selections. - * Instead, the control group will handle unsaved changes for filters. - */ - const unsavedChanges = new BehaviorSubject | undefined>( - undefined + const unsavedChanges = initializeControlGroupUnsavedChanges( + controlsManager.api.children$, + { + ...controlsManager.comparators, + autoApplySelections: [ + autoApplySelections$, + (next: boolean) => autoApplySelections$.next(next), + ], + chainingSystem: [ + chainingSystem$, + (next: ControlGroupChainingSystem) => chainingSystem$.next(next), + ], + ignoreParentSettings: [ + ignoreParentSettings$, + (next: ParentIgnoreSettings | undefined) => ignoreParentSettings$.next(next), + fastIsEqual, + ], + labelPosition: [labelPosition$, (next: ControlStyle) => labelPosition$.next(next)], + }, + controlsManager.snapshotControlsRuntimeState, + parentApi, + lastSavedRuntimeState ); const api = setApi({ ...controlsManager.api, + getLastSavedControlState: (controlUuid: string) => { + return lastSavedRuntimeState.initialChildControlState[controlUuid] ?? {}; + }, + ...unsavedChanges.api, ...selectionsManager.api, controlFetch$: (controlUuid: string) => controlFetch$( @@ -116,10 +139,6 @@ export const getControlGroupEmbeddableFactory = (services: { ignoreParentSettings$, autoApplySelections$, allowExpensiveQueries$, - unsavedChanges, - resetUnsavedChanges: () => { - // TODO: Implement this - }, snapshotRuntimeState: () => { // TODO: Remove this if it ends up being unnecessary return {} as unknown as ControlGroupRuntimeState; @@ -159,6 +178,9 @@ export const getControlGroupEmbeddableFactory = (services: { width, dataViews, labelPosition: labelPosition$, + saveNotification$: apiHasSaveNotification(parentApi) + ? parentApi.saveNotification$ + : undefined, }); /** Subscribe to all children's output data views, combine them, and output them */ diff --git a/examples/controls_example/public/react_controls/control_group/init_controls_manager.test.ts b/examples/controls_example/public/react_controls/control_group/init_controls_manager.test.ts index 98210d3d19ea..cc3b8492dce4 100644 --- a/examples/controls_example/public/react_controls/control_group/init_controls_manager.test.ts +++ b/examples/controls_example/public/react_controls/control_group/init_controls_manager.test.ts @@ -16,12 +16,12 @@ jest.mock('uuid', () => ({ describe('PresentationContainer api', () => { test('addNewPanel should add control at end of controls', async () => { const controlsManager = initControlsManager({ - alpha: { type: 'whatever', order: 0 }, - bravo: { type: 'whatever', order: 1 }, - charlie: { type: 'whatever', order: 2 }, + alpha: { type: 'testControl', order: 0 }, + bravo: { type: 'testControl', order: 1 }, + charlie: { type: 'testControl', order: 2 }, }); const addNewPanelPromise = controlsManager.api.addNewPanel({ - panelType: 'whatever', + panelType: 'testControl', initialState: {}, }); controlsManager.setControlApi('delta', {} as unknown as DefaultControlApi); @@ -36,9 +36,9 @@ describe('PresentationContainer api', () => { test('removePanel should remove control', () => { const controlsManager = initControlsManager({ - alpha: { type: 'whatever', order: 0 }, - bravo: { type: 'whatever', order: 1 }, - charlie: { type: 'whatever', order: 2 }, + alpha: { type: 'testControl', order: 0 }, + bravo: { type: 'testControl', order: 1 }, + charlie: { type: 'testControl', order: 2 }, }); controlsManager.api.removePanel('bravo'); expect(controlsManager.controlsInOrder$.value.map((element) => element.id)).toEqual([ @@ -49,12 +49,12 @@ describe('PresentationContainer api', () => { test('replacePanel should replace control', async () => { const controlsManager = initControlsManager({ - alpha: { type: 'whatever', order: 0 }, - bravo: { type: 'whatever', order: 1 }, - charlie: { type: 'whatever', order: 2 }, + alpha: { type: 'testControl', order: 0 }, + bravo: { type: 'testControl', order: 1 }, + charlie: { type: 'testControl', order: 2 }, }); const replacePanelPromise = controlsManager.api.replacePanel('bravo', { - panelType: 'whatever', + panelType: 'testControl', initialState: {}, }); controlsManager.setControlApi('delta', {} as unknown as DefaultControlApi); @@ -69,8 +69,8 @@ describe('PresentationContainer api', () => { describe('untilInitialized', () => { test('should not resolve until all controls are initialized', async () => { const controlsManager = initControlsManager({ - alpha: { type: 'whatever', order: 0 }, - bravo: { type: 'whatever', order: 1 }, + alpha: { type: 'testControl', order: 0 }, + bravo: { type: 'testControl', order: 1 }, }); let isDone = false; controlsManager.api.untilInitialized().then(() => { @@ -90,8 +90,8 @@ describe('PresentationContainer api', () => { test('should resolve when all control already initialized ', async () => { const controlsManager = initControlsManager({ - alpha: { type: 'whatever', order: 0 }, - bravo: { type: 'whatever', order: 1 }, + alpha: { type: 'testControl', order: 0 }, + bravo: { type: 'testControl', order: 1 }, }); controlsManager.setControlApi('alpha', {} as unknown as DefaultControlApi); controlsManager.setControlApi('bravo', {} as unknown as DefaultControlApi); @@ -106,3 +106,34 @@ describe('PresentationContainer api', () => { }); }); }); + +describe('snapshotControlsRuntimeState', () => { + test('should snapshot runtime state for all controls', async () => { + const controlsManager = initControlsManager({ + alpha: { type: 'testControl', order: 1 }, + bravo: { type: 'testControl', order: 0 }, + }); + controlsManager.setControlApi('alpha', { + snapshotRuntimeState: () => { + return { key1: 'alpha value' }; + }, + } as unknown as DefaultControlApi); + controlsManager.setControlApi('bravo', { + snapshotRuntimeState: () => { + return { key1: 'bravo value' }; + }, + } as unknown as DefaultControlApi); + expect(controlsManager.snapshotControlsRuntimeState()).toEqual({ + alpha: { + key1: 'alpha value', + order: 1, + type: 'testControl', + }, + bravo: { + key1: 'bravo value', + order: 0, + type: 'testControl', + }, + }); + }); +}); diff --git a/examples/controls_example/public/react_controls/control_group/init_controls_manager.ts b/examples/controls_example/public/react_controls/control_group/init_controls_manager.ts index 378bfe4d8256..dad214daf96b 100644 --- a/examples/controls_example/public/react_controls/control_group/init_controls_manager.ts +++ b/examples/controls_example/public/react_controls/control_group/init_controls_manager.ts @@ -7,6 +7,7 @@ */ import { v4 as generateId } from 'uuid'; +import fastIsEqual from 'fast-deep-equal'; import { HasSerializedChildState, PanelPackage, @@ -14,28 +15,35 @@ import { } from '@kbn/presentation-containers'; import type { Reference } from '@kbn/content-management-utils'; import { BehaviorSubject, first, merge } from 'rxjs'; -import { PublishingSubject } from '@kbn/presentation-publishing'; +import { PublishingSubject, StateComparators } from '@kbn/presentation-publishing'; import { omit } from 'lodash'; +import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state'; import { ControlPanelsState, ControlPanelState } from './types'; import { DefaultControlApi, DefaultControlState } from '../types'; +import { ControlGroupComparatorState } from './control_group_unsaved_changes_api'; export type ControlsInOrder = Array<{ id: string; type: string }>; +export function getControlsInOrder(initialControlPanelsState: ControlPanelsState) { + return Object.keys(initialControlPanelsState) + .map((key) => ({ + id: key, + order: initialControlPanelsState[key].order, + type: initialControlPanelsState[key].type, + })) + .sort((a, b) => (a.order > b.order ? 1 : -1)) + .map(({ id, type }) => ({ id, type })); // filter out `order` +} + export function initControlsManager(initialControlPanelsState: ControlPanelsState) { + const lastSavedControlsPanelState$ = new BehaviorSubject(initialControlPanelsState); const initialControlIds = Object.keys(initialControlPanelsState); const children$ = new BehaviorSubject<{ [key: string]: DefaultControlApi }>({}); - const controlsPanelState: { [panelId: string]: DefaultControlState } = { + let controlsPanelState: { [panelId: string]: DefaultControlState } = { ...initialControlPanelsState, }; const controlsInOrder$ = new BehaviorSubject( - Object.keys(initialControlPanelsState) - .map((key) => ({ - id: key, - order: initialControlPanelsState[key].order, - type: initialControlPanelsState[key].type, - })) - .sort((a, b) => (a.order > b.order ? 1 : -1)) - .map(({ id, type }) => ({ id, type })) // filter out `order` + getControlsInOrder(initialControlPanelsState) ); function untilControlLoaded( @@ -133,6 +141,20 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat references, }; }, + snapshotControlsRuntimeState: () => { + const controlsRuntimeState: ControlPanelsState = {}; + controlsInOrder$.getValue().forEach(({ id, type }, index) => { + const controlApi = getControlApi(id); + if (controlApi && apiHasSnapshottableState(controlApi)) { + controlsRuntimeState[id] = { + order: index, + type, + ...controlApi.snapshotRuntimeState(), + }; + } + }); + return controlsRuntimeState; + }, api: { getSerializedStateForChild: (childId: string) => { const controlPanelState = controlsPanelState[childId]; @@ -175,5 +197,28 @@ export function initControlsManager(initialControlPanelsState: ControlPanelsStat }, } as PresentationContainer & HasSerializedChildState & { untilInitialized: () => Promise }, + comparators: { + controlsInOrder: [ + controlsInOrder$, + (next: ControlsInOrder) => controlsInOrder$.next(next), + fastIsEqual, + ], + // Control state differences tracked by controlApi comparators + // Control ordering differences tracked by controlsInOrder comparator + // initialChildControlState comparatator exists to reset controls manager to last saved state + initialChildControlState: [ + lastSavedControlsPanelState$, + (lastSavedControlPanelsState: ControlPanelsState) => { + lastSavedControlsPanelState$.next(lastSavedControlPanelsState); + controlsPanelState = { + ...lastSavedControlPanelsState, + }; + controlsInOrder$.next(getControlsInOrder(lastSavedControlPanelsState)); + }, + () => true, + ], + } as StateComparators< + Pick + >, }; } diff --git a/examples/controls_example/public/react_controls/control_group/types.ts b/examples/controls_example/public/react_controls/control_group/types.ts index 9d1a390125da..65db5b8121b1 100644 --- a/examples/controls_example/public/react_controls/control_group/types.ts +++ b/examples/controls_example/public/react_controls/control_group/types.ts @@ -11,7 +11,11 @@ import { ParentIgnoreSettings } from '@kbn/controls-plugin/public'; import { ControlStyle, ControlWidth } from '@kbn/controls-plugin/public/types'; import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; import { Filter } from '@kbn/es-query'; -import { HasSerializedChildState, PresentationContainer } from '@kbn/presentation-containers'; +import { + HasSaveNotification, + HasSerializedChildState, + PresentationContainer, +} from '@kbn/presentation-containers'; import { HasEditCapabilities, HasParentApi, @@ -54,9 +58,10 @@ export type ControlGroupApi = PresentationContainer & PublishesUnsavedChanges & PublishesControlGroupDisplaySettings & PublishesTimeslice & - Partial> & { + Partial & HasSaveNotification> & { autoApplySelections$: PublishingSubject; controlFetch$: (controlUuid: string) => Observable; + getLastSavedControlState: (controlUuid: string) => object; ignoreParentSettings$: PublishingSubject; allowExpensiveQueries$: PublishingSubject; untilInitialized: () => Promise; @@ -84,16 +89,8 @@ export type ControlGroupEditorState = Pick< 'chainingSystem' | 'labelPosition' | 'autoApplySelections' | 'ignoreParentSettings' >; -export type ControlGroupSerializedState = Omit< - ControlGroupRuntimeState, - | 'labelPosition' - | 'ignoreParentSettings' - | 'defaultControlGrow' - | 'defaultControlWidth' - | 'anyChildHasUnsavedChanges' - | 'initialChildControlState' - | 'autoApplySelections' -> & { +export interface ControlGroupSerializedState { + chainingSystem: ControlGroupChainingSystem; panelsJSON: string; ignoreParentSettingsJSON: string; // In runtime state, we refer to this property as `labelPosition`; @@ -102,4 +99,4 @@ export type ControlGroupSerializedState = Omit< // In runtime state, we refer to the inverse of this property as `autoApplySelections` // to avoid migrations, we will continue to refer to this property as `showApplySelections` in the serialized state showApplySelections: boolean | undefined; -}; +} diff --git a/examples/controls_example/public/react_controls/control_renderer.tsx b/examples/controls_example/public/react_controls/control_renderer.tsx index cdaf53276760..2c248d7c05f7 100644 --- a/examples/controls_example/public/react_controls/control_renderer.tsx +++ b/examples/controls_example/public/react_controls/control_renderer.tsx @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import React, { useEffect, useImperativeHandle, useState } from 'react'; +import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; +import { initializeUnsavedChanges } from '@kbn/presentation-containers'; import { StateComparators } from '@kbn/presentation-publishing'; import { getControlFactory } from './control_factory_registry'; @@ -35,6 +36,8 @@ export const ControlRenderer = < onApiAvailable?: (api: ApiType) => void; isControlGroupInitialized: boolean; }) => { + const cleanupFunction = useRef<(() => void) | null>(null); + const [component, setComponent] = useState>( undefined ); @@ -48,25 +51,29 @@ export const ControlRenderer = < const factory = getControlFactory(type); const buildApi = ( apiRegistration: ControlApiRegistration, - comparators: StateComparators // TODO: Use these to calculate unsaved changes + comparators: StateComparators ): ApiType => { + const unsavedChanges = initializeUnsavedChanges( + parentApi.getLastSavedControlState(uuid) as StateType, + parentApi, + comparators + ); + + cleanupFunction.current = () => unsavedChanges.cleanup(); + return { ...apiRegistration, + ...unsavedChanges.api, uuid, parentApi, - unsavedChanges: new BehaviorSubject | undefined>(undefined), - resetUnsavedChanges: () => {}, type: factory.type, } as unknown as ApiType; }; - const { rawState: initialState } = parentApi.getSerializedStateForChild(uuid) ?? {}; - - return await factory.buildControl( - initialState as unknown as StateType, - buildApi, - uuid, - parentApi - ); + + const { rawState: initialState } = parentApi.getSerializedStateForChild(uuid) ?? { + rawState: {}, + }; + return await factory.buildControl(initialState as StateType, buildApi, uuid, parentApi); } buildControl() @@ -118,6 +125,12 @@ export const ControlRenderer = < [type] ); + useEffect(() => { + return () => { + cleanupFunction.current?.(); + }; + }, []); + return component && isControlGroupInitialized ? ( // @ts-expect-error Component={component} uuid={uuid} /> diff --git a/examples/controls_example/public/react_controls/data_controls/range_slider/components/range_slider_control.tsx b/examples/controls_example/public/react_controls/data_controls/range_slider/components/range_slider_control.tsx index f52dd0c3e528..b653cc954258 100644 --- a/examples/controls_example/public/react_controls/data_controls/range_slider/components/range_slider_control.tsx +++ b/examples/controls_example/public/react_controls/data_controls/range_slider/components/range_slider_control.tsx @@ -21,7 +21,7 @@ interface Props { max: number | undefined; min: number | undefined; onChange: (value: RangeValue | undefined) => void; - step: number | undefined; + step: number; value: RangeValue | undefined; uuid: string; controlPanelClassName?: string; diff --git a/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx b/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx index 8893d114c98f..385a93cf7e1d 100644 --- a/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx +++ b/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx @@ -100,7 +100,11 @@ export const getRangesliderControlFactory = ( }, { ...dataControl.comparators, - step: [step$, (nextStep: number | undefined) => step$.next(nextStep)], + step: [ + step$, + (nextStep: number | undefined) => step$.next(nextStep), + (a, b) => (a ?? 1) === (b ?? 1), + ], value: [value$, setValue], } ); @@ -237,7 +241,7 @@ export const getRangesliderControlFactory = ( max={max} min={min} onChange={setValue} - step={step} + step={step ?? 1} value={value} uuid={uuid} /> diff --git a/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx b/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx index f57e7a19c00f..6eed314ccdaa 100644 --- a/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx +++ b/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx @@ -122,6 +122,7 @@ export const getSearchControlFactory = ( searchTechnique, (newTechnique: SearchControlTechniques | undefined) => searchTechnique.next(newTechnique), + (a, b) => (a ?? DEFAULT_SEARCH_TECHNIQUE) === (b ?? DEFAULT_SEARCH_TECHNIQUE), ], searchString: [ searchString, diff --git a/examples/controls_example/public/react_controls/timeslider_control/get_timeslider_control_factory.tsx b/examples/controls_example/public/react_controls/timeslider_control/get_timeslider_control_factory.tsx index 4f086444da56..709166ca6fed 100644 --- a/examples/controls_example/public/react_controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/examples/controls_example/public/react_controls/timeslider_control/get_timeslider_control_factory.tsx @@ -13,6 +13,7 @@ import { EuiInputPopover } from '@elastic/eui'; import { apiHasParentApi, apiPublishesDataLoading, + getUnchangingComparator, getViewModeSubject, useBatchedPublishingSubjects, ViewMode, @@ -185,7 +186,6 @@ export const getTimesliderControlFactory = ( const viewModeSubject = getViewModeSubject(controlGroupApi) ?? new BehaviorSubject('view' as ViewMode); - // overwrite the `width` attribute because time slider should always have a width of large const defaultControl = initializeDefaultControlApi({ ...initialState, width: 'large' }); const dashboardDataLoading$ = @@ -243,6 +243,7 @@ export const getTimesliderControlFactory = ( }, { ...defaultControl.comparators, + width: getUnchangingComparator(), ...timeRangePercentage.comparators, isAnchored: [isAnchored$, setIsAnchored], } 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 4422bdbb8811..c7041427950d 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/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts index b430ac174b37..0976f43da7cf 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts @@ -67,7 +67,11 @@ export async function mergeDocuments( mergedDocument.security = mergeSecurityRequirements(documentsToMerge); } - mergedDocument.tags = mergeTags(documentsToMerge); + const mergedTags = [...(options.addTags ?? []), ...(mergeTags(documentsToMerge) ?? [])]; + + if (mergedTags.length) { + mergedDocument.tags = mergedTags; + } mergedByVersion.set(mergedDocument.info.version, mergedDocument); } diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts index e0b4c3972d6c..e48f7e8a958f 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts @@ -26,10 +26,17 @@ export function mergeOperations( continue; } + // Adding tags before merging helps to reuse already existing functionality + // without changes. It imitates a case when such tags already existed in source operations. + const extendedTags = [ + ...(options.addTags?.map((t) => t.name) ?? []), + ...(sourceOperation.tags ?? []), + ]; const normalizedSourceOperation = { ...sourceOperation, ...(options.skipServers ? { servers: undefined } : { servers: sourceOperation.servers }), ...(options.skipSecurity ? { security: undefined } : { security: sourceOperation.security }), + ...(extendedTags.length > 0 ? { tags: extendedTags } : {}), }; if (!mergedOperation || deepEqual(normalizedSourceOperation, mergedOperation)) { diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts index 24bb048b2a5e..837ef9db8114 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ +import { OpenAPIV3 } from 'openapi-types'; + export interface MergeOptions { skipServers: boolean; skipSecurity: boolean; + addTags?: OpenAPIV3.TagObject[]; } diff --git a/packages/kbn-openapi-bundler/src/openapi_bundler.ts b/packages/kbn-openapi-bundler/src/openapi_bundler.ts index 44bca8919450..7d424af177dd 100644 --- a/packages/kbn-openapi-bundler/src/openapi_bundler.ts +++ b/packages/kbn-openapi-bundler/src/openapi_bundler.ts @@ -32,7 +32,7 @@ interface BundleOptions { */ prototypeDocument?: PrototypeDocument | string; /** - * When specified the produced bundle will contain only + * When `includeLabels` are specified the produced bundle will contain only * operations objects with matching labels */ includeLabels?: string[]; @@ -40,7 +40,7 @@ interface BundleOptions { export const bundle = async ({ sourceGlob, - outputFilePath = 'bundled-{version}.schema.yaml', + outputFilePath = 'bundled_{version}.schema.yaml', options, }: BundlerConfig) => { const prototypeDocument = options?.prototypeDocument @@ -82,6 +82,7 @@ export const bundle = async ({ splitDocumentsByVersion: true, skipServers: Boolean(prototypeDocument?.servers), skipSecurity: Boolean(prototypeDocument?.security), + addTags: prototypeDocument?.tags, }); await writeDocuments(resultDocumentsMap, outputFilePath); diff --git a/packages/kbn-openapi-bundler/src/openapi_merger.ts b/packages/kbn-openapi-bundler/src/openapi_merger.ts index d0f532ca47d0..3fb04de279d1 100644 --- a/packages/kbn-openapi-bundler/src/openapi_merger.ts +++ b/packages/kbn-openapi-bundler/src/openapi_merger.ts @@ -75,6 +75,7 @@ export const merge = async ({ splitDocumentsByVersion: false, skipServers: Boolean(prototypeDocument?.servers), skipSecurity: Boolean(prototypeDocument?.security), + addTags: prototypeDocument?.tags, }); // Only one document is expected when `splitDocumentsByVersion` is set to `false` const mergedDocument = Array.from(resultDocumentsMap.values())[0]; diff --git a/packages/kbn-openapi-bundler/src/prototype_document.ts b/packages/kbn-openapi-bundler/src/prototype_document.ts index 5f6c939e375c..924ca223cb0b 100644 --- a/packages/kbn-openapi-bundler/src/prototype_document.ts +++ b/packages/kbn-openapi-bundler/src/prototype_document.ts @@ -9,21 +9,37 @@ import { OpenAPIV3 } from 'openapi-types'; /** - * `PrototypeDocument` is used as a prototype for the result file. In the other words - * it provides a way to specify the following properties - * - * - `info` info object - * - `servers` servers used to replace `servers` in the source OpenAPI specs - * - `security` security requirements used to replace `security` in the source OpenAPI specs - * It must be specified together with `components.securitySchemes`. - * - * All the other properties will be ignored. + * `PrototypeDocument` is used as a prototype for the result file. + * Only specified properties are used. All the other properties will be ignored. */ export interface PrototypeDocument { + /** + * Defines OpenAPI Info Object to be used in the result document. + * `bundle()` utility doesn't use `info.version`. + */ info?: Partial; + /** + * Defines `servers` to be used in the result document. When `servers` + * are set existing source documents `servers` aren't included into + * the result document. + */ servers?: OpenAPIV3.ServerObject[]; + /** + * Defines security requirements to be used in the result document. It must + * be used together with `components.securitySchemes` When `security` + * is set existing source documents `security` isn't included into + * the result document. + */ security?: OpenAPIV3.SecurityRequirementObject[]; components?: { + /** + * Defines security schemes for security requirements. + */ securitySchemes: Record; }; + /** + * Defines tags to be added to the result document. Tags are added to + * root level tags and prepended to operation object tags. + */ + tags?: OpenAPIV3.TagObject[]; } diff --git a/packages/kbn-openapi-bundler/src/validate_prototype_document.ts b/packages/kbn-openapi-bundler/src/validate_prototype_document.ts index 82bfc0b0a609..7e87bd565c51 100644 --- a/packages/kbn-openapi-bundler/src/validate_prototype_document.ts +++ b/packages/kbn-openapi-bundler/src/validate_prototype_document.ts @@ -25,6 +25,34 @@ export async function validatePrototypeDocument( ? await readDocument(prototypeDocumentOrString) : prototypeDocumentOrString; + if (prototypeDocument.servers && !Array.isArray(prototypeDocument.servers)) { + throw new Error(`Prototype document's ${chalk.bold('servers')} must be an array`); + } + + if (prototypeDocument.servers && prototypeDocument.servers.length === 0) { + throw new Error( + `Prototype document's ${chalk.bold('servers')} should have as minimum one entry` + ); + } + + if (prototypeDocument.security && !Array.isArray(prototypeDocument.security)) { + throw new Error(`Prototype document's ${chalk.bold('security')} must be an array`); + } + + if (prototypeDocument.security && prototypeDocument.security.length === 0) { + throw new Error( + `Prototype document's ${chalk.bold('security')} should have as minimum one entry` + ); + } + + if (prototypeDocument.tags && !Array.isArray(prototypeDocument.tags)) { + throw new Error(`Prototype document's ${chalk.bold('tags')} must be an array`); + } + + if (prototypeDocument.tags && prototypeDocument.tags.length === 0) { + throw new Error(`Prototype document's ${chalk.bold('tags')} should have as minimum one entry`); + } + if (prototypeDocument.security && !prototypeDocument.components?.securitySchemes) { throw new Error( `Prototype document must contain ${chalk.bold( diff --git a/packages/kbn-openapi-bundler/tests/bundler/result_overrides/add_tags.test.ts b/packages/kbn-openapi-bundler/tests/bundler/result_overrides/add_tags.test.ts new file mode 100644 index 000000000000..405f429f30ce --- /dev/null +++ b/packages/kbn-openapi-bundler/tests/bundler/result_overrides/add_tags.test.ts @@ -0,0 +1,182 @@ +/* + * 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 { createOASDocument } from '../../create_oas_document'; +import { bundleSpecs } from '../bundle_specs'; + +describe('OpenAPI Bundler - assign a tag', () => { + it('adds tags when nothing is set', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + tags: [ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + ], + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.tags).toEqual(['Some Tag', 'Another Tag']); + expect(bundledSpec.paths['/api/another_api']?.get?.tags).toEqual(['Some Tag', 'Another Tag']); + expect(bundledSpec.tags).toEqual([ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + ]); + }); + + it('adds tags to existing tags', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + tags: ['Local tag'], + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + tags: ['Global tag'], + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + tags: [{ name: 'Global tag', description: 'Global tag description' }], + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + tags: [ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + ], + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.tags).toEqual([ + 'Some Tag', + 'Another Tag', + 'Local tag', + ]); + expect(bundledSpec.paths['/api/another_api']?.get?.tags).toEqual([ + 'Some Tag', + 'Another Tag', + 'Global tag', + ]); + expect(bundledSpec.tags).toEqual([ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + { name: 'Global tag', description: 'Global tag description' }, + ]); + }); +}); diff --git a/packages/kbn-openapi-bundler/tests/merger/result_overrides/add_tags.test.ts b/packages/kbn-openapi-bundler/tests/merger/result_overrides/add_tags.test.ts new file mode 100644 index 000000000000..ad665f16228b --- /dev/null +++ b/packages/kbn-openapi-bundler/tests/merger/result_overrides/add_tags.test.ts @@ -0,0 +1,182 @@ +/* + * 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 { createOASDocument } from '../../create_oas_document'; +import { mergeSpecs } from '../merge_specs'; + +describe('OpenAPI Bundler - assign a tag', () => { + it('adds tags when nothing is set', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const [mergedSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + tags: [ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + ], + }, + } + ) + ); + + expect(mergedSpec.paths['/api/some_api']?.get?.tags).toEqual(['Some Tag', 'Another Tag']); + expect(mergedSpec.paths['/api/another_api']?.get?.tags).toEqual(['Some Tag', 'Another Tag']); + expect(mergedSpec.tags).toEqual([ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + ]); + }); + + it('adds tags to existing tags', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + tags: ['Local tag'], + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + tags: ['Global tag'], + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + tags: [{ name: 'Global tag', description: 'Global tag description' }], + }); + + const [mergedSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + tags: [ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + ], + }, + } + ) + ); + + expect(mergedSpec.paths['/api/some_api']?.get?.tags).toEqual([ + 'Some Tag', + 'Another Tag', + 'Local tag', + ]); + expect(mergedSpec.paths['/api/another_api']?.get?.tags).toEqual([ + 'Some Tag', + 'Another Tag', + 'Global tag', + ]); + expect(mergedSpec.tags).toEqual([ + { + name: 'Some Tag', + description: 'Some tag description', + }, + { + name: 'Another Tag', + description: 'Another tag description', + }, + { name: 'Global tag', description: 'Global tag description' }, + ]); + }); +}); diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml index 12132ce5562b..9703e31287ae 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list/create_endpoint_list.schema.yaml @@ -10,8 +10,6 @@ paths: operationId: CreateEndpointList summary: Creates an endpoint list description: Creates an endpoint list or does nothing if the list already exists - tags: - - Endpoint exceptions API responses: 200: description: Successful response diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml index 575e77ef8491..8807f6cf76f7 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/api/create_endpoint_list_item/create_endpoint_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: x-codegen-enabled: true operationId: CreateEndpointListItem summary: Creates an endpoint list item - tags: - - Endpoint exceptions API requestBody: description: Exception list item's properties required: true diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml index e60fe54abb26..cd0ddd9dd69c 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/api/delete_endpoint_list_item/delete_endpoint_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: x-codegen-enabled: true operationId: DeleteEndpointListItem summary: Deletes an endpoint list item - tags: - - Endpoint exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml index ca5d656c57c8..e727367ff9c7 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/api/find_endpoint_list_item/find_endpoint_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: x-codegen-enabled: true operationId: FindEndpointListItems summary: Finds endpoint list items - tags: - - Endpoint exceptions API parameters: - name: filter in: query diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml index c4025370763f..81be7c67a00b 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/api/read_endpoint_list_item/read_endpoint_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: x-codegen-enabled: true operationId: ReadEndpointListItem summary: Reads an endpoint list item - tags: - - Endpoint exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml index 740ebb0107eb..dcbf24be2876 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/api/update_endpoint_list_item/update_endpoint_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: x-codegen-enabled: true operationId: UpdateEndpointListItem summary: Updates an endpoint list item - tags: - - Endpoint exceptions API requestBody: description: Exception list item's properties required: true diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index 166d29063f35..cbd98091ca37 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -49,8 +49,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Creates an endpoint list - tags: - - Endpoint exceptions API /api/endpoint_list/items: delete: operationId: DeleteEndpointListItem @@ -107,8 +105,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Deletes an endpoint list item - tags: - - Endpoint exceptions API get: operationId: ReadEndpointListItem parameters: @@ -166,8 +162,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Reads an endpoint list item - tags: - - Endpoint exceptions API post: operationId: CreateEndpointListItem requestBody: @@ -244,8 +238,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Creates an endpoint list item - tags: - - Endpoint exceptions API put: operationId: UpdateEndpointListItem requestBody: @@ -327,8 +319,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Updates an endpoint list item - tags: - - Endpoint exceptions API /api/endpoint_list/items/_find: get: operationId: FindEndpointListItems @@ -433,8 +423,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Finds endpoint list items - tags: - - Endpoint exceptions API components: schemas: EndpointList: diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index fe40b86970bf..e8a8966c1858 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -49,8 +49,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Creates an endpoint list - tags: - - Endpoint exceptions API /api/endpoint_list/items: delete: operationId: DeleteEndpointListItem @@ -107,8 +105,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Deletes an endpoint list item - tags: - - Endpoint exceptions API get: operationId: ReadEndpointListItem parameters: @@ -166,8 +162,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Reads an endpoint list item - tags: - - Endpoint exceptions API post: operationId: CreateEndpointListItem requestBody: @@ -244,8 +238,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Creates an endpoint list item - tags: - - Endpoint exceptions API put: operationId: UpdateEndpointListItem requestBody: @@ -327,8 +319,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Updates an endpoint list item - tags: - - Endpoint exceptions API /api/endpoint_list/items/_find: get: operationId: FindEndpointListItems @@ -433,8 +423,6 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Finds endpoint list items - tags: - - Endpoint exceptions API components: schemas: EndpointList: diff --git a/packages/kbn-securitysolution-exceptions-common/api/create_exception_list/create_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/create_exception_list/create_exception_list.schema.yaml index 823ead299133..463009b233af 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/create_exception_list/create_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/create_exception_list/create_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateExceptionList x-codegen-enabled: true summary: Creates an exception list - tags: - - Exceptions API requestBody: description: Exception list's properties required: true diff --git a/packages/kbn-securitysolution-exceptions-common/api/create_exception_list_item/create_exception_list_item.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/create_exception_list_item/create_exception_list_item.schema.yaml index 6ee00569336e..f7eb416f953a 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/create_exception_list_item/create_exception_list_item.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/create_exception_list_item/create_exception_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateExceptionListItem x-codegen-enabled: true summary: Creates an exception list item - tags: - - Exceptions API requestBody: description: Exception list item's properties required: true diff --git a/packages/kbn-securitysolution-exceptions-common/api/create_rule_exceptions/create_rule_exceptions.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/create_rule_exceptions/create_rule_exceptions.schema.yaml index b7cad73c9bb2..0928d9e7f4e2 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/create_rule_exceptions/create_rule_exceptions.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/create_rule_exceptions/create_rule_exceptions.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateRuleExceptionListItems x-codegen-enabled: true summary: Creates rule exception list items - tags: - - Exceptions API parameters: - name: id in: path diff --git a/packages/kbn-securitysolution-exceptions-common/api/create_shared_exceptions_list/create_shared_exceptions_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/create_shared_exceptions_list/create_shared_exceptions_list.schema.yaml index 0a072e855f0d..e76ec4c50c5c 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/create_shared_exceptions_list/create_shared_exceptions_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/create_shared_exceptions_list/create_shared_exceptions_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateSharedExceptionList x-codegen-enabled: true summary: Creates a shared exception list - tags: - - Exceptions API requestBody: required: true content: diff --git a/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list/delete_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list/delete_exception_list.schema.yaml index f8522ddf2de6..ccc0749dd206 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list/delete_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list/delete_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: DeleteExceptionList x-codegen-enabled: true summary: Deletes an exception list - tags: - - Exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list_item/delete_exception_list_item.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list_item/delete_exception_list_item.schema.yaml index a34339e4f4c3..e9d7fa068704 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list_item/delete_exception_list_item.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/delete_exception_list_item/delete_exception_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: DeleteExceptionListItem x-codegen-enabled: true summary: Deletes an exception list item - tags: - - Exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/duplicate_exception_list/duplicate_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/duplicate_exception_list/duplicate_exception_list.schema.yaml index 50d5c5e4d5c2..f041fcb1d106 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/duplicate_exception_list/duplicate_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/duplicate_exception_list/duplicate_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: DuplicateExceptionList x-codegen-enabled: true summary: Duplicates an exception list - tags: - - Exceptions API parameters: - name: list_id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/export_exception_list/export_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/export_exception_list/export_exception_list.schema.yaml index 85482d6339f7..41637963d992 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/export_exception_list/export_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/export_exception_list/export_exception_list.schema.yaml @@ -10,8 +10,6 @@ paths: x-codegen-enabled: true summary: Exports an exception list description: Exports an exception list and its associated items to an .ndjson file - tags: - - Exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/find_exception_list/find_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/find_exception_list/find_exception_list.schema.yaml index dae00e044102..a1df1d12a27e 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/find_exception_list/find_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/find_exception_list/find_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: FindExceptionLists x-codegen-enabled: true summary: Finds exception lists - tags: - - Exceptions API parameters: - name: filter in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/find_exception_list_item/find_exception_list_item.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/find_exception_list_item/find_exception_list_item.schema.yaml index 2fae12499d51..f2b3aafdee10 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/find_exception_list_item/find_exception_list_item.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/find_exception_list_item/find_exception_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: FindExceptionListItems x-codegen-enabled: true summary: Finds exception list items - tags: - - Exceptions API parameters: - name: list_id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/import_exceptions/import_exceptions.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/import_exceptions/import_exceptions.schema.yaml index baa4a3e6b3a6..dc67d1386475 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/import_exceptions/import_exceptions.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/import_exceptions/import_exceptions.schema.yaml @@ -10,8 +10,6 @@ paths: x-codegen-enabled: true summary: Imports an exception list description: Imports an exception list and associated items - tags: - - Exceptions API requestBody: required: true content: diff --git a/packages/kbn-securitysolution-exceptions-common/api/read_exception_list/read_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/read_exception_list/read_exception_list.schema.yaml index f9dcf6ea5117..69d9b86cf2e5 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/read_exception_list/read_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/read_exception_list/read_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetExceptionList x-codegen-enabled: true summary: Retrieves an exception list using its `id` or `list_id` field - tags: - - Exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/read_exception_list_item/read_exception_list_item.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/read_exception_list_item/read_exception_list_item.schema.yaml index 1ebad1012516..eb2d00396a73 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/read_exception_list_item/read_exception_list_item.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/read_exception_list_item/read_exception_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetExceptionListItem x-codegen-enabled: true summary: Gets an exception list item - tags: - - Exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/summary_exception_list/summary_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/summary_exception_list/summary_exception_list.schema.yaml index cc288ee524ff..8a8796d6cad2 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/summary_exception_list/summary_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/summary_exception_list/summary_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetExceptionListSummary x-codegen-enabled: true summary: Retrieves an exception list summary - tags: - - Exceptions API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-exceptions-common/api/update_exception_list/update_exception_list.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/update_exception_list/update_exception_list.schema.yaml index af31f2af4273..a58caeb46542 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/update_exception_list/update_exception_list.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/update_exception_list/update_exception_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: UpdateExceptionList x-codegen-enabled: true summary: Updates an exception list - tags: - - Exceptions API requestBody: description: Exception list's properties required: true diff --git a/packages/kbn-securitysolution-exceptions-common/api/update_exception_list_item/update_exception_list_item.schema.yaml b/packages/kbn-securitysolution-exceptions-common/api/update_exception_list_item/update_exception_list_item.schema.yaml index 02ff5e2d0171..180d4865f887 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/update_exception_list_item/update_exception_list_item.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/api/update_exception_list_item/update_exception_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: UpdateExceptionListItem x-codegen-enabled: true summary: Updates an exception list item - tags: - - Exceptions API requestBody: description: Exception list item's properties required: true diff --git a/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml index 1c12ee8057a3..6e353a82432d 100644 --- a/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/docs/openapi/ess/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml @@ -75,7 +75,7 @@ paths: description: Internal server error response summary: Creates rule exception list items tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists: delete: operationId: DeleteExceptionList @@ -139,7 +139,7 @@ paths: description: Internal server error response summary: Deletes an exception list tags: - - Exceptions API + - Security Solution Exceptions API get: operationId: GetExceptionList parameters: @@ -202,7 +202,7 @@ paths: description: Internal server error response summary: Retrieves an exception list using its `id` or `list_id` field tags: - - Exceptions API + - Security Solution Exceptions API post: operationId: CreateExceptionList requestBody: @@ -279,7 +279,7 @@ paths: description: Internal server error response summary: Creates an exception list tags: - - Exceptions API + - Security Solution Exceptions API put: operationId: UpdateExceptionList requestBody: @@ -359,7 +359,7 @@ paths: description: Internal server error response summary: Updates an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_duplicate: post: operationId: DuplicateExceptionList @@ -428,7 +428,7 @@ paths: description: Internal server error response summary: Duplicates an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_export: post: description: Exports an exception list and its associated items to an .ndjson file @@ -508,7 +508,7 @@ paths: description: Internal server error response summary: Exports an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_find: get: operationId: FindExceptionLists @@ -628,7 +628,7 @@ paths: description: Internal server error response summary: Finds exception lists tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_import: post: description: Imports an exception list and associated items @@ -744,7 +744,7 @@ paths: description: Internal server error response summary: Imports an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/items: delete: operationId: DeleteExceptionListItem @@ -808,7 +808,7 @@ paths: description: Internal server error response summary: Deletes an exception list item tags: - - Exceptions API + - Security Solution Exceptions API get: operationId: GetExceptionListItem parameters: @@ -871,7 +871,7 @@ paths: description: Internal server error response summary: Gets an exception list item tags: - - Exceptions API + - Security Solution Exceptions API post: operationId: CreateExceptionListItem requestBody: @@ -958,7 +958,7 @@ paths: description: Internal server error response summary: Creates an exception list item tags: - - Exceptions API + - Security Solution Exceptions API put: operationId: UpdateExceptionListItem requestBody: @@ -1049,7 +1049,7 @@ paths: description: Internal server error response summary: Updates an exception list item tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/items/_find: get: operationId: FindExceptionListItems @@ -1185,7 +1185,7 @@ paths: description: Internal server error response summary: Finds exception list items tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/summary: get: operationId: GetExceptionListSummary @@ -1268,7 +1268,7 @@ paths: description: Internal server error response summary: Retrieves an exception list summary tags: - - Exceptions API + - Security Solution Exceptions API /api/exceptions/shared: post: operationId: CreateSharedExceptionList @@ -1327,7 +1327,7 @@ paths: description: Internal server error response summary: Creates a shared exception list tags: - - Exceptions API + - Security Solution Exceptions API components: schemas: CreateExceptionListItemComment: @@ -1852,3 +1852,9 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + Exceptions API allows you to manage detection rule exceptions to prevent a + rule from generating an alert from incoming events even when the rule's + other criteria are met. + name: Security Solution Exceptions API diff --git a/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml index ebf3c74f39e2..3423915ff54a 100644 --- a/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-exceptions-common/docs/openapi/serverless/security_solution_exceptions_api_2023_10_31.bundled.schema.yaml @@ -75,7 +75,7 @@ paths: description: Internal server error response summary: Creates rule exception list items tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists: delete: operationId: DeleteExceptionList @@ -139,7 +139,7 @@ paths: description: Internal server error response summary: Deletes an exception list tags: - - Exceptions API + - Security Solution Exceptions API get: operationId: GetExceptionList parameters: @@ -202,7 +202,7 @@ paths: description: Internal server error response summary: Retrieves an exception list using its `id` or `list_id` field tags: - - Exceptions API + - Security Solution Exceptions API post: operationId: CreateExceptionList requestBody: @@ -279,7 +279,7 @@ paths: description: Internal server error response summary: Creates an exception list tags: - - Exceptions API + - Security Solution Exceptions API put: operationId: UpdateExceptionList requestBody: @@ -359,7 +359,7 @@ paths: description: Internal server error response summary: Updates an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_duplicate: post: operationId: DuplicateExceptionList @@ -428,7 +428,7 @@ paths: description: Internal server error response summary: Duplicates an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_export: post: description: Exports an exception list and its associated items to an .ndjson file @@ -508,7 +508,7 @@ paths: description: Internal server error response summary: Exports an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_find: get: operationId: FindExceptionLists @@ -628,7 +628,7 @@ paths: description: Internal server error response summary: Finds exception lists tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/_import: post: description: Imports an exception list and associated items @@ -744,7 +744,7 @@ paths: description: Internal server error response summary: Imports an exception list tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/items: delete: operationId: DeleteExceptionListItem @@ -808,7 +808,7 @@ paths: description: Internal server error response summary: Deletes an exception list item tags: - - Exceptions API + - Security Solution Exceptions API get: operationId: GetExceptionListItem parameters: @@ -871,7 +871,7 @@ paths: description: Internal server error response summary: Gets an exception list item tags: - - Exceptions API + - Security Solution Exceptions API post: operationId: CreateExceptionListItem requestBody: @@ -958,7 +958,7 @@ paths: description: Internal server error response summary: Creates an exception list item tags: - - Exceptions API + - Security Solution Exceptions API put: operationId: UpdateExceptionListItem requestBody: @@ -1049,7 +1049,7 @@ paths: description: Internal server error response summary: Updates an exception list item tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/items/_find: get: operationId: FindExceptionListItems @@ -1185,7 +1185,7 @@ paths: description: Internal server error response summary: Finds exception list items tags: - - Exceptions API + - Security Solution Exceptions API /api/exception_lists/summary: get: operationId: GetExceptionListSummary @@ -1268,7 +1268,7 @@ paths: description: Internal server error response summary: Retrieves an exception list summary tags: - - Exceptions API + - Security Solution Exceptions API /api/exceptions/shared: post: operationId: CreateSharedExceptionList @@ -1327,7 +1327,7 @@ paths: description: Internal server error response summary: Creates a shared exception list tags: - - Exceptions API + - Security Solution Exceptions API components: schemas: CreateExceptionListItemComment: @@ -1852,3 +1852,9 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + Exceptions API allows you to manage detection rule exceptions to prevent a + rule from generating an alert from incoming events even when the rule's + other criteria are met. + name: Security Solution Exceptions API diff --git a/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js b/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js index 2ed569154bd4..eed4b9c8a8ac 100644 --- a/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js +++ b/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js @@ -27,6 +27,13 @@ const ROOT = resolve(__dirname, '..'); description: "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", }, + tags: [ + { + name: 'Security Solution Exceptions API', + description: + "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + }, + ], }, }, }); @@ -45,6 +52,13 @@ const ROOT = resolve(__dirname, '..'); description: "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", }, + tags: [ + { + name: 'Security Solution Exceptions API', + description: + "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + }, + ], }, }, }); diff --git a/packages/kbn-securitysolution-lists-common/api/create_list/create_list.schema.yaml b/packages/kbn-securitysolution-lists-common/api/create_list/create_list.schema.yaml index 0cd635c46ff7..bae6565b0f1e 100644 --- a/packages/kbn-securitysolution-lists-common/api/create_list/create_list.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/create_list/create_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateList x-codegen-enabled: true summary: Creates a list - tags: - - List API requestBody: description: List's properties required: true diff --git a/packages/kbn-securitysolution-lists-common/api/create_list_index/create_list_index.schema.yaml b/packages/kbn-securitysolution-lists-common/api/create_list_index/create_list_index.schema.yaml index f42937a8885d..dcb1baa3ee3d 100644 --- a/packages/kbn-securitysolution-lists-common/api/create_list_index/create_list_index.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/create_list_index/create_list_index.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateListIndex x-codegen-enabled: true summary: Creates necessary list data streams - tags: - - List API responses: 200: description: Successful response diff --git a/packages/kbn-securitysolution-lists-common/api/create_list_item/create_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/create_list_item/create_list_item.schema.yaml index ca75b8555b9c..10fef88e7a04 100644 --- a/packages/kbn-securitysolution-lists-common/api/create_list_item/create_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/create_list_item/create_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: CreateListItem x-codegen-enabled: true summary: Creates a list item - tags: - - List item API requestBody: description: List item's properties required: true diff --git a/packages/kbn-securitysolution-lists-common/api/delete_list/delete_list.schema.yaml b/packages/kbn-securitysolution-lists-common/api/delete_list/delete_list.schema.yaml index c116d43edd93..4a5974003918 100644 --- a/packages/kbn-securitysolution-lists-common/api/delete_list/delete_list.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/delete_list/delete_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: DeleteList x-codegen-enabled: true summary: Deletes a list - tags: - - List API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-lists-common/api/delete_list_index/delete_list_index.schema.yaml b/packages/kbn-securitysolution-lists-common/api/delete_list_index/delete_list_index.schema.yaml index c3b4969aa328..34dcf91f548d 100644 --- a/packages/kbn-securitysolution-lists-common/api/delete_list_index/delete_list_index.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/delete_list_index/delete_list_index.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: DeleteListIndex x-codegen-enabled: true summary: Deletes list data streams - tags: - - List API responses: 200: description: Successful response diff --git a/packages/kbn-securitysolution-lists-common/api/delete_list_item/delete_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/delete_list_item/delete_list_item.schema.yaml index 63938d313aea..413c85b55dd3 100644 --- a/packages/kbn-securitysolution-lists-common/api/delete_list_item/delete_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/delete_list_item/delete_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: DeleteListItem x-codegen-enabled: true summary: Deletes a list item - tags: - - List item API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-lists-common/api/export_list_item/export_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/export_list_item/export_list_item.schema.yaml index 69dd492e8663..06eda41d042f 100644 --- a/packages/kbn-securitysolution-lists-common/api/export_list_item/export_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/export_list_item/export_list_item.schema.yaml @@ -10,8 +10,6 @@ paths: x-codegen-enabled: true summary: Exports list items description: Exports list item values from the specified list - tags: - - List items Import/Export API parameters: - name: list_id in: query diff --git a/packages/kbn-securitysolution-lists-common/api/find_list/find_list.schema.yaml b/packages/kbn-securitysolution-lists-common/api/find_list/find_list.schema.yaml index 236fa747599a..071dba08254e 100644 --- a/packages/kbn-securitysolution-lists-common/api/find_list/find_list.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/find_list/find_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: FindLists x-codegen-enabled: true summary: Finds lists - tags: - - List API parameters: - name: page in: query diff --git a/packages/kbn-securitysolution-lists-common/api/find_list_item/find_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/find_list_item/find_list_item.schema.yaml index 1e822d442a4e..b08e7b471937 100644 --- a/packages/kbn-securitysolution-lists-common/api/find_list_item/find_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/find_list_item/find_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: FindListItems x-codegen-enabled: true summary: Finds list items - tags: - - List API parameters: - name: list_id in: query diff --git a/packages/kbn-securitysolution-lists-common/api/get_list_privileges/get_list_privileges.schema.yaml b/packages/kbn-securitysolution-lists-common/api/get_list_privileges/get_list_privileges.schema.yaml index 729da9b8f62a..0605dda5dde8 100644 --- a/packages/kbn-securitysolution-lists-common/api/get_list_privileges/get_list_privileges.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/get_list_privileges/get_list_privileges.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetListPrivileges x-codegen-enabled: true summary: Gets list privileges - tags: - - List API responses: 200: description: Successful response diff --git a/packages/kbn-securitysolution-lists-common/api/import_list_item/import_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/import_list_item/import_list_item.schema.yaml index 04a0e18d7678..895e222c0520 100644 --- a/packages/kbn-securitysolution-lists-common/api/import_list_item/import_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/import_list_item/import_list_item.schema.yaml @@ -13,8 +13,6 @@ paths: Imports a list of items from a `.txt` or `.csv` file. The maximum file size is 9 million bytes. You can import items to a new or existing list. - tags: - - List items Import/Export API requestBody: required: true content: diff --git a/packages/kbn-securitysolution-lists-common/api/patch_list/patch_list.schema.yaml b/packages/kbn-securitysolution-lists-common/api/patch_list/patch_list.schema.yaml index cae09887db31..1ca568acb2bb 100644 --- a/packages/kbn-securitysolution-lists-common/api/patch_list/patch_list.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/patch_list/patch_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: PatchList x-codegen-enabled: true summary: Patches a list - tags: - - List API requestBody: description: List's properties required: true diff --git a/packages/kbn-securitysolution-lists-common/api/patch_list_item/patch_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/patch_list_item/patch_list_item.schema.yaml index 36ca55c7ae06..a17982db1445 100644 --- a/packages/kbn-securitysolution-lists-common/api/patch_list_item/patch_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/patch_list_item/patch_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: PatchListItem x-codegen-enabled: true summary: Patches a list item - tags: - - List item API requestBody: description: List item's properties required: true diff --git a/packages/kbn-securitysolution-lists-common/api/read_list/read_list.schema.yaml b/packages/kbn-securitysolution-lists-common/api/read_list/read_list.schema.yaml index 4a2ae5d2cd42..86a5f0df2fc1 100644 --- a/packages/kbn-securitysolution-lists-common/api/read_list/read_list.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/read_list/read_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetList x-codegen-enabled: true summary: Retrieves a list using its id field - tags: - - List API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-lists-common/api/read_list_index/read_list_index.schema.yaml b/packages/kbn-securitysolution-lists-common/api/read_list_index/read_list_index.schema.yaml index accef8b58411..695fe5a10ee8 100644 --- a/packages/kbn-securitysolution-lists-common/api/read_list_index/read_list_index.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/read_list_index/read_list_index.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetListIndex x-codegen-enabled: true summary: Get list data stream existence status - tags: - - List API responses: 200: description: Successful response diff --git a/packages/kbn-securitysolution-lists-common/api/read_list_item/read_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/read_list_item/read_list_item.schema.yaml index 3a651617163a..946320ad3c8f 100644 --- a/packages/kbn-securitysolution-lists-common/api/read_list_item/read_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/read_list_item/read_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: GetListItem x-codegen-enabled: true summary: Gets a list item - tags: - - List item API parameters: - name: id in: query diff --git a/packages/kbn-securitysolution-lists-common/api/update_list/update_list.schema.yaml b/packages/kbn-securitysolution-lists-common/api/update_list/update_list.schema.yaml index d25b21157c72..b31bea393c91 100644 --- a/packages/kbn-securitysolution-lists-common/api/update_list/update_list.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/update_list/update_list.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: UpdateList x-codegen-enabled: true summary: Updates a list - tags: - - List API requestBody: description: List's properties required: true diff --git a/packages/kbn-securitysolution-lists-common/api/update_list_item/update_list_item.schema.yaml b/packages/kbn-securitysolution-lists-common/api/update_list_item/update_list_item.schema.yaml index 21df82f4ba40..95a4df349ff9 100644 --- a/packages/kbn-securitysolution-lists-common/api/update_list_item/update_list_item.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/api/update_list_item/update_list_item.schema.yaml @@ -9,8 +9,6 @@ paths: operationId: UpdateListItem x-codegen-enabled: true summary: Updates a list item - tags: - - List item API requestBody: description: List item's properties required: true diff --git a/packages/kbn-securitysolution-lists-common/docs/openapi/ess/security_solution_lists_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-lists-common/docs/openapi/ess/security_solution_lists_api_2023_10_31.bundled.schema.yaml index 07d6236b5c51..f57fc9ae0379 100644 --- a/packages/kbn-securitysolution-lists-common/docs/openapi/ess/security_solution_lists_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/docs/openapi/ess/security_solution_lists_api_2023_10_31.bundled.schema.yaml @@ -74,7 +74,7 @@ paths: description: Internal server error response summary: Deletes a list tags: - - List API + - Security Solution Lists API get: operationId: GetList parameters: @@ -125,7 +125,7 @@ paths: description: Internal server error response summary: Retrieves a list using its id field tags: - - List API + - Security Solution Lists API patch: operationId: PatchList requestBody: @@ -192,7 +192,7 @@ paths: description: Internal server error response summary: Patches a list tags: - - List API + - Security Solution Lists API post: operationId: CreateList requestBody: @@ -266,7 +266,7 @@ paths: description: Internal server error response summary: Creates a list tags: - - List API + - Security Solution Lists API put: operationId: UpdateList requestBody: @@ -335,7 +335,7 @@ paths: description: Internal server error response summary: Updates a list tags: - - List API + - Security Solution Lists API /api/lists/_find: get: operationId: FindLists @@ -448,7 +448,7 @@ paths: description: Internal server error response summary: Finds lists tags: - - List API + - Security Solution Lists API /api/lists/index: delete: operationId: DeleteListIndex @@ -498,7 +498,7 @@ paths: description: Internal server error response summary: Deletes list data streams tags: - - List API + - Security Solution Lists API get: operationId: GetListIndex responses: @@ -550,7 +550,7 @@ paths: description: Internal server error response summary: Get list data stream existence status tags: - - List API + - Security Solution Lists API post: operationId: CreateListIndex responses: @@ -599,7 +599,7 @@ paths: description: Internal server error response summary: Creates necessary list data streams tags: - - List API + - Security Solution Lists API /api/lists/items: delete: operationId: DeleteListItem @@ -680,7 +680,7 @@ paths: description: Internal server error response summary: Deletes a list item tags: - - List item API + - Security Solution Lists API get: operationId: GetListItem parameters: @@ -747,7 +747,7 @@ paths: description: Internal server error response summary: Gets a list item tags: - - List item API + - Security Solution Lists API patch: operationId: PatchListItem requestBody: @@ -818,7 +818,7 @@ paths: description: Internal server error response summary: Patches a list item tags: - - List item API + - Security Solution Lists API post: operationId: CreateListItem requestBody: @@ -890,7 +890,7 @@ paths: description: Internal server error response summary: Creates a list item tags: - - List item API + - Security Solution Lists API put: operationId: UpdateListItem requestBody: @@ -953,7 +953,7 @@ paths: description: Internal server error response summary: Updates a list item tags: - - List item API + - Security Solution Lists API /api/lists/items/_export: post: description: Exports list item values from the specified list @@ -1008,7 +1008,7 @@ paths: description: Internal server error response summary: Exports list items tags: - - List items Import/Export API + - Security Solution Lists API /api/lists/items/_find: get: operationId: FindListItems @@ -1127,7 +1127,7 @@ paths: description: Internal server error response summary: Finds list items tags: - - List API + - Security Solution Lists API /api/lists/items/_import: post: description: > @@ -1234,7 +1234,7 @@ paths: description: Internal server error response summary: Imports list items tags: - - List items Import/Export API + - Security Solution Lists API /api/lists/privileges: get: operationId: GetListPrivileges @@ -1284,7 +1284,7 @@ paths: description: Internal server error response summary: Gets list privileges tags: - - List API + - Security Solution Lists API components: schemas: FindListItemsCursor: @@ -1520,3 +1520,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.' + name: Security Solution Lists API diff --git a/packages/kbn-securitysolution-lists-common/docs/openapi/serverless/security_solution_lists_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-lists-common/docs/openapi/serverless/security_solution_lists_api_2023_10_31.bundled.schema.yaml index 8c5bcdc93edb..2e33bf87aae1 100644 --- a/packages/kbn-securitysolution-lists-common/docs/openapi/serverless/security_solution_lists_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-lists-common/docs/openapi/serverless/security_solution_lists_api_2023_10_31.bundled.schema.yaml @@ -74,7 +74,7 @@ paths: description: Internal server error response summary: Deletes a list tags: - - List API + - Security Solution Lists API get: operationId: GetList parameters: @@ -125,7 +125,7 @@ paths: description: Internal server error response summary: Retrieves a list using its id field tags: - - List API + - Security Solution Lists API patch: operationId: PatchList requestBody: @@ -192,7 +192,7 @@ paths: description: Internal server error response summary: Patches a list tags: - - List API + - Security Solution Lists API post: operationId: CreateList requestBody: @@ -266,7 +266,7 @@ paths: description: Internal server error response summary: Creates a list tags: - - List API + - Security Solution Lists API put: operationId: UpdateList requestBody: @@ -335,7 +335,7 @@ paths: description: Internal server error response summary: Updates a list tags: - - List API + - Security Solution Lists API /api/lists/_find: get: operationId: FindLists @@ -448,7 +448,7 @@ paths: description: Internal server error response summary: Finds lists tags: - - List API + - Security Solution Lists API /api/lists/index: delete: operationId: DeleteListIndex @@ -498,7 +498,7 @@ paths: description: Internal server error response summary: Deletes list data streams tags: - - List API + - Security Solution Lists API get: operationId: GetListIndex responses: @@ -550,7 +550,7 @@ paths: description: Internal server error response summary: Get list data stream existence status tags: - - List API + - Security Solution Lists API post: operationId: CreateListIndex responses: @@ -599,7 +599,7 @@ paths: description: Internal server error response summary: Creates necessary list data streams tags: - - List API + - Security Solution Lists API /api/lists/items: delete: operationId: DeleteListItem @@ -680,7 +680,7 @@ paths: description: Internal server error response summary: Deletes a list item tags: - - List item API + - Security Solution Lists API get: operationId: GetListItem parameters: @@ -747,7 +747,7 @@ paths: description: Internal server error response summary: Gets a list item tags: - - List item API + - Security Solution Lists API patch: operationId: PatchListItem requestBody: @@ -818,7 +818,7 @@ paths: description: Internal server error response summary: Patches a list item tags: - - List item API + - Security Solution Lists API post: operationId: CreateListItem requestBody: @@ -890,7 +890,7 @@ paths: description: Internal server error response summary: Creates a list item tags: - - List item API + - Security Solution Lists API put: operationId: UpdateListItem requestBody: @@ -953,7 +953,7 @@ paths: description: Internal server error response summary: Updates a list item tags: - - List item API + - Security Solution Lists API /api/lists/items/_export: post: description: Exports list item values from the specified list @@ -1008,7 +1008,7 @@ paths: description: Internal server error response summary: Exports list items tags: - - List items Import/Export API + - Security Solution Lists API /api/lists/items/_find: get: operationId: FindListItems @@ -1127,7 +1127,7 @@ paths: description: Internal server error response summary: Finds list items tags: - - List API + - Security Solution Lists API /api/lists/items/_import: post: description: > @@ -1234,7 +1234,7 @@ paths: description: Internal server error response summary: Imports list items tags: - - List items Import/Export API + - Security Solution Lists API /api/lists/privileges: get: operationId: GetListPrivileges @@ -1284,7 +1284,7 @@ paths: description: Internal server error response summary: Gets list privileges tags: - - List API + - Security Solution Lists API components: schemas: FindListItemsCursor: @@ -1520,3 +1520,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.' + name: Security Solution Lists API diff --git a/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js b/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js index fccbf4cc34f6..7034c662aa4c 100644 --- a/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js +++ b/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js @@ -26,6 +26,13 @@ const ROOT = resolve(__dirname, '..'); title: 'Security Solution Lists API (Elastic Cloud Serverless)', description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', }, + tags: [ + { + name: 'Security Solution Lists API', + description: + 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', + }, + ], }, }, }); @@ -43,6 +50,13 @@ const ROOT = resolve(__dirname, '..'); title: 'Security Solution Lists API (Elastic Cloud and self-hosted)', description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', }, + tags: [ + { + name: 'Security Solution Lists API', + description: + 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', + }, + ], }, }, }); diff --git a/packages/presentation/presentation_containers/index.ts b/packages/presentation/presentation_containers/index.ts index 06c1d7c04ee9..224cfbb87621 100644 --- a/packages/presentation/presentation_containers/index.ts +++ b/packages/presentation/presentation_containers/index.ts @@ -13,6 +13,8 @@ export { type HasRuntimeChildState, type HasSerializedChildState, } from './interfaces/child_state'; +export { childrenUnsavedChanges$ } from './interfaces/unsaved_changes/children_unsaved_changes'; +export { initializeUnsavedChanges } from './interfaces/unsaved_changes/initialize_unsaved_changes'; export { apiHasSaveNotification, type HasSaveNotification, diff --git a/packages/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.test.ts b/packages/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.test.ts new file mode 100644 index 000000000000..d02ac5ad2e9a --- /dev/null +++ b/packages/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { BehaviorSubject, skip } from 'rxjs'; +import { childrenUnsavedChanges$, DEBOUNCE_TIME } from './children_unsaved_changes'; + +describe('childrenUnsavedChanges$', () => { + const child1Api = { + unsavedChanges: new BehaviorSubject(undefined), + resetUnsavedChanges: () => undefined, + }; + const child2Api = { + unsavedChanges: new BehaviorSubject(undefined), + resetUnsavedChanges: () => undefined, + }; + const children$ = new BehaviorSubject<{ [key: string]: unknown }>({}); + const onFireMock = jest.fn(); + + beforeEach(() => { + onFireMock.mockReset(); + child1Api.unsavedChanges.next(undefined); + child2Api.unsavedChanges.next(undefined); + children$.next({ + child1: child1Api, + child2: child2Api, + }); + }); + + test('should emit on subscribe', async () => { + const subscription = childrenUnsavedChanges$(children$).subscribe(onFireMock); + await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_TIME + 1)); + + expect(onFireMock).toHaveBeenCalledTimes(1); + const childUnsavedChanges = onFireMock.mock.calls[0][0]; + expect(childUnsavedChanges).toBeUndefined(); + + subscription.unsubscribe(); + }); + + test('should emit when child has new unsaved changes', async () => { + const subscription = childrenUnsavedChanges$(children$).pipe(skip(1)).subscribe(onFireMock); + await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_TIME + 1)); + expect(onFireMock).toHaveBeenCalledTimes(0); + + child1Api.unsavedChanges.next({ + key1: 'modified value', + }); + await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_TIME + 1)); + + expect(onFireMock).toHaveBeenCalledTimes(1); + const childUnsavedChanges = onFireMock.mock.calls[0][0]; + expect(childUnsavedChanges).toEqual({ + child1: { + key1: 'modified value', + }, + }); + + subscription.unsubscribe(); + }); + + test('should emit when children changes', async () => { + const subscription = childrenUnsavedChanges$(children$).pipe(skip(1)).subscribe(onFireMock); + await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_TIME + 1)); + expect(onFireMock).toHaveBeenCalledTimes(0); + + // add child + children$.next({ + ...children$.value, + child3: { + unsavedChanges: new BehaviorSubject({ key1: 'modified value' }), + resetUnsavedChanges: () => undefined, + }, + }); + await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_TIME + 1)); + + expect(onFireMock).toHaveBeenCalledTimes(1); + const childUnsavedChanges = onFireMock.mock.calls[0][0]; + expect(childUnsavedChanges).toEqual({ + child3: { + key1: 'modified value', + }, + }); + + subscription.unsubscribe(); + }); +}); diff --git a/packages/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts b/packages/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts new file mode 100644 index 000000000000..fa504f8eec4a --- /dev/null +++ b/packages/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { combineLatest, debounceTime, distinctUntilChanged, map, of, switchMap } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; +import { apiPublishesUnsavedChanges, PublishesUnsavedChanges } from '@kbn/presentation-publishing'; +import { PresentationContainer } from '../presentation_container'; + +export const DEBOUNCE_TIME = 100; + +/** + * Create an observable stream of unsaved changes from all react embeddable children + */ +export function childrenUnsavedChanges$(children$: PresentationContainer['children$']) { + return children$.pipe( + map((children) => Object.keys(children)), + distinctUntilChanged(deepEqual), + + // children may change, so make sure we subscribe/unsubscribe with switchMap + switchMap((newChildIds: string[]) => { + if (newChildIds.length === 0) return of([]); + const childrenThatPublishUnsavedChanges = Object.entries(children$.value).filter( + ([childId, child]) => apiPublishesUnsavedChanges(child) + ) as Array<[string, PublishesUnsavedChanges]>; + + return childrenThatPublishUnsavedChanges.length === 0 + ? of([]) + : combineLatest( + childrenThatPublishUnsavedChanges.map(([childId, child]) => + child.unsavedChanges.pipe(map((unsavedChanges) => ({ childId, unsavedChanges }))) + ) + ); + }), + debounceTime(DEBOUNCE_TIME), + map((unsavedChildStates) => { + const unsavedChildrenState: { [key: string]: object } = {}; + unsavedChildStates.forEach(({ childId, unsavedChanges }) => { + if (unsavedChanges) { + unsavedChildrenState[childId] = unsavedChanges; + } + }); + return Object.keys(unsavedChildrenState).length ? unsavedChildrenState : undefined; + }) + ); +} diff --git a/packages/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.test.ts b/packages/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.test.ts new file mode 100644 index 000000000000..fbbe33e4ffde --- /dev/null +++ b/packages/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { BehaviorSubject, Subject } from 'rxjs'; +import { + COMPARATOR_SUBJECTS_DEBOUNCE, + initializeUnsavedChanges, +} from './initialize_unsaved_changes'; +import { PublishesUnsavedChanges, StateComparators } from '@kbn/presentation-publishing'; + +interface TestState { + key1: string; + key2: string; +} + +describe('unsavedChanges api', () => { + const lastSavedState = { + key1: 'original key1 value', + key2: 'original key2 value', + } as TestState; + const key1$ = new BehaviorSubject(lastSavedState.key1); + const key2$ = new BehaviorSubject(lastSavedState.key2); + const comparators = { + key1: [key1$, (next: string) => key1$.next(next)], + key2: [key2$, (next: string) => key2$.next(next)], + } as StateComparators; + const parentApi = { + saveNotification$: new Subject(), + }; + + let api: undefined | PublishesUnsavedChanges; + beforeEach(() => { + key1$.next(lastSavedState.key1); + key2$.next(lastSavedState.key2); + ({ api } = initializeUnsavedChanges(lastSavedState, parentApi, comparators)); + }); + + test('should have no unsaved changes after initialization', () => { + expect(api?.unsavedChanges.value).toBeUndefined(); + }); + + test('should have unsaved changes when state changes', async () => { + key1$.next('modified key1 value'); + await new Promise((resolve) => setTimeout(resolve, COMPARATOR_SUBJECTS_DEBOUNCE + 1)); + expect(api?.unsavedChanges.value).toEqual({ + key1: 'modified key1 value', + }); + }); + + test('should have no unsaved changes after save', async () => { + key1$.next('modified key1 value'); + await new Promise((resolve) => setTimeout(resolve, COMPARATOR_SUBJECTS_DEBOUNCE + 1)); + expect(api?.unsavedChanges.value).not.toBeUndefined(); + + // trigger save + parentApi.saveNotification$.next(); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(api?.unsavedChanges.value).toBeUndefined(); + }); + + test('should have no unsaved changes after reset', async () => { + key1$.next('modified key1 value'); + await new Promise((resolve) => setTimeout(resolve, COMPARATOR_SUBJECTS_DEBOUNCE + 1)); + expect(api?.unsavedChanges.value).not.toBeUndefined(); + + // trigger reset + api?.resetUnsavedChanges(); + + await new Promise((resolve) => setTimeout(resolve, COMPARATOR_SUBJECTS_DEBOUNCE + 1)); + expect(api?.unsavedChanges.value).toBeUndefined(); + }); +}); diff --git a/packages/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts b/packages/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts new file mode 100644 index 000000000000..7f4770d39cd2 --- /dev/null +++ b/packages/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts @@ -0,0 +1,110 @@ +/* + * 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 { + BehaviorSubject, + combineLatest, + combineLatestWith, + debounceTime, + map, + skip, + Subscription, +} from 'rxjs'; +import { + getInitialValuesFromComparators, + PublishesUnsavedChanges, + PublishingSubject, + runComparators, + StateComparators, +} from '@kbn/presentation-publishing'; +import { HasSnapshottableState } from '../serialized_state'; +import { apiHasSaveNotification } from '../has_save_notification'; + +export const COMPARATOR_SUBJECTS_DEBOUNCE = 100; + +export const initializeUnsavedChanges = ( + initialLastSavedState: RuntimeState, + parentApi: unknown, + comparators: StateComparators +) => { + const subscriptions: Subscription[] = []; + const lastSavedState$ = new BehaviorSubject(initialLastSavedState); + + const snapshotRuntimeState = () => { + const comparatorKeys = Object.keys(comparators) as Array; + const snapshot = {} as RuntimeState; + comparatorKeys.forEach((key) => { + const comparatorSubject = comparators[key][0]; // 0th element of tuple is the subject + snapshot[key] = comparatorSubject.value as RuntimeState[typeof key]; + }); + return snapshot; + }; + + if (apiHasSaveNotification(parentApi)) { + subscriptions.push( + // any time the parent saves, the current state becomes the last saved state... + parentApi.saveNotification$.subscribe(() => { + lastSavedState$.next(snapshotRuntimeState()); + }) + ); + } + + const comparatorSubjects: Array> = []; + const comparatorKeys: Array = []; // index maps comparator subject to comparator key + for (const key of Object.keys(comparators) as Array) { + const comparatorSubject = comparators[key][0]; // 0th element of tuple is the subject + comparatorSubjects.push(comparatorSubject as PublishingSubject); + comparatorKeys.push(key); + } + + const unsavedChanges = new BehaviorSubject | undefined>( + runComparators( + comparators, + comparatorKeys, + lastSavedState$.getValue() as RuntimeState, + getInitialValuesFromComparators(comparators, comparatorKeys) + ) + ); + + subscriptions.push( + combineLatest(comparatorSubjects) + .pipe( + skip(1), + debounceTime(COMPARATOR_SUBJECTS_DEBOUNCE), + map((latestStates) => + comparatorKeys.reduce((acc, key, index) => { + acc[key] = latestStates[index] as RuntimeState[typeof key]; + return acc; + }, {} as Partial) + ), + combineLatestWith(lastSavedState$) + ) + .subscribe(([latestState, lastSavedState]) => { + unsavedChanges.next( + runComparators(comparators, comparatorKeys, lastSavedState, latestState) + ); + }) + ); + + return { + api: { + unsavedChanges, + resetUnsavedChanges: () => { + const lastSaved = lastSavedState$.getValue(); + for (const key of comparatorKeys) { + const setter = comparators[key][1]; // setter function is the 1st element of the tuple + setter(lastSaved?.[key] as RuntimeState[typeof key]); + } + }, + snapshotRuntimeState, + } as PublishesUnsavedChanges & HasSnapshottableState, + cleanup: () => { + subscriptions.forEach((subscription) => subscription.unsubscribe()); + }, + }; +}; diff --git a/packages/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts b/packages/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts index 4ac551620c37..626959f41a94 100644 --- a/packages/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts +++ b/packages/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts @@ -8,8 +8,8 @@ import { PublishingSubject } from '../publishing_subject'; -export interface PublishesUnsavedChanges { - unsavedChanges: PublishingSubject; +export interface PublishesUnsavedChanges { + unsavedChanges: PublishingSubject | undefined>; resetUnsavedChanges: () => void; } 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 a36017c9dcda..8c4299fd5fa5 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), diff --git a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts index 4fe3184f619c..89f71c074d9f 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts @@ -6,12 +6,10 @@ * Side Public License, v 1. */ import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; -import { apiPublishesUnsavedChanges, PublishesUnsavedChanges } from '@kbn/presentation-publishing'; -import deepEqual from 'fast-deep-equal'; +import { childrenUnsavedChanges$ } from '@kbn/presentation-containers'; import { omit } from 'lodash'; import { AnyAction, Middleware } from 'redux'; import { combineLatest, debounceTime, Observable, of, startWith, switchMap } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs'; import { DashboardContainer, DashboardCreationOptions } from '../..'; import { DashboardContainerInput } from '../../../../common'; import { CHANGE_CHECK_DEBOUNCE } from '../../../dashboard_constants'; @@ -84,32 +82,6 @@ export function startDiffingDashboardState( this: DashboardContainer, creationOptions?: DashboardCreationOptions ) { - /** - * Create an observable stream of unsaved changes from all react embeddable children - */ - const reactEmbeddableUnsavedChanges = this.children$.pipe( - map((children) => Object.keys(children)), - distinctUntilChanged(deepEqual), - - // children may change, so make sure we subscribe/unsubscribe with switchMap - switchMap((newChildIds: string[]) => { - if (newChildIds.length === 0) return of([]); - const childrenThatPublishUnsavedChanges = Object.entries(this.children$.value).filter( - ([childId, child]) => apiPublishesUnsavedChanges(child) - ) as Array<[string, PublishesUnsavedChanges]>; - - if (childrenThatPublishUnsavedChanges.length === 0) return of([]); - - return combineLatest( - childrenThatPublishUnsavedChanges.map(([childId, child]) => - child.unsavedChanges.pipe(map((unsavedChanges) => ({ childId, unsavedChanges }))) - ) - ); - }), - debounceTime(CHANGE_CHECK_DEBOUNCE), - map((children) => children.filter((child) => Boolean(child.unsavedChanges))) - ); - /** * Create an observable stream that checks for unsaved changes in the Dashboard state * and the state of all of its legacy embeddable children. @@ -138,30 +110,26 @@ export function startDiffingDashboardState( this.diffingSubscription.add( combineLatest([ dashboardUnsavedChanges, - reactEmbeddableUnsavedChanges, + childrenUnsavedChanges$(this.children$), this.controlGroup?.unsavedChanges ?? (of(undefined) as Observable), - ]).subscribe(([dashboardChanges, reactEmbeddableChanges, controlGroupChanges]) => { + ]).subscribe(([dashboardChanges, unsavedPanelState, controlGroupChanges]) => { // calculate unsaved changes const hasUnsavedChanges = Object.keys(omit(dashboardChanges, keysNotConsideredUnsavedChanges)).length > 0 || - reactEmbeddableChanges.length > 0 || + unsavedPanelState !== undefined || controlGroupChanges !== undefined; if (hasUnsavedChanges !== this.getState().componentState.hasUnsavedChanges) { this.dispatch.setHasUnsavedChanges(hasUnsavedChanges); } - const unsavedPanelState = reactEmbeddableChanges.reduce( - (acc, { childId, unsavedChanges }) => { - acc[childId] = unsavedChanges; - return acc; - }, - {} as UnsavedPanelState - ); - // backup unsaved changes if configured to do so if (creationOptions?.useSessionStorageIntegration) { - backupUnsavedChanges.bind(this)(dashboardChanges, unsavedPanelState, controlGroupChanges); + backupUnsavedChanges.bind(this)( + dashboardChanges, + unsavedPanelState ? unsavedPanelState : {}, + controlGroupChanges + ); } }) ); 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 864c44fd6ce0..7d71e73d1b70 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 aed3e6f9a022..2f7134e81061 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 5f6d35afe843..10fe94f82468 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 841badc11537..599b1ccd88ce 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 75ae6208be87..ffc566d10f31 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 5ec9ca4d7215..f364530faba5 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 05668e0406f9..1c300aaf7cc1 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 d467d965f012..6e34982dc91a 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 4ebbe94832d0..cb5a9d66d7f8 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 169e596a2cf9..a8c8ce3419eb 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 4e486e588b8e..59a5010e178f 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 65e029260130..df48b56922ac 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 000000000000..e36a753bd4c2 --- /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 000000000000..b1bc2bc3e3f9 --- /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 9e94c4b9b485..5d55dfa306b8 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 f9cba1592dc3..911e69e6d0d3 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 000000000000..0bde960cf002 --- /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 000000000000..e0165a4a101c --- /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 000000000000..6d57c342aa41 --- /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 000000000000..63903953f732 --- /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 000000000000..b8d24fed9ae5 --- /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 12522bbf915c..a1cefc04aa48 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 000000000000..c7e2536751a9 --- /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 000000000000..6fbc5b60a0ea --- /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 f7d780da6ef0..0f1024cf2b2b 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 17f53f30f397..7e1a6874466e 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 000000000000..adc84f24949b --- /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 000000000000..8ed7f5c40639 --- /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 000000000000..9d1cd1d6c166 --- /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 000000000000..d517f670d074 --- /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 000000000000..98be1f003db7 --- /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 000000000000..3c1f5cfa114b --- /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 000000000000..dcad9eea0f7c --- /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 000000000000..506d2d924d10 --- /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 000000000000..6c04777873dc --- /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 000000000000..874bf561bdb4 --- /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 000000000000..3959b27eb96e --- /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 000000000000..9123d00f6b94 --- /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 000000000000..ee34cf12f204 --- /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 000000000000..8b7fca5fdae1 --- /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 000000000000..b6ad5234ab9d --- /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 000000000000..0d2e4c0e1652 --- /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 1f4f2fbb7d93..22ed673cd955 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 f2555eae52aa..54a29704c91b 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 19e51decc62f..38c7116a765b 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/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx index 3d909ae28717..085798658cc7 100644 --- a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx +++ b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx @@ -123,12 +123,14 @@ describe('saved search embeddable', () => { describe('search embeddable component', () => { it('should render empty grid when empty data is returned', async () => { const { search, resolveSearch } = createSearchFnMock(0); + const initialRuntimeState = getInitialRuntimeState({ searchMock: search }); const { Component, api } = await factory.buildEmbeddable( - getInitialRuntimeState({ searchMock: search }), + initialRuntimeState, buildApiMock, uuid, mockedDashboardApi, - jest.fn().mockImplementation((newApi) => newApi) + jest.fn().mockImplementation((newApi) => newApi), + initialRuntimeState // initialRuntimeState only contains lastSavedRuntimeState ); await waitOneTick(); // wait for build to complete const discoverComponent = render(); @@ -148,15 +150,17 @@ describe('saved search embeddable', () => { it('should render field stats table in AGGREGATED_LEVEL view mode', async () => { const { search, resolveSearch } = createSearchFnMock(0); + const initialRuntimeState = getInitialRuntimeState({ + searchMock: search, + partialState: { viewMode: VIEW_MODE.AGGREGATED_LEVEL }, + }); const { Component, api } = await factory.buildEmbeddable( - getInitialRuntimeState({ - searchMock: search, - partialState: { viewMode: VIEW_MODE.AGGREGATED_LEVEL }, - }), + initialRuntimeState, buildApiMock, uuid, mockedDashboardApi, - jest.fn().mockImplementation((newApi) => newApi) + jest.fn().mockImplementation((newApi) => newApi), + initialRuntimeState // initialRuntimeState only contains lastSavedRuntimeState ); await waitOneTick(); // wait for build to complete @@ -178,15 +182,17 @@ describe('saved search embeddable', () => { describe('search embeddable api', () => { it('should not fetch data if only a new input title is set', async () => { const { search, resolveSearch } = createSearchFnMock(1); + const initialRuntimeState = getInitialRuntimeState({ + searchMock: search, + partialState: { viewMode: VIEW_MODE.AGGREGATED_LEVEL }, + }); const { api } = await factory.buildEmbeddable( - getInitialRuntimeState({ - searchMock: search, - partialState: { viewMode: VIEW_MODE.AGGREGATED_LEVEL }, - }), + initialRuntimeState, buildApiMock, uuid, mockedDashboardApi, - jest.fn().mockImplementation((newApi) => newApi) + jest.fn().mockImplementation((newApi) => newApi), + initialRuntimeState // initialRuntimeState only contains lastSavedRuntimeState ); await waitOneTick(); // wait for build to complete @@ -219,12 +225,14 @@ describe('saved search embeddable', () => { discoverServiceMock.profilesManager, 'resolveRootProfile' ); + const initialRuntimeState = getInitialRuntimeState(); await factory.buildEmbeddable( - getInitialRuntimeState(), + initialRuntimeState, buildApiMock, uuid, mockedDashboardApi, - jest.fn().mockImplementation((newApi) => newApi) + jest.fn().mockImplementation((newApi) => newApi), + initialRuntimeState // initialRuntimeState only contains lastSavedRuntimeState ); await waitOneTick(); // wait for build to complete @@ -238,12 +246,14 @@ describe('saved search embeddable', () => { discoverServiceMock.profilesManager, 'resolveDataSourceProfile' ); + const initialRuntimeState = getInitialRuntimeState(); const { api } = await factory.buildEmbeddable( - getInitialRuntimeState(), + initialRuntimeState, buildApiMock, uuid, mockedDashboardApi, - jest.fn().mockImplementation((newApi) => newApi) + jest.fn().mockImplementation((newApi) => newApi), + initialRuntimeState // initialRuntimeState only contains lastSavedRuntimeState ); await waitOneTick(); // wait for build to complete @@ -263,15 +273,17 @@ describe('saved search embeddable', () => { it('should pass cell renderers from profile', async () => { const { search, resolveSearch } = createSearchFnMock(1); + const initialRuntimeState = getInitialRuntimeState({ + searchMock: search, + partialState: { columns: ['rootProfile', 'message', 'extension'] }, + }); const { Component, api } = await factory.buildEmbeddable( - getInitialRuntimeState({ - searchMock: search, - partialState: { columns: ['rootProfile', 'message', 'extension'] }, - }), + initialRuntimeState, buildApiMock, uuid, mockedDashboardApi, - jest.fn().mockImplementation((newApi) => newApi) + jest.fn().mockImplementation((newApi) => newApi), + initialRuntimeState // initialRuntimeState only contains lastSavedRuntimeState ); await waitOneTick(); // wait for build to complete diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx index 5c753777eae9..b92556bfc6b5 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx @@ -94,7 +94,8 @@ describe('react embeddable renderer', () => { expect.any(Function), expect.any(String), expect.any(Object), - expect.any(Function) + expect.any(Function), + { bork: 'blorp?' } ); }); }); @@ -120,7 +121,8 @@ describe('react embeddable renderer', () => { expect.any(Function), '12345', expect.any(Object), - expect.any(Function) + expect.any(Function), + { bork: 'blorp?' } ); }); }); @@ -142,7 +144,8 @@ describe('react embeddable renderer', () => { expect.any(Function), expect.any(String), parentApi, - expect.any(Function) + expect.any(Function), + { bork: 'blorp?' } ); }); }); diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx index 953b57e4b207..f538c7b1164b 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx @@ -7,9 +7,11 @@ */ import { + apiHasRuntimeChildState, apiIsPresentationContainer, HasSerializedChildState, HasSnapshottableState, + initializeUnsavedChanges, SerializedPanelState, } from '@kbn/presentation-containers'; import { PresentationPanel, PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; @@ -23,7 +25,6 @@ import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import { BehaviorSubject, combineLatest, debounceTime, skip, Subscription, switchMap } from 'rxjs'; import { v4 as generateId } from 'uuid'; import { getReactEmbeddableFactory } from './react_embeddable_registry'; -import { initializeReactEmbeddableState } from './react_embeddable_state'; import { BuildReactEmbeddableApiRegistration, DefaultEmbeddableApi, @@ -115,11 +116,18 @@ export const ReactEmbeddableRenderer = < }; const buildEmbeddable = async () => { - const { initialState, startStateDiffing } = await initializeReactEmbeddableState< - SerializedState, - RuntimeState, - Api - >(uuid, factory, parentApi); + const serializedState = parentApi.getSerializedStateForChild(uuid); + const lastSavedRuntimeState = serializedState + ? await factory.deserializeState(serializedState) + : ({} as RuntimeState); + + // If the parent provides runtime state for the child (usually as a state backup or cache), + // we merge it with the last saved runtime state. + const partialRuntimeState = apiHasRuntimeChildState(parentApi) + ? parentApi.getRuntimeStateForChild(uuid) ?? ({} as Partial) + : ({} as Partial); + + const initialRuntimeState = { ...lastSavedRuntimeState, ...partialRuntimeState }; const buildApi = ( apiRegistration: BuildReactEmbeddableApiRegistration< @@ -152,32 +160,34 @@ export const ReactEmbeddableRenderer = < : Promise.resolve(apiRegistration.serializeState()); }) ) - .subscribe((serializedState) => { - onAnyStateChange(serializedState); + .subscribe((nextSerializedState) => { + onAnyStateChange(nextSerializedState); }) ); } - const { unsavedChanges, resetUnsavedChanges, cleanup, snapshotRuntimeState } = - startStateDiffing(comparators); + const unsavedChanges = initializeUnsavedChanges( + lastSavedRuntimeState, + parentApi, + comparators + ); const fullApi = setApi({ ...apiRegistration, - unsavedChanges, - resetUnsavedChanges, - snapshotRuntimeState, + ...unsavedChanges.api, } as unknown as SetReactEmbeddableApiRegistration); - cleanupFunction.current = () => cleanup(); + cleanupFunction.current = () => unsavedChanges.cleanup(); return fullApi as Api & HasSnapshottableState; }; const { api, Component } = await factory.buildEmbeddable( - initialState, + initialRuntimeState, buildApi, uuid, parentApi, - setApi + setApi, + lastSavedRuntimeState ); if (apiPublishesDataLoading(api)) { diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_state.test.ts b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_state.test.ts deleted file mode 100644 index 6f34b4f04bff..000000000000 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_state.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { - HasRuntimeChildState, - HasSaveNotification, - HasSerializedChildState, - PresentationContainer, -} from '@kbn/presentation-containers'; -import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; -import { StateComparators } from '@kbn/presentation-publishing'; -import { waitFor } from '@testing-library/react'; -import { BehaviorSubject, Subject } from 'rxjs'; -import { initializeReactEmbeddableState } from './react_embeddable_state'; -import { ReactEmbeddableFactory } from './types'; - -interface SuperTestStateType { - name: string; - age: number; - tagline: string; -} - -describe('react embeddable unsaved changes', () => { - let serializedStateForChild: SuperTestStateType; - - let comparators: StateComparators; - let parentApi: PresentationContainer & - HasSerializedChildState & - Partial> & - HasSaveNotification; - - beforeEach(() => { - serializedStateForChild = { - name: 'Sir Testsalot', - age: 42, - tagline: `Oh he's a glutton for testing!`, - }; - parentApi = { - saveNotification$: new Subject(), - ...getMockPresentationContainer(), - getSerializedStateForChild: () => ({ rawState: serializedStateForChild }), - getRuntimeStateForChild: () => undefined, - }; - }); - - const initializeDefaultComparators = () => { - const latestState: SuperTestStateType = { - ...serializedStateForChild, - ...(parentApi.getRuntimeStateForChild?.('uuid') ?? {}), - }; - const nameSubject = new BehaviorSubject(latestState.name); - const ageSubject = new BehaviorSubject(latestState.age); - const taglineSubject = new BehaviorSubject(latestState.tagline); - const defaultComparators: StateComparators = { - name: [nameSubject, jest.fn((nextName) => nameSubject.next(nextName))], - age: [ageSubject, jest.fn((nextAge) => ageSubject.next(nextAge))], - tagline: [taglineSubject, jest.fn((nextTagline) => taglineSubject.next(nextTagline))], - }; - return defaultComparators; - }; - - const startTrackingUnsavedChanges = async ( - customComparators?: StateComparators - ) => { - comparators = customComparators ?? initializeDefaultComparators(); - - const factory: ReactEmbeddableFactory = { - type: 'superTest', - deserializeState: jest.fn().mockImplementation((state) => state.rawState), - buildEmbeddable: async (runtimeState, buildApi) => { - const api = buildApi({ serializeState: jest.fn() }, comparators); - return { api, Component: () => null }; - }, - }; - const { startStateDiffing } = await initializeReactEmbeddableState('uuid', factory, parentApi); - return startStateDiffing(comparators); - }; - - it('should return undefined unsaved changes when parent API does not provide runtime state', async () => { - const unsavedChangesApi = await startTrackingUnsavedChanges(); - parentApi.getRuntimeStateForChild = undefined; - expect(unsavedChangesApi).toBeDefined(); - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - }); - - it('should return undefined unsaved changes when parent API does not have runtime state for this child', async () => { - const unsavedChangesApi = await startTrackingUnsavedChanges(); - // no change here becuase getRuntimeStateForChild already returns undefined - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - }); - - it('should return unsaved changes subject initialized to undefined when no unsaved changes are detected', async () => { - parentApi.getRuntimeStateForChild = () => ({ - name: 'Sir Testsalot', - age: 42, - tagline: `Oh he's a glutton for testing!`, - }); - const unsavedChangesApi = await startTrackingUnsavedChanges(); - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - }); - - it('should return unsaved changes subject initialized with diff when unsaved changes are detected', async () => { - parentApi.getRuntimeStateForChild = () => ({ - tagline: 'Testing is my speciality!', - }); - const unsavedChangesApi = await startTrackingUnsavedChanges(); - expect(unsavedChangesApi.unsavedChanges.value).toEqual({ - tagline: 'Testing is my speciality!', - }); - }); - - it('should detect unsaved changes when state changes during the lifetime of the component', async () => { - const unsavedChangesApi = await startTrackingUnsavedChanges(); - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - - comparators.tagline[1]('Testing is my speciality!'); - await waitFor(() => { - expect(unsavedChangesApi.unsavedChanges.value).toEqual({ - tagline: 'Testing is my speciality!', - }); - }); - }); - - it('current runtime state should become last saved state when parent save notification is triggered', async () => { - const unsavedChangesApi = await startTrackingUnsavedChanges(); - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - - comparators.tagline[1]('Testing is my speciality!'); - await waitFor(() => { - expect(unsavedChangesApi.unsavedChanges.value).toEqual({ - tagline: 'Testing is my speciality!', - }); - }); - - parentApi.saveNotification$.next(); - await waitFor(() => { - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - }); - }); - - it('should reset unsaved changes, calling given setters with last saved values. This should remove all unsaved state', async () => { - const unsavedChangesApi = await startTrackingUnsavedChanges(); - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - - comparators.tagline[1]('Testing is my speciality!'); - await waitFor(() => { - expect(unsavedChangesApi.unsavedChanges.value).toEqual({ - tagline: 'Testing is my speciality!', - }); - }); - - unsavedChangesApi.resetUnsavedChanges(); - expect(comparators.tagline[1]).toHaveBeenCalledWith(`Oh he's a glutton for testing!`); - await waitFor(() => { - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - }); - }); - - it('uses a custom comparator when supplied', async () => { - serializedStateForChild.age = 20; - parentApi.getRuntimeStateForChild = () => ({ - age: 50, - }); - const ageSubject = new BehaviorSubject(50); - const customComparators: StateComparators = { - ...initializeDefaultComparators(), - age: [ - ageSubject, - jest.fn((nextAge) => ageSubject.next(nextAge)), - (lastAge, currentAge) => lastAge?.toString().length === currentAge?.toString().length, - ], - }; - - const unsavedChangesApi = await startTrackingUnsavedChanges(customComparators); - - // here we expect there to be no unsaved changes, both unsaved state and last saved state have two digits. - expect(unsavedChangesApi.unsavedChanges.value).toBe(undefined); - - comparators.age[1](101); - - await waitFor(() => { - // here we expect there to be unsaved changes, because now the latest state has three digits. - expect(unsavedChangesApi.unsavedChanges.value).toEqual({ - age: 101, - }); - }); - }); -}); diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_state.ts b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_state.ts deleted file mode 100644 index f29d418bd396..000000000000 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_state.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { - apiHasRuntimeChildState, - apiHasSaveNotification, - HasSerializedChildState, -} from '@kbn/presentation-containers'; -import { - getInitialValuesFromComparators, - PublishingSubject, - runComparators, - StateComparators, -} from '@kbn/presentation-publishing'; -import { - BehaviorSubject, - combineLatest, - combineLatestWith, - debounceTime, - map, - Subscription, -} from 'rxjs'; -import { DefaultEmbeddableApi, ReactEmbeddableFactory } from './types'; - -export const initializeReactEmbeddableState = async < - SerializedState extends object = object, - RuntimeState extends object = SerializedState, - Api extends DefaultEmbeddableApi = DefaultEmbeddableApi< - SerializedState, - RuntimeState - > ->( - uuid: string, - factory: ReactEmbeddableFactory, - parentApi: HasSerializedChildState -) => { - const serializedState = parentApi.getSerializedStateForChild(uuid); - const lastSavedRuntimeState = serializedState - ? await factory.deserializeState(serializedState) - : ({} as RuntimeState); - - // If the parent provides runtime state for the child (usually as a state backup or cache), - // we merge it with the last saved runtime state. - const partialRuntimeState = apiHasRuntimeChildState(parentApi) - ? parentApi.getRuntimeStateForChild(uuid) ?? ({} as Partial) - : ({} as Partial); - - const initialRuntimeState = { ...lastSavedRuntimeState, ...partialRuntimeState }; - - const startStateDiffing = (comparators: StateComparators) => { - const subscription = new Subscription(); - const snapshotRuntimeState = () => { - const comparatorKeys = Object.keys(comparators) as Array; - return comparatorKeys.reduce((acc, key) => { - acc[key] = comparators[key][0].value as RuntimeState[typeof key]; - return acc; - }, {} as RuntimeState); - }; - - // the last saved state subject is always initialized with the deserialized state from the parent. - const lastSavedState$ = new BehaviorSubject(lastSavedRuntimeState); - if (apiHasSaveNotification(parentApi)) { - subscription.add( - // any time the parent saves, the current state becomes the last saved state... - parentApi.saveNotification$.subscribe(() => { - lastSavedState$.next(snapshotRuntimeState()); - }) - ); - } - - const comparatorSubjects: Array> = []; - const comparatorKeys: Array = []; - for (const key of Object.keys(comparators) as Array) { - const comparatorSubject = comparators[key][0]; // 0th element of tuple is the subject - comparatorSubjects.push(comparatorSubject as PublishingSubject); - comparatorKeys.push(key); - } - - const unsavedChanges = new BehaviorSubject | undefined>( - runComparators( - comparators, - comparatorKeys, - lastSavedState$.getValue() as RuntimeState, - getInitialValuesFromComparators(comparators, comparatorKeys) - ) - ); - - subscription.add( - combineLatest(comparatorSubjects) - .pipe( - debounceTime(100), - map((latestStates) => - comparatorKeys.reduce((acc, key, index) => { - acc[key] = latestStates[index] as RuntimeState[typeof key]; - return acc; - }, {} as Partial) - ), - combineLatestWith(lastSavedState$) - ) - .subscribe(([latest, last]) => { - unsavedChanges.next(runComparators(comparators, comparatorKeys, last, latest)); - }) - ); - return { - unsavedChanges, - resetUnsavedChanges: () => { - const lastSaved = lastSavedState$.getValue(); - for (const key of comparatorKeys) { - const setter = comparators[key][1]; // setter function is the 1st element of the tuple - setter(lastSaved?.[key] as RuntimeState[typeof key]); - } - }, - snapshotRuntimeState, - cleanup: () => subscription.unsubscribe(), - }; - }; - - return { initialState: initialRuntimeState, startStateDiffing }; -}; diff --git a/src/plugins/embeddable/public/react_embeddable_system/types.ts b/src/plugins/embeddable/public/react_embeddable_system/types.ts index e9a5a697f07e..8973cef9ce10 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/types.ts +++ b/src/plugins/embeddable/public/react_embeddable_system/types.ts @@ -106,7 +106,10 @@ export interface ReactEmbeddableFactory< * function. */ buildEmbeddable: ( - initialState: RuntimeState, + /** + * Initial runtime state. Composed from last saved state and previous sessions's unsaved changes + */ + initialRuntimeState: RuntimeState, /** * `buildApi` should be used by most embeddables that are used in dashboards, since it implements the unsaved * changes logic that the dashboard expects using the provided comparators @@ -118,6 +121,11 @@ export interface ReactEmbeddableFactory< uuid: string, parentApi: unknown | undefined, /** `setApi` should be used when the unsaved changes logic in `buildApi` is unnecessary */ - setApi: (api: SetReactEmbeddableApiRegistration) => Api + setApi: (api: SetReactEmbeddableApiRegistration) => Api, + /** + * Last saved runtime state. Different from initialRuntimeState in that it does not contain previous sessions's unsaved changes + * Compare with initialRuntimeState to flag unsaved changes on load + */ + lastSavedRuntimeState: RuntimeState ) => Promise<{ Component: React.FC<{}>; api: Api }>; } 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 594e6dee5dd7..d42a4e8b9c4c 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 c0bb4885699f..bf4fee50704f 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 eadb9db7fd70..00531b80f4a7 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 000000000000..4991aa5f36ee --- /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 7f60f92cf619..e2c91143d53f 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 72dafc0f46e7..8efa852cbfb2 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 3fdd8ab79926..82f03e7f54bb 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 d659ed49a797..5014846a6740 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/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml index cceb3c90a27a..34d7625691d2 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -65,6 +65,7 @@ paths: description: Generic Error summary: Applies a bulk action to multiple anonymization fields tags: + - Security AI Assistant API - Bulk API /api/security_ai_assistant/anonymization_fields/_find: get: @@ -150,6 +151,7 @@ paths: description: Generic Error summary: Finds anonymization fields that match the given query. tags: + - Security AI Assistant API - AnonymizationFields API /api/security_ai_assistant/chat/complete: post: @@ -184,6 +186,7 @@ paths: description: Generic Error summary: Creates a model response for the given chat conversation. tags: + - Security AI Assistant API - Chat Complete API /api/security_ai_assistant/current_user/conversations: post: @@ -217,6 +220,7 @@ paths: description: Generic Error summary: Create a conversation tags: + - Security AI Assistant API - Conversation API /api/security_ai_assistant/current_user/conversations/_find: get: @@ -302,6 +306,7 @@ paths: description: Generic Error summary: Finds conversations that match the given query. tags: + - Security AI Assistant API - Conversations API '/api/security_ai_assistant/current_user/conversations/{id}': delete: @@ -336,6 +341,7 @@ paths: description: Generic Error summary: Deletes a single conversation using the `id` field. tags: + - Security AI Assistant API - Conversation API get: description: Read a single conversation @@ -369,6 +375,7 @@ paths: description: Generic Error summary: Read a single conversation tags: + - Security AI Assistant API - Conversations API put: description: Update a single conversation @@ -408,6 +415,7 @@ paths: description: Generic Error summary: Update a conversation tags: + - Security AI Assistant API - Conversation API /api/security_ai_assistant/prompts/_bulk_action: post: @@ -463,6 +471,7 @@ paths: description: Generic Error summary: Applies a bulk action to multiple prompts tags: + - Security AI Assistant API - Bulk API /api/security_ai_assistant/prompts/_find: get: @@ -548,6 +557,7 @@ paths: description: Generic Error summary: Finds prompts that match the given query. tags: + - Security AI Assistant API - Prompts API components: schemas: @@ -1222,3 +1232,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: Manage and interact with Security Assistant resources. + name: Security AI Assistant API diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml index 9fa31b0920ab..4e5d49eb1205 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -65,6 +65,7 @@ paths: description: Generic Error summary: Applies a bulk action to multiple anonymization fields tags: + - Security AI Assistant API - Bulk API /api/security_ai_assistant/anonymization_fields/_find: get: @@ -150,6 +151,7 @@ paths: description: Generic Error summary: Finds anonymization fields that match the given query. tags: + - Security AI Assistant API - AnonymizationFields API /api/security_ai_assistant/chat/complete: post: @@ -184,6 +186,7 @@ paths: description: Generic Error summary: Creates a model response for the given chat conversation. tags: + - Security AI Assistant API - Chat Complete API /api/security_ai_assistant/current_user/conversations: post: @@ -217,6 +220,7 @@ paths: description: Generic Error summary: Create a conversation tags: + - Security AI Assistant API - Conversation API /api/security_ai_assistant/current_user/conversations/_find: get: @@ -302,6 +306,7 @@ paths: description: Generic Error summary: Finds conversations that match the given query. tags: + - Security AI Assistant API - Conversations API '/api/security_ai_assistant/current_user/conversations/{id}': delete: @@ -336,6 +341,7 @@ paths: description: Generic Error summary: Deletes a single conversation using the `id` field. tags: + - Security AI Assistant API - Conversation API get: description: Read a single conversation @@ -369,6 +375,7 @@ paths: description: Generic Error summary: Read a single conversation tags: + - Security AI Assistant API - Conversations API put: description: Update a single conversation @@ -408,6 +415,7 @@ paths: description: Generic Error summary: Update a conversation tags: + - Security AI Assistant API - Conversation API /api/security_ai_assistant/prompts/_bulk_action: post: @@ -463,6 +471,7 @@ paths: description: Generic Error summary: Applies a bulk action to multiple prompts tags: + - Security AI Assistant API - Bulk API /api/security_ai_assistant/prompts/_find: get: @@ -548,6 +557,7 @@ paths: description: Generic Error summary: Finds prompts that match the given query. tags: + - Security AI Assistant API - Prompts API components: schemas: @@ -1222,3 +1232,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: Manage and interact with Security Assistant resources. + name: Security AI Assistant API diff --git a/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js b/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js index 49ff53134ebf..63d21a2d93ae 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js +++ b/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js @@ -26,6 +26,12 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); title: 'Security AI Assistant API (Elastic Cloud Serverless)', description: 'Manage and interact with Security Assistant resources.', }, + tags: [ + { + name: 'Security AI Assistant API', + description: 'Manage and interact with Security Assistant resources.', + }, + ], }, }, }); @@ -43,6 +49,12 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); title: 'Security AI Assistant API (Elastic Cloud & self-hosted)', description: 'Manage and interact with Security Assistant resources.', }, + tags: [ + { + name: 'Security AI Assistant API', + description: 'Manage and interact with Security Assistant resources.', + }, + ], }, }, }); diff --git a/x-pack/packages/kbn-entities-schema/kibana.jsonc b/x-pack/packages/kbn-entities-schema/kibana.jsonc index 9895c2074a58..732a640df908 100644 --- a/x-pack/packages/kbn-entities-schema/kibana.jsonc +++ b/x-pack/packages/kbn-entities-schema/kibana.jsonc @@ -1,5 +1,5 @@ { "type": "shared-common", "id": "@kbn/entities-schema", - "owner": "@elastic/obs-knowledge-team" + "owner": "@elastic/obs-entities" } 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 217862e03e41..8a54757c1798 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 e8a29e8d08c4..6e9341a6d45e 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 500337e3fc06..d3bab49a0137 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 f13ab5a89d6d..7aa9d80a8906 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 a2946137cf91..837b0295fa22 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 b8dfa5682a34..cf27bce80de4 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 1d6592814526..0ff0db5b3698 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 a0f5a6dfbf31..272f482cdc8e 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 000000000000..176507e6e345 --- /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 b756876eb321..8f21bf8f1c69 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 7e53735bafab..07ec54619670 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 f1aa16f4e4f3..8bf8c0cb74a7 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 eaff14fd7c64..bf91af259249 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 1343970f04a3..f852c8cc102b 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 fcc634680283..ec8a63ea1fb6 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 000000000000..b94af60d802b --- /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 000000000000..e8a29e8d08c4 --- /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 19d4963c3cec..df61d8d9c370 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 000000000000..500337e3fc06 --- /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 000000000000..f13ab5a89d6d --- /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 bd4d6e51ccc0..720a924dddcb 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 000000000000..a2946137cf91 --- /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 f322ff2eb910..9dfcc5a454cc 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 f1899b8f4c2d..9fbd6eb4cefa 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 253a0993d1a8..09fca8ab2331 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 8a305c9601de..54cd8e9eeb9a 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 000000000000..3c195b752c85 --- /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 8165c7329b6b..c142fa932ff4 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 000000000000..30a2ff30d98e --- /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 000000000000..de895259edec --- /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 000000000000..c93d549e2b1d --- /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 bba13bc6fea3..71b878794180 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 ca049603b5c5..3e1c9fcbb1c7 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 000000000000..e8bee4e431dc --- /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 000000000000..edcea39884d9 --- /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 39c21c8aa001..c8f9b38a8387 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 000000000000..b1e768edf373 --- /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 000000000000..1343970f04a3 --- /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 faa5185404fd..cdcfd3ec3d02 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" ] } diff --git a/x-pack/plugins/observability_solution/entity_manager/kibana.jsonc b/x-pack/plugins/observability_solution/entity_manager/kibana.jsonc index 1b9a56cd77da..0b292b322c74 100644 --- a/x-pack/plugins/observability_solution/entity_manager/kibana.jsonc +++ b/x-pack/plugins/observability_solution/entity_manager/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/entityManager-plugin", - "owner": "@elastic/obs-knowledge-team", + "owner": "@elastic/obs-entities", "description": "Entity manager plugin for entity assets (inventory, topology, etc)", "plugin": { "id": "entityManager", diff --git a/x-pack/plugins/osquery/docs/openapi/ess/osquery_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/osquery/docs/openapi/ess/osquery_api_2023_10_31.bundled.schema.yaml index 6457bcb8c040..e8635fbc478e 100644 --- a/x-pack/plugins/osquery/docs/openapi/ess/osquery_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/osquery/docs/openapi/ess/osquery_api_2023_10_31.bundled.schema.yaml @@ -28,6 +28,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Find live queries + tags: + - Security Solution Osquery API post: operationId: OsqueryCreateLiveQuery requestBody: @@ -44,6 +46,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Create a live query + tags: + - Security Solution Osquery API '/api/osquery/live_queries/{id}': get: operationId: OsqueryGetLiveQueryDetails @@ -66,6 +70,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get live query details + tags: + - Security Solution Osquery API '/api/osquery/live_queries/{id}/results/{actionId}': get: operationId: OsqueryGetLiveQueryResults @@ -93,6 +99,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get live query results + tags: + - Security Solution Osquery API /api/osquery/packs: get: operationId: OsqueryFindPacks @@ -110,6 +118,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Find packs + tags: + - Security Solution Osquery API post: operationId: OsqueryCreatePacks requestBody: @@ -126,6 +136,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Create a packs + tags: + - Security Solution Osquery API '/api/osquery/packs/{id}': delete: operationId: OsqueryDeletePacks @@ -143,6 +155,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Delete packs + tags: + - Security Solution Osquery API get: operationId: OsqueryGetPacksDetails parameters: @@ -159,6 +173,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get packs details + tags: + - Security Solution Osquery API put: operationId: OsqueryUpdatePacks parameters: @@ -181,6 +197,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Update packs + tags: + - Security Solution Osquery API /api/osquery/saved_queries: get: operationId: OsqueryFindSavedQueries @@ -198,6 +216,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Find saved queries + tags: + - Security Solution Osquery API post: operationId: OsqueryCreateSavedQuery requestBody: @@ -214,6 +234,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Create a saved query + tags: + - Security Solution Osquery API '/api/osquery/saved_queries/{id}': delete: operationId: OsqueryDeleteSavedQuery @@ -231,6 +253,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Delete saved query + tags: + - Security Solution Osquery API get: operationId: OsqueryGetSavedQueryDetails parameters: @@ -247,6 +271,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get saved query details + tags: + - Security Solution Osquery API put: operationId: OsqueryUpdateSavedQuery parameters: @@ -269,6 +295,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Update saved query + tags: + - Security Solution Osquery API components: schemas: ArrayQueries: @@ -588,3 +616,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: 'Run live queries, manage packs and saved queries.' + name: Security Solution Osquery API diff --git a/x-pack/plugins/osquery/docs/openapi/serverless/osquery_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/osquery/docs/openapi/serverless/osquery_api_2023_10_31.bundled.schema.yaml index 0af3441e888c..5ee7cc382c48 100644 --- a/x-pack/plugins/osquery/docs/openapi/serverless/osquery_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/osquery/docs/openapi/serverless/osquery_api_2023_10_31.bundled.schema.yaml @@ -28,6 +28,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Find live queries + tags: + - Security Solution Osquery API post: operationId: OsqueryCreateLiveQuery requestBody: @@ -44,6 +46,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Create a live query + tags: + - Security Solution Osquery API '/api/osquery/live_queries/{id}': get: operationId: OsqueryGetLiveQueryDetails @@ -66,6 +70,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get live query details + tags: + - Security Solution Osquery API '/api/osquery/live_queries/{id}/results/{actionId}': get: operationId: OsqueryGetLiveQueryResults @@ -93,6 +99,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get live query results + tags: + - Security Solution Osquery API /api/osquery/packs: get: operationId: OsqueryFindPacks @@ -110,6 +118,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Find packs + tags: + - Security Solution Osquery API post: operationId: OsqueryCreatePacks requestBody: @@ -126,6 +136,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Create a packs + tags: + - Security Solution Osquery API '/api/osquery/packs/{id}': delete: operationId: OsqueryDeletePacks @@ -143,6 +155,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Delete packs + tags: + - Security Solution Osquery API get: operationId: OsqueryGetPacksDetails parameters: @@ -159,6 +173,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get packs details + tags: + - Security Solution Osquery API put: operationId: OsqueryUpdatePacks parameters: @@ -181,6 +197,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Update packs + tags: + - Security Solution Osquery API /api/osquery/saved_queries: get: operationId: OsqueryFindSavedQueries @@ -198,6 +216,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Find saved queries + tags: + - Security Solution Osquery API post: operationId: OsqueryCreateSavedQuery requestBody: @@ -214,6 +234,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Create a saved query + tags: + - Security Solution Osquery API '/api/osquery/saved_queries/{id}': delete: operationId: OsqueryDeleteSavedQuery @@ -231,6 +253,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Delete saved query + tags: + - Security Solution Osquery API get: operationId: OsqueryGetSavedQueryDetails parameters: @@ -247,6 +271,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Get saved query details + tags: + - Security Solution Osquery API put: operationId: OsqueryUpdateSavedQuery parameters: @@ -269,6 +295,8 @@ paths: $ref: '#/components/schemas/DefaultSuccessResponse' description: OK summary: Update saved query + tags: + - Security Solution Osquery API components: schemas: ArrayQueries: @@ -588,3 +616,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: 'Run live queries, manage packs and saved queries.' + name: Security Solution Osquery API diff --git a/x-pack/plugins/osquery/scripts/openapi/bundle.js b/x-pack/plugins/osquery/scripts/openapi/bundle.js index e68c7b497715..519b83bcc8a5 100644 --- a/x-pack/plugins/osquery/scripts/openapi/bundle.js +++ b/x-pack/plugins/osquery/scripts/openapi/bundle.js @@ -25,6 +25,12 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); title: 'Security Solution Osquery API (Elastic Cloud Serverless)', description: 'Run live queries, manage packs and saved queries.', }, + tags: [ + { + name: 'Security Solution Osquery API', + description: 'Run live queries, manage packs and saved queries.', + }, + ], }, }, }); @@ -40,6 +46,12 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); title: 'Security Solution Osquery API (Elastic Cloud and self-hosted)', description: 'Run live queries, manage packs and saved queries.', }, + tags: [ + { + name: 'Security Solution Osquery API', + description: 'Run live queries, manage packs and saved queries.', + }, + ], }, }, }); 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 3d828e0e38a7..8f53ab8c558c 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 548237265d0f..267665613b7c 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 5b3538917f78..8e6f3841b8f6 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 63aa739d2133..67428b261a0f 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 afb71bbd5bb1..9d3c3a29bdeb 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 000000000000..db07db331e47 --- /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 000000000000..0fcaf08f10c1 --- /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 97f11da2ef09..94d587cd2bfc 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 e0111b3d6787..caf7b640582a 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 b0bbc39609b3..b03b9e292132 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/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 406cb3ac5c91..0cdd0fabf9a0 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -55,6 +55,7 @@ paths: description: Internal server error response summary: Delete an alerts index tags: + - Security Solution Detections API - Alert index API get: operationId: GetAlertsIndex @@ -100,6 +101,7 @@ paths: description: Internal server error response summary: Gets the alert index name if it exists tags: + - Security Solution Detections API - Alert index API post: operationId: CreateAlertsIndex @@ -141,6 +143,7 @@ paths: description: Internal server error response summary: Create an alerts index tags: + - Security Solution Detections API - Alert index API /api/detection_engine/privileges: get: @@ -183,6 +186,7 @@ paths: description: Internal server error response summary: Returns user privileges for the Kibana space tags: + - Security Solution Detections API - Privileges API /api/detection_engine/rules: delete: @@ -210,6 +214,7 @@ paths: description: Indicates a successful call. summary: Delete a detection rule tags: + - Security Solution Detections API - Rules API get: description: Retrieve a detection rule using the `rule_id` or `id` field. @@ -236,6 +241,7 @@ paths: description: Indicates a successful call. summary: Retrieve a detection rule tags: + - Security Solution Detections API - Rules API patch: description: >- @@ -257,6 +263,7 @@ paths: description: Indicates a successful call. summary: Patch a detection rule tags: + - Security Solution Detections API - Rules API post: description: Create a new detection rule. @@ -276,6 +283,7 @@ paths: description: Indicates a successful call. summary: Create a detection rule tags: + - Security Solution Detections API - Rules API put: description: > @@ -301,6 +309,7 @@ paths: description: Indicates a successful call. summary: Update a detection rule tags: + - Security Solution Detections API - Rules API /api/detection_engine/rules/_bulk_action: post: @@ -339,6 +348,7 @@ paths: description: OK summary: Apply a bulk action to detection rules tags: + - Security Solution Detections API - Bulk API /api/detection_engine/rules/_bulk_create: post: @@ -363,6 +373,7 @@ paths: description: Indicates a successful call. summary: Create multiple detection rules tags: + - Security Solution Detections API - Bulk API /api/detection_engine/rules/_bulk_delete: delete: @@ -414,6 +425,7 @@ paths: description: Internal server error response summary: Delete multiple detection rules tags: + - Security Solution Detections API - Bulk API post: deprecated: true @@ -463,6 +475,7 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error response tags: + - Security Solution Detections API - Bulk API /api/detection_engine/rules/_bulk_update: patch: @@ -489,6 +502,7 @@ paths: description: Indicates a successful call. summary: Patch multiple detection rules tags: + - Security Solution Detections API - Bulk API put: deprecated: true @@ -520,6 +534,7 @@ paths: description: Indicates a successful call. summary: Update multiple detection rules tags: + - Security Solution Detections API - Bulk API /api/detection_engine/rules/_export: post: @@ -583,6 +598,7 @@ paths: description: Indicates a successful call. summary: Export detection rules tags: + - Security Solution Detections API - Import/Export API /api/detection_engine/rules/_find: get: @@ -657,6 +673,7 @@ paths: description: Successful response summary: List all detection rules tags: + - Security Solution Detections API - Rules API /api/detection_engine/rules/_import: post: @@ -771,6 +788,7 @@ paths: description: Indicates a successful call. summary: Import detection rules tags: + - Security Solution Detections API - Import/Export API /api/detection_engine/rules/prepackaged: put: @@ -808,6 +826,7 @@ paths: description: Indicates a successful call summary: Install prebuilt detection rules and Timelines tags: + - Security Solution Detections API - Prebuilt Rules API /api/detection_engine/rules/prepackaged/_status: get: @@ -866,6 +885,7 @@ paths: description: Indicates a successful call summary: Retrieve the status of prebuilt detection rules and Timelines tags: + - Security Solution Detections API - Prebuilt Rules API /api/detection_engine/rules/preview: post: @@ -945,6 +965,7 @@ paths: description: Internal server error response summary: Preview rule alerts generated on specified time range tags: + - Security Solution Detections API - Rule preview API /api/detection_engine/signals/assignees: post: @@ -975,6 +996,8 @@ paths: '400': description: Invalid request. summary: Assign and unassign users from detection alerts + tags: + - Security Solution Detections API /api/detection_engine/signals/finalize_migration: post: description: > @@ -1032,6 +1055,7 @@ paths: description: Internal server error response summary: Finalize detection alert migrations tags: + - Security Solution Detections API - Alerts migration API /api/detection_engine/signals/migration: delete: @@ -1099,6 +1123,7 @@ paths: description: Internal server error response summary: Clean up detection alert migrations tags: + - Security Solution Detections API - Alerts migration API post: description: > @@ -1165,6 +1190,7 @@ paths: description: Internal server error response summary: Initiate a detection alert migration tags: + - Security Solution Detections API - Alerts migration API /api/detection_engine/signals/migration_status: post: @@ -1222,6 +1248,7 @@ paths: description: Internal server error response summary: Retrieve the status of detection alert migrations tags: + - Security Solution Detections API - Alerts migration API /api/detection_engine/signals/search: post: @@ -1294,6 +1321,7 @@ paths: description: Internal server error response summary: Find and/or aggregate detection alerts tags: + - Security Solution Detections API - Alerts API /api/detection_engine/signals/status: post: @@ -1341,6 +1369,7 @@ paths: description: Internal server error response summary: Set a detection alert status tags: + - Security Solution Detections API - Alerts API /api/detection_engine/signals/tags: post: @@ -1397,6 +1426,7 @@ paths: description: Internal server error response summary: Add and remove detection alert tags tags: + - Security Solution Detections API - Alerts API /api/detection_engine/tags: get: @@ -1411,6 +1441,7 @@ paths: description: Indicates a successful call summary: List all detection rule tags tags: + - Security Solution Detections API - Tags API components: schemas: @@ -6926,3 +6957,9 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + You can create rules that automatically turn events and external alerts + sent to Elastic Security into detection alerts. These alerts are displayed + on the Detections page. + name: Security Solution Detections API diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index c23158972fd7..93e0b5298c4a 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -28,6 +28,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Actions List schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action_log/{agent_id}': get: operationId: EndpointGetActionAuditLog @@ -50,6 +52,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get action audit log schema + tags: + - Security Solution Endpoint Management API /api/endpoint/action_status: get: operationId: EndpointGetActionsStatus @@ -70,6 +74,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Actions status schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action/{action_id}': get: operationId: EndpointGetActionsDetails @@ -87,6 +93,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Action details schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action/{action_id}/file/{file_id}/download`': get: operationId: EndpointFileDownload @@ -104,6 +112,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: File Download schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action/{action_id}/file/{file_id}`': get: operationId: EndpointFileInfo @@ -121,6 +131,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: File Info schema + tags: + - Security Solution Endpoint Management API /api/endpoint/action/execute: post: operationId: EndpointExecuteAction @@ -138,6 +150,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Execute Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/get_file: post: operationId: EndpointGetFileAction @@ -155,6 +169,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get File Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/isolate: post: operationId: EndpointIsolateHostAction @@ -183,6 +199,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Isolate host Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/kill_process: post: operationId: EndpointKillProcessAction @@ -200,6 +218,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Kill process Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/running_procs: post: operationId: EndpointGetRunningProcessesAction @@ -228,6 +248,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Running Processes Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/scan: post: operationId: EndpointScanAction @@ -245,6 +267,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Scan Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/state: get: operationId: EndpointGetActionsState @@ -256,6 +280,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Action State schema + tags: + - Security Solution Endpoint Management API /api/endpoint/action/suspend_process: post: operationId: EndpointSuspendProcessAction @@ -273,6 +299,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Suspend process Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/unisolate: post: operationId: EndpointUnisolateHostAction @@ -301,6 +329,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Unisolate host Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/upload: post: operationId: EndpointUploadAction @@ -318,6 +348,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Upload Action + tags: + - Security Solution Endpoint Management API /api/endpoint/isolate: post: operationId: EndpointIsolateRedirect @@ -354,6 +386,8 @@ paths: example: /api/endpoint/action/isolate type: string summary: Permanently redirects to a new location + tags: + - Security Solution Endpoint Management API /api/endpoint/metadata: get: operationId: GetEndpointMetadataList @@ -371,6 +405,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Metadata List schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/metadata/{id}': get: operationId: GetEndpointMetadata @@ -391,6 +427,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Metadata schema + tags: + - Security Solution Endpoint Management API /api/endpoint/metadata/transforms: get: operationId: GetEndpointMetadataTransform @@ -402,6 +440,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Metadata Transform schema + tags: + - Security Solution Endpoint Management API /api/endpoint/policy_response: get: operationId: GetPolicyResponse @@ -422,6 +462,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Policy Response schema + tags: + - Security Solution Endpoint Management API /api/endpoint/policy/summaries: get: operationId: GetAgentPolicySummary @@ -445,6 +487,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Agent Policy Summary schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/protection_updates_note/{package_policy_id}': get: operationId: GetProtectionUpdatesNote @@ -462,6 +506,8 @@ paths: $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' description: OK summary: Get Protection Updates Note schema + tags: + - Security Solution Endpoint Management API post: operationId: CreateUpdateProtectionUpdatesNote parameters: @@ -487,6 +533,8 @@ paths: $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' description: OK summary: Create Update Protection Updates Note schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/suggestions/{suggestion_type}': post: operationId: GetEndpointSuggestions @@ -521,6 +569,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get suggestions + tags: + - Security Solution Endpoint Management API /api/endpoint/unisolate: post: operationId: EndpointUnisolateRedirect @@ -557,6 +607,8 @@ paths: example: /api/endpoint/action/unisolate type: string summary: Permanently redirects to a new location + tags: + - Security Solution Endpoint Management API components: schemas: AgentId: @@ -935,3 +987,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: Interact with and manage endpoints running the Elastic Defend integration. + name: Security Solution Endpoint Management API diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 2b8a646d58f4..bef21b7133be 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -42,6 +42,8 @@ paths: '400': description: Invalid request summary: Delete Criticality Record + tags: + - Security Solution Entity Analytics API get: operationId: GetAssetCriticalityRecord parameters: @@ -70,6 +72,8 @@ paths: '404': description: Criticality record not found summary: Get Criticality Record + tags: + - Security Solution Entity Analytics API post: operationId: CreateAssetCriticalityRecord requestBody: @@ -98,6 +102,8 @@ paths: '400': description: Invalid request summary: Create Criticality Record + tags: + - Security Solution Entity Analytics API /api/asset_criticality/bulk: post: operationId: BulkUpsertAssetCriticalityRecords @@ -153,6 +159,8 @@ paths: summary: >- Bulk upsert asset criticality data, creating or updating records as needed + tags: + - Security Solution Entity Analytics API /api/asset_criticality/list: post: operationId: FindAssetCriticalityRecords @@ -226,6 +234,8 @@ paths: - total description: Bulk upload successful summary: 'List asset criticality data, filtering and sorting as needed' + tags: + - Security Solution Entity Analytics API components: schemas: AssetCriticalityBulkUploadErrorItem: @@ -304,3 +314,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: '' + name: Security Solution Entity Analytics API diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 05163df07c27..65bd6b951715 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -51,6 +51,7 @@ paths: description: Indicates the note was successfully deleted. summary: Deletes a note from a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' get: description: Gets notes @@ -96,6 +97,7 @@ paths: description: Indicates the requested notes were returned. summary: Get all notes for a given document. tags: + - Security Solution Timeline API - 'access:securitySolution' patch: operationId: PersistNoteRoute @@ -159,6 +161,7 @@ paths: description: Indicates the note was successfully created. summary: Persists a note to a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/pinned_event: patch: @@ -207,6 +210,7 @@ paths: description: Indicate the event was successfully pinned in the timeline. summary: Persists a pinned event to a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline: delete: @@ -251,6 +255,7 @@ paths: description: Indicates the timeline was successfully deleted. summary: Deletes one or more timelines or timeline templates. tags: + - Security Solution Timeline API - 'access:securitySolution' get: operationId: GetTimeline @@ -287,6 +292,7 @@ paths: Get an existing saved timeline or timeline template. This API is used to retrieve an existing saved timeline or timeline template. tags: + - Security Solution Timeline API - 'access:securitySolution' patch: description: >- @@ -354,6 +360,7 @@ paths: a draft timeline. summary: Updates an existing timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' post: operationId: CreateTimelines @@ -422,6 +429,7 @@ paths: description: Indicates that there was an error in the timeline creation. summary: Creates a new timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_draft: get: @@ -486,6 +494,7 @@ paths: Retrieves the draft timeline for the current user. If the user does not have a draft timeline, an empty timeline is returned. tags: + - Security Solution Timeline API - 'access:securitySolution' post: description: > @@ -559,6 +568,7 @@ paths: timelineId. summary: Retrieves a draft timeline or timeline template. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_export: post: @@ -604,6 +614,7 @@ paths: description: Indicates that the export size limit was exceeded summary: Exports timelines as an NDJSON file tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_favorite: patch: @@ -665,6 +676,7 @@ paths: the favorite status. summary: Persists a given users favorite status of a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_import: post: @@ -754,6 +766,7 @@ paths: description: Indicates the import of timelines was unsuccessful. summary: Imports timelines. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_prepackaged: post: @@ -811,6 +824,7 @@ paths: unsuccessful. summary: Installs prepackaged timelines. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/resolve: get: @@ -850,6 +864,7 @@ paths: description: The (template) timeline was not found summary: Get an existing saved timeline or timeline template. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timelines: get: @@ -954,6 +969,7 @@ paths: This API is used to retrieve a list of existing saved timelines or timeline templates. tags: + - Security Solution Timeline API - 'access:securitySolution' components: schemas: @@ -1473,3 +1489,8 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + You can create Timelines and Timeline templates via the API, as well as + import new Timelines from an ndjson file. + name: Security Solution Timeline API diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 275457b1273c..6dec54fe63dd 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -55,6 +55,7 @@ paths: description: Internal server error response summary: Returns user privileges for the Kibana space tags: + - Security Solution Detections API - Privileges API /api/detection_engine/rules: delete: @@ -82,6 +83,7 @@ paths: description: Indicates a successful call. summary: Delete a detection rule tags: + - Security Solution Detections API - Rules API get: description: Retrieve a detection rule using the `rule_id` or `id` field. @@ -108,6 +110,7 @@ paths: description: Indicates a successful call. summary: Retrieve a detection rule tags: + - Security Solution Detections API - Rules API patch: description: >- @@ -129,6 +132,7 @@ paths: description: Indicates a successful call. summary: Patch a detection rule tags: + - Security Solution Detections API - Rules API post: description: Create a new detection rule. @@ -148,6 +152,7 @@ paths: description: Indicates a successful call. summary: Create a detection rule tags: + - Security Solution Detections API - Rules API put: description: > @@ -173,6 +178,7 @@ paths: description: Indicates a successful call. summary: Update a detection rule tags: + - Security Solution Detections API - Rules API /api/detection_engine/rules/_bulk_action: post: @@ -211,6 +217,7 @@ paths: description: OK summary: Apply a bulk action to detection rules tags: + - Security Solution Detections API - Bulk API /api/detection_engine/rules/_export: post: @@ -274,6 +281,7 @@ paths: description: Indicates a successful call. summary: Export detection rules tags: + - Security Solution Detections API - Import/Export API /api/detection_engine/rules/_find: get: @@ -348,6 +356,7 @@ paths: description: Successful response summary: List all detection rules tags: + - Security Solution Detections API - Rules API /api/detection_engine/rules/_import: post: @@ -462,6 +471,7 @@ paths: description: Indicates a successful call. summary: Import detection rules tags: + - Security Solution Detections API - Import/Export API /api/detection_engine/rules/preview: post: @@ -541,6 +551,7 @@ paths: description: Internal server error response summary: Preview rule alerts generated on specified time range tags: + - Security Solution Detections API - Rule preview API /api/detection_engine/signals/assignees: post: @@ -571,6 +582,8 @@ paths: '400': description: Invalid request. summary: Assign and unassign users from detection alerts + tags: + - Security Solution Detections API /api/detection_engine/signals/search: post: description: Find and/or aggregate detection alerts that match the given query. @@ -642,6 +655,7 @@ paths: description: Internal server error response summary: Find and/or aggregate detection alerts tags: + - Security Solution Detections API - Alerts API /api/detection_engine/signals/status: post: @@ -689,6 +703,7 @@ paths: description: Internal server error response summary: Set a detection alert status tags: + - Security Solution Detections API - Alerts API /api/detection_engine/signals/tags: post: @@ -745,6 +760,7 @@ paths: description: Internal server error response summary: Add and remove detection alert tags tags: + - Security Solution Detections API - Alerts API /api/detection_engine/tags: get: @@ -759,6 +775,7 @@ paths: description: Indicates a successful call summary: List all detection rule tags tags: + - Security Solution Detections API - Tags API components: schemas: @@ -6087,3 +6104,9 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + You can create rules that automatically turn events and external alerts + sent to Elastic Security into detection alerts. These alerts are displayed + on the Detections page. + name: Security Solution Detections API diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index 5b6a8ffc8a18..76bf0dede41b 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -28,6 +28,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Actions List schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action_log/{agent_id}': get: operationId: EndpointGetActionAuditLog @@ -50,6 +52,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get action audit log schema + tags: + - Security Solution Endpoint Management API /api/endpoint/action_status: get: operationId: EndpointGetActionsStatus @@ -70,6 +74,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Actions status schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action/{action_id}': get: operationId: EndpointGetActionsDetails @@ -87,6 +93,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Action details schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action/{action_id}/file/{file_id}/download`': get: operationId: EndpointFileDownload @@ -104,6 +112,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: File Download schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/action/{action_id}/file/{file_id}`': get: operationId: EndpointFileInfo @@ -121,6 +131,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: File Info schema + tags: + - Security Solution Endpoint Management API /api/endpoint/action/execute: post: operationId: EndpointExecuteAction @@ -138,6 +150,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Execute Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/get_file: post: operationId: EndpointGetFileAction @@ -155,6 +169,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get File Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/isolate: post: operationId: EndpointIsolateHostAction @@ -183,6 +199,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Isolate host Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/kill_process: post: operationId: EndpointKillProcessAction @@ -200,6 +218,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Kill process Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/running_procs: post: operationId: EndpointGetRunningProcessesAction @@ -228,6 +248,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Running Processes Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/scan: post: operationId: EndpointScanAction @@ -245,6 +267,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Scan Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/state: get: operationId: EndpointGetActionsState @@ -256,6 +280,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Action State schema + tags: + - Security Solution Endpoint Management API /api/endpoint/action/suspend_process: post: operationId: EndpointSuspendProcessAction @@ -273,6 +299,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Suspend process Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/unisolate: post: operationId: EndpointUnisolateHostAction @@ -301,6 +329,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Unisolate host Action + tags: + - Security Solution Endpoint Management API /api/endpoint/action/upload: post: operationId: EndpointUploadAction @@ -318,6 +348,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Upload Action + tags: + - Security Solution Endpoint Management API /api/endpoint/metadata: get: operationId: GetEndpointMetadataList @@ -335,6 +367,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Metadata List schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/metadata/{id}': get: operationId: GetEndpointMetadata @@ -355,6 +389,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Metadata schema + tags: + - Security Solution Endpoint Management API /api/endpoint/metadata/transforms: get: operationId: GetEndpointMetadataTransform @@ -366,6 +402,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Metadata Transform schema + tags: + - Security Solution Endpoint Management API /api/endpoint/policy_response: get: operationId: GetPolicyResponse @@ -386,6 +424,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Policy Response schema + tags: + - Security Solution Endpoint Management API /api/endpoint/policy/summaries: get: operationId: GetAgentPolicySummary @@ -409,6 +449,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get Agent Policy Summary schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/protection_updates_note/{package_policy_id}': get: operationId: GetProtectionUpdatesNote @@ -426,6 +468,8 @@ paths: $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' description: OK summary: Get Protection Updates Note schema + tags: + - Security Solution Endpoint Management API post: operationId: CreateUpdateProtectionUpdatesNote parameters: @@ -451,6 +495,8 @@ paths: $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' description: OK summary: Create Update Protection Updates Note schema + tags: + - Security Solution Endpoint Management API '/api/endpoint/suggestions/{suggestion_type}': post: operationId: GetEndpointSuggestions @@ -485,6 +531,8 @@ paths: $ref: '#/components/schemas/SuccessResponse' description: OK summary: Get suggestions + tags: + - Security Solution Endpoint Management API components: schemas: AgentId: @@ -863,3 +911,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: Interact with and manage endpoints running the Elastic Defend integration. + name: Security Solution Endpoint Management API diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 87188a743461..4d6b8f0c5aa7 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -42,6 +42,8 @@ paths: '400': description: Invalid request summary: Delete Criticality Record + tags: + - Security Solution Entity Analytics API get: operationId: GetAssetCriticalityRecord parameters: @@ -70,6 +72,8 @@ paths: '404': description: Criticality record not found summary: Get Criticality Record + tags: + - Security Solution Entity Analytics API post: operationId: CreateAssetCriticalityRecord requestBody: @@ -98,6 +102,8 @@ paths: '400': description: Invalid request summary: Create Criticality Record + tags: + - Security Solution Entity Analytics API /api/asset_criticality/bulk: post: operationId: BulkUpsertAssetCriticalityRecords @@ -153,6 +159,8 @@ paths: summary: >- Bulk upsert asset criticality data, creating or updating records as needed + tags: + - Security Solution Entity Analytics API /api/asset_criticality/list: post: operationId: FindAssetCriticalityRecords @@ -226,6 +234,8 @@ paths: - total description: Bulk upload successful summary: 'List asset criticality data, filtering and sorting as needed' + tags: + - Security Solution Entity Analytics API components: schemas: AssetCriticalityBulkUploadErrorItem: @@ -304,3 +314,6 @@ components: type: http security: - BasicAuth: [] +tags: + - description: '' + name: Security Solution Entity Analytics API diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 60825950b518..5a9ecaa0e78b 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -51,6 +51,7 @@ paths: description: Indicates the note was successfully deleted. summary: Deletes a note from a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' get: description: Gets notes @@ -96,6 +97,7 @@ paths: description: Indicates the requested notes were returned. summary: Get all notes for a given document. tags: + - Security Solution Timeline API - 'access:securitySolution' patch: operationId: PersistNoteRoute @@ -159,6 +161,7 @@ paths: description: Indicates the note was successfully created. summary: Persists a note to a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/pinned_event: patch: @@ -207,6 +210,7 @@ paths: description: Indicate the event was successfully pinned in the timeline. summary: Persists a pinned event to a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline: delete: @@ -251,6 +255,7 @@ paths: description: Indicates the timeline was successfully deleted. summary: Deletes one or more timelines or timeline templates. tags: + - Security Solution Timeline API - 'access:securitySolution' get: operationId: GetTimeline @@ -287,6 +292,7 @@ paths: Get an existing saved timeline or timeline template. This API is used to retrieve an existing saved timeline or timeline template. tags: + - Security Solution Timeline API - 'access:securitySolution' patch: description: >- @@ -354,6 +360,7 @@ paths: a draft timeline. summary: Updates an existing timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' post: operationId: CreateTimelines @@ -422,6 +429,7 @@ paths: description: Indicates that there was an error in the timeline creation. summary: Creates a new timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_draft: get: @@ -486,6 +494,7 @@ paths: Retrieves the draft timeline for the current user. If the user does not have a draft timeline, an empty timeline is returned. tags: + - Security Solution Timeline API - 'access:securitySolution' post: description: > @@ -559,6 +568,7 @@ paths: timelineId. summary: Retrieves a draft timeline or timeline template. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_export: post: @@ -604,6 +614,7 @@ paths: description: Indicates that the export size limit was exceeded summary: Exports timelines as an NDJSON file tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_favorite: patch: @@ -665,6 +676,7 @@ paths: the favorite status. summary: Persists a given users favorite status of a timeline. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_import: post: @@ -754,6 +766,7 @@ paths: description: Indicates the import of timelines was unsuccessful. summary: Imports timelines. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/_prepackaged: post: @@ -811,6 +824,7 @@ paths: unsuccessful. summary: Installs prepackaged timelines. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timeline/resolve: get: @@ -850,6 +864,7 @@ paths: description: The (template) timeline was not found summary: Get an existing saved timeline or timeline template. tags: + - Security Solution Timeline API - 'access:securitySolution' /api/timelines: get: @@ -954,6 +969,7 @@ paths: This API is used to retrieve a list of existing saved timelines or timeline templates. tags: + - Security Solution Timeline API - 'access:securitySolution' components: schemas: @@ -1473,3 +1489,8 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + You can create Timelines and Timeline templates via the API, as well as + import new Timelines from an ndjson file. + name: Security Solution Timeline API 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 f317f6ded4b1..e9d06cd157c2 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/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 500c327d86b0..9351e34ab4b5 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/scripts/openapi/bundle_detections.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js index ff137076a74c..0d503403b566 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js @@ -26,6 +26,13 @@ const ROOT = resolve(__dirname, '../..'); description: 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', }, + tags: [ + { + name: 'Security Solution Detections API', + description: + 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', + }, + ], }, }, }); @@ -44,6 +51,13 @@ const ROOT = resolve(__dirname, '../..'); description: 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', }, + tags: [ + { + name: 'Security Solution Detections API', + description: + 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', + }, + ], }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js index 130686ca5a69..2a63affc932b 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js @@ -25,6 +25,13 @@ const ROOT = resolve(__dirname, '../..'); title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)', description: 'Interact with and manage endpoints running the Elastic Defend integration.', }, + tags: [ + { + name: 'Security Solution Endpoint Management API', + description: + 'Interact with and manage endpoints running the Elastic Defend integration.', + }, + ], }, }, }); @@ -42,6 +49,13 @@ const ROOT = resolve(__dirname, '../..'); title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)', description: 'Interact with and manage endpoints running the Elastic Defend integration.', }, + tags: [ + { + name: 'Security Solution Endpoint Management API', + description: + 'Interact with and manage endpoints running the Elastic Defend integration.', + }, + ], }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js index 2e5413ce4a7d..3975e57f1c01 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js @@ -25,6 +25,12 @@ const ROOT = resolve(__dirname, '../..'); title: 'Security Solution Entity Analytics API (Elastic Cloud Serverless)', description: '', }, + tags: [ + { + name: 'Security Solution Entity Analytics API', + description: '', + }, + ], }, }, }); @@ -42,6 +48,12 @@ const ROOT = resolve(__dirname, '../..'); title: 'Security Solution Entity Analytics API (Elastic Cloud and self-hosted)', description: '', }, + tags: [ + { + name: 'Security Solution Entity Analytics API', + description: '', + }, + ], }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_timeline.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_timeline.js index ef3ab374be8b..a6b4a17d6cae 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_timeline.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_timeline.js @@ -26,6 +26,13 @@ const ROOT = resolve(__dirname, '../..'); description: 'You can create Timelines and Timeline templates via the API, as well as import new Timelines from an ndjson file.', }, + tags: [ + { + name: 'Security Solution Timeline API', + description: + 'You can create Timelines and Timeline templates via the API, as well as import new Timelines from an ndjson file.', + }, + ], }, }, }); @@ -44,6 +51,13 @@ const ROOT = resolve(__dirname, '../..'); description: 'You can create Timelines and Timeline templates via the API, as well as import new Timelines from an ndjson file.', }, + tags: [ + { + name: 'Security Solution Timeline API', + description: + 'You can create Timelines and Timeline templates via the API, as well as import new Timelines from an ndjson file.', + }, + ], }, }, }); 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 822c8a644d9b..960f6c87be28 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 99f7d3ff97ae..ed63f6207fec 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 711426e4df51..64bbca127ed7 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 a3b4c48d828d..7f6b80dd9290 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 9d77817a20d9..a0070503a3f8 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 6f69695f20a7..cbe434ccb25c 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 02ff12b1b91d..8feeb822bddd 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 df45eb4ddb93..59b4b4f77537 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 e537a49b498a..24b3c3816440 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 160d040f6d9f..4657d21cbcbe 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 38b48aca7e5a..f14e06fa7286 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 1d39fbaf1842..e300f012b86c 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 00806bfd4372..b3d0cc408244 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 c72a1706f089..4b1cf773a572 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 68e7f2fc50b7..ae265d415288 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 3877bb3faa6d..7d545b6d9ebb 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') 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 1b9e193bb622..efd73ddb54b0 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); 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 61a20883bbad..e03a6d2e081a 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 8ad7d97b13da..e7eb75384d67 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 2a81561199f4..7bce934099e1 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 000000000000..2808686ab0b0 --- /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 61a9684481ee..52b514a6673b 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 27c27360a28b..c7b402665e68 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 298dad659cf4..e8c8f1234aab 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')); }); }