From 15d21cb2ba64c99b26be22e6ab1158b724b560e7 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Mon, 7 Feb 2022 09:01:28 +0100 Subject: [PATCH 001/161] use dashboard locator for url generation (#124285) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/public/application/app.tsx | 2 +- .../jobs/components/custom_url_editor/utils.js | 12 +++++------- .../public/application/util/dependency_cache.ts | 16 ++++++++-------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 3c39f0367c54f..2b00d53e35e54 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -135,7 +135,7 @@ export const renderApp = ( application: coreStart.application, http: coreStart.http, security: deps.security, - urlGenerators: deps.share.urlGenerators, + dashboard: deps.dashboard, maps: deps.maps, dataVisualizer: deps.dataVisualizer, dataViews: deps.data.dataViews, diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index 623e9a69cca93..f4df795a527fa 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -10,14 +10,12 @@ import { TIME_RANGE_TYPE, URL_TYPE } from './constants'; import rison from 'rison-node'; import url from 'url'; -import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../../src/plugins/dashboard/public'; - import { getPartitioningFieldNames } from '../../../../../common/util/job_utils'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; import { ml } from '../../../services/ml_api_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; -import { getSavedObjectsClient, getGetUrlGenerator } from '../../../util/dependency_cache'; +import { getSavedObjectsClient, getDashboard } from '../../../util/dependency_cache'; export function getNewCustomUrlDefaults(job, dashboards, dataViews) { // Returns the settings object in the format used by the custom URL editor @@ -152,10 +150,10 @@ function buildDashboardUrlFromSettings(settings) { query = queryFromEntityFieldNames; } - const getUrlGenerator = getGetUrlGenerator(); - const generator = getUrlGenerator(DASHBOARD_APP_URL_GENERATOR); - return generator - .createUrl({ + const dashboard = getDashboard(); + + dashboard.locator + .getUrl({ dashboardId, timeRange: { from: '$earliest$', diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index c928b554c05ad..bba71a23f98c3 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -21,7 +21,7 @@ import type { IBasePath, } from 'kibana/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; -import type { SharePluginStart } from 'src/plugins/share/public'; +import type { DashboardStart } from 'src/plugins/dashboard/public'; import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; import type { DataViewsContract } from '../../../../../../src/plugins/data_views/public'; import type { SecurityPluginSetup } from '../../../../security/public'; @@ -45,7 +45,7 @@ export interface DependencyCache { http: HttpStart | null; security: SecurityPluginSetup | undefined | null; i18n: I18nStart | null; - urlGenerators: SharePluginStart['urlGenerators'] | null; + dashboard: DashboardStart | null; maps: MapsStartApi | null; dataVisualizer: DataVisualizerPluginStart | null; dataViews: DataViewsContract | null; @@ -68,7 +68,7 @@ const cache: DependencyCache = { http: null, security: null, i18n: null, - urlGenerators: null, + dashboard: null, maps: null, dataVisualizer: null, dataViews: null, @@ -91,7 +91,7 @@ export function setDependencyCache(deps: Partial) { cache.http = deps.http || null; cache.security = deps.security || null; cache.i18n = deps.i18n || null; - cache.urlGenerators = deps.urlGenerators || null; + cache.dashboard = deps.dashboard || null; cache.dataVisualizer = deps.dataVisualizer || null; cache.dataViews = deps.dataViews || null; } @@ -214,11 +214,11 @@ export function getI18n() { return cache.i18n; } -export function getGetUrlGenerator() { - if (cache.urlGenerators === null) { - throw new Error("urlGenerators hasn't been initialized"); +export function getDashboard() { + if (cache.dashboard === null) { + throw new Error("dashboard hasn't been initialized"); } - return cache.urlGenerators.getUrlGenerator; + return cache.dashboard; } export function getDataViews() { From 51856f9194fbaebebbf59e6a15782fa57fa025b4 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Mon, 7 Feb 2022 09:36:48 +0100 Subject: [PATCH 002/161] remove start export in ui_actions_enhanced (#121811) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ui_actions_enhanced/common/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/x-pack/plugins/ui_actions_enhanced/common/index.ts index 7b5bd71c9e8be..30192e925d6f7 100644 --- a/x-pack/plugins/ui_actions_enhanced/common/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/common/index.ts @@ -5,7 +5,9 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/109891 -/* eslint-disable @kbn/eslint/no_export_all */ - -export * from './types'; +export type { + BaseActionConfig, + SerializedAction, + SerializedEvent, + DynamicActionsState, +} from './types'; From 164eaf27f2c8d87904f31483e6d80c6e7fc5c640 Mon Sep 17 00:00:00 2001 From: Aswath <32518356+aswath86@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:56:08 +0100 Subject: [PATCH 003/161] [Vega] Add Filter custom label for kibanaAddFilter (#124498) * [Vega] Add Filter custom label for kibanaAddFilter * extend documentation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Reuter --- docs/user/dashboard/vega-reference.asciidoc | 3 ++- .../vis_types/vega/public/vega_view/vega_base_view.js | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 860261129a364..b9fdf0c9a7ec5 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -429,8 +429,9 @@ To keep signal values set `restoreSignalValuesOnRefresh: true` in the Vega confi /** * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor * @param {string} [index] as defined in Kibana, or default if missing + * @param {string} Custom label of the filter shown in the filter bar */ -kibanaAddFilter(query, index) +kibanaAddFilter(query, index, alias) /** * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index 4485e2ed855f5..ce6454ef0a0e1 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -340,10 +340,11 @@ export class VegaBaseView { /** * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor * @param {string} [index] as defined in Kibana, or default if missing + * @param {string} Elastic Query DSL's Custom label for kibanaAddFilter, as used in '+ Add Filter' */ - async addFilterHandler(query, index) { + async addFilterHandler(query, index, alias) { const indexId = await this.findIndex(index); - const filter = esFilters.buildQueryFilter(query, indexId); + const filter = esFilters.buildQueryFilter(query, indexId, alias); this._fireEvent({ name: 'applyFilter', data: { filters: [filter] } }); } From ca77565228367ace82a0bba254a4be8d6f0fb028 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 7 Feb 2022 13:18:51 +0100 Subject: [PATCH 004/161] [ML] Transforms: Fix retention policy reset (#124698) The transform edit form was not able to reset the retention policy configuration by emptying the existing form field. This fix adds a switch similar to the creation wizard to allow the user to completely enable/disable the retention policy. The form state management was updated to support passing on `{ retention_policy: null }` to reset the config. --- .../common/api_schemas/update_transforms.ts | 4 +- .../edit_transform_flyout.tsx | 4 +- .../edit_transform_flyout_form.tsx | 193 ++++++++++-------- .../use_edit_transform_flyout.test.ts | 54 ++--- .../use_edit_transform_flyout.ts | 142 ++++++++++--- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../test/functional/apps/transform/editing.ts | 83 +++++--- .../permissions/full_transform_access.ts | 9 +- .../services/transform/edit_flyout.ts | 27 ++- .../services/transform/transform_table.ts | 31 +++ 11 files changed, 365 insertions(+), 184 deletions(-) diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts index 9bd4df5108049..7f59e7ef1f052 100644 --- a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts @@ -22,7 +22,9 @@ export const postTransformsUpdateRequestSchema = schema.object({ }) ), frequency: schema.maybe(schema.string()), - retention_policy: schema.maybe(retentionPolicySchema), + // maybe: If not set, any existing `retention_policy` config will not be updated. + // nullable: If set to `null`, any existing `retention_policy` will be removed. + retention_policy: schema.maybe(schema.nullable(retentionPolicySchema)), settings: schema.maybe(settingsSchema), source: schema.maybe(sourceSchema), sync: schema.maybe(syncSchema), diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index fd8360d02eca0..b988b61c5b0b7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -35,7 +35,7 @@ import { useApi } from '../../../../hooks/use_api'; import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout'; import { EditTransformFlyoutForm } from './edit_transform_flyout_form'; import { - applyFormFieldsToTransformConfig, + applyFormStateToTransformConfig, useEditTransformFlyout, } from './use_edit_transform_flyout'; import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; @@ -60,7 +60,7 @@ export const EditTransformFlyout: FC = ({ async function submitFormHandler() { setErrorMessage(undefined); - const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields); + const requestConfig = applyFormStateToTransformConfig(config, state); const transformId = config.id; const resp = await api.updateTransform(transformId, requestConfig); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index 1bdaf2cc31763..22f31fc6139e8 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -7,7 +7,15 @@ import React, { FC, useEffect, useMemo, useState } from 'react'; -import { EuiComboBox, EuiForm, EuiAccordion, EuiSpacer, EuiSelect, EuiFormRow } from '@elastic/eui'; +import { + EuiAccordion, + EuiComboBox, + EuiForm, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -28,10 +36,12 @@ export const EditTransformFlyoutForm: FC = ({ editTransformFlyout: [state, dispatch], indexPatternId, }) => { - const formFields = state.formFields; + const { formFields, formSections } = state; const [dateFieldNames, setDateFieldNames] = useState([]); const [ingestPipelineNames, setIngestPipelineNames] = useState([]); + const isRetentionPolicyAvailable = dateFieldNames.length > 0; + const appDeps = useAppDependencies(); const indexPatternsClient = appDeps.data.indexPatterns; const api = useApi(); @@ -119,6 +129,100 @@ export const EditTransformFlyoutForm: FC = ({ + + dispatch({ + section: 'retentionPolicy', + enabled: e.target.checked, + }) + } + disabled={!isRetentionPolicyAvailable} + data-test-subj="transformEditRetentionPolicySwitch" + /> + {formSections.retentionPolicy.enabled && ( +
+ + { + // If data view or date fields info not available + // gracefully defaults to text input + indexPatternId ? ( + 0} + error={formFields.retentionPolicyField.errorMessages} + helpText={i18n.translate( + 'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText', + { + defaultMessage: + 'Select the date field that can be used to identify out of date documents in the destination index.', + } + )} + > + + dispatch({ field: 'retentionPolicyField', value: e.target.value }) + } + hasNoInitialSelection={ + !retentionDateFieldOptions + .map((d) => d.text) + .includes(formFields.retentionPolicyField.value) + } + /> + + ) : ( + dispatch({ field: 'retentionPolicyField', value })} + value={formFields.retentionPolicyField.value} + /> + ) + } + dispatch({ field: 'retentionPolicyMaxAge', value })} + value={formFields.retentionPolicyMaxAge.value} + /> +
+ )} + + + = ({ - -
- { - // If data view or date fields info not available - // gracefully defaults to text input - indexPatternId ? ( - 0} - error={formFields.retentionPolicyField.errorMessages} - helpText={i18n.translate( - 'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText', - { - defaultMessage: - 'Select the date field that can be used to identify out of date documents in the destination index.', - } - )} - > - - dispatch({ field: 'retentionPolicyField', value: e.target.value }) - } - hasNoInitialSelection={ - !retentionDateFieldOptions - .map((d) => d.text) - .includes(formFields.retentionPolicyField.value) - } - /> - - ) : ( - dispatch({ field: 'retentionPolicyField', value })} - value={formFields.retentionPolicyField.value} - /> - ) - } - dispatch({ field: 'retentionPolicyMaxAge', value })} - value={formFields.retentionPolicyMaxAge.value} - /> -
-
- - - ({ description: 'the-description', }); -describe('Transform: applyFormFieldsToTransformConfig()', () => { +describe('Transform: applyFormStateToTransformConfig()', () => { it('should exclude unchanged form fields', () => { const transformConfigMock = getTransformConfigMock(); const formState = getDefaultState(transformConfigMock); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); // This case will return an empty object. In the actual UI, this case should not happen // because the Update-Button will be disabled when no form field was changed. @@ -84,10 +81,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(4); expect(updateConfig.description).toBe('the-new-description'); @@ -108,10 +102,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(2); expect(updateConfig.description).toBe('the-updated-description'); @@ -132,10 +123,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(1); // It should include the dependent unchanged destination index expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); @@ -159,10 +147,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(1); // It should include the dependent unchanged destination index expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); @@ -177,15 +162,32 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { description: 'the-updated-description', }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(1); // It should exclude the dependent unchanged destination section expect(typeof updateConfig.dest).toBe('undefined'); expect(updateConfig.description).toBe('the-updated-description'); }); + + it('should return the config to reset retention policy', () => { + const transformConfigMock = getTransformConfigMock(); + + const formState = getDefaultState({ + ...transformConfigMock, + retention_policy: { + time: { field: 'the-time-field', max_age: '1d' }, + }, + }); + + formState.formSections.retentionPolicy.enabled = false; + + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + + expect(Object.keys(updateConfig)).toHaveLength(1); + // It should exclude the dependent unchanged destination section + expect(typeof updateConfig.dest).toBe('undefined'); + expect(updateConfig.retention_policy).toBe(null); + }); }); describe('Transform: formReducerFactory()', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index 09e9702604ef5..6c8c6ea78187c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -12,6 +12,7 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '../../../../../../common/shared_imports'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { TransformConfigUnion } from '../../../../../../common/types/transform'; import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils'; @@ -40,22 +41,39 @@ type EditTransformFormFields = | 'maxPageSearchSize' | 'retentionPolicyField' | 'retentionPolicyMaxAge'; + type EditTransformFlyoutFieldsState = Record; -// The inner reducers apply validation based on supplied attributes of each field. export interface FormField { - formFieldName: string; + formFieldName: EditTransformFormFields; configFieldName: string; defaultValue: string; dependsOn: EditTransformFormFields[]; errorMessages: string[]; isNullable: boolean; isOptional: boolean; + section?: EditTransformFormSections; validator: keyof typeof validate; value: string; valueParser: (value: string) => any; } +// Defining these sections is only necessary for options where a reset/deletion of that part of the +// configuration is supported by the API. For example, this isn't suitable to use with `dest` since +// this overall part of the configuration is not optional. However, `retention_policy` is optional, +// so we need to support to recognize this based on the form state and be able to reset it by +// created a request body containing `{ retention_policy: null }`. +type EditTransformFormSections = 'retentionPolicy'; + +export interface FormSection { + formSectionName: EditTransformFormSections; + configFieldName: string; + defaultEnabled: boolean; + enabled: boolean; +} + +type EditTransformFlyoutSectionsState = Record; + // The reducers and utility functions in this file provide the following features: // - getDefaultState() // Sets up the initial form state. It supports overrides to apply a pre-existing configuration. @@ -66,7 +84,7 @@ export interface FormField { // - formReducerFactory() / formFieldReducer() // These nested reducers take care of updating and validating the form state. // -// - applyFormFieldsToTransformConfig() (iterates over getUpdateValue()) +// - applyFormStateToTransformConfig() (iterates over getUpdateValue()) // Once a user hits the update button, these functions take care of extracting the information // necessary to create the update request. They take into account whether a field needs to // be included at all in the request (for example, if it hadn't been changed). @@ -221,18 +239,47 @@ export const initializeField = ( }; }; +export const initializeSection = ( + formSectionName: EditTransformFormSections, + configFieldName: string, + config: TransformConfigUnion, + overloads?: Partial +): FormSection => { + const defaultEnabled = overloads?.defaultEnabled ?? false; + const rawEnabled = getNestedProperty(config, configFieldName, undefined); + const enabled = rawEnabled !== undefined && rawEnabled !== null; + + return { + formSectionName, + configFieldName, + defaultEnabled, + enabled, + }; +}; + export interface EditTransformFlyoutState { formFields: EditTransformFlyoutFieldsState; + formSections: EditTransformFlyoutSectionsState; isFormTouched: boolean; isFormValid: boolean; } -// This is not a redux type action, -// since for now we only have one action type. -interface Action { +// Actions for fields and sections +interface FormFieldAction { field: EditTransformFormFields; value: string; } +function isFormFieldAction(action: unknown): action is FormFieldAction { + return isPopulatedObject(action, ['field']); +} +interface FormSectionAction { + section: EditTransformFormSections; + enabled: boolean; +} +function isFormSectionAction(action: unknown): action is FormSectionAction { + return isPopulatedObject(action, ['section']); +} +type Action = FormFieldAction | FormSectionAction; // Takes a value from form state and applies it to the structure // of the expected final configuration request object. @@ -240,12 +287,18 @@ interface Action { const getUpdateValue = ( attribute: EditTransformFormFields, config: TransformConfigUnion, - formState: EditTransformFlyoutFieldsState, + formState: EditTransformFlyoutState, enforceFormValue = false ) => { - const formStateAttribute = formState[attribute]; + const { formFields, formSections } = formState; + const formStateAttribute = formFields[attribute]; const fallbackValue = formStateAttribute.isNullable ? null : formStateAttribute.defaultValue; + const enabledBasedOnSection = + formStateAttribute.section !== undefined + ? formSections[formStateAttribute.section].enabled + : true; + const formValue = formStateAttribute.value !== '' ? formStateAttribute.valueParser(formStateAttribute.value) @@ -268,7 +321,17 @@ const getUpdateValue = ( return formValue !== configValue ? dependsOnConfig : {}; } - return formValue !== configValue || enforceFormValue + // If the resettable section the form field belongs to is disabled, + // the whole section will be set to `null` to do the actual reset. + if (formStateAttribute.section !== undefined && !enabledBasedOnSection) { + return setNestedProperty( + dependsOnConfig, + formSections[formStateAttribute.section].configFieldName, + null + ); + } + + return enabledBasedOnSection && (formValue !== configValue || enforceFormValue) ? setNestedProperty(dependsOnConfig, formStateAttribute.configFieldName, formValue) : {}; }; @@ -276,13 +339,13 @@ const getUpdateValue = ( // Takes in the form configuration and returns a // request object suitable to be sent to the // transform update API endpoint. -export const applyFormFieldsToTransformConfig = ( +export const applyFormStateToTransformConfig = ( config: TransformConfigUnion, - formState: EditTransformFlyoutFieldsState + formState: EditTransformFlyoutState ): PostTransformsUpdateRequestSchema => // Iterates over all form fields and only if necessary applies them to // the request object used for updating the transform. - (Object.keys(formState) as EditTransformFormFields[]).reduce( + (Object.keys(formState.formFields) as EditTransformFormFields[]).reduce( (updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formState)), {} ); @@ -335,7 +398,12 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo 'retentionPolicyField', 'retention_policy.time.field', config, - { dependsOn: ['retentionPolicyMaxAge'], isNullable: false, isOptional: true } + { + dependsOn: ['retentionPolicyMaxAge'], + isNullable: false, + isOptional: true, + section: 'retentionPolicy', + } ), retentionPolicyMaxAge: initializeField( 'retentionPolicyMaxAge', @@ -345,10 +413,14 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo dependsOn: ['retentionPolicyField'], isNullable: false, isOptional: true, + section: 'retentionPolicy', validator: 'retentionPolicyMaxAge', } ), }, + formSections: { + retentionPolicy: initializeSection('retentionPolicy', 'retention_policy', config), + }, isFormTouched: false, isFormValid: true, }); @@ -375,27 +447,49 @@ const formFieldReducer = (state: FormField, value: string): FormField => { }; }; +const formSectionReducer = (state: FormSection, enabled: boolean): FormSection => { + return { + ...state, + enabled, + }; +}; + +const getFieldValues = (fields: EditTransformFlyoutFieldsState) => + Object.values(fields).map((f) => f.value); +const getSectionValues = (sections: EditTransformFlyoutSectionsState) => + Object.values(sections).map((s) => s.enabled); + // Main form reducer triggers // - `formFieldReducer` to update the actions field // - compares the most recent state against the original one to update `isFormTouched` // - sets `isFormValid` to have a flag if any of the form fields contains an error. export const formReducerFactory = (config: TransformConfigUnion) => { const defaultState = getDefaultState(config); - const defaultFieldValues = Object.values(defaultState.formFields).map((f) => f.value); - - return (state: EditTransformFlyoutState, { field, value }: Action): EditTransformFlyoutState => { - const formFields = { - ...state.formFields, - [field]: formFieldReducer(state.formFields[field], value), - }; + const defaultFieldValues = getFieldValues(defaultState.formFields); + const defaultSectionValues = getSectionValues(defaultState.formSections); + + return (state: EditTransformFlyoutState, action: Action): EditTransformFlyoutState => { + const formFields = isFormFieldAction(action) + ? { + ...state.formFields, + [action.field]: formFieldReducer(state.formFields[action.field], action.value), + } + : state.formFields; + + const formSections = isFormSectionAction(action) + ? { + ...state.formSections, + [action.section]: formSectionReducer(state.formSections[action.section], action.enabled), + } + : state.formSections; return { ...state, formFields, - isFormTouched: !isEqual( - defaultFieldValues, - Object.values(formFields).map((f) => f.value) - ), + formSections, + isFormTouched: + !isEqual(defaultFieldValues, getFieldValues(formFields)) || + !isEqual(defaultSectionValues, getSectionValues(formSections)), isFormValid: isFormValid(formFields), }; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8c9548e9b1f8e..f1015055b8243 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26012,7 +26012,6 @@ "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "値は10~10000の範囲の整数でなければなりません。", "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必須フィールド。", "xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "最大年齢", - "xpack.transform.transformList.editFlyoutFormRetentionPolicyButtonContent": "保持ポリシー", "xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText": "デスティネーションインデックスで古いドキュメントを特定するために使用できる日付フィールドを選択します。", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel": "フィールド", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldSelectAriaLabel": "保持ポリシーを設定する日付フィールド", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 20f0748703770..9db8a18e535a1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26226,7 +26226,6 @@ "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "值必须是介于 10 到 10000 之间的整数。", "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必填字段。", "xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "最大存在时间", - "xpack.transform.transformList.editFlyoutFormRetentionPolicyButtonContent": "保留策略", "xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText": "选择可用于从目标索引中识别出日期文档的日期字段。", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel": "字段", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldSelectAriaLabel": "设置保留策略的日期字段", diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 154c91ef6c149..36177fcaa3016 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -6,6 +6,10 @@ */ import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import type { + TransformLatestConfig, + TransformPivotConfig, +} from '../../../../plugins/transform/common/types/transform'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getLatestTransformConfig, getPivotTransformConfig } from './index'; @@ -15,8 +19,11 @@ export default function ({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('editing', function () { - const transformConfigWithPivot = getPivotTransformConfig('editing'); - const transformConfigWithLatest = getLatestTransformConfig('editing'); + const transformConfigWithPivot: TransformPivotConfig = getPivotTransformConfig('editing'); + const transformConfigWithLatest: TransformLatestConfig = { + ...getLatestTransformConfig('editing'), + retention_policy: { time: { field: 'order_date', max_age: '1d' } }, + }; before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); @@ -51,6 +58,7 @@ export default function ({ getService }: FtrProviderContext) { transformDescription: 'updated description', transformDocsPerSecond: '1000', transformFrequency: '10m', + resetRetentionPolicy: false, transformRetentionPolicyField: 'order_date', transformRetentionPolicyMaxAge: '1d', expected: { @@ -73,13 +81,12 @@ export default function ({ getService }: FtrProviderContext) { transformDescription: 'updated description', transformDocsPerSecond: '1000', transformFrequency: '10m', - transformRetentionPolicyField: 'order_date', - transformRetentionPolicyMaxAge: '1d', + resetRetentionPolicy: true, expected: { messageText: 'updated transform.', retentionPolicy: { - field: '', - maxAge: '', + field: 'order_date', + maxAge: '1d', }, row: { status: TRANSFORM_STATE.STOPPED, @@ -150,30 +157,40 @@ export default function ({ getService }: FtrProviderContext) { ); await transform.testExecution.logTestStep('should update the transform retention policy'); - await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings(); - - await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled( - true - ); - await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectValue( - testData.expected.retentionPolicy.field - ); - await transform.editFlyout.setTransformEditFlyoutRetentionPolicyFieldSelectValue( - testData.transformRetentionPolicyField - ); - - await transform.editFlyout.assertTransformEditFlyoutInputEnabled( - 'RetentionPolicyMaxAge', - true - ); - await transform.editFlyout.assertTransformEditFlyoutInputValue( - 'RetentionPolicyMaxAge', - testData.expected.retentionPolicy.maxAge - ); - await transform.editFlyout.setTransformEditFlyoutInputValue( - 'RetentionPolicyMaxAge', - testData.transformRetentionPolicyMaxAge - ); + await transform.editFlyout.clickTransformEditRetentionPolicySettings( + !testData.resetRetentionPolicy + ); + + if ( + !testData.resetRetentionPolicy && + testData?.transformRetentionPolicyField && + testData?.transformRetentionPolicyMaxAge + ) { + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled( + true + ); + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectValue( + testData.expected.retentionPolicy.field + ); + + await transform.editFlyout.setTransformEditFlyoutRetentionPolicyFieldSelectValue( + testData.transformRetentionPolicyField + ); + + await transform.editFlyout.assertTransformEditFlyoutInputEnabled( + 'RetentionPolicyMaxAge', + true + ); + await transform.editFlyout.assertTransformEditFlyoutInputValue( + 'RetentionPolicyMaxAge', + testData.expected.retentionPolicy.maxAge + ); + + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'RetentionPolicyMaxAge', + testData.transformRetentionPolicyMaxAge + ); + } }); it('updates the transform and displays it correctly in the job list', async () => { @@ -206,6 +223,12 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep( 'should display the messages tab and include an update message' ); + + await transform.table.assertTransformExpandedRowJson( + 'retention_policy', + !testData.resetRetentionPolicy + ); + await transform.table.assertTransformExpandedRowJson('updated description'); await transform.table.assertTransformExpandedRowMessages(testData.expected.messageText); }); }); diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index 878307ec71996..fe29510e82497 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -154,14 +154,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.editFlyout.assertTransformEditFlyoutIngestPipelineFieldSelectExists(); await transform.testExecution.logTestStep( - 'should have the retention policy inputs enabled' - ); - await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings(); - await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled(true); - await transform.editFlyout.assertTransformEditFlyoutInputEnabled( - 'RetentionPolicyMaxAge', - true + 'should have the retention policy switch enabled' ); + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicySwitchEnabled(true); await transform.testExecution.logTestStep( 'should have the advanced settings inputs enabled' diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index f8cedb67aa37a..fb1d77f7abc6c 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -41,6 +41,19 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail(`transformEditFlyoutDestinationIngestPipelineFieldSelect`); }, + async assertTransformEditFlyoutRetentionPolicySwitchEnabled(expectedValue: boolean) { + await testSubjects.existOrFail(`transformEditRetentionPolicySwitch`, { + timeout: 1000, + }); + const isEnabled = await testSubjects.isEnabled(`transformEditRetentionPolicySwitch`); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditRetentionPolicySwitch' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + async assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled(expectedValue: boolean) { await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, { timeout: 1000, @@ -95,16 +108,20 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail('transformEditAccordionDestinationContent'); }, - async openTransformEditAccordionRetentionPolicySettings() { - await testSubjects.click('transformEditAccordionRetentionPolicy'); - await testSubjects.existOrFail('transformEditAccordionRetentionPolicyContent'); - }, - async openTransformEditAccordionAdvancedSettings() { await testSubjects.click('transformEditAccordionAdvancedSettings'); await testSubjects.existOrFail('transformEditAccordionAdvancedSettingsContent'); }, + async clickTransformEditRetentionPolicySettings(expectExists: boolean) { + await testSubjects.click('transformEditRetentionPolicySwitch'); + if (expectExists) { + await testSubjects.existOrFail('transformEditRetentionPolicyContent'); + } else { + await testSubjects.missingOrFail('transformEditRetentionPolicyContent'); + } + }, + async setTransformEditFlyoutInputValue(input: string, value: string) { await testSubjects.setValue(`transformEditFlyout${input}Input`, value, { clearWithKeyboard: true, diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 67b2c37438405..bd413eb2893c2 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -234,6 +234,34 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await this.switchToExpandedRowTab('transformPreviewTab', '~transformPivotPreview'); } + public async assertTransformExpandedRowJson(expectedText: string, expectedToContain = true) { + await this.ensureDetailsOpen(); + + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab'); + await testSubjects.existOrFail('~transformDetailsTabContent'); + + // Click on the JSON tab and assert the messages + await this.switchToExpandedRowTab('transformJsonTab', '~transformJsonTabContent'); + await retry.tryForTime(30 * 1000, async () => { + const actualText = await testSubjects.getVisibleText('~transformJsonTabContent'); + if (expectedToContain) { + expect(actualText.toLowerCase()).to.contain( + expectedText.toLowerCase(), + `Expected transform messages text to include '${expectedText}'` + ); + } else { + expect(actualText.toLowerCase()).to.not.contain( + expectedText.toLowerCase(), + `Expected transform messages text to not include '${expectedText}'` + ); + } + }); + + // Switch back to details tab + await this.switchToExpandedRowTab('transformDetailsTab', '~transformDetailsTabContent'); + } + public async assertTransformExpandedRowMessages(expectedText: string) { await this.ensureDetailsOpen(); @@ -250,6 +278,9 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { `Expected transform messages text to include '${expectedText}'` ); }); + + // Switch back to details tab + await this.switchToExpandedRowTab('transformDetailsTab', '~transformDetailsTabContent'); } public rowSelector(transformId: string, subSelector?: string) { From 781f80855b0e5dac6da529b0279c773c3bc3a6cf Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 7 Feb 2022 13:22:23 +0100 Subject: [PATCH 005/161] [TSVB] Save default data view (#123997) * save default index pattern in all the places * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/annotations_editor.tsx | 13 ++++++++++--- .../application/components/index_pattern.js | 10 +++++++--- .../components/panel_config/timeseries.tsx | 1 + .../application/components/panel_config/types.ts | 2 ++ .../application/components/series_config.js | 16 +++++++++++++++- .../public/application/components/vis_editor.tsx | 3 +++ .../components/vis_types/timeseries/config.js | 16 +++++++++++++++- .../public/application/editor_controller.tsx | 6 ++++-- .../timeseries/public/metrics_type.test.ts | 10 +++++----- .../vis_types/timeseries/public/metrics_type.ts | 15 ++++++++++++++- 10 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/plugins/vis_types/timeseries/public/application/components/annotations_editor.tsx b/src/plugins/vis_types/timeseries/public/application/components/annotations_editor.tsx index e524301ec7769..12e69e9043aae 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/annotations_editor.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/annotations_editor.tsx @@ -10,6 +10,7 @@ import React, { useCallback } from 'react'; import uuid from 'uuid'; import { EuiSpacer, EuiTitle, EuiButton, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { IndexPattern } from 'src/plugins/data/public'; import { AnnotationRow } from './annotation_row'; import { collectionActions, CollectionActionsProps } from './lib/collection_actions'; @@ -21,12 +22,14 @@ interface AnnotationsEditorProps { fields: VisFields; model: Panel; onChange: (partialModel: Partial) => void; + defaultIndexPattern?: IndexPattern; } -export const newAnnotation = () => ({ +export const newAnnotation = (defaultIndexPattern?: IndexPattern) => () => ({ id: uuid.v1(), color: '#F00', - index_pattern: '', + index_pattern: + defaultIndexPattern && defaultIndexPattern.id ? { id: defaultIndexPattern.id } : '', time_field: '', icon: 'fa-tag', ignore_global_filters: 1, @@ -60,7 +63,11 @@ export const AnnotationsEditor = (props: AnnotationsEditorProps) => { const { annotations } = props.model; const handleAdd = useCallback( - () => collectionActions.handleAdd(getCollectionActionsProps(props), newAnnotation), + () => + collectionActions.handleAdd( + getCollectionActionsProps(props), + newAnnotation(props.defaultIndexPattern) + ), [props] ); diff --git a/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js index aa5333f2facfe..217b3948e1cd8 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_types/timeseries/public/application/components/index_pattern.js @@ -69,6 +69,7 @@ export const IndexPattern = ({ model: _model, allowLevelOfDetail, allowIndexSwitchingMode, + baseIndexPattern, }) => { const config = getUISettings(); const timeFieldName = `${prefix}time_field`; @@ -148,9 +149,11 @@ export const IndexPattern = ({ indexPatternString: undefined, }; + const indexPatternToFetch = index || baseIndexPattern; + try { - fetchedIndexPattern = index - ? await fetchIndexPattern(index, indexPatterns, { + fetchedIndexPattern = indexPatternToFetch + ? await fetchIndexPattern(indexPatternToFetch, indexPatterns, { fetchKibanaIndexForStringIndexes: true, }) : { @@ -165,7 +168,7 @@ export const IndexPattern = ({ } fetchIndex(); - }, [index]); + }, [index, baseIndexPattern]); const toggleIndicatorDisplay = useCallback( () => onChange({ [HIDE_LAST_VALUE_INDICATOR]: !model.hide_last_value_indicator }), @@ -386,6 +389,7 @@ IndexPattern.defaultProps = { }; IndexPattern.propTypes = { + baseIndexPattern: PropTypes.oneOf([PropTypes.object, PropTypes.string]), model: PropTypes.object.isRequired, fields: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, diff --git a/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx index 7c2a0b4527a3e..a613595622b01 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/timeseries.tsx @@ -166,6 +166,7 @@ export class TimeseriesPanelConfig extends Component< fields={this.props.fields} model={this.props.model} onChange={this.props.onChange} + defaultIndexPattern={this.props.defaultIndexPattern} /> ); } else { diff --git a/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts b/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts index 653235a672825..ecbc5af601be7 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/panel_config/types.ts @@ -8,6 +8,7 @@ import { Observable } from 'rxjs'; import { IUiSettingsClient } from 'kibana/public'; +import type { IndexPattern } from 'src/plugins/data/public'; import type { TimeseriesVisData } from '../../../../common/types'; import { TimeseriesVisParams } from '../../../types'; import { VisFields } from '../../lib/fetch_fields'; @@ -18,6 +19,7 @@ export interface PanelConfigProps { visData$: Observable; getConfig: IUiSettingsClient['get']; onChange: (partialModel: Partial) => void; + defaultIndexPattern?: IndexPattern; } export enum PANEL_CONFIG_TABS { diff --git a/src/plugins/vis_types/timeseries/public/application/components/series_config.js b/src/plugins/vis_types/timeseries/public/application/components/series_config.js index 7c40bfcaa8c24..53d2fb994c919 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/series_config.js @@ -43,6 +43,19 @@ export const SeriesConfig = (props) => { ); const isKibanaIndexPattern = props.panel.use_kibana_indexes || seriesIndexPattern === ''; + const { indexPatternForQuery, onChange } = props; + const onChangeOverride = useCallback( + (partialState) => { + const stateUpdate = { ...partialState }; + const isEnabling = partialState.override_index_pattern; + if (isEnabling && !model.series_index_pattern) { + stateUpdate.series_index_pattern = indexPatternForQuery; + } + onChange(stateUpdate); + }, + [model.series_index_pattern, indexPatternForQuery, onChange] + ); + return (
@@ -126,12 +139,13 @@ export const SeriesConfig = (props) => { ; + defaultIndexPattern?: IndexPattern; } interface TimeseriesEditorState { @@ -216,6 +218,7 @@ export class VisEditor extends Component
diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js index 87e2efd818af6..2ce95093e2e3d 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/timeseries/config.js @@ -338,6 +338,19 @@ export const TimeseriesConfig = injectI18n(function (props) { ); const isKibanaIndexPattern = props.panel.use_kibana_indexes || seriesIndexPattern === ''; + const { indexPatternForQuery, onChange } = props; + const onChangeOverride = useCallback( + (partialState) => { + const stateUpdate = { ...partialState }; + const isEnabling = partialState.override_index_pattern; + if (isEnabling && !model.series_index_pattern) { + stateUpdate.series_index_pattern = indexPatternForQuery; + } + onChange(stateUpdate); + }, + [model.series_index_pattern, indexPatternForQuery, onChange] + ); + const initialPalette = model.palette ?? { type: 'palette', name: 'default', @@ -545,7 +558,7 @@ export const TimeseriesConfig = injectI18n(function (props) { @@ -556,6 +569,7 @@ export const TimeseriesConfig = injectI18n(function (props) { prefix="series_" disabled={!model.override_index_pattern} allowLevelOfDetail={true} + baseIndexPattern={indexPatternForQuery} /> diff --git a/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx index 001762b0f0d5b..bdf265b37b26c 100644 --- a/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx +++ b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx @@ -15,7 +15,7 @@ import type { IEditorController, EditorRenderProps, } from 'src/plugins/visualizations/public'; -import { getUISettings, getI18n, getCoreStart } from '../services'; +import { getUISettings, getI18n, getCoreStart, getDataStart } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; import type { TimeseriesVisParams } from '../types'; import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -30,8 +30,9 @@ export class EditorController implements IEditorController { private embeddableHandler: VisualizeEmbeddableContract ) {} - render({ timeRange, uiState, filters, query }: EditorRenderProps) { + async render({ timeRange, uiState, filters, query }: EditorRenderProps) { const I18nContext = getI18n().Context; + const defaultIndexPattern = (await getDataStart().dataViews.getDefault()) || undefined; render( @@ -45,6 +46,7 @@ export class EditorController implements IEditorController { uiState={uiState} filters={filters} query={query} + defaultIndexPattern={defaultIndexPattern} /> , diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.test.ts b/src/plugins/vis_types/timeseries/public/metrics_type.test.ts index 3d75a36cca424..f9eda5a18b79d 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.test.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.test.ts @@ -20,11 +20,6 @@ describe('metricsVisDefinition', () => { let defaultParams: TimeseriesVisParams; beforeEach(async () => { - defaultParams = ( - await metricsVisDefinition.setup!({ - params: cloneDeep(metricsVisDefinition.visConfig.defaults), - } as unknown as Vis) - ).params; setDataStart({ indexPatterns: { async getDefault() { @@ -42,6 +37,11 @@ describe('metricsVisDefinition', () => { }, } as unknown as DataViewsContract, } as DataPublicPluginStart); + defaultParams = ( + await metricsVisDefinition.setup!({ + params: cloneDeep(metricsVisDefinition.visConfig.defaults), + } as unknown as Vis) + ).params; }); it('should resolve correctly the base index pattern by id', async () => { diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 548368b30759a..5cb77efc5bcba 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -52,6 +52,19 @@ export const withReplacedIds = ( return vis; }; +async function withDefaultIndexPattern( + vis: Vis +): Promise> { + const { indexPatterns } = getDataStart(); + + const defaultIndex = await indexPatterns.getDefault(); + if (!defaultIndex || !defaultIndex.id) return vis; + vis.params.index_pattern = { + id: defaultIndex.id, + }; + return vis; +} + async function resolveIndexPattern( indexPatternValue: IndexPatternValue, indexPatterns: DataViewsContract @@ -138,7 +151,7 @@ export const metricsVisDefinition: VisTypeDefinition< drop_last_bucket: 0, }, }, - setup: (vis) => Promise.resolve(withReplacedIds(vis)), + setup: (vis) => withDefaultIndexPattern(withReplacedIds(vis)), editorConfig: { editor: TSVB_EDITOR_NAME, }, From de77a99450def1b42678f1418c2cdd81aab1b1e5 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Mon, 7 Feb 2022 13:38:40 +0100 Subject: [PATCH 006/161] Rules table refactoring and clean-up (#124344) --- .../rules/rule_switch/index.test.tsx | 90 ++++-------- .../components/rules/rule_switch/index.tsx | 80 +++-------- .../rules/rules_table/use_find_rules.ts | 128 ----------------- .../rules/use_create_rule.tsx | 2 +- .../rules/use_find_rules_query.ts | 134 ++++++++++++++++++ .../rules/use_update_rule.tsx | 2 +- .../detection_engine/rules/all/actions.ts | 10 +- .../all/bulk_actions/use_bulk_actions.tsx | 6 +- .../bulk_actions/use_bulk_edit_form_flyout.ts | 2 +- .../detection_engine/rules/all/index.test.tsx | 5 +- .../detection_engine/rules/all/index.tsx | 3 - .../__mocks__/rules_table_context.tsx | 0 .../all}/rules_table/rules_table_context.tsx | 15 +- .../rules_table/use_async_confirmation.ts | 0 .../rules/all/rules_table/use_find_rules.ts | 50 +++++++ .../rules/all}/rules_table/utils.ts | 6 +- .../rules/all/rules_table_actions.tsx | 2 +- .../rules/all/rules_table_toolbar.tsx | 4 +- .../rules/all/rules_tables.tsx | 22 +-- .../rules/all/use_columns.tsx | 2 +- .../rules/all/utility_bar.tsx | 4 +- .../rules/create/index.test.tsx | 2 +- .../detection_engine/rules/details/index.tsx | 13 +- .../rules/edit/index.test.tsx | 2 +- .../detection_engine/rules/index.test.tsx | 3 +- .../pages/detection_engine/rules/index.tsx | 46 ++---- .../detection_engine/rules/translations.ts | 2 +- .../routes/rules/perform_bulk_action_route.ts | 10 +- .../rules/bulk_action_edit.test.ts | 24 ++-- .../rules/bulk_action_edit.ts | 2 +- .../lib/detection_engine/rules/enable_rule.ts | 25 ---- .../lib/detection_engine/rules/patch_rules.ts | 3 +- .../detection_engine/rules/update_rules.ts | 3 +- 33 files changed, 309 insertions(+), 393 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts rename x-pack/plugins/security_solution/public/detections/{containers/detection_engine/rules => pages/detection_engine/rules/all}/rules_table/__mocks__/rules_table_context.tsx (100%) rename x-pack/plugins/security_solution/public/detections/{containers/detection_engine/rules => pages/detection_engine/rules/all}/rules_table/rules_table_context.tsx (96%) rename x-pack/plugins/security_solution/public/detections/{containers/detection_engine/rules => pages/detection_engine/rules/all}/rules_table/use_async_confirmation.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.ts rename x-pack/plugins/security_solution/public/detections/{containers/detection_engine/rules => pages/detection_engine/rules/all}/rules_table/utils.ts (96%) delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 53c4e5b882406..7c10fd63b463a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -10,19 +10,16 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { enableRules } from '../../../containers/detection_engine/rules'; -import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; import { RuleSwitchComponent } from './index'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; -import { useRulesTableContextOptional } from '../../../containers/detection_engine/rules/rules_table/rules_table_context'; -import { useRulesTableContextMock } from '../../../containers/detection_engine/rules/rules_table/__mocks__/rules_table_context'; +import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context'; +import { useRulesTableContextMock } from '../../../pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context'; import { TestProviders } from '../../../../common/mock'; jest.mock('../../../../common/components/toasters'); jest.mock('../../../containers/detection_engine/rules'); -jest.mock('../../../containers/detection_engine/rules/rules_table/rules_table_context'); -jest.mock('../../../pages/detection_engine/rules/all/actions'); +jest.mock('../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); describe('RuleSwitch', () => { beforeEach(() => { @@ -36,77 +33,43 @@ describe('RuleSwitch', () => { }); test('it renders loader if "isLoading" is true', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('[data-test-subj="ruleSwitchLoader"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="ruleSwitch"]').exists()).toBeFalsy(); }); test('it renders switch disabled if "isDisabled" is true', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().disabled).toBeTruthy(); }); test('it renders switch enabled if "enabled" is true', () => { - const wrapper = mount(, { + const wrapper = mount(, { wrappingComponent: TestProviders, }); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeTruthy(); }); test('it renders switch disabled if "enabled" is false', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); - expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); - }); - - test('it renders an off switch enabled on click', async () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); - wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeTruthy(); - }); - }); - - test('it renders an on switch off on click', async () => { - const rule: RulesSchema = { ...getRulesSchemaMock(), enabled: false }; - - (enableRules as jest.Mock).mockResolvedValue([rule]); - - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); - wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeFalsy(); + const wrapper = mount(, { + wrappingComponent: TestProviders, }); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); }); test('it dispatches error toaster if "enableRules" call rejects', async () => { const mockError = new Error('uh oh'); (enableRules as jest.Mock).mockRejectedValue(mockError); - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); await waitFor(() => { @@ -122,10 +85,9 @@ describe('RuleSwitch', () => { { error: { status_code: 400, message: 'error' } }, ]); - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); await waitFor(() => { @@ -134,18 +96,18 @@ describe('RuleSwitch', () => { }); }); - test('it invokes "enableRulesAction" if in rules table context', async () => { - (useRulesTableContextOptional as jest.Mock).mockReturnValue(useRulesTableContextMock.create()); + test('it calls "setLoadingRules" if in rules table context', async () => { + const rulesTableContext = useRulesTableContextMock.create(); + (useRulesTableContextOptional as jest.Mock).mockReturnValue(rulesTableContext); - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); await waitFor(() => { wrapper.update(); - expect(enableRulesAction).toHaveBeenCalledTimes(1); + expect(rulesTableContext.actions.setLoadingRules).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index e596c552e623f..893a0d4d8de8b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -12,17 +12,12 @@ import { EuiSwitch, EuiSwitchEvent, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; -import React, { useMemo, useCallback, useState, useEffect } from 'react'; - -import * as i18n from '../../../pages/detection_engine/rules/translations'; -import { enableRules } from '../../../containers/detection_engine/rules'; +import { useStateToaster } from '../../../../common/components/toasters'; +import { useUpdateRulesCache } from '../../../containers/detection_engine/rules/use_find_rules_query'; import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; -import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; -import { bucketRulesResponse } from '../../../pages/detection_engine/rules/all/helpers'; -import { useRulesTableContextOptional } from '../../../containers/detection_engine/rules/rules_table/rules_table_context'; -import { useInvalidateRules } from '../../../containers/detection_engine/rules/rules_table/use_find_rules'; +import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -38,7 +33,6 @@ export interface RuleSwitchProps { enabled: boolean; isDisabled?: boolean; isLoading?: boolean; - optionLabel?: string; onChange?: (enabled: boolean) => void; } @@ -50,70 +44,31 @@ export const RuleSwitchComponent = ({ isDisabled, isLoading, enabled, - optionLabel, onChange, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); - const [myEnabled, setMyEnabled] = useState(enabled ?? false); const [, dispatchToaster] = useStateToaster(); const rulesTableContext = useRulesTableContextOptional(); - const invalidateRules = useInvalidateRules(); + const updateRulesCache = useUpdateRulesCache(); const onRuleStateChange = useCallback( async (event: EuiSwitchEvent) => { setMyIsLoading(true); - if (rulesTableContext != null) { - await enableRulesAction( - [id], - event.target.checked, - dispatchToaster, - rulesTableContext.actions.setLoadingRules - ); - } else { - const enabling = event.target.checked; - const title = enabling - ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) - : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); - try { - const response = await enableRules({ - ids: [id], - enabled: enabling, - }); - const { rules, errors } = bucketRulesResponse(response); - - if (errors.length > 0) { - setMyIsLoading(false); - - displayErrorToast( - title, - errors.map((e) => e.error.message), - dispatchToaster - ); - } else { - const [rule] = rules; - setMyEnabled(rule.enabled); - if (onChange != null) { - onChange(rule.enabled); - } - } - } catch (err) { - setMyIsLoading(false); - displayErrorToast(title, err.message, dispatchToaster); - } + const rules = await enableRulesAction( + [id], + event.target.checked, + dispatchToaster, + rulesTableContext?.actions.setLoadingRules + ); + if (rules?.[0]) { + updateRulesCache(rules); + onChange?.(rules[0].enabled); } - invalidateRules(); setMyIsLoading(false); }, - [dispatchToaster, id, invalidateRules, onChange, rulesTableContext] + [dispatchToaster, id, onChange, rulesTableContext?.actions.setLoadingRules, updateRulesCache] ); - useEffect(() => { - if (myEnabled !== enabled) { - setMyEnabled(enabled); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [enabled]); - const showLoader = useMemo((): boolean => { if (myIsLoading !== isLoading) { return isLoading || myIsLoading; @@ -130,10 +85,9 @@ export const RuleSwitchComponent = ({ ) : ( )} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts deleted file mode 100644 index 5088ea8a3e2b0..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_find_rules.ts +++ /dev/null @@ -1,128 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback } from 'react'; -import { QueryKey, useQuery, useQueryClient, UseQueryOptions } from 'react-query'; -import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; -import { fetchRules } from '../api'; -import * as i18n from '../translations'; -import { FilterOptions, PaginationOptions, Rule, SortingOptions } from '../types'; - -interface FindRulesQueryArgs { - filterOptions?: FilterOptions; - sortingOptions?: SortingOptions; - pagination?: Pick; -} - -interface UseFindRulesArgs extends FindRulesQueryArgs { - isInMemorySorting: boolean; - refetchInterval: number | false; -} - -const MAX_RULES_PER_PAGE = 10000; -const FIND_RULES_QUERY_KEY = 'findRules'; - -/** - * This hook is used to fetch detection rules. Under the hood, it implements a - * "feature switch" that allows switching from an in-memory implementation to a - * backend-based implementation on the fly. - * - * @param args - find rules arguments - * @returns rules query result - */ -export const useFindRules = (args: UseFindRulesArgs) => { - const { pagination, filterOptions, sortingOptions, isInMemorySorting, refetchInterval } = args; - - // Use this query result when isInMemorySorting = true - const allRules = useFindRulesQuery( - getFindRulesQueryKey({ pagination, filterOptions, sortingOptions, isInMemorySorting: true }), - { pagination: { page: 1, perPage: MAX_RULES_PER_PAGE } }, - { refetchInterval, enabled: isInMemorySorting } - ); - - // Use this query result when isInMemorySorting = false - const pagedRules = useFindRulesQuery( - getFindRulesQueryKey({ pagination, filterOptions, sortingOptions, isInMemorySorting: false }), - { pagination, filterOptions, sortingOptions }, - { - refetchInterval, - enabled: !isInMemorySorting, - keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change - } - ); - - return isInMemorySorting ? allRules : pagedRules; -}; - -/** - * A helper method used to construct a query key to be used as a cache key for - * react-query - * - * @param args - query arguments - * @returns Query key - */ -export const getFindRulesQueryKey = ({ - isInMemorySorting, - filterOptions, - sortingOptions, - pagination, -}: FindRulesQueryArgs & Pick) => - isInMemorySorting - ? [FIND_RULES_QUERY_KEY, 'all'] // For the in-memory implementation we fetch data only once and cache it, thus the key is constant and do not depend on input arguments - : [FIND_RULES_QUERY_KEY, 'paged', filterOptions, sortingOptions, pagination]; - -interface RulesQueryData { - rules: Rule[]; - total: number; -} - -const useFindRulesQuery = ( - queryKey: QueryKey, - queryArgs: FindRulesQueryArgs, - queryOptions: UseQueryOptions -) => { - const { addError } = useAppToasts(); - - return useQuery( - queryKey, - async ({ signal }) => { - const response = await fetchRules({ signal, ...queryArgs }); - - return { rules: response.data, total: response.total }; - }, - { - refetchIntervalInBackground: false, - onError: (error: Error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), - ...queryOptions, - } - ); -}; - -/** - * We should use this hook to invalidate the rules cache. Any rule mutation, - * like creation, deletion, modification, or rule activation, should lead to - * cache invalidation. - * - * We invalidate all rules cache entries for simplicity so that we don't need to - * look for cache entries that contain mutated rules. - * - * @returns A rules cache invalidation callback - */ -export const useInvalidateRules = () => { - const queryClient = useQueryClient(); - - return useCallback(() => { - /** - * Invalidate all queries that start with FIND_RULES_QUERY_KEY. This - * includes the in-memory query cache and paged query cache. - */ - queryClient.invalidateQueries(FIND_RULES_QUERY_KEY, { - refetchActive: true, - refetchInactive: false, - }); - }, [queryClient]); -}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index 86d10d16a4a23..f71f20cb6c189 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -13,7 +13,7 @@ import { CreateRulesSchema } from '../../../../../common/detection_engine/schema import { createRule } from './api'; import * as i18n from './translations'; import { transformOutput } from './transforms'; -import { useInvalidateRules } from './rules_table/use_find_rules'; +import { useInvalidateRules } from './use_find_rules_query'; interface CreateRuleReturn { isLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts new file mode 100644 index 0000000000000..47778be0d9c91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_find_rules_query.ts @@ -0,0 +1,134 @@ +/* + * 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 { useCallback } from 'react'; +import { useQuery, useQueryClient, UseQueryOptions } from 'react-query'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { fetchRules } from './api'; +import * as i18n from './translations'; +import { FilterOptions, PaginationOptions, Rule, SortingOptions } from './types'; + +export interface FindRulesQueryArgs { + filterOptions?: FilterOptions; + sortingOptions?: SortingOptions; + pagination?: Pick; +} + +const FIND_RULES_QUERY_KEY = 'findRules'; + +export interface RulesQueryData { + rules: Rule[]; + total: number; +} + +/** + * A wrapper around useQuery provides default values to the underlying query, + * like query key, abortion signal, and error handler. + * + * @param queryPrefix - query prefix used to differentiate the query from other + * findRules queries + * @param queryArgs - fetch rules filters/pagination + * @param queryOptions - react-query options + * @returns useQuery result + */ +export const useFindRulesQuery = ( + queryPrefix: string[], + queryArgs: FindRulesQueryArgs, + queryOptions: UseQueryOptions< + RulesQueryData, + Error, + RulesQueryData, + [...string[], FindRulesQueryArgs] + > +) => { + const { addError } = useAppToasts(); + + return useQuery( + [FIND_RULES_QUERY_KEY, ...queryPrefix, queryArgs], + async ({ signal }) => { + const response = await fetchRules({ signal, ...queryArgs }); + + return { rules: response.data, total: response.total }; + }, + { + refetchIntervalInBackground: false, + onError: (error: Error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), + ...queryOptions, + } + ); +}; + +/** + * We should use this hook to invalidate the rules cache. For example, rule + * mutations that affect rule set size, like creation or deletion, should lead + * to cache invalidation. + * + * @returns A rules cache invalidation callback + */ +export const useInvalidateRules = () => { + const queryClient = useQueryClient(); + + return useCallback(() => { + /** + * Invalidate all queries that start with FIND_RULES_QUERY_KEY. This + * includes the in-memory query cache and paged query cache. + */ + queryClient.invalidateQueries(FIND_RULES_QUERY_KEY, { + refetchActive: true, + refetchInactive: false, + }); + }, [queryClient]); +}; + +/** + * We should use this hook to update the rules cache when modifying rules + * without changing the rules collection size. Use it with the new rules data + * after operations like bulk or single rule edit or rule activation, but not + * when adding or removing rules. When adding/removing rules, we should + * invalidate the cache instead. + * + * @returns A rules cache update callback + */ +export const useUpdateRulesCache = () => { + const queryClient = useQueryClient(); + /** + * Use this method to update rules data cached by react-query. + * It is useful when we receive new rules back from a mutation query (bulk edit, etc.); + * we can merge those rules with the existing cache to avoid an extra roundtrip to re-fetch updated rules. + */ + return useCallback( + (newRules: Rule[]) => { + queryClient.setQueriesData['data']>( + FIND_RULES_QUERY_KEY, + (currentData) => + currentData + ? { + rules: updateRules(currentData.rules, newRules), + total: currentData.total, + } + : undefined + ); + }, + [queryClient] + ); +}; + +/** + * Update cached rules with the new ones + * + * @param currentRules + * @param newRules + */ +export function updateRules(currentRules: Rule[], newRules: Rule[]): Rule[] { + const newRulesMap = new Map(newRules.map((rule) => [rule.id, rule])); + + if (currentRules.some((rule) => newRulesMap.has(rule.id))) { + return currentRules.map((rule) => newRulesMap.get(rule.id) ?? rule); + } + + return currentRules; +} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx index aee965a8b8ef0..3f48f55cad4a4 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx @@ -14,7 +14,7 @@ import { transformOutput } from './transforms'; import { updateRule } from './api'; import * as i18n from './translations'; -import { useInvalidateRules } from './rules_table/use_find_rules'; +import { useInvalidateRules } from './use_find_rules_query'; interface UpdateRuleReturn { isLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts index 951d71162a9a2..5b2fdbc5e290e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts @@ -33,7 +33,7 @@ import { performBulkAction, Rule, } from '../../../../containers/detection_engine/rules'; -import { RulesTableActions } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { RulesTableActions } from './rules_table/rules_table_context'; import { transformOutput } from '../../../../containers/detection_engine/rules/transforms'; import * as i18n from '../translations'; import { bucketRulesResponse, getExportedRulesCounts } from './helpers'; @@ -135,14 +135,14 @@ export const enableRulesAction = async ( ids: string[], enabled: boolean, dispatchToaster: Dispatch, - setLoadingRules: RulesTableActions['setLoadingRules'] + setLoadingRules?: RulesTableActions['setLoadingRules'] ) => { const errorTitle = enabled ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(ids.length) : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length); try { - setLoadingRules({ ids, action: enabled ? 'enable' : 'disable' }); + setLoadingRules?.({ ids, action: enabled ? 'enable' : 'disable' }); const response = await enableRules({ ids, enabled }); const { rules, errors } = bucketRulesResponse(response); @@ -167,10 +167,12 @@ export const enableRulesAction = async ( enabled ? TELEMETRY_EVENT.CUSTOM_RULE_ENABLED : TELEMETRY_EVENT.CUSTOM_RULE_DISABLED ); } + + return rules; } catch (e) { displayErrorToast(errorTitle, [e.message], dispatchToaster); } finally { - setLoadingRules({ ids: [], action: null }); + setLoadingRules?.({ ids: [], action: null }); } }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index a31240fae29ae..14b6e394cb146 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -28,7 +28,7 @@ import { import { isMlRule } from '../../../../../../../common/machine_learning/helpers'; import { displayWarningToast, useStateToaster } from '../../../../../../common/components/toasters'; import { canEditRuleWithActions } from '../../../../../../common/utils/privileges'; -import { useRulesTableContext } from '../../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { useRulesTableContext } from '../rules_table/rules_table_context'; import * as detectionI18n from '../../../translations'; import * as i18n from '../../translations'; import { @@ -48,7 +48,7 @@ import { convertRulesFilterToKQL } from '../../../../../containers/detection_eng import type { FilterOptions } from '../../../../../containers/detection_engine/rules/types'; import type { BulkActionPartialErrorResponse } from '../../../../../../../common/detection_engine/schemas/response/perform_bulk_action_schema'; import type { HTTPError } from '../../../../../../../common/detection_engine/types'; -import { useInvalidateRules } from '../../../../../containers/detection_engine/rules/rules_table/use_find_rules'; +import { useInvalidateRules } from '../../../../../containers/detection_engine/rules/use_find_rules_query'; interface UseBulkActionsArgs { filterOptions: FilterOptions; @@ -310,7 +310,7 @@ export const useBulkActions = ({ error.stack = JSON.stringify(error.body, null, 2); toasts.addError(error, { title: i18n.BULK_EDIT_ERROR_TOAST_TITLE, - toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCIRPTION(failedRulesCount), + toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCRIPTION(failedRulesCount), }); } catch (e) { // toast error has failed diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts index 1f3dad4d50aae..19f01bddae0f5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts @@ -5,7 +5,7 @@ * 2.0. */ import { useState, useCallback, useRef } from 'react'; -import { useAsyncConfirmation } from '../../../../../containers/detection_engine/rules/rules_table/use_async_confirmation'; +import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; import { BulkActionEditType, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index ba9cb5eded38f..3b24dda539174 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -17,7 +17,7 @@ import { AllRules } from './index'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/rules'); -jest.mock('../../../../containers/detection_engine/rules/rules_table/rules_table_context'); +jest.mock('../../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); const useKibanaMock = useKibana as jest.Mocked; @@ -47,7 +47,6 @@ describe('AllRules', () => { rulesInstalled={0} rulesNotInstalled={0} rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} /> ); @@ -67,7 +66,6 @@ describe('AllRules', () => { rulesInstalled={0} rulesNotInstalled={0} rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} /> ); @@ -91,7 +89,6 @@ describe('AllRules', () => { rulesInstalled={0} rulesNotInstalled={0} rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 2cf26bbbd23ea..e8c7742125c74 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -20,7 +20,6 @@ interface AllRulesProps { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: () => Promise) => void; } /** @@ -41,7 +40,6 @@ export const AllRules = React.memo( rulesInstalled, rulesNotInstalled, rulesNotUpdated, - setRefreshRulesData, }) => { const [activeTab, setActiveTab] = useState(AllRulesTabs.rules); @@ -59,7 +57,6 @@ export const AllRules = React.memo( rulesNotInstalled={rulesNotInstalled} rulesNotUpdated={rulesNotUpdated} selectedTab={activeTab} - setRefreshRulesData={setRefreshRulesData} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/__mocks__/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/__mocks__/rules_table_context.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx index 45b7bb1557f36..2bf20acfb9334 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx @@ -6,10 +6,15 @@ */ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants'; -import { invariant } from '../../../../../../common/utils/invariant'; -import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; -import { FilterOptions, PaginationOptions, Rule, SortingOptions } from '../types'; +import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../../common/constants'; +import { invariant } from '../../../../../../../common/utils/invariant'; +import { useKibana, useUiSetting$ } from '../../../../../../common/lib/kibana'; +import { + FilterOptions, + PaginationOptions, + Rule, + SortingOptions, +} from '../../../../../containers/detection_engine/rules/types'; import { useFindRules } from './use_find_rules'; import { getRulesComparator, getRulesPredicate } from './utils'; @@ -102,7 +107,7 @@ export type LoadingRuleAction = | 'edit' | null; -interface LoadingRules { +export interface LoadingRules { ids: string[]; action: LoadingRuleAction; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_async_confirmation.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_async_confirmation.ts diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.ts new file mode 100644 index 0000000000000..47a2617dd2e25 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/use_find_rules.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + useFindRulesQuery, + FindRulesQueryArgs, +} from '../../../../../containers/detection_engine/rules/use_find_rules_query'; + +interface UseFindRulesArgs extends FindRulesQueryArgs { + isInMemorySorting: boolean; + refetchInterval: number | false; +} + +const MAX_RULES_PER_PAGE = 10000; + +/** + * This hook is used to fetch detection rules. Under the hood, it implements a + * "feature switch" that allows switching from an in-memory implementation to a + * backend-based implementation on the fly. + * + * @param args - find rules arguments + * @returns rules query result + */ +export const useFindRules = (args: UseFindRulesArgs) => { + const { pagination, filterOptions, sortingOptions, isInMemorySorting, refetchInterval } = args; + + // Use this query result when isInMemorySorting = true + const allRules = useFindRulesQuery( + ['all'], + { pagination: { page: 1, perPage: MAX_RULES_PER_PAGE } }, + { refetchInterval, enabled: isInMemorySorting } + ); + + // Use this query result when isInMemorySorting = false + const pagedRules = useFindRulesQuery( + ['paged'], + { pagination, filterOptions, sortingOptions }, + { + refetchInterval, + enabled: !isInMemorySorting, + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + } + ); + + return isInMemorySorting ? allRules : pagedRules; +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/utils.ts similarity index 96% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/utils.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/utils.ts index b3e612537c5d7..37deade0d1316 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/utils.ts @@ -6,7 +6,11 @@ */ import { get } from 'lodash'; -import { FilterOptions, Rule, SortingOptions } from '../types'; +import { + FilterOptions, + Rule, + SortingOptions, +} from '../../../../../containers/detection_engine/rules/types'; /** * Returns a comparator function to be used with .sort() diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx index a85c5213ca599..3c960108fddf8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx @@ -16,7 +16,7 @@ import { NavigateToAppOptions } from '../../../../../../../../../src/core/public import { ActionToaster } from '../../../../../common/components/toasters'; import { canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { Rule } from '../../../../containers/detection_engine/rules'; -import { RulesTableActions } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { RulesTableActions } from './rules_table/rules_table_context'; import * as i18n from '../translations'; import { deleteRulesAction, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx index 14a1d93ee66d6..261e14fd1411b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx @@ -8,7 +8,7 @@ import { EuiSwitch, EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { useRulesTableContext } from './rules_table/rules_table_context'; import * as i18n from '../translations'; const ToolbarLayout = styled.div` @@ -16,7 +16,7 @@ const ToolbarLayout = styled.div` grid-template-columns: 1fr auto; align-items: center; grid-gap: 16px; - box-shadow: inset 0 -1px 0 #d3dae6; + box-shadow: inset 0 -1px 0 ${({ theme }) => theme.eui.euiBorderColor}; `; interface RulesTableToolbarProps { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index ccf5f5e7407a3..1b8adb0bd11d3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -14,7 +14,7 @@ import { EuiLoadingContent, EuiProgress, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { partition } from 'lodash/fp'; import { AllRulesTabs } from './rules_table_toolbar'; @@ -30,8 +30,8 @@ import { Rule, RulesSortingFields, } from '../../../../containers/detection_engine/rules'; -import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; -import { useAsyncConfirmation } from '../../../../containers/detection_engine/rules/rules_table/use_async_confirmation'; +import { useRulesTableContext } from './rules_table/rules_table_context'; +import { useAsyncConfirmation } from './rules_table/use_async_confirmation'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; import { EuiBasicTableOnChange } from '../types'; @@ -59,7 +59,6 @@ interface RulesTableProps { rulesNotInstalled: number | null; rulesNotUpdated: number | null; selectedTab: AllRulesTabs; - setRefreshRulesData: (refreshRule: () => Promise) => void; } const NO_ITEMS_MESSAGE = ( @@ -85,7 +84,6 @@ export const RulesTables = React.memo( rulesNotInstalled, rulesNotUpdated, selectedTab, - setRefreshRulesData, }) => { const { timelines } = useKibana().services; const tableRef = useRef(null); @@ -137,11 +135,11 @@ export const RulesTables = React.memo( onFinish: hideDeleteConfirmation, }); - const [isBulkEditConfirmationVisible, showBulkEditonfirmation, hideBulkEditConfirmation] = + const [isBulkEditConfirmationVisible, showBulkEditConfirmation, hideBulkEditConfirmation] = useBoolState(); const [confirmBulkEdit, handleBulkEditConfirm, handleBulkEditCancel] = useAsyncConfirmation({ - onInit: showBulkEditonfirmation, + onInit: showBulkEditConfirmation, onFinish: hideBulkEditConfirmation, }); @@ -162,8 +160,8 @@ export const RulesTables = React.memo( const hasPagination = pagination.total > pagination.perPage; const [selectedElasticRuleIds, selectedCustomRuleIds] = useMemo(() => { - const ruleImmutablityMap = new Map(rules.map((rule) => [rule.id, rule.immutable])); - const predicate = (id: string) => ruleImmutablityMap.get(id); + const ruleImmutabilityMap = new Map(rules.map((rule) => [rule.id, rule.immutable])); + const predicate = (id: string) => ruleImmutabilityMap.get(id); return partition(predicate, selectedRuleIds); }, [rules, selectedRuleIds]); @@ -210,12 +208,6 @@ export const RulesTables = React.memo( const rulesColumns = useRulesColumns({ hasPermissions }); const monitoringColumns = useMonitoringColumns({ hasPermissions }); - useEffect(() => { - setRefreshRulesData(async () => { - await reFetchRules(); - }); - }, [reFetchRules, setRefreshRulesData]); - const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index f41112e23c4d9..f241a3df87327 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -33,7 +33,7 @@ import { canEditRuleWithActions, getToolTipContent } from '../../../../../common import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { SeverityBadge } from '../../../../components/rules/severity_badge'; import { Rule } from '../../../../containers/detection_engine/rules'; -import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { useRulesTableContext } from './rules_table/rules_table_context'; import * as i18n from '../translations'; import { PopoverTooltip } from './popover_tooltip'; import { TableHeaderTooltipCell } from './table_header_tooltip_cell'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx index 7276415b852b2..6d9c2f92b214e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx @@ -55,7 +55,7 @@ export const AllRulesUtilityBar = React.memo( isBulkActionInProgress, hasDisabledActions, }) => { - const handleGetBuIktemsPopoverContent = useCallback( + const handleGetBulkItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element | null => { if (onGetBulkItemsPopoverContent != null) { return ( @@ -141,7 +141,7 @@ export const AllRulesUtilityBar = React.memo( iconSide="right" iconType="arrowDown" popoverPanelPaddingSize="none" - popoverContent={handleGetBuIktemsPopoverContent} + popoverContent={handleGetBulkItemsPopoverContent} > {i18n.BATCH_ACTIONS} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index ced24a576eea7..b3cf1ded717fa 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -27,7 +27,7 @@ jest.mock('react-router-dom', () => { }); jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); -jest.mock('../../../../containers/detection_engine/rules/rules_table/use_find_rules'); +jest.mock('../../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('../../../../../common/hooks/use_app_toasts'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 9ad0dfd70e336..464c4e58b88d2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -227,8 +227,6 @@ const RuleDetailsPageComponent: React.FC = ({ const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); const isLoading = ruleLoading && rule == null; - // This is used to re-trigger api rule status when user de/activate rule - const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); const [pageTabs, setTabs] = useState(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = @@ -514,14 +512,9 @@ const RuleDetailsPageComponent: React.FC = ({ [dispatch] ); - const handleOnChangeEnabledRule = useCallback( - (enabled: boolean) => { - if (ruleEnabled == null || enabled !== ruleEnabled) { - setRuleEnabled(enabled); - } - }, - [ruleEnabled, setRuleEnabled] - ); + const handleOnChangeEnabledRule = useCallback((enabled: boolean) => { + setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule)); + }, []); const goToEditRule = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index c9aee723d599f..4532982032772 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -18,7 +18,7 @@ import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); -jest.mock('../../../../containers/detection_engine/rules/rules_table/use_find_rules'); +jest.mock('../../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 4de35119f80aa..4f40f1c86c697 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -27,8 +27,9 @@ jest.mock('react-router-dom', () => { }; }); -jest.mock('../../../containers/detection_engine/rules/rules_table/rules_table_context'); +jest.mock('../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); jest.mock('../../../containers/detection_engine/lists/use_lists_config'); +jest.mock('../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 65837879862af..e49e6801a1d8a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -6,7 +6,7 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config'; @@ -37,16 +37,16 @@ import { MlJobCompatibilityCallout } from '../../../components/callouts/ml_job_c import { MissingPrivilegesCallOut } from '../../../components/callouts/missing_privileges_callout'; import { APP_UI_ID } from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; -import { RulesTableContextProvider } from '../../../containers/detection_engine/rules/rules_table/rules_table_context'; +import { RulesTableContextProvider } from './all/rules_table/rules_table_context'; import { HeaderPage } from '../../../../common/components/header_page'; - -type Func = () => Promise; +import { useInvalidateRules } from '../../../containers/detection_engine/rules/use_find_rules_query'; +import { useBoolState } from '../../../../common/hooks/use_bool_state'; const RulesPageComponent: React.FC = () => { - const [showImportModal, setShowImportModal] = useState(false); - const [showValueListsModal, setShowValueListsModal] = useState(false); - const refreshRulesData = useRef(null); + const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); + const [isValueListModalVisible, showValueListModal, hideValueListModal] = useBoolState(); const { navigateToApp } = useKibana().services.application; + const invalidateRules = useInvalidateRules(); const [ { @@ -98,18 +98,12 @@ const RulesPageComponent: React.FC = () => { ); const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const handleRefreshRules = useCallback(async () => { - if (refreshRulesData.current != null) { - await refreshRulesData.current(); - } - }, [refreshRulesData]); - const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); - return handleRefreshRules(); + invalidateRules(); } - }, [createPrePackagedRules, handleRefreshRules]); + }, [createPrePackagedRules, invalidateRules]); const handleRefetchPrePackagedRulesStatus = useCallback(() => { if (refetchPrePackagedRulesStatus != null) { @@ -119,10 +113,6 @@ const RulesPageComponent: React.FC = () => { } }, [refetchPrePackagedRulesStatus]); - const handleSetRefreshRulesData = useCallback((refreshRule: Func) => { - refreshRulesData.current = refreshRule; - }, []); - const goToNewRule = useCallback( (ev) => { ev.preventDefault(); @@ -169,20 +159,17 @@ const RulesPageComponent: React.FC = () => { - setShowValueListsModal(false)} - /> + setShowImportModal(false)} + closeModal={hideImportModal} description={i18n.SELECT_RULE} errorMessage={i18n.IMPORT_FAILED} failedDetailed={i18n.IMPORT_FAILED_DETAILED} - importComplete={handleRefreshRules} + importComplete={invalidateRules} importData={importRules} successMessage={i18n.SUCCESSFULLY_IMPORTED_RULES} - showModal={showImportModal} + showModal={isImportModalVisible} submitBtnText={i18n.IMPORT_RULE_BTN_TITLE} subtitle={i18n.INITIAL_PROMPT_TEXT} title={i18n.IMPORT_RULE} @@ -207,7 +194,7 @@ const RulesPageComponent: React.FC = () => { data-test-subj="open-value-lists-modal-button" iconType="importAction" isDisabled={!canWriteListsIndex || loading} - onClick={() => setShowValueListsModal(true)} + onClick={showValueListModal} > {i18n.UPLOAD_VALUE_LISTS} @@ -218,9 +205,7 @@ const RulesPageComponent: React.FC = () => { data-test-subj="rules-import-modal-button" iconType="importAction" isDisabled={!userHasPermissions(canUserCRUD) || loading} - onClick={() => { - setShowImportModal(true); - }} + onClick={showImportModal} > {i18n.IMPORT_RULE} @@ -259,7 +244,6 @@ const RulesPageComponent: React.FC = () => { rulesInstalled={rulesInstalled} rulesNotInstalled={rulesNotInstalled} rulesNotUpdated={rulesNotUpdated} - setRefreshRulesData={handleSetRefreshRulesData} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 24d842eb930a8..1de060c16a97a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -247,7 +247,7 @@ export const BULK_EDIT_ERROR_TOAST_TITLE = i18n.translate( } ); -export const BULK_EDIT_ERROR_TOAST_DESCIRPTION = (rulesCount: number) => +export const BULK_EDIT_ERROR_TOAST_DESCRIPTION = (rulesCount: number) => i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastDescription', { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 369bff6f615fc..0ac11b80c2a65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -30,11 +30,10 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { deleteRules } from '../../rules/delete_rules'; import { duplicateRule } from '../../rules/duplicate_rule'; -import { enableRule } from '../../rules/enable_rule'; import { findRules } from '../../rules/find_rules'; import { readRules } from '../../rules/read_rules'; import { patchRules } from '../../rules/patch_rules'; -import { appplyBulkActionEditToRule } from '../../rules/bulk_action_edit'; +import { applyBulkActionEditToRule } from '../../rules/bulk_action_edit'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { buildSiemResponse } from '../utils'; @@ -297,10 +296,7 @@ export const performBulkActionRoute = ( async (rule) => { if (!rule.enabled) { throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); - await enableRule({ - rule, - rulesClient, - }); + await rulesClient.enable({ id: rule.id }); } }, abortController.signal @@ -375,7 +371,7 @@ export const performBulkActionRoute = ( throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); const editedRule = body[BulkAction.edit].reduce( - (acc, action) => appplyBulkActionEditToRule(acc, action), + (acc, action) => applyBulkActionEditToRule(acc, action), rule ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts index db6ef37cade36..783e710cb7d4d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.test.ts @@ -8,7 +8,7 @@ import { addItemsToArray, deleteItemsFromArray, - appplyBulkActionEditToRule, + applyBulkActionEditToRule, } from './bulk_action_edit'; import { BulkActionEditType } from '../../../../common/detection_engine/schemas/common/schemas'; import { RuleAlertType } from './types'; @@ -41,21 +41,21 @@ describe('bulk_action_edit', () => { }); }); - describe('appplyBulkActionEditToRule', () => { + describe('applyBulkActionEditToRule', () => { const ruleMock = { tags: ['tag1', 'tag2'], params: { index: ['initial-index-*'] }, }; describe('tags', () => { test('should add new tags to rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.add_tags, value: ['new_tag'], }); expect(editedRule.tags).toEqual(['tag1', 'tag2', 'new_tag']); }); test('should remove tag from rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.delete_tags, value: ['tag1'], }); @@ -63,7 +63,7 @@ describe('bulk_action_edit', () => { }); test('should rewrite tags in rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.set_tags, value: ['tag_r_1', 'tag_r_2'], }); @@ -73,14 +73,14 @@ describe('bulk_action_edit', () => { describe('index_patterns', () => { test('should add new index pattern to rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.add_index_patterns, value: ['my-index-*'], }); expect(editedRule.params).toHaveProperty('index', ['initial-index-*', 'my-index-*']); }); test('should remove index pattern from rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.delete_index_patterns, value: ['initial-index-*'], }); @@ -88,7 +88,7 @@ describe('bulk_action_edit', () => { }); test('should rewrite index pattern in rule', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.set_index_patterns, value: ['index'], }); @@ -96,7 +96,7 @@ describe('bulk_action_edit', () => { }); test('should not add new index pattern to rule if index pattern is absent', () => { - const editedRule = appplyBulkActionEditToRule({ params: {} } as RuleAlertType, { + const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, { type: BulkActionEditType.add_index_patterns, value: ['my-index-*'], }); @@ -104,7 +104,7 @@ describe('bulk_action_edit', () => { }); test('should not remove index pattern to rule if index pattern is absent', () => { - const editedRule = appplyBulkActionEditToRule({ params: {} } as RuleAlertType, { + const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, { type: BulkActionEditType.delete_index_patterns, value: ['initial-index-*'], }); @@ -112,7 +112,7 @@ describe('bulk_action_edit', () => { }); test('should not set index pattern to rule if index pattern is absent', () => { - const editedRule = appplyBulkActionEditToRule({ params: {} } as RuleAlertType, { + const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, { type: BulkActionEditType.set_index_patterns, value: ['index-*'], }); @@ -122,7 +122,7 @@ describe('bulk_action_edit', () => { describe('timeline', () => { test('should set timeline', () => { - const editedRule = appplyBulkActionEditToRule(ruleMock as RuleAlertType, { + const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, { type: BulkActionEditType.set_timeline, value: { timeline_id: '91832785-286d-4ebe-b884-1a208d111a70', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts index 0f56fd86be8ed..eab37e6474f32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_action_edit.ts @@ -20,7 +20,7 @@ export const deleteItemsFromArray = (arr: T[], items: T[]): T[] => { return arr.filter((item) => !itemsSet.has(item)); }; -export const appplyBulkActionEditToRule = ( +export const applyBulkActionEditToRule = ( existingRule: RuleAlertType, action: BulkActionEditPayload ): RuleAlertType => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts deleted file mode 100644 index fb9aeefc06125..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts +++ /dev/null @@ -1,25 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SanitizedAlert } from '../../../../../alerting/common'; -import { RulesClient } from '../../../../../alerting/server'; -import { RuleParams } from '../schemas/rule_schemas'; - -interface EnableRuleArgs { - rule: SanitizedAlert; - rulesClient: RulesClient; -} - -/** - * Enables the rule - * - * @param rule - rule to enable - * @param rulesClient - Alerts client - */ -export const enableRule = async ({ rule, rulesClient }: EnableRuleArgs) => { - await rulesClient.enable({ id: rule.id }); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index a10247005c826..b862dca6a022a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -15,7 +15,6 @@ import { } from '../../../../common/detection_engine/utils'; import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { addTags } from './add_tags'; -import { enableRule } from './enable_rule'; import { PatchRulesOptions } from './types'; import { calculateInterval, @@ -219,7 +218,7 @@ export const patchRules = async ({ if (rule.enabled && enabled === false) { await rulesClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { - await enableRule({ rule, rulesClient }); + await rulesClient.enable({ id: rule.id }); } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 62c59bc6a698f..6c13955aaab58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -15,7 +15,6 @@ import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; -import { enableRule } from './enable_rule'; import { maybeMute, transformToAlertThrottle, transformToNotifyWhen } from './utils'; class UpdateError extends Error { @@ -102,7 +101,7 @@ export const updateRules = async ({ if (existingRule.enabled && enabled === false) { await rulesClient.disable({ id: existingRule.id }); } else if (!existingRule.enabled && enabled === true) { - await enableRule({ rule: existingRule, rulesClient }); + await rulesClient.enable({ id: existingRule.id }); } return { ...update, enabled }; }; From 807c625ea9b41e749f178c0b14c1cfeef9c12ae7 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 7 Feb 2022 15:40:41 +0300 Subject: [PATCH 007/161] [Dashboard] Pass in title to Charts (#122319) * Pass title from dashboard panel to charts * Fix lint * Fix CI * Fix CI * Fix CI * Add 'ariaLabel' in expressions * Fix text Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/gauge_function.test.ts.snap | 1 + .../expression_functions/gauge_function.ts | 17 +++++++++++++++-- .../common/types/expression_functions.ts | 1 + .../gauge_component.test.tsx.snap | 1 + .../public/components/gauge_component.tsx | 7 ++++++- .../heatmap_function.test.ts.snap | 1 + .../expression_functions/heatmap_function.ts | 15 ++++++++++++++- .../common/types/expression_functions.ts | 1 + .../public/components/heatmap_component.tsx | 2 ++ .../mosaic_vis_function.test.ts.snap | 1 + .../pie_vis_function.test.ts.snap | 2 ++ .../treemap_vis_function.test.ts.snap | 1 + .../waffle_vis_function.test.ts.snap | 1 + .../common/expression_functions/i18n.ts | 4 ++++ .../mosaic_vis_function.ts | 9 +++++++++ .../expression_functions/pie_vis_function.ts | 9 +++++++++ .../treemap_vis_function.ts | 9 +++++++++ .../waffle_vis_function.ts | 9 +++++++++ .../common/types/expression_renderers.ts | 1 + .../partition_vis_component.test.tsx.snap | 5 +++++ .../components/partition_vis_component.tsx | 2 ++ .../tagcloud_function.test.ts.snap | 2 ++ .../expression_functions/tagcloud_function.ts | 12 ++++++++++++ .../common/types/expression_functions.ts | 1 + .../public/components/tagcloud_component.tsx | 7 ++++++- .../components/timelion_vis_component.tsx | 4 ++++ .../timelion/public/timelion_vis_fn.ts | 19 +++++++++++++++++-- .../timelion/public/timelion_vis_renderer.tsx | 1 + .../xy/public/components/xy_settings.tsx | 4 ++++ .../public/expression_functions/xy_vis_fn.ts | 11 +++++++++++ .../vis_types/xy/public/types/param.ts | 2 ++ .../vis_types/xy/public/vis_component.tsx | 1 + .../embeddable/visualize_embeddable.tsx | 10 ++++++++++ .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../baseline/tagcloud_empty_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../common/expressions/pie_chart/pie_chart.ts | 15 +++++++++++++-- .../common/expressions/pie_chart/types.ts | 1 + .../common/expressions/xy_chart/xy_args.ts | 1 + .../common/expressions/xy_chart/xy_chart.ts | 17 +++++++++++++++-- .../lens/public/embeddable/embeddable.tsx | 12 ++++++++++-- .../pie_visualization/render_function.tsx | 2 ++ .../visualizations/gauge/visualization.tsx | 2 +- .../__snapshots__/expression.test.tsx.snap | 7 +++++++ .../public/xy_visualization/expression.tsx | 2 ++ 48 files changed, 224 insertions(+), 20 deletions(-) diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap index 1a7ca53228a60..a0b8cddaf4f9c 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap @@ -6,6 +6,7 @@ Object { "type": "render", "value": Object { "args": Object { + "ariaLabel": undefined, "colorMode": "none", "goalAccessor": undefined, "labelMajor": "title", diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts index 2c24aa292319c..61de491595f05 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts @@ -93,14 +93,27 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({ }), required: false, }, + ariaLabel: { + types: ['string'], + help: i18n.translate('expressionGauge.functions.gaugeChart.config.ariaLabel.help', { + defaultMessage: 'Specifies the aria label of the gauge chart', + }), + required: false, + }, }, - fn(data, args) { + fn(data, args, handlers) { return { type: 'render', as: EXPRESSION_GAUGE_NAME, value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts index 16f246bf24713..e1cebae438758 100644 --- a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts @@ -46,6 +46,7 @@ export type GaugeArguments = GaugeState & { shape: GaugeShape; colorMode: GaugeColorMode; palette?: PaletteOutput; + ariaLabel?: string; }; export type GaugeInput = Datatable; diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap b/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap index bd39344807643..9af3bb2be8a57 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_gauge/public/components/__snapshots__/gauge_component.test.tsx.snap @@ -5,6 +5,7 @@ exports[`GaugeComponent renders the chart 1`] = ` renderer="canvas" > diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx index dfd7755c47681..593c18e5e9b05 100644 --- a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx +++ b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx @@ -231,7 +231,12 @@ export const GaugeComponent: FC = memo( return ( - + ({ defaultMessage: 'The id of the split column or the corresponding dimension', }), }, + ariaLabel: { + types: ['string'], + help: i18n.translate('expressionHeatmap.functions.args.ariaLabelHelpText', { + defaultMessage: 'Specifies the aria label of the heat map', + }), + required: false, + }, }, fn(data, args, handlers) { if (handlers?.inspectorAdapters?.tables) { @@ -203,7 +210,13 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({ as: EXPRESSION_HEATMAP_NAME, value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts index efd4e1a8b990c..10e43e426317d 100644 --- a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts @@ -77,6 +77,7 @@ export interface HeatmapArguments { splitColumnAccessor?: string | ExpressionValueVisDimension; legend: HeatmapLegendConfigResult; gridConfig: HeatmapGridConfigResult; + ariaLabel?: string; } export type HeatmapInput = Datatable; diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 3c751956c0ea2..3e6e06de31c62 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -521,6 +521,8 @@ export const HeatmapComponent: FC = memo( : NaN, }} onBrushEnd={interactive ? (onBrushEnd as BrushEndListener) : undefined} + ariaLabel={args.ariaLabel} + ariaUseDefaultSummary={!args.ariaLabel} /> + i18n.translate('expressionPartitionVis.reusable.functions.args.ariaLabelHelpText', { + defaultMessage: 'Specifies the aria label of the chart', + }), getSliceSizeHelp: () => i18n.translate('expressionPartitionVis.reusable.function.dimension.metric', { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts index 388b0741d23d3..142bc6290d476 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts @@ -82,6 +82,11 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ help: strings.getLabelsArgHelp(), default: `{${PARTITION_LABELS_FUNCTION}}`, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -95,6 +100,10 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts index c054d572538ce..80302f877698c 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -109,6 +109,11 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ help: strings.getStartFromSecondLargestSliceArgHelp(), default: true, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -117,6 +122,10 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts index d0ae42b4b7942..65f016729eabe 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts @@ -82,6 +82,11 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => help: strings.getLabelsArgHelp(), default: `{${PARTITION_LABELS_FUNCTION}}`, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -95,6 +100,10 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts index ade524aad59c8..b1b30539949c4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts @@ -81,6 +81,11 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ help: strings.getShowValuesInLegendArgHelp(), default: false, }, + ariaLabel: { + types: ['string'], + help: strings.getAriaLabelHelp(), + required: false, + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -90,6 +95,10 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ const buckets = args.bucket ? [args.bucket] : []; const visConfig: PartitionVisParams = { ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 87358d5dbe659..01ca39c9cbb36 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -52,6 +52,7 @@ interface VisCommonParams { legendPosition: Position; truncateLegend: boolean; maxLegendLines: number; + ariaLabel?: string; } interface VisCommonConfig extends VisCommonParams { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap index 4e56d2c5efa4c..b367db1af5437 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap @@ -238,6 +238,7 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` > { ]} baseTheme={chartBaseTheme} onRenderChange={onRenderChange} + ariaLabel={props.visParams.ariaLabel} + ariaUseDefaultSummary={!props.visParams.ariaLabel} /> { types: ['vis_dimension'], help: argHelp.bucket, }, + ariaLabel: { + types: ['string'], + help: argHelp.ariaLabel, + required: false, + }, }, fn(input, args, handlers) { const visParams: TagCloudRendererParams = { @@ -136,6 +144,10 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { bucket: args.bucket, }), palette: args.palette, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index 091b3e861332d..44fc6f3048790 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -20,6 +20,7 @@ interface TagCloudCommonParams { minFontSize: number; maxFontSize: number; showLabel: boolean; + ariaLabel?: string; } export interface TagCloudVisConfig extends TagCloudCommonParams { diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index 2bec25534f49b..560507f84831a 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -191,7 +191,12 @@ export const TagCloudChart = ({ {(resizeRef) => (
- + void; renderComplete: IInterpreterRenderHandlers['done']; + ariaLabel?: string; } const DefaultYAxis = () => ( @@ -98,6 +99,7 @@ export const TimelionVisComponent = ({ seriesList, renderComplete, onBrushEvent, + ariaLabel, }: TimelionVisComponentProps) => { const kibana = useKibana(); const chartRef = useRef(null); @@ -206,6 +208,8 @@ export const TimelionVisComponent = ({ type: TooltipType.VerticalCursor, }} externalPointerEvents={{ tooltip: { visible: false } }} + ariaLabel={ariaLabel} + ariaUseDefaultSummary={!ariaLabel} /> = ({ legendPosition, maxLegendLines, truncateLegend, + ariaLabel, }) => { const themeService = getThemeService(); const theme = themeService.useChartsTheme(); @@ -173,6 +175,8 @@ export const XYSettings: FC = ({ onRenderChange={onRenderChange} legendAction={legendAction} tooltip={tooltipProps} + ariaLabel={ariaLabel} + ariaUseDefaultSummary={!ariaLabel} orderOrdinalBinsBy={ orderBucketsBySum ? { diff --git a/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts index ccad0c520f8ea..28908901d71a6 100644 --- a/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts +++ b/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts @@ -232,10 +232,21 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ }), multi: true, }, + ariaLabel: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.ariaLabel.help', { + defaultMessage: 'Specifies the aria label of the xy chart', + }), + required: false, + }, }, fn(context, args, handlers) { const visType = args.chartType; const visConfig = { + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, type: args.chartType, addLegend: args.addLegend, addTooltip: args.addTooltip, diff --git a/src/plugins/vis_types/xy/public/types/param.ts b/src/plugins/vis_types/xy/public/types/param.ts index 81eeca55108ca..943705deb3726 100644 --- a/src/plugins/vis_types/xy/public/types/param.ts +++ b/src/plugins/vis_types/xy/public/types/param.ts @@ -145,6 +145,7 @@ export interface VisParams { palette: PaletteOutput; fillOpacity?: number; fittingFunction?: Exclude; + ariaLabel?: string; } export interface XYVisConfig { @@ -185,4 +186,5 @@ export interface XYVisConfig { seriesDimension?: ExpressionValueXYDimension[]; splitRowDimension?: ExpressionValueXYDimension[]; splitColumnDimension?: ExpressionValueXYDimension[]; + ariaLabel?: string; } diff --git a/src/plugins/vis_types/xy/public/vis_component.tsx b/src/plugins/vis_types/xy/public/vis_component.tsx index 8574e86a23096..c4e438d54dfb5 100644 --- a/src/plugins/vis_types/xy/public/vis_component.tsx +++ b/src/plugins/vis_types/xy/public/vis_component.tsx @@ -373,6 +373,7 @@ const VisComponent = (props: VisComponentProps) => { splitSeriesAccessors, splitChartColumnAccessor ?? splitChartRowAccessor )} + ariaLabel={visParams.ariaLabel} onBrushEnd={handleBrush(visData, xAccessor, 'interval' in config.aspects.x.params)} onRenderChange={onRenderChange} legendAction={ diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index c3b8834605d1d..a12195e34a81e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -97,6 +97,7 @@ export class VisualizeEmbeddable private filters?: Filter[]; private searchSessionId?: string; private syncColors?: boolean; + private embeddableTitle?: string; private visCustomizations?: Pick; private subscriptions: Subscription[] = []; private expression?: ExpressionAstExpression; @@ -140,6 +141,7 @@ export class VisualizeEmbeddable this.syncColors = this.input.syncColors; this.searchSessionId = this.input.searchSessionId; this.query = this.input.query; + this.embeddableTitle = this.getTitle(); this.vis = vis; this.vis.uiState.on('change', this.uiStateChangeHandler); @@ -259,6 +261,11 @@ export class VisualizeEmbeddable dirty = true; } + if (this.embeddableTitle !== this.getTitle()) { + this.embeddableTitle = this.getTitle(); + dirty = true; + } + if (this.vis.description && this.domNode) { this.domNode.setAttribute('data-description', this.vis.description); } @@ -406,6 +413,9 @@ export class VisualizeEmbeddable query: this.input.query, filters: this.input.filters, }, + variables: { + embeddableTitle: this.getTitle(), + }, searchSessionId: this.input.searchSessionId, syncColors: this.input.syncColors, uiState: this.vis.uiState, diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 6bbd5ea72015d..9e6888a319c38 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index b1e76880dc912..775839764b410 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index ce352d1f63c28..70c7ea6d7827b 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 90cc06d2088c8..dc251faaee827 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 7850768e9466c..89df8d2f4146b 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 19b76ac66efcf..7bd4ff7dedfa0 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts index ce909152e71b9..feec2117632c0 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts @@ -105,15 +105,26 @@ export const pie: ExpressionFunctionDefinition< types: ['number'], help: '', }, + ariaLabel: { + types: ['string'], + help: '', + required: false, + }, }, inputTypes: ['lens_multitable'], - fn(data: LensMultiTable, args: PieExpressionArgs) { + fn(data: LensMultiTable, args: PieExpressionArgs, handlers) { return { type: 'render', as: 'lens_pie_renderer', value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts index 0a7d705ec3fbc..aa84488dbc2c2 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts @@ -42,6 +42,7 @@ export type PieExpressionArgs = SharedPieLayerState & { shape: PieChartTypes; hideLabels: boolean; palette: PaletteOutput; + ariaLabel?: string; }; export interface PieExpressionProps { diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts index f00608135820a..4e712f7ca3bf4 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts @@ -37,4 +37,5 @@ export interface XYArgs { fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; + ariaLabel?: string; } diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts index 0e58105447689..00baf894de046 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts @@ -148,14 +148,27 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Show values in legend', }), }, + ariaLabel: { + types: ['string'], + help: i18n.translate('xpack.lens.xyChart.ariaLabel.help', { + defaultMessage: 'Specifies the aria label of the xy chart', + }), + required: false, + }, }, - fn(data: LensMultiTable, args: XYArgs) { + fn(data: LensMultiTable, args: XYArgs, handlers) { return { type: 'render', as: 'lens_xy_chart_renderer', value: { data, - args, + args: { + ...args, + ariaLabel: + args.ariaLabel ?? + (handlers.variables?.embeddableTitle as string) ?? + handlers.getExecutionContext?.()?.description, + }, }, }; }, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index a430a72276ca3..ca37580ad682f 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -151,6 +151,7 @@ export class Embeddable private errors: ErrorMessage[] | undefined; private inputReloadSubscriptions: Subscription[]; private isDestroyed?: boolean; + private embeddableTitle?: string; private lensInspector: LensInspector; private logError(type: 'runtime' | 'validation') { @@ -188,6 +189,7 @@ export class Embeddable ); const input$ = this.getInput$(); + this.embeddableTitle = this.getTitle(); this.inputReloadSubscriptions = []; @@ -366,7 +368,8 @@ export class Embeddable !isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || !isEqual(containerState.query, this.externalSearchContext.query) || !isEqual(cleanedFilters, this.externalSearchContext.filters) || - this.externalSearchContext.searchSessionId !== containerState.searchSessionId + this.externalSearchContext.searchSessionId !== containerState.searchSessionId || + this.embeddableTitle !== this.getTitle() ) { this.externalSearchContext = { timeRange: containerState.timeRange, @@ -374,6 +377,7 @@ export class Embeddable filters: cleanedFilters, searchSessionId: containerState.searchSessionId, }; + this.embeddableTitle = this.getTitle(); isDirty = true; } return isDirty; @@ -428,7 +432,11 @@ export class Embeddable errors={this.errors} lensInspector={this.lensInspector} searchContext={this.getMergedSearchContext()} - variables={input.palette ? { theme: { palette: input.palette } } : {}} + variables={ + input.palette + ? { theme: { palette: input.palette }, embeddableTitle: this.getTitle() } + : { embeddableTitle: this.getTitle() } + } searchSessionId={this.externalSearchContext.searchSessionId} handleEvent={this.handleEvent} onData$={this.updateActiveData} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 008ab9a9cae9e..15706e69d1e16 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -333,6 +333,8 @@ export function PieComponent( legendAction={props.interactive ? getLegendAction(firstTable, onClickValue) : undefined} theme={[themeOverrides, chartTheme]} baseTheme={chartBaseTheme} + ariaLabel={props.args.ariaLabel} + ariaUseDefaultSummary={!props.args.ariaLabel} /> , - attributes?: Partial> + attributes?: Partial> ): Ast | null => { const datasource = datasourceLayers[state.layerId]; diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index 1402cd715283a..b34d5e8639382 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -6,6 +6,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` > Date: Mon, 7 Feb 2022 13:46:50 +0000 Subject: [PATCH 008/161] chore(NA): splits types from code on @kbn/logging (#124688) * chore(NA): splits types from code on @kbn/test * chore(NA): create new @kbn/test-jest-helpers * chore(NA): move wrong files into @kbn/test * chore(NA): remove @kbn/test/jest references * chore(NA): splits types from code on @kbn/logging * chore(NA): import type from new @kbn/logging-mocks pkg * chore(NA): missing deps on bazel build files Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 3 + packages/BUILD.bazel | 3 + packages/kbn-cli-dev-mode/BUILD.bazel | 2 +- packages/kbn-config/BUILD.bazel | 4 +- .../kbn-config/src/config_service.test.ts | 2 +- packages/kbn-logging-mocks/BUILD.bazel | 106 ++++++++++++++++++ packages/kbn-logging-mocks/package.json | 7 ++ .../mocks => kbn-logging-mocks/src}/index.ts | 0 .../src}/logger.mock.ts | 2 +- packages/kbn-logging-mocks/tsconfig.json | 20 ++++ packages/kbn-logging/BUILD.bazel | 27 ++++- packages/kbn-logging/mocks/package.json | 5 - packages/kbn-logging/package.json | 3 +- .../http/integration_tests/router.test.ts | 2 +- src/core/server/logging/logger.mock.ts | 4 +- .../server/metrics/collectors/cgroup.test.ts | 2 +- src/core/server/metrics/collectors/os.test.ts | 2 +- .../metrics/ops_metrics_collector.test.ts | 2 +- .../custom_integration_registry.test.ts | 2 +- .../saved_objects/is_rule_exportable.test.ts | 2 +- .../cases/server/services/cases/index.test.ts | 2 +- .../server/services/configure/index.test.ts | 2 +- .../services/user_actions/index.test.ts | 2 +- .../services/epm/elasticsearch/retry.test.ts | 2 +- .../template/default_settings.test.ts | 2 +- .../elasticsearch/template/install.test.ts | 2 +- .../elasticsearch/template/template.test.ts | 2 +- .../elasticsearch/transform/transform.test.ts | 2 +- .../services/epm/package_service.test.ts | 2 +- .../epm/packages/_install_package.test.ts | 2 +- .../extension_point_storage.mock.ts | 2 +- .../extension_point_storage_client.test.ts | 2 +- .../usage/reporting_usage_collector.test.ts | 2 +- .../resource_installer.test.ts | 2 +- .../rule_data_plugin_service.test.ts | 2 +- .../utils/create_lifecycle_executor.test.ts | 2 +- .../utils/create_lifecycle_rule_type.test.ts | 2 +- yarn.lock | 12 ++ 39 files changed, 206 insertions(+), 41 deletions(-) create mode 100644 packages/kbn-logging-mocks/BUILD.bazel create mode 100644 packages/kbn-logging-mocks/package.json rename packages/{kbn-logging/src/mocks => kbn-logging-mocks/src}/index.ts (100%) rename packages/{kbn-logging/src/mocks => kbn-logging-mocks/src}/logger.mock.ts (97%) create mode 100644 packages/kbn-logging-mocks/tsconfig.json delete mode 100644 packages/kbn-logging/mocks/package.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 604179ec75706..3f36f4b67e56b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -268,6 +268,7 @@ /packages/kbn-std/ @elastic/kibana-core /packages/kbn-config/ @elastic/kibana-core /packages/kbn-logging/ @elastic/kibana-core +/packages/kbn-logging-mocks/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core diff --git a/package.json b/package.json index 8738fdb8e0f9f..24322b8cccb2d 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", "@kbn/logging": "link:bazel-bin/packages/kbn-logging", + "@kbn/logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", "@kbn/react-field": "link:bazel-bin/packages/kbn-react-field", @@ -586,6 +587,8 @@ "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/kbn__interpreter": "link:bazel-bin/packages/kbn-interpreter/npm_module_types", "@types/kbn__io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils/npm_module_types", + "@types/kbn__logging": "link:bazel-bin/packages/kbn-logging/npm_module_types", + "@types/kbn__logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks/npm_module_types", "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 7e6d06922aed2..6421f36bf73b7 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -33,6 +33,7 @@ filegroup( "//packages/kbn-interpreter:build", "//packages/kbn-io-ts-utils:build", "//packages/kbn-logging:build", + "//packages/kbn-logging-mocks:build", "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", "//packages/kbn-optimizer:build", @@ -101,6 +102,8 @@ filegroup( "//packages/kbn-i18n-react:build_types", "//packages/kbn-interpreter:build_types", "//packages/kbn-io-ts-utils:build_types", + "//packages/kbn-logging:build_types", + "//packages/kbn-logging-mocks:build_types", "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", "//packages/kbn-optimizer:build_types", diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 87d4a116f13b1..133474a3aefa6 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -51,7 +51,7 @@ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", "//packages/kbn-config-schema:npm_module_types", "//packages/kbn-dev-utils:npm_module_types", - "//packages/kbn-logging", + "//packages/kbn-logging:npm_module_types", "//packages/kbn-optimizer:npm_module_types", "//packages/kbn-server-http-tools:npm_module_types", "//packages/kbn-std:npm_module_types", diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index d7046a26ff92f..0577014768d4c 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -34,6 +34,7 @@ RUNTIME_DEPS = [ "//packages/elastic-safer-lodash-set", "//packages/kbn-config-schema", "//packages/kbn-logging", + "//packages/kbn-logging-mocks", "//packages/kbn-std", "//packages/kbn-utility-types", "//packages/kbn-i18n", @@ -47,7 +48,8 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/elastic-safer-lodash-set:npm_module_types", "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-logging", + "//packages/kbn-logging:npm_module_types", + "//packages/kbn-logging-mocks:npm_module_types", "//packages/kbn-std:npm_module_types", "//packages/kbn-utility-types:npm_module_types", "//packages/kbn-i18n:npm_module_types", diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index 32b2d8969d0cc..51e67956637ee 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -13,7 +13,7 @@ import { mockApplyDeprecations, mockedChangedPaths } from './config_service.test import { rawConfigServiceMock } from './raw/raw_config_service.mock'; import { schema } from '@kbn/config-schema'; -import { MockedLogger, loggerMock } from '@kbn/logging/mocks'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; import type { ConfigDeprecationContext } from './deprecation'; import { ConfigService, Env, RawPackageInfo } from '.'; diff --git a/packages/kbn-logging-mocks/BUILD.bazel b/packages/kbn-logging-mocks/BUILD.bazel new file mode 100644 index 0000000000000..74fb9c2651e5d --- /dev/null +++ b/packages/kbn-logging-mocks/BUILD.bazel @@ -0,0 +1,106 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_BASE_NAME = "kbn-logging-mocks" +PKG_REQUIRE_NAME = "@kbn/logging-mocks" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__logging-mocks" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "//packages/kbn-logging" +] + +TYPES_DEPS = [ + "//packages/kbn-logging:npm_module_types", + "@npm//@types/jest", + "@npm//@types/node", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-logging-mocks/package.json b/packages/kbn-logging-mocks/package.json new file mode 100644 index 0000000000000..789ffe4500bce --- /dev/null +++ b/packages/kbn-logging-mocks/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/logging-mocks", + "version": "1.0.0", + "private": true, + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target_node/index.js" +} \ No newline at end of file diff --git a/packages/kbn-logging/src/mocks/index.ts b/packages/kbn-logging-mocks/src/index.ts similarity index 100% rename from packages/kbn-logging/src/mocks/index.ts rename to packages/kbn-logging-mocks/src/index.ts diff --git a/packages/kbn-logging/src/mocks/logger.mock.ts b/packages/kbn-logging-mocks/src/logger.mock.ts similarity index 97% rename from packages/kbn-logging/src/mocks/logger.mock.ts rename to packages/kbn-logging-mocks/src/logger.mock.ts index 1b9cdcf71bfa1..b5f1f409ee457 100644 --- a/packages/kbn-logging/src/mocks/logger.mock.ts +++ b/packages/kbn-logging-mocks/src/logger.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Logger } from '../logger'; +import { Logger } from '@kbn/logging'; export type MockedLogger = jest.Mocked & { context: string[] }; diff --git a/packages/kbn-logging-mocks/tsconfig.json b/packages/kbn-logging-mocks/tsconfig.json new file mode 100644 index 0000000000000..ce53e016c2830 --- /dev/null +++ b/packages/kbn-logging-mocks/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-logging-mocks/src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/kbn-logging/BUILD.bazel b/packages/kbn-logging/BUILD.bazel index 8e55456069ee4..09ff3f0d83b2d 100644 --- a/packages/kbn-logging/BUILD.bazel +++ b/packages/kbn-logging/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-logging" PKG_REQUIRE_NAME = "@kbn/logging" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__logging" SOURCE_FILES = glob( [ @@ -22,7 +23,6 @@ filegroup( ) NPM_MODULE_EXTRA_FILES = [ - "mocks/package.json", "package.json", "README.md" ] @@ -69,7 +69,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -88,3 +88,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-logging/mocks/package.json b/packages/kbn-logging/mocks/package.json deleted file mode 100644 index 8410f557e9524..0000000000000 --- a/packages/kbn-logging/mocks/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "private": true, - "main": "../target_node/mocks/index.js", - "types": "../target_types/mocks/index.d.ts" -} \ No newline at end of file diff --git a/packages/kbn-logging/package.json b/packages/kbn-logging/package.json index c35c2f5d06095..0220da8709d30 100644 --- a/packages/kbn-logging/package.json +++ b/packages/kbn-logging/package.json @@ -3,6 +3,5 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target_node/index.js", - "types": "./target_types/index.d.ts" + "main": "./target_node/index.js" } \ No newline at end of file diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 1bc42a556dbc4..57403aff5d7eb 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -17,7 +17,7 @@ import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createHttpServer } from '../test_utils'; import { HttpService } from '../http_service'; import { Router } from '../router'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; let server: HttpService; let logger: ReturnType; diff --git a/src/core/server/logging/logger.mock.ts b/src/core/server/logging/logger.mock.ts index cfabaeb72adf7..55ce55bc035e4 100644 --- a/src/core/server/logging/logger.mock.ts +++ b/src/core/server/logging/logger.mock.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export { loggerMock } from '@kbn/logging/mocks'; -export type { MockedLogger } from '@kbn/logging/mocks'; +export { loggerMock } from '@kbn/logging-mocks'; +export type { MockedLogger } from '@kbn/logging-mocks'; diff --git a/src/core/server/metrics/collectors/cgroup.test.ts b/src/core/server/metrics/collectors/cgroup.test.ts index 269437f026f2f..3f12107c80ea1 100644 --- a/src/core/server/metrics/collectors/cgroup.test.ts +++ b/src/core/server/metrics/collectors/cgroup.test.ts @@ -7,7 +7,7 @@ */ import mockFs from 'mock-fs'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { OsCgroupMetricsCollector } from './cgroup'; describe('OsCgroupMetricsCollector', () => { diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts index 5592038f1416a..4715fab16cb74 100644 --- a/src/core/server/metrics/collectors/os.test.ts +++ b/src/core/server/metrics/collectors/os.test.ts @@ -8,7 +8,7 @@ jest.mock('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' })); -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import os from 'os'; import { cgroupCollectorMock } from './os.test.mocks'; import { OsMetricsCollector } from './os'; diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts index 7d263d8b7d6af..78160729f7bdc 100644 --- a/src/core/server/metrics/ops_metrics_collector.test.ts +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { mockOsCollector, mockProcessCollector, diff --git a/src/plugins/custom_integrations/server/custom_integration_registry.test.ts b/src/plugins/custom_integrations/server/custom_integration_registry.test.ts index 8904aa8a257f6..3d2975f6c2fdf 100644 --- a/src/plugins/custom_integrations/server/custom_integration_registry.test.ts +++ b/src/plugins/custom_integrations/server/custom_integration_registry.test.ts @@ -7,7 +7,7 @@ */ import { CustomIntegrationRegistry } from './custom_integration_registry'; -import { loggerMock, MockedLogger } from '@kbn/logging/mocks'; +import { loggerMock, MockedLogger } from '@kbn/logging-mocks'; import { IntegrationCategory, CustomIntegration } from '../common'; describe('CustomIntegrationsRegistry', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts index 8322e42b0743c..1337c5a70979b 100644 --- a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MockedLogger, loggerMock } from '@kbn/logging/mocks'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; import { TaskRunnerFactory } from '../task_runner'; import { RuleTypeRegistry, ConstructorOptions } from '../rule_type_registry'; import { taskManagerMock } from '../../../task_manager/server/mocks'; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 5ed2d2978f154..821f0a8aaa2f9 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -25,7 +25,7 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; import { CasesService } from '.'; diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 2b30e4d4de628..94df7c21a82a9 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -22,7 +22,7 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CaseConfigureService } from '.'; import { ESCasesConfigureAttributes } from './types'; import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts index eb1e69a3014ab..084f319cbb1e0 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -6,7 +6,7 @@ */ import { get } from 'lodash'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { SavedObject, SavedObjectsFindResponse, SavedObjectsFindResult } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts index 5b9a1bf1539f0..51a516e68ad6d 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.test.ts @@ -8,7 +8,7 @@ jest.mock('timers/promises'); import { setTimeout } from 'timers/promises'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { retryTransientEsErrors } from './retry'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts index feab9cb882393..ee6d7086cdd3c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import type { Logger } from 'src/core/server'; import { appContextService } from '../../../app_context'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index eba645ae1aae4..554105bb00a92 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -6,7 +6,7 @@ */ import { elasticsearchServiceMock } from 'src/core/server/mocks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../../../services'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index f52d594d07ded..927b7cb75816c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -9,7 +9,7 @@ import { readFileSync } from 'fs'; import path from 'path'; import { safeLoad } from 'js-yaml'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { createAppContextStartContractMock } from '../../../../mocks'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 94e2e3f6d73af..144bd2240aa01 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -21,7 +21,7 @@ jest.mock('./common', () => { import { errors } from '@elastic/elasticsearch'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { ElasticsearchAssetType } from '../../../../types'; import type { Installation, RegistryPackage } from '../../../../types'; diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts index fb92b341928da..97ee5acc71023 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts @@ -7,7 +7,7 @@ jest.mock('../../routes/security'); -import type { MockedLogger } from '@kbn/logging/target_types/mocks'; +import type { MockedLogger } from '@kbn/logging-mocks'; import type { ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index f1ac8382a9ba7..5bbfb4f9d15ab 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -7,7 +7,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { DEFAULT_SPACE_ID } from '../../../../../spaces/common/constants'; diff --git a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts index 47eeee057e072..80c89e14dc2cf 100644 --- a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts +++ b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MockedLogger, loggerMock } from '@kbn/logging/mocks'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { ExceptionListClient } from '../exception_lists/exception_list_client'; diff --git a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts index 567a83fd2eb35..70d3eca73313d 100644 --- a/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts +++ b/x-pack/plugins/lists/server/services/extension_points/extension_point_storage_client.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CreateExceptionListItemOptions } from '../exception_lists/exception_list_client_types'; import { getCreateExceptionListItemOptionsMock } from '../exception_lists/exception_list_client.mock'; diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 9543039ab576a..ad7a947273a23 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { Collector, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts index d67c755cb6bc1..0685fcc5bab93 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.test.ts @@ -6,7 +6,7 @@ */ import { ResourceInstaller } from './resource_installer'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { Dataset } from './index_options'; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts index 897af855bd4b2..befc42278196d 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { RuleDataService } from './rule_data_plugin_service'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { AlertConsumers } from '@kbn/rule-data-utils'; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index d1c20e0667e24..1768f0666fd51 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { ALERT_INSTANCE_ID, ALERT_RULE_CATEGORY, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 05a71677c7535..3b9d8904c89b8 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -13,7 +13,7 @@ import { ALERT_STATUS_RECOVERED, ALERT_UUID, } from '@kbn/rule-data-utils'; -import { loggerMock } from '@kbn/logging/mocks'; +import { loggerMock } from '@kbn/logging-mocks'; import { castArray, omit } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; diff --git a/yarn.lock b/yarn.lock index dbc92542b3693..3eb9aa406e50e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3826,6 +3826,10 @@ version "0.0.0" uid "" +"@kbn/logging-mocks@link:bazel-bin/packages/kbn-logging-mocks": + version "0.0.0" + uid "" + "@kbn/logging@link:bazel-bin/packages/kbn-logging": version "0.0.0" uid "" @@ -6798,6 +6802,14 @@ version "0.0.0" uid "" +"@types/kbn__logging-mocks@link:bazel-bin/packages/kbn-logging-mocks/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__logging@link:bazel-bin/packages/kbn-logging/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__mapbox-gl@link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types": version "0.0.0" uid "" From 7c7d1cf5272cc86e1dec3c553e6b30b76f6217ac Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 7 Feb 2022 14:49:36 +0100 Subject: [PATCH 009/161] [Monitor management] Hydrate Url values (#124264) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../uptime/common/types/synthetics_monitor.ts | 4 +- .../lib/adapters/framework/adapter_types.ts | 1 + .../hydrate_saved_object.ts | 104 ++++++++++++++++++ .../synthetics_service/synthetics_service.ts | 7 ++ .../synthetics_service/edit_monitor.ts | 6 +- .../server/rest_api/uptime_route_wrapper.ts | 3 + 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts diff --git a/x-pack/plugins/uptime/common/types/synthetics_monitor.ts b/x-pack/plugins/uptime/common/types/synthetics_monitor.ts index 4045aa952506b..ec6b87bb0bf53 100644 --- a/x-pack/plugins/uptime/common/types/synthetics_monitor.ts +++ b/x-pack/plugins/uptime/common/types/synthetics_monitor.ts @@ -12,4 +12,6 @@ export interface MonitorIdParam { monitorId: string; } -export type SyntheticsMonitorSavedObject = SimpleSavedObject; +export type SyntheticsMonitorSavedObject = SimpleSavedObject & { + updated_at: string; +}; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index c6f5eae2e9d82..b9dfd61e91b70 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -52,6 +52,7 @@ export interface UptimeServerSetup { syntheticsService: SyntheticsService; kibanaVersion: string; logger: Logger; + uptimeEsClient: UptimeESClient; } export interface UptimeCorePluginsSetup { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts new file mode 100644 index 0000000000000..2e98b62ddee66 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts @@ -0,0 +1,104 @@ +/* + * 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 { UptimeESClient } from '../lib'; +import { UptimeServerSetup } from '../adapters'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; +import { MonitorFields, Ping } from '../../../common/runtime_types'; + +export const hydrateSavedObjects = async ({ + monitors, + server, +}: { + monitors: SyntheticsMonitorSavedObject[]; + server: UptimeServerSetup; +}) => { + const missingUrlInfoIds: string[] = []; + + monitors + .filter((monitor) => monitor.attributes.type === 'browser') + .forEach(({ attributes, id }) => { + const monitor = attributes as MonitorFields; + if (!monitor || !monitor.urls) { + missingUrlInfoIds.push(id); + } + }); + + if (missingUrlInfoIds.length > 0 && server.uptimeEsClient) { + const esDocs: Ping[] = await fetchSampleMonitorDocuments( + server.uptimeEsClient, + missingUrlInfoIds + ); + const updatedObjects = monitors + .filter((monitor) => missingUrlInfoIds.includes(monitor.id)) + .map((monitor) => { + let url = ''; + esDocs.forEach((doc) => { + // to make sure the document is ingested after the latest update of the monitor + const diff = moment(monitor.updated_at).diff(moment(doc.timestamp), 'minutes'); + if (doc.config_id === monitor.id && doc.url?.full && diff > 1) { + url = doc.url?.full; + } + }); + if (url) { + return { ...monitor, attributes: { ...monitor.attributes, urls: url } }; + } + return monitor; + }); + await server.authSavedObjectsClient?.bulkUpdate(updatedObjects); + } +}; + +const fetchSampleMonitorDocuments = async (esClient: UptimeESClient, configIds: string[]) => { + const data = await esClient.search({ + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: 'now-15m', + lt: 'now', + }, + }, + }, + { + terms: { + config_id: configIds, + }, + }, + { + term: { + 'monitor.type': 'browser', + }, + }, + { + exists: { + field: 'summary', + }, + }, + { + exists: { + field: 'url.full', + }, + }, + ], + }, + }, + _source: ['url', 'config_id', '@timestamp'], + collapse: { + field: 'config_id', + }, + }, + }); + + return data.body.hits.hits.map( + ({ _source: doc }) => ({ ...(doc as any), timestamp: (doc as any)['@timestamp'] } as Ping) + ); +}; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 20f11fe3b8900..450ab324e7e48 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -31,6 +31,8 @@ import { SyntheticsMonitorWithId, } from '../../../common/runtime_types'; import { getServiceLocations } from './get_service_locations'; +import { hydrateSavedObjects } from './hydrate_saved_object'; +import { SyntheticsMonitorSavedObject } from '../../../common/types'; const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE = 'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects'; @@ -280,6 +282,11 @@ export class SyntheticsService { namespaces: ['*'], }); + hydrateSavedObjects({ + monitors: findResult.saved_objects as unknown as SyntheticsMonitorSavedObject[], + server: this.server, + }); + return (findResult.saved_objects ?? []).map(({ attributes, id }) => ({ ...attributes, id, diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts index 530716f709f64..62a542b2b2037 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/edit_monitor.ts @@ -41,7 +41,11 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ try { const editMonitor: SavedObjectsUpdateResponse = - await savedObjectsClient.update(syntheticsMonitorType, monitorId, monitor); + await savedObjectsClient.update( + syntheticsMonitorType, + monitorId, + monitor.type === 'browser' ? { ...monitor, urls: '' } : monitor + ); const errors = await syntheticsService.pushConfigs(request, [ { diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index 2b96ce369294a..ffe4d4ed4bf93 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -43,6 +43,9 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => savedObjectsClient, esClient: esClient.asCurrentUser, }); + + server.uptimeEsClient = uptimeEsClient; + if (isInspectorEnabled) { inspectableEsQueriesMap.set(request, []); } From afb458ede7d3f995d0770da963cd5a3b1a91a929 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 7 Feb 2022 15:26:33 +0000 Subject: [PATCH 010/161] [ML] Fixing results service space checks (#124707) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../new_job/categorization/top_categories.ts | 6 +++--- .../models/results_service/results_service.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 0d3d0e9e39cdc..65fa3769d0fed 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -34,7 +34,7 @@ export function topCategoriesProvider(mlClient: MlClient) { }, }, }, - [] + [jobId] ); return typeof body.hits.total === 'number' ? body.hits.total : body.hits.total!.value; } @@ -75,7 +75,7 @@ export function topCategoriesProvider(mlClient: MlClient) { }, }, }, - [] + [jobId] ); const catCounts: Array<{ @@ -123,7 +123,7 @@ export function topCategoriesProvider(mlClient: MlClient) { }, }, }, - [] + [jobId] ); // @ts-expect-error incorrect search response type diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 9f1aecfdb2978..5ffe8ed5eb481 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -214,7 +214,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust sort: [{ record_score: { order: 'desc' } }], }, }, - [] + jobIds ); const tableData: { @@ -345,7 +345,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }; - const { body } = await mlClient.anomalySearch(query, []); + const { body } = await mlClient.anomalySearch(query, jobIds); const maxScore = get(body, ['aggregations', 'max_score', 'value'], null); return { maxScore }; @@ -409,7 +409,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + jobIds ); const bucketsByJobId: Array<{ key: string; maxTimestamp: { value?: number } }> = get( @@ -440,7 +440,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + [jobId] ); const examplesByCategoryId: { [key: string]: any } = {}; @@ -477,7 +477,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + [jobId] ); const definition = { categoryId, terms: null, regex: null, examples: [] }; @@ -526,7 +526,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }, }, }, - [] + [jobId] ); return body ? body.hits.hits.map((r) => r._source) : []; } @@ -617,7 +617,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust aggs, }, }, - [] + jobIds ); if (fieldToBucket === JOB_ID) { finalResults = { From 2e04a8fa82a5be108f92638adb3ca0b49dfe46fa Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:50:52 +0100 Subject: [PATCH 011/161] Add github action for adding tech debt items to Fleet project (#124824) --- .github/workflows/add-to-fleet-project.yml | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/add-to-fleet-project.yml diff --git a/.github/workflows/add-to-fleet-project.yml b/.github/workflows/add-to-fleet-project.yml new file mode 100644 index 0000000000000..06a39a04e8e34 --- /dev/null +++ b/.github/workflows/add-to-fleet-project.yml @@ -0,0 +1,28 @@ +name: Add to Fleet:Quality project +on: + issues: + types: + - labeled +jobs: + add_to_project: + runs-on: ubuntu-latest + if: | + contains(github.event.issue.labels.*.name, 'Team:Fleet') && contains(github.event.issue.labels.*.name, 'technical debt') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:String!,$contentid:String!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAGc3Zs4AAsH6" + GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }} From 004f9e2dde3aa8e896e5bbfeafc7d33a24481dd7 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 7 Feb 2022 18:01:25 +0200 Subject: [PATCH 012/161] [Canvas] Fixes Incorrect Datasource Form (#124656) * Fixed incorrect Datasource Form behavior. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/expression_types/datasource.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.tsx b/x-pack/plugins/canvas/public/expression_types/datasource.tsx index 5f85b5ec20abd..a7d5cd4e7a2b4 100644 --- a/x-pack/plugins/canvas/public/expression_types/datasource.tsx +++ b/x-pack/plugins/canvas/public/expression_types/datasource.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useRef, useCallback, ReactPortal, useState, memo } from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; +import usePrevious from 'react-use/lib/usePrevious'; import deepEqual from 'react-fast-compare'; import { Ast } from '@kbn/interpreter'; import { createPortal } from 'react-dom'; @@ -58,6 +59,7 @@ const DatasourceWrapperComponent: React.FunctionComponent(); const { spec, datasourceProps, handlers } = props; + const prevSpec = usePrevious(spec); const onMount = useCallback((ref) => { datasourceRef.current = ref ?? undefined; @@ -83,15 +85,25 @@ const DatasourceWrapperComponent: React.FunctionComponent { + if (argument && prevSpec?.name !== spec?.name) { + setArgument(undefined); + datasourceRef.current = undefined; + } + }, [argument, prevSpec?.name, spec?.name]); + useEffect(() => { if (datasourceRef.current) { datasourceRef.current.updateProps(datasourceProps); } }, [datasourceProps]); - useEffectOnce(() => () => { + useEffectOnce(() => { datasourceRef.current = undefined; - handlers.destroy(); + return () => { + datasourceRef.current = undefined; + handlers.destroy(); + }; }); return ( From f121c545cfd095b62d0f75171a3d7ad3ec9b5ab6 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 7 Feb 2022 17:17:48 +0100 Subject: [PATCH 013/161] Collect code coverage for server side (#124277) * [code coverage] collect coverage on server side * fix script * copy server coverage to functional * [code coverage] run all e2e and api tests * enable all configs for coverage, fix reportDir * [code coverage] use KIBANA_DIR var * create target/kibana-coverage/functional if not exist * remove empty lines and unused code * adjust include/exclude filters * exclude more test-related paths * Update functional nyc config * Update server nyc config * exclude more dirs * exclude stub/ --- .../steps/code_coverage/oss_cigroup.sh | 11 ++++++- .../steps/code_coverage/xpack_cigroup.sh | 11 ++++++- package.json | 1 + scripts/functional_tests.js | 15 ++------- .../nyc_config/nyc.functional.config.js | 17 +++++++++- .../nyc_config/nyc.server.config.js | 33 +++++++++++++++++++ x-pack/scripts/functional_tests.js | 11 ++----- yarn.lock | 7 ++++ 8 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 src/dev/code_coverage/nyc_config/nyc.server.config.js diff --git a/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh b/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh index 44d4bb500e1d2..7a54e770b8a3e 100755 --- a/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh +++ b/.buildkite/scripts/steps/code_coverage/oss_cigroup.sh @@ -20,10 +20,19 @@ export CODE_COVERAGE=1 echo "--- OSS CI Group $CI_GROUP" echo " -> Running Functional tests with code coverage" -node scripts/functional_tests \ +NODE_OPTIONS=--max_old_space_size=14336 \ + ./node_modules/.bin/nyc \ + --nycrc-path src/dev/code_coverage/nyc_config/nyc.server.config.js \ + node scripts/functional_tests \ --include-tag "ciGroup$CI_GROUP" \ --exclude-tag "skipCoverage" || true +if [[ -d "$KIBANA_DIR/target/kibana-coverage/server" ]]; then + echo "--- Server side code coverage collected" + mkdir -p target/kibana-coverage/functional + mv target/kibana-coverage/server/coverage-final.json "target/kibana-coverage/functional/oss-${CI_GROUP}-server-coverage.json" +fi + if [[ -d "$KIBANA_DIR/target/kibana-coverage/functional" ]]; then echo "--- Merging code coverage for CI Group $CI_GROUP" yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.functional.config.js --reporter json diff --git a/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh b/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh index c85191e4e4632..a3fdff6690485 100755 --- a/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh +++ b/.buildkite/scripts/steps/code_coverage/xpack_cigroup.sh @@ -22,12 +22,21 @@ echo " -> Running X-Pack functional tests with code coverage" cd "$XPACK_DIR" -node scripts/functional_tests \ +NODE_OPTIONS=--max_old_space_size=14336 \ + ./../node_modules/.bin/nyc \ + --nycrc-path ./../src/dev/code_coverage/nyc_config/nyc.server.config.js \ + node scripts/functional_tests \ --include-tag "ciGroup$CI_GROUP" \ --exclude-tag "skipCoverage" || true cd "$KIBANA_DIR" +if [[ -d "$KIBANA_DIR/target/kibana-coverage/server" ]]; then + echo "--- Server side code coverage collected" + mkdir -p target/kibana-coverage/functional + mv target/kibana-coverage/server/coverage-final.json "target/kibana-coverage/functional/xpack-${CI_GROUP}-server-coverage.json" +fi + if [[ -d "$KIBANA_DIR/target/kibana-coverage/functional" ]]; then echo "--- Merging code coverage for CI Group $CI_GROUP" yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.functional.config.js --reporter json diff --git a/package.json b/package.json index 24322b8cccb2d..9ea3e8335f886 100644 --- a/package.json +++ b/package.json @@ -455,6 +455,7 @@ "@elastic/synthetics": "^1.0.0-beta.16", "@emotion/babel-preset-css-prop": "^11.2.0", "@emotion/jest": "^11.3.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/schema": "^0.1.2", "@jest/console": "^26.6.2", "@jest/reporters": "^26.6.2", diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index b286cf05a6d71..6cfd1d57e9c96 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -// eslint-disable-next-line no-restricted-syntax -const alwaysImportedTests = [ +require('../src/setup_node_env'); +require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), @@ -25,18 +25,7 @@ const alwaysImportedTests = [ require.resolve( '../test/interactive_setup_functional/manual_configuration_without_tls.config.ts' ), -]; -// eslint-disable-next-line no-restricted-syntax -const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config.js'), require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/examples/config.js'), -]; - -require('../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - // eslint-disable-next-line no-restricted-syntax - ...alwaysImportedTests, - // eslint-disable-next-line no-restricted-syntax - ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests), ]); diff --git a/src/dev/code_coverage/nyc_config/nyc.functional.config.js b/src/dev/code_coverage/nyc_config/nyc.functional.config.js index 479c40ec9e109..1b68c23db5f4b 100644 --- a/src/dev/code_coverage/nyc_config/nyc.functional.config.js +++ b/src/dev/code_coverage/nyc_config/nyc.functional.config.js @@ -7,7 +7,18 @@ */ const defaultExclude = require('@istanbuljs/schema/default-exclude'); -const extraExclude = ['data/optimize/**', 'src/core/server/**', '**/{test, types}/**/*']; +const extraExclude = [ + 'data/optimize/**', + '**/{__jest__,__test__,__examples__,__fixtures__,__snapshots__,__stories__,*mock*,*storybook,target,types}/**/*', + '**/{integration_tests,test,tests,test_helpers,test_data,test_samples,test_utils,test_utilities,*scripts}/**/*', + '**/{*e2e*,fixtures,manual_tests,stub*}/**', + '**/*mock*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/*.spec.{ts,tsx}', + '**/types.ts', + '**/*.d.ts', + '**/index.{js,ts,tsx}', +]; const path = require('path'); module.exports = { @@ -16,5 +27,9 @@ module.exports = { : 'target/kibana-coverage/functional', 'report-dir': 'target/kibana-coverage/functional-combined', reporter: ['html', 'json-summary'], + include: [ + 'src/{core,plugins}/**/*.{js,mjs,jsx,ts,tsx}', + 'x-pack/plugins/**/*.{js,mjs,jsx,ts,tsx}', + ], exclude: extraExclude.concat(defaultExclude), }; diff --git a/src/dev/code_coverage/nyc_config/nyc.server.config.js b/src/dev/code_coverage/nyc_config/nyc.server.config.js new file mode 100644 index 0000000000000..d8cebf468d0db --- /dev/null +++ b/src/dev/code_coverage/nyc_config/nyc.server.config.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +const path = require('path'); + +module.exports = { + extends: '@istanbuljs/nyc-config-typescript', + 'report-dir': process.env.KIBANA_DIR + ? path.resolve(process.env.KIBANA_DIR, 'target/kibana-coverage/server') + : 'target/kibana-coverage/server', + reporter: ['json'], + all: true, + include: [ + 'src/{core,plugins}/**/*.{js,mjs,jsx,ts,tsx}', + 'x-pack/plugins/**/*.{js,mjs,jsx,ts,tsx}', + ], + exclude: [ + '**/{__jest__,__test__,__examples__,__fixtures__,__snapshots__,__stories__,*mock*,*storybook,target,types}/**/*', + '**/{integration_tests,test,tests,test_helpers,test_data,test_samples,test_utils,test_utilities,*scripts}/**/*', + '**/{*e2e*,fixtures,manual_tests,stub*}/**', + '**/*mock*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/*.spec.{ts,tsx}', + '**/types.ts', + '**/*.d.ts', + '**/index.{js,ts,tsx}', + ], +}; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 7f83b1464805a..977d0c3f1768c 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -5,7 +5,8 @@ * 2.0. */ -const alwaysImportedTests = [ +require('../../src/setup_node_env'); +require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/functional_basic/config.ts'), require.resolve('../test/security_solution_endpoint/config.ts'), @@ -25,8 +26,6 @@ const alwaysImportedTests = [ require.resolve('../test/usage_collection/config.ts'), require.resolve('../test/fleet_functional/config.ts'), require.resolve('../test/functional_synthetics/config.js'), -]; -const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config_security_basic.ts'), require.resolve('../test/api_integration/config_security_trial.ts'), require.resolve('../test/api_integration/config.ts'), @@ -93,10 +92,4 @@ const onlyNotInCoverageTests = [ require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), require.resolve('../test/examples/config.ts'), require.resolve('../test/functional_execution_context/config.ts'), -]; - -require('../../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - ...alwaysImportedTests, - ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests), ]); diff --git a/yarn.lock b/yarn.lock index 3eb9aa406e50e..ad5df52a1655c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3195,6 +3195,13 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" +"@istanbuljs/nyc-config-typescript@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz#1f5235b28540a07219ae0dd42014912a0b19cf89" + integrity sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + "@istanbuljs/schema@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" From 19476a1f9cd694fb99a1e81492cc5a16fb4570d8 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 7 Feb 2022 17:47:22 +0100 Subject: [PATCH 014/161] Add bug and performance labels to Fleet assigner (#124837) --- .github/workflows/add-to-fleet-project.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/add-to-fleet-project.yml b/.github/workflows/add-to-fleet-project.yml index 06a39a04e8e34..fc5676887f3ae 100644 --- a/.github/workflows/add-to-fleet-project.yml +++ b/.github/workflows/add-to-fleet-project.yml @@ -7,7 +7,11 @@ jobs: add_to_project: runs-on: ubuntu-latest if: | - contains(github.event.issue.labels.*.name, 'Team:Fleet') && contains(github.event.issue.labels.*.name, 'technical debt') + contains(github.event.issue.labels.*.name, 'Team:Fleet') && ( + contains(github.event.issue.labels.*.name, 'technical debt') || + contains(github.event.issue.labels.*.name, 'bug') || + contains(github.event.issue.labels.*.name, 'performance') + ) steps: - uses: octokit/graphql-action@v2.x id: add_to_project From ec47913330eb1243729fd78adb21a47158354a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Mon, 7 Feb 2022 18:02:13 +0100 Subject: [PATCH 015/161] [Unified observability] Wrap exploratory view in the page component (#124269) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../exploratory_view.test.tsx | 15 +- .../exploratory_view/exploratory_view.tsx | 176 +++++++++--------- .../exploratory_view/header/header.test.tsx | 26 --- .../shared/exploratory_view/header/header.tsx | 56 ------ .../shared/exploratory_view/index.tsx | 15 +- 5 files changed, 108 insertions(+), 180 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 458bcc0ea4a5f..b1a1b55b7ed1e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -50,8 +50,6 @@ describe('ExploratoryView', () => { expect(await screen.findByText(/No series found. Please add a series./i)).toBeInTheDocument(); expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); - expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); - expect(await screen.findByRole('heading', { name: /Explore data/i })).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { @@ -62,4 +60,17 @@ describe('ExploratoryView', () => { expect(screen.getByTestId('exploratoryViewSeriesPanel0')).toBeInTheDocument(); }); + + it('shows/hides the chart', async () => { + render(); + expect(screen.queryByText('Refresh')).toBeInTheDocument(); + + const toggleButton = await screen.findByText('Hide chart'); + expect(toggleButton).toBeInTheDocument(); + + toggleButton.click(); + + expect(toggleButton.textContent).toBe('Show chart'); + expect(screen.queryByText('Refresh')).toBeNull(); + }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 62b54a3fec203..a383bc37880ae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -9,17 +9,16 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { + EuiButton, EuiButtonEmpty, EuiResizableContainer, EuiTitle, - EuiPanel, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; -import { ExploratoryViewHeader } from './header/header'; import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; @@ -27,7 +26,9 @@ import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -import type { ChartTimeRange } from './header/last_updated'; +import { ChartTimeRange, LastUpdated } from './header/last_updated'; +import { useExpViewTimeRange } from './hooks/use_time_range'; +import { ExpViewActionMenu } from './components/action_menu'; export type PanelId = 'seriesPanel' | 'chartPanel'; @@ -53,9 +54,10 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { firstSeries, allSeries, lastRefresh, reportType, setLastRefresh } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); + const timeRange = useExpViewTimeRange(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { @@ -100,80 +102,85 @@ export function ExploratoryView({ } }; - return ( - - {lens ? ( - <> - - - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); - - return ( - <> - + return lens ? ( + <> + + + + {(EuiResizablePanel, _EuiResizableButton, { togglePanel }) => { + collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); + + return ( + <> + + + onChange('chartPanel')} + > + {hiddenPanel === 'chartPanel' ? SHOW_CHART_LABEL : HIDE_CHART_LABEL} + + + {hiddenPanel === 'chartPanel' ? null : ( + <> + + + - onChange('chartPanel')} - > - {hiddenPanel === 'chartPanel' ? SHOW_CHART_LABEL : HIDE_CHART_LABEL} - + setLastRefresh(Date.now())}> + {REFRESH_LABEL} + - - - - {lensAttributes ? ( - - ) : ( - - )} - - - - - - - ); - }} - - {hiddenPanel === 'seriesPanel' && ( - onChange('seriesPanel')} iconType="arrowUp"> - {PREVIEW_LABEL} - - )} - - - ) : ( - -

{LENS_NOT_AVAILABLE}

-
- )} -
+ + )} + + + + {lensAttributes ? ( + + ) : ( + + )} + + + + + + + ); + }} + + {hiddenPanel === 'seriesPanel' && ( + onChange('seriesPanel')} iconType="arrowUp"> + {PREVIEW_LABEL} + + )} + + + ) : ( + +

{LENS_NOT_AVAILABLE}

+
); } const LensWrapper = styled.div<{ height: string }>` @@ -192,19 +199,6 @@ const ResizableContainer = styled(EuiResizableContainer)` } `; -const Wrapper = styled(EuiPanel)` - max-width: 1800px; - min-width: 800px; - margin: 0 auto; - width: 100%; - overflow-x: auto; - position: relative; - - .echLegendItem__action { - display: none; - } -`; - const ShowPreview = styled(EuiButtonEmpty)` position: absolute; bottom: 34px; @@ -222,6 +216,10 @@ const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryVi defaultMessage: 'Preview', }); +const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { + defaultMessage: 'Refresh', +}); + const LENS_NOT_AVAILABLE = i18n.translate( 'xpack.observability.overview.exploratoryView.lensDisabled', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx deleted file mode 100644 index 82055ba128c9d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ /dev/null @@ -1,26 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '../rtl_helpers'; -import { ExploratoryViewHeader } from './header'; -import * as pluginHook from '../../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); - -describe('ExploratoryViewHeader', function () { - it('should render properly', function () { - const { getByText } = render( - - ); - getByText('Refresh'); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx deleted file mode 100644 index 8ef3f228a5854..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ /dev/null @@ -1,56 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput } from '../../../../../../lens/public'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { ExpViewActionMenu } from '../components/action_menu'; -import { useExpViewTimeRange } from '../hooks/use_time_range'; -import { LastUpdated } from './last_updated'; -import type { ChartTimeRange } from './last_updated'; - -interface Props { - chartTimeRange?: ChartTimeRange; - lensAttributes: TypedLensByValueInput['attributes'] | null; -} - -export function ExploratoryViewHeader({ lensAttributes, chartTimeRange }: Props) { - const { setLastRefresh } = useSeriesStorage(); - - const timeRange = useExpViewTimeRange(); - - return ( - <> - - - - -

- {i18n.translate('xpack.observability.expView.heading.label', { - defaultMessage: 'Explore data', - })} -

-
-
- - - - - setLastRefresh(Date.now())}> - {REFRESH_LABEL} - - -
- - ); -} - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 5dbe0c2a6c078..4fc5293e03723 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { useHistory } from 'react-router-dom'; import { ExploratoryView } from './exploratory_view'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; @@ -22,6 +21,11 @@ import { import { UrlStorageContextProvider } from './hooks/use_series_storage'; import { useTrackPageview } from '../../..'; import { TypedLensByValueInput } from '../../../../../lens/public'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +const PAGE_TITLE = i18n.translate('xpack.observability.expView.heading.label', { + defaultMessage: 'Explore data', +}); export interface ExploratoryViewPageProps { useSessionStorage?: boolean; @@ -52,6 +56,7 @@ export function ExploratoryViewPage({ app ); + const { ObservabilityPageTemplate } = usePluginContext(); const { services: { uiSettings, notifications }, } = useKibana(); @@ -67,19 +72,15 @@ export function ExploratoryViewPage({ }); return ( - + - + ); } -const Wrapper = euiStyled.div` - padding: ${(props) => props.theme.eui.paddingSizes.l}; -`; - // eslint-disable-next-line import/no-default-export export default ExploratoryViewPage; From 179c4ad66fb942070d16f423ea5286afedf8c048 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 7 Feb 2022 17:22:58 +0000 Subject: [PATCH 016/161] skip flaky suite (#106547) --- .../functional/tests/dashboard_integration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index 1bd95ca9f16e4..dbcb520c2a5f5 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -153,7 +153,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('editing', () => { + // FLAKY: https://github.com/elastic/kibana/issues/106547 + describe.skip('editing', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.gotoDashboardLandingPage(); From f4d19820ec14f2e8460ded9ecb843a0dfe8b8957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Mon, 7 Feb 2022 18:27:38 +0100 Subject: [PATCH 017/161] [Security Solution][Endpoint] Common list api service class to be used in all of endpoint artifacts (#124245) * Adds new generic exceptions api client class to be used for our different artifacts * Adds custom implementations for artifact exceptions list api client * Fixes wrong path * Adds generic exceptions list api client unit test * Stores instances by listId so one instance from one list id doesn't overwritte the other one * Adds list id validation when creating an entry * Ensures list is created just one time when creating new instances Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../service/event_filters_api_client.ts | 26 ++ .../host_isolation_exceptions_api_client.ts | 34 +++ .../service/trusted_apps_api_client.ts | 30 +++ .../exceptions_list_api_client.test.ts | 211 ++++++++++++++++ .../exceptions_list_api_client.ts | 233 ++++++++++++++++++ 5 files changed, 534 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts create mode 100644 x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts create mode 100644 x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts new file mode 100644 index 0000000000000..89cf2f0cbd276 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { EVENT_FILTER_LIST_DEFINITION } from '../constants'; + +/** + * Event filters Api client class using ExceptionsListApiClient as base class + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class EventFiltersApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super(http, ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_DEFINITION); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance(http, ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_DEFINITION); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.ts new file mode 100644 index 0000000000000..7e711732fa6f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../services/exceptions_list/exceptions_list_api_client'; +import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from './constants'; + +/** + * Host isolation exceptions Api client class using ExceptionsListApiClient as base class + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class HostIsolationExceptionsApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super( + http, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION + ); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance( + http, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION + ); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts new file mode 100644 index 0000000000000..4d1f0170b4117 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../constants'; + +/** + * Trusted apps Api client class using ExceptionsListApiClient as base class + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class TrustedAppsApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super(http, ENDPOINT_TRUSTED_APPS_LIST_ID, TRUSTED_APPS_EXCEPTION_LIST_DEFINITION); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance( + http, + ENDPOINT_TRUSTED_APPS_LIST_ID, + TRUSTED_APPS_EXCEPTION_LIST_DEFINITION + ); + } +} diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts new file mode 100644 index 0000000000000..c1e3a2f76064f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -0,0 +1,211 @@ +/* + * 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 { CoreStart, HttpSetup } from 'kibana/public'; +import { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { ExceptionsListItemGenerator } from '../../../../common/endpoint/data_generators/exceptions_list_item_generator'; +import { ExceptionsListApiClient } from './exceptions_list_api_client'; + +const getFakeListId: () => string = () => 'FAKE_LIST_ID'; +const getFakeListDefinition: () => CreateExceptionListSchema = () => ({ + name: 'FAKE_LIST_NAME', + namespace_type: 'agnostic', + description: 'FAKE_LIST_DESCRIPTION', + list_id: getFakeListId(), + type: 'endpoint', +}); +const getQueryParams = () => ({ + page: 1, + perPage: 10, + filter: 'this is a KQL filter', + sortField: 'id', + sortOrder: 'asc', +}); + +describe('Exceptions List Api Client', () => { + let fakeCoreStart: jest.Mocked; + let fakeHttpServices: jest.Mocked; + let getInstance: () => ExceptionsListApiClient; + + // Initialize mocks once as the ExceptionsListApiClient is a singleton + beforeAll(() => { + fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); + fakeHttpServices = fakeCoreStart.http as jest.Mocked; + }); + + // Clear every function mock on each execution + beforeEach(() => { + fakeHttpServices.post.mockClear(); + fakeHttpServices.get.mockClear(); + fakeHttpServices.put.mockClear(); + fakeHttpServices.delete.mockClear(); + getInstance = () => + ExceptionsListApiClient.getInstance( + fakeHttpServices, + getFakeListId(), + getFakeListDefinition() + ); + }); + + describe('Wen getting an instance', () => { + /** + * ATENTION: Skipping or modifying this test may cause the other test fails because it's creating the initial Singleton instance. + * If you want to run tests individually, add this one to the execution with the .only method + */ + it('New instance is created the first time and the create list method is called', () => { + const exceptionsListApiClientInstance = getInstance(); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.post).toHaveBeenCalledWith(EXCEPTION_LIST_URL, { + body: JSON.stringify(getFakeListDefinition()), + }); + expect(exceptionsListApiClientInstance).toBeDefined(); + }); + + it('No new instance is created the second time and the creat list method is not called', () => { + const exceptionsListApiClientInstance = getInstance(); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(0); + expect(exceptionsListApiClientInstance).toBeDefined(); + }); + + it('Creating three instances from the same listId only creates the list one time', () => { + const newFakeListId = 'fakeListIdV2'; + const exceptionsListApiClientInstanceV1 = new ExceptionsListApiClient( + fakeHttpServices, + newFakeListId, + getFakeListDefinition() + ); + const exceptionsListApiClientInstanceV2 = new ExceptionsListApiClient( + fakeHttpServices, + newFakeListId, + getFakeListDefinition() + ); + const exceptionsListApiClientInstanceV3 = new ExceptionsListApiClient( + fakeHttpServices, + newFakeListId, + getFakeListDefinition() + ); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(1); + expect(exceptionsListApiClientInstanceV1).toBeDefined(); + expect(exceptionsListApiClientInstanceV2).toBeDefined(); + expect(exceptionsListApiClientInstanceV3).toBeDefined(); + }); + }); + + describe('Wen using public methods', () => { + it('Find method calls http.get with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + + await exceptionsListApiClientInstance.find(getQueryParams()); + + expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); + const expectedQueryParams = getQueryParams(); + expect(fakeHttpServices.get).toHaveBeenCalledWith(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: { + page: expectedQueryParams.page, + per_page: expectedQueryParams.perPage, + filter: expectedQueryParams.filter, + sort_field: expectedQueryParams.sortField, + sort_order: expectedQueryParams.sortOrder, + namespace_type: ['agnostic'], + list_id: [getFakeListId()], + }, + }); + }); + + it('Get method calls http.get with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + const fakeItemId = 'fakeId'; + + await exceptionsListApiClientInstance.get(fakeItemId); + + expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.get).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + query: { + id: fakeItemId, + namespace_type: 'agnostic', + }, + }); + }); + + it('Create method calls http.post with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + + const exceptionItem = { + ...new ExceptionsListItemGenerator('seed').generate(), + list_id: getFakeListId(), + }; + await exceptionsListApiClientInstance.create(exceptionItem); + + expect(fakeHttpServices.post).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.post).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exceptionItem), + }); + }); + + it('Throws when create method has wrong listId', async () => { + const wrongListId = 'wrong'; + const expectedError = new Error( + `The list id you are using is not valid, expected [${getFakeListId()}] list id but received [${wrongListId}] list id` + ); + const exceptionsListApiClientInstance = getInstance(); + + const exceptionItem = new ExceptionsListItemGenerator('seed').generate(); + try { + await exceptionsListApiClientInstance.create({ ...exceptionItem, list_id: wrongListId }); + } catch (err) { + expect(err).toEqual(expectedError); + } + }); + + it('Update method calls http.put with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + + const exceptionItem = new ExceptionsListItemGenerator('seed').generate(); + await exceptionsListApiClientInstance.update(exceptionItem); + + expect(fakeHttpServices.put).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.put).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(ExceptionsListApiClient.cleanExceptionsBeforeUpdate(exceptionItem)), + }); + }); + + it('Delete method calls http.delete with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + const fakeItemId = 'fakeId'; + + await exceptionsListApiClientInstance.delete(fakeItemId); + + expect(fakeHttpServices.delete).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.delete).toHaveBeenCalledWith(EXCEPTION_LIST_ITEM_URL, { + query: { + id: fakeItemId, + namespace_type: 'agnostic', + }, + }); + }); + + it('Summary method calls http.get with params', async () => { + const exceptionsListApiClientInstance = getInstance(); + const fakeQklFilter = 'KQL filter'; + + await exceptionsListApiClientInstance.summary(fakeQklFilter); + + expect(fakeHttpServices.get).toHaveBeenCalledTimes(1); + expect(fakeHttpServices.get).toHaveBeenCalledWith(`${EXCEPTION_LIST_URL}/summary`, { + query: { + filter: fakeQklFilter, + namespace_type: 'agnostic', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts new file mode 100644 index 0000000000000..226d70ca30797 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -0,0 +1,233 @@ +/* + * 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 { + CreateExceptionListItemSchema, + CreateExceptionListSchema, + ExceptionListItemSchema, + ExceptionListSummarySchema, + FoundExceptionListItemSchema, + ListId, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { HttpStart } from 'kibana/public'; +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; + +/** + * A generic class to be used for each artifact type. + * It follow the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class ExceptionsListApiClient { + private static instance: Map = new Map(); + private static wasListCreated: Map> = new Map(); + private ensureListExists: Promise; + + constructor( + private readonly http: HttpStart, + private readonly listId: ListId, + private readonly listDefinition: CreateExceptionListSchema + ) { + this.ensureListExists = this.createExceptionList(); + } + + /** + * PrivateStatic method that creates the list and don't throw if list already exists. + * This method is being used when initializing an instance only once. + */ + private async createExceptionList(): Promise { + if (ExceptionsListApiClient.wasListCreated.has(this.listId)) { + return ExceptionsListApiClient.wasListCreated.get(this.listId); + } + ExceptionsListApiClient.wasListCreated.set( + this.listId, + new Promise(async (resolve, reject) => { + try { + await this.http.post(EXCEPTION_LIST_URL, { + body: JSON.stringify({ ...this.listDefinition, list_id: this.listId }), + }); + + resolve(); + } catch (err) { + // Ignore 409 errors. List already created + if (err.response?.status !== 409) { + reject(err); + } + + resolve(); + } + }) + ); + + ExceptionsListApiClient.wasListCreated.get(this.listId)?.catch(() => { + ExceptionsListApiClient.wasListCreated.delete(this.listId); + }); + + return ExceptionsListApiClient.wasListCreated.get(this.listId); + } + + /** + * Private method that throws an error when some of the checks to ensure the instance + * we are using is the right one fail + */ + private checkIfIsUsingTheRightInstance(listId: ListId): void { + if (listId !== this.listId) { + throw new Error( + `The list id you are using is not valid, expected [${this.listId}] list id but received [${listId}] list id` + ); + } + } + + /** + * Static method to get a fresh or existing instance. + * It will ensure we only check and create the list once. + */ + public static getInstance( + http: HttpStart, + listId: string, + listDefinition: CreateExceptionListSchema + ): ExceptionsListApiClient { + if (!ExceptionsListApiClient.instance.has(listId)) { + ExceptionsListApiClient.instance.set( + listId, + new ExceptionsListApiClient(http, listId, listDefinition) + ); + } + const currentInstance = ExceptionsListApiClient.instance.get(listId); + if (currentInstance) { + return currentInstance; + } else { + return new ExceptionsListApiClient(http, listId, listDefinition); + } + } + + /** + * Static method to clean an exception item before sending it to update. + */ + public static cleanExceptionsBeforeUpdate( + exception: UpdateExceptionListItemSchema + ): UpdateExceptionListItemSchema { + const exceptionToUpdateCleaned = { ...exception }; + // Clean unnecessary fields for update action + [ + 'created_at', + 'created_by', + 'created_at', + 'created_by', + 'list_id', + 'tie_breaker_id', + 'updated_at', + 'updated_by', + ].forEach((field) => { + delete exceptionToUpdateCleaned[field as keyof UpdateExceptionListItemSchema]; + }); + + exceptionToUpdateCleaned.comments = exceptionToUpdateCleaned.comments?.map((comment) => ({ + comment: comment.comment, + id: comment.id, + })); + + return exceptionToUpdateCleaned as UpdateExceptionListItemSchema; + } + + /** + * Returns a list of items with pagination params. + * It accepts the allowed filtering, sorting and pagination options as param. + */ + async find({ + perPage = MANAGEMENT_DEFAULT_PAGE_SIZE, + page = MANAGEMENT_DEFAULT_PAGE + 1, + sortField, + sortOrder, + filter, + }: Partial<{ + page: number; + perPage: number; + sortField: string; + sortOrder: string; + filter: string; + }> = {}): Promise { + await this.ensureListExists; + return this.http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: { + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + list_id: [this.listId], + namespace_type: ['agnostic'], + filter, + }, + }); + } + + /** + * Returns an item filtered by id + * It requires an id in order to get the desired item + */ + async get(id: string): Promise { + await this.ensureListExists; + return this.http.get(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); + } + + /** + * It creates an item and returns the created one. + * It requires a CreateExceptionListItemSchema object. + */ + async create(exception: CreateExceptionListItemSchema): Promise { + await this.ensureListExists; + this.checkIfIsUsingTheRightInstance(exception.list_id); + return this.http.post(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exception), + }); + } + + /** + * It updates an existing item and returns the updated one. + * It requires a UpdateExceptionListItemSchema object. + */ + async update(exception: UpdateExceptionListItemSchema): Promise { + await this.ensureListExists; + return this.http.put(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(ExceptionsListApiClient.cleanExceptionsBeforeUpdate(exception)), + }); + } + + /** + * It deletes an existing item. + * It requires a valid item id. + */ + async delete(id: string): Promise { + await this.ensureListExists; + return this.http.delete(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); + } + + /** + * It returns a summary of the current list_id + * It accepts a filter param to filter the summary results using KQL filtering. + */ + async summary(filter?: string): Promise { + await this.ensureListExists; + return this.http.get(`${EXCEPTION_LIST_URL}/summary`, { + query: { + filter, + namespace_type: 'agnostic', + }, + }); + } +} From 778cc79c2985da50de28815e0ac14d8058f35e1a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 7 Feb 2022 17:30:39 +0000 Subject: [PATCH 018/161] skip flaky suite (#118288) --- .../apps/saved_objects_management/inspect_saved_objects.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts index 2faa66d258eb6..059f172e51b46 100644 --- a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts +++ b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts @@ -33,7 +33,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { return bools.every((currBool) => currBool === true); }; - describe('saved objects inspect page', () => { + // FLAKY: https://github.com/elastic/kibana/issues/118288 + describe.skip('saved objects inspect page', () => { beforeEach(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' From 1d963b2827828f82416eb7894552536162c38230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Mon, 7 Feb 2022 18:58:44 +0100 Subject: [PATCH 019/161] [ResponseOps] Improves check interval tooltip (#124671) --- .../public/application/sections/alert_form/alert_form.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 90d43e476e2ff..06542cbb3a1a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -585,7 +585,8 @@ export const AlertForm = ({ position="right" type="questionInCircle" content={i18n.translate('xpack.triggersActionsUI.sections.alertForm.checkWithTooltip', { - defaultMessage: 'Define how often to evaluate the condition.', + defaultMessage: + 'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows.', })} /> From 330284384276d0b18d6d7342089fc32e2815c566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Mon, 7 Feb 2022 19:29:01 +0100 Subject: [PATCH 020/161] [IM] Added missing "Docs Deleted" and "Primary Storage Size" values in the index details flyout (#124731) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/extend_index_management.test.tsx.snap | 4 ++-- .../__jest__/extend_index_management.test.tsx | 6 +++--- x-pack/plugins/index_management/common/types/indices.ts | 2 ++ .../index_management/server/lib/fetch_indices.test.ts | 1 + x-pack/plugins/index_management/server/lib/fetch_indices.ts | 2 ++ .../server/test/helpers/indices_fixtures.ts | 5 ++++- .../apis/management/index_management/indices.js | 4 ++++ 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 7e90618079021..25930c07fcd8b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -101,7 +101,7 @@ exports[`extend index management ilm summary extension should return extension w Object { "aliases": "none", "documents": 2, - "documents_deleted": "0", + "documents_deleted": 0, "health": "yellow", "hidden": false, "ilm": Object { @@ -649,7 +649,7 @@ exports[`extend index management ilm summary extension should return extension w Object { "aliases": "none", "documents": 2, - "documents_deleted": "0", + "documents_deleted": 0, "health": "yellow", "hidden": false, "ilm": Object { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 240cc18fdc02d..eaebd6381d984 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -44,7 +44,7 @@ const indexWithoutLifecyclePolicy: Index = { primary: 1, replica: 1, documents: 1, - documents_deleted: '0', + documents_deleted: 0, size: '3.4kb', primary_size: '3.4kb', aliases: 'none', @@ -64,7 +64,7 @@ const indexWithLifecyclePolicy: Index = { primary: 1, replica: 1, documents: 2, - documents_deleted: '0', + documents_deleted: 0, size: '6.5kb', primary_size: '6.5kb', aliases: 'none', @@ -92,7 +92,7 @@ const indexWithLifecycleError = { primary: 1, replica: 1, documents: 2, - documents_deleted: '0', + documents_deleted: 0, size: '6.5kb', primary_size: '6.5kb', aliases: 'none', diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index 28877dee77e57..ba7b58453c36b 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -59,7 +59,9 @@ export interface Index { primary?: number | string; replica?: number | string; documents: number; + documents_deleted: number; size: string; + primary_size: string; isFrozen: boolean; hidden: boolean; aliases: string | string[]; diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts index 759819d8cb1fa..900e3ecc32501 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.test.ts @@ -178,6 +178,7 @@ describe('[Index management API Routes] fetch indices lib function', () => { status: undefined, documents: 0, size: '0b', + primary_size: '0b', }), ], }); diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index 7b25956703db2..f4b39784dde22 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -62,7 +62,9 @@ async function fetchIndicesCall( primary: indexData.settings?.index?.number_of_shards, replica: indexData.settings?.index?.number_of_replicas, documents: indexStats?.total?.docs?.count ?? 0, + documents_deleted: indexStats?.total?.docs?.deleted ?? 0, size: new ByteSizeValue(indexStats?.total?.store?.size_in_bytes ?? 0).toString(), + primary_size: new ByteSizeValue(indexStats?.primaries?.store?.size_in_bytes ?? 0).toString(), // @ts-expect-error isFrozen: indexData.settings?.index?.frozen === 'true', aliases: aliases.length ? aliases : 'none', diff --git a/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts b/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts index 51793bf4d6347..bffbf858d96a0 100644 --- a/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts +++ b/x-pack/plugins/index_management/server/test/helpers/indices_fixtures.ts @@ -23,7 +23,8 @@ export const createTestIndexStats = (index?: Partial) health: 'green', status: 'open', uuid: 'test_index', - total: { docs: { count: 1 }, store: { size_in_bytes: 100 } }, + total: { docs: { count: 1, deleted: 0 }, store: { size_in_bytes: 100 } }, + primaries: { store: { size_in_bytes: 100 } }, ...index, }; }; @@ -33,6 +34,7 @@ export const createTestIndexResponse = (index?: Partial) => { aliases: 'none', data_stream: undefined, documents: 1, + documents_deleted: 0, health: 'green', hidden: false, isFrozen: false, @@ -40,6 +42,7 @@ export const createTestIndexResponse = (index?: Partial) => { primary: 1, replica: 1, size: '100b', + primary_size: '100b', status: 'open', uuid: 'test_index', ...index, diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 3a6bc03e29f59..02b391bf1a2d6 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -199,7 +199,9 @@ export default function ({ getService }) { 'primary', 'replica', 'documents', + 'documents_deleted', 'size', + 'primary_size', 'isFrozen', 'aliases', // Cloud disables CCR, so wouldn't expect follower indices. @@ -233,7 +235,9 @@ export default function ({ getService }) { 'primary', 'replica', 'documents', + 'documents_deleted', 'size', + 'primary_size', 'isFrozen', 'aliases', // Cloud disables CCR, so wouldn't expect follower indices. From cabec1413b22dfcfc4102ba939857003c8729812 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:02:41 -0500 Subject: [PATCH 021/161] Passing pre-constructed service (#124836) --- x-pack/plugins/cases/server/client/factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 3cbcee62d8c09..da43d6864f156 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -116,7 +116,7 @@ export class CasesClientFactory { caseConfigureService: new CaseConfigureService(this.logger), connectorMappingsService: new ConnectorMappingsService(this.logger), userActionService: new CaseUserActionService(this.logger), - attachmentService: new AttachmentService(this.logger), + attachmentService, logger: this.logger, lensEmbeddableFactory: this.options.lensEmbeddableFactory, authorization: auth, From fd1c63a2c7dfa8cbd368125df0ed6bb38e18e2e5 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 7 Feb 2022 14:53:19 -0500 Subject: [PATCH 022/161] [DOCS] Fix typo (#124872) Fixes a minor typo in the 8.x upgrade docs. --- docs/setup/upgrade.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index d8e08b460e5f6..4eabfa0c07714 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -2,7 +2,7 @@ == Upgrade {kib} To upgrade from 7.16 or earlier to {version}, -**You must first upgrade to {prev-major-last}**. +**you must first upgrade to {prev-major-last}**. This enables you to use the Upgrade Assistant to {stack-ref}/upgrading-elastic-stack.html#prepare-to-upgrade[prepare to upgrade]. You must resolve all critical issues identified by the Upgrade Assistant From 91b0b5f0261688265c44fcb646f201ad70403d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 7 Feb 2022 15:03:53 -0500 Subject: [PATCH 023/161] Initial commit (#124858) --- x-pack/plugins/actions/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index f838832b6ea66..3c5c459d5a780 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -556,6 +556,8 @@ When creating a new action type, your plugin will eventually call `server.plugin Consider working with the alerting team on early structure /design feedback of new actions, especially as the APIs and infrastructure are still under development. +Don't forget to ping @elastic/security-detections-response to see if the new connector should be enabled within their solution. + ## licensing Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. From aedbc9f4c95e053878c1e4628d75b95ee2ff00ab Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Mon, 7 Feb 2022 16:08:09 -0500 Subject: [PATCH 024/161] Filter out 'signal.*' fields to prevent alias clashes (#124471) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../factories/utils/filter_source.ts | 2 + .../tests/generating_signals.ts | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts index 3493025749f98..35c91ba398f6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts @@ -13,11 +13,13 @@ export const filterSource = (doc: SignalSourceHit): Partial => { const docSource = doc._source ?? {}; const { event, + signal, threshold_result: siemSignalsThresholdResult, [ALERT_THRESHOLD_RESULT]: alertThresholdResult, ...filteredSource } = docSource || { event: null, + signal: null, threshold_result: null, [ALERT_THRESHOLD_RESULT]: null, }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index efdf862c3070e..6dd569d891fdc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -941,6 +941,49 @@ export default ({ getService }: FtrProviderContext) => { }); }); + /** + * Here we test that 8.0.x alerts can be generated on legacy (pre-8.x) alerts. + */ + describe('Signals generated from legacy signals', async () => { + beforeEach(async () => { + await deleteSignalsIndex(supertest, log); + await createSignalsIndex(supertest, log); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/legacy_cti_signals' + ); + }); + + afterEach(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/legacy_cti_signals' + ); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + it('should generate a signal-on-legacy-signal with legacy index pattern', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting([`.siem-signals-*`]), + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, log, [id]); + expect(signalsOpen.hits.hits.length).greaterThan(0); + }); + + it('should generate a signal-on-legacy-signal with AAD index pattern', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting([`.alerts-security.alerts-default`]), + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, log, [id]); + expect(signalsOpen.hits.hits.length).greaterThan(0); + }); + }); + /** * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" * in the code). If the rule specifies a mapping, then the final Severity or Risk Score From 270adf49587db4f77dbe3c46f41f92e5d739b6c1 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 7 Feb 2022 16:38:24 -0500 Subject: [PATCH 025/161] [Alerting] Rename alert instance to alert and changing signature of alert (instance) factory alert creation (#124390) * Rename alert instance to alert and add create fn to alert factory * Rename alert instance to alert and add create fn to alert factory * Fixing types * Fixing types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/alert_types/always_firing.ts | 4 +- .../server/alert_types/astros.ts | 2 +- x-pack/plugins/alerting/README.md | 15 +- .../alerting/server/alert/alert.test.ts | 488 ++++++++++++++ .../alert_instance.ts => alert/alert.ts} | 6 +- .../create_alert_factory.test.ts} | 32 +- .../server/alert/create_alert_factory.ts | 33 + .../server/{alert_instance => alert}/index.ts | 6 +- .../alert_instance/alert_instance.test.ts | 604 ------------------ .../create_alert_instance_factory.ts | 23 - x-pack/plugins/alerting/server/index.ts | 2 +- x-pack/plugins/alerting/server/mocks.ts | 59 +- .../server/rules_client/rules_client.ts | 106 +-- .../server/task_runner/task_runner.test.ts | 46 +- .../server/task_runner/task_runner.ts | 44 +- .../task_runner/task_runner_cancel.test.ts | 6 +- x-pack/plugins/alerting/server/types.ts | 8 +- .../register_error_count_alert_type.test.ts | 4 +- ...action_duration_anomaly_alert_type.test.ts | 10 +- ..._transaction_error_rate_alert_type.test.ts | 8 +- .../server/routes/alerts/test_utils/index.ts | 2 +- .../inventory_metric_threshold_executor.ts | 5 +- .../log_threshold_executor.test.ts | 4 +- .../log_threshold/log_threshold_executor.ts | 2 +- .../metric_anomaly/metric_anomaly_executor.ts | 2 +- .../metric_threshold_executor.test.ts | 4 +- .../metric_threshold_executor.ts | 5 +- .../register_anomaly_detection_alert_type.ts | 2 +- .../register_jobs_monitoring_rule_type.ts | 2 +- .../monitoring/server/alerts/base_rule.ts | 15 +- .../alerts/ccr_read_exceptions_rule.test.ts | 16 +- .../server/alerts/ccr_read_exceptions_rule.ts | 4 +- .../server/alerts/cluster_health_rule.test.ts | 16 +- .../server/alerts/cluster_health_rule.ts | 4 +- .../server/alerts/cpu_usage_rule.test.ts | 16 +- .../server/alerts/cpu_usage_rule.ts | 4 +- .../server/alerts/disk_usage_rule.test.ts | 16 +- .../server/alerts/disk_usage_rule.ts | 4 +- ...lasticsearch_version_mismatch_rule.test.ts | 16 +- .../elasticsearch_version_mismatch_rule.ts | 4 +- .../kibana_version_mismatch_rule.test.ts | 16 +- .../alerts/kibana_version_mismatch_rule.ts | 4 +- .../alerts/large_shard_size_rule.test.ts | 16 +- .../server/alerts/large_shard_size_rule.ts | 4 +- .../alerts/license_expiration_rule.test.ts | 16 +- .../server/alerts/license_expiration_rule.ts | 4 +- .../logstash_version_mismatch_rule.test.ts | 16 +- .../alerts/logstash_version_mismatch_rule.ts | 4 +- .../server/alerts/memory_usage_rule.test.ts | 16 +- .../server/alerts/memory_usage_rule.ts | 4 +- .../missing_monitoring_data_rule.test.ts | 16 +- .../alerts/missing_monitoring_data_rule.ts | 4 +- .../server/alerts/nodes_changed_rule.test.ts | 16 +- .../server/alerts/nodes_changed_rule.ts | 4 +- .../thread_pool_rejections_rule_base.ts | 8 +- ...thread_pool_search_rejections_rule.test.ts | 16 +- .../thread_pool_write_rejections_rule.test.ts | 16 +- .../server/utils/create_lifecycle_executor.ts | 8 +- .../utils/create_lifecycle_rule_type.test.ts | 12 +- .../utils/lifecycle_alert_services_mock.ts | 2 +- .../server/utils/rule_executor_test_utils.ts | 3 +- ...gacy_rules_notification_alert_type.test.ts | 20 +- .../legacy_rules_notification_alert_type.ts | 2 +- .../schedule_notification_actions.test.ts | 2 +- .../schedule_notification_actions.ts | 6 +- ...dule_throttle_notification_actions.test.ts | 24 +- .../schedule_throttle_notification_actions.ts | 4 +- .../routes/rules/preview_rules_route.ts | 28 +- .../rule_types/__mocks__/rule_type.ts | 2 +- .../create_security_rule_type_wrapper.ts | 8 +- .../preview/alert_instance_factory_stub.ts | 8 +- .../alert_types/es_query/alert_type.test.ts | 18 +- .../server/alert_types/es_query/alert_type.ts | 4 +- .../geo_containment/geo_containment.ts | 8 +- .../tests/geo_containment.test.ts | 29 +- .../index_threshold/alert_type.test.ts | 6 +- .../alert_types/index_threshold/alert_type.ts | 4 +- .../register_transform_health_rule_type.ts | 4 +- .../uptime/server/lib/alerts/tls_legacy.ts | 11 +- .../plugins/alerts/server/alert_types.ts | 14 +- .../fixtures/plugins/alerts/server/plugin.ts | 4 +- .../tests/trial/lifecycle_executor.ts | 2 +- 82 files changed, 997 insertions(+), 1065 deletions(-) create mode 100644 x-pack/plugins/alerting/server/alert/alert.test.ts rename x-pack/plugins/alerting/server/{alert_instance/alert_instance.ts => alert/alert.ts} (97%) rename x-pack/plugins/alerting/server/{alert_instance/create_alert_instance_factory.test.ts => alert/create_alert_factory.test.ts} (61%) create mode 100644 x-pack/plugins/alerting/server/alert/create_alert_factory.ts rename x-pack/plugins/alerting/server/{alert_instance => alert}/index.ts (57%) delete mode 100644 x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts delete mode 100644 x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index dc89a473a38ab..1896db426da5f 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -65,8 +65,8 @@ export const alertType: RuleType< range(instances) .map(() => uuid.v4()) .forEach((id: string) => { - services - .alertInstanceFactory(id) + services.alertFactory + .create(id) .replaceState({ triggerdOnCycle: count }) .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds)); }); diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index c5d4af6872c83..a29b280a34fff 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -70,7 +70,7 @@ export const alertType: RuleType< if (getOperator(op)(peopleInCraft.length, outerSpaceCapacity)) { peopleInCraft.forEach(({ craft, name }) => { - services.alertInstanceFactory(name).replaceState({ craft }).scheduleActions('default'); + services.alertFactory.create(name).replaceState({ craft }).scheduleActions('default'); }); } diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 90c02b6e1c254..bc917fbf43bc4 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -40,8 +40,6 @@ Table of Contents > References to `rule` and `rule type` entities are still named `AlertType` within the codebase. -> References to `alert` and `alert factory` entities are still named `AlertInstance` and `alertInstanceFactory` within the codebase. - **Rule Type**: A function that takes parameters and executes actions on alerts. **Rule**: A configuration that defines a schedule, a rule type w/ parameters, state information and actions. @@ -113,7 +111,7 @@ This is the primary function for a rule type. Whenever the rule needs to execute |---|---| |services.scopedClusterClient|This is an instance of the Elasticsearch client. Use this to do Elasticsearch queries in the context of the user who created the alert when security is enabled.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to perform CRUD operations on any saved object that lives in the same space as the rule.

The scope of the saved objects client is tied to the user who created the rule (only when security is enabled).| -|services.alertInstanceFactory(id)|This [alert factory](#alert-factory) creates alerts and must be used in order to execute actions. The id you give to the alert factory is a unique identifier for the alert.| +|services.alertFactory|This [alert factory](#alert-factory) creates alerts and must be used in order to schedule action execution. The id you give to the alert factory create function() is a unique identifier for the alert.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| |services.shouldWriteAlerts()|This returns a boolean indicating whether the executor should write out alerts as data. This is determined by whether rule execution has been cancelled due to timeout AND whether both the Kibana `cancelAlertsOnRuleTimeout` flag and the rule type `cancelAlertsOnRuleTimeout` are set to `true`.| |services.shouldStopExecution()|This returns a boolean indicating whether rule execution has been cancelled due to timeout.| @@ -310,7 +308,7 @@ const myRuleType: RuleType< // scenario the provided server will be used. Also, this ID will be // used to make `getState()` return previous state, if any, on // matching identifiers. - const alert = services.alertInstanceFactory(server); + const alert = services.alertFactory.create(server); // State from the last execution. This will exist if an alert was // created and executed in the previous execution @@ -731,13 +729,13 @@ Query: ## Alert Factory -**alertInstanceFactory(id)** +**alertFactory.create(id)** -One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to execute actions. The `id` you give to the alert factory is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. +One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to schedule action execution. The `id` you give to the alert factory create fn() is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. Note that the `id` only needs to be unique **within the scope of a specific rule**, not unique across all rules or rule types. For example, Rule 1 and Rule 2 can both create an alert with an `id` of `"a"` without conflicting with one another. But if Rule 1 creates 2 alerts, then they must be differentiated with `id`s of `"a"` and `"b"`. -This factory returns an instance of `AlertInstance`. The `AlertInstance` class has the following methods. Note that we have removed the methods that you shouldn't touch. +This factory returns an instance of `Alert`. The `Alert` class has the following methods. Note that we have removed the methods that you shouldn't touch. |Method|Description| |---|---| @@ -781,7 +779,8 @@ The templating engine is [mustache]. General definition for the [mustache variab The following code would be within a rule type. As you can see `cpuUsage` will replace the state of the alert and `server` is the context for the alert to execute. The difference between the two is that `cpuUsage` will be accessible at the next execution. ``` -alertInstanceFactory('server_1') +alertFactory + .create('server_1') .replaceState({ cpuUsage: 80, }) diff --git a/x-pack/plugins/alerting/server/alert/alert.test.ts b/x-pack/plugins/alerting/server/alert/alert.test.ts new file mode 100644 index 0000000000000..83b82de904703 --- /dev/null +++ b/x-pack/plugins/alerting/server/alert/alert.test.ts @@ -0,0 +1,488 @@ +/* + * 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 sinon from 'sinon'; +import { Alert } from './alert'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; + +let clock: sinon.SinonFakeTimers; + +beforeAll(() => { + clock = sinon.useFakeTimers(); +}); +beforeEach(() => clock.reset()); +afterAll(() => clock.restore()); + +describe('hasScheduledActions()', () => { + test('defaults to false', () => { + const alert = new Alert(); + expect(alert.hasScheduledActions()).toEqual(false); + }); + + test('returns true when scheduleActions is called', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.hasScheduledActions()).toEqual(true); + }); +}); + +describe('isThrottled', () => { + test(`should throttle when group didn't change and throttle period is still active`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(30000); + alert.scheduleActions('default'); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test(`shouldn't throttle when group didn't change and throttle period expired`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(30000); + alert.scheduleActions('default'); + expect(alert.isThrottled('15s')).toEqual(false); + }); + + test(`shouldn't throttle when group changes`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(5000); + alert.scheduleActions('other-group'); + expect(alert.isThrottled('1m')).toEqual(false); + }); +}); + +describe('scheduledActionGroupOrSubgroupHasChanged()', () => { + test('should be false if no last scheduled and nothing scheduled', () => { + const alert = new Alert(); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group and subgroup does not change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'subgroup'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change and subgroup goes from undefined to defined', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'subgroup'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change and subgroup goes from defined to undefined', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be true if no last scheduled and has scheduled action', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('penguin'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does change and subgroup does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('penguin', 'fish'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does not change and subgroup does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'fish'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); +}); + +describe('getScheduledActionOptions()', () => { + test('defaults to undefined', () => { + const alert = new Alert(); + expect(alert.getScheduledActionOptions()).toBeUndefined(); + }); +}); + +describe('unscheduleActions()', () => { + test('makes hasScheduledActions() return false', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.hasScheduledActions()).toEqual(true); + alert.unscheduleActions(); + expect(alert.hasScheduledActions()).toEqual(false); + }); + + test('makes getScheduledActionOptions() return undefined', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + context: {}, + state: {}, + }); + alert.unscheduleActions(); + expect(alert.getScheduledActionOptions()).toBeUndefined(); + }); +}); + +describe('getState()', () => { + test('returns state passed to constructor', () => { + const state = { foo: true }; + const alert = new Alert({ + state, + }); + expect(alert.getState()).toEqual(state); + }); +}); + +describe('scheduleActions()', () => { + test('makes hasScheduledActions() return true', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.hasScheduledActions()).toEqual(true); + }); + + test('makes isThrottled() return true when throttled', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('make isThrottled() return false when throttled expired', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(120000); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('makes getScheduledActionOptions() return given options', () => { + const alert = new Alert({ + state: { foo: true }, + meta: {}, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + context: { field: true }, + state: { otherField: true }, + }); + }); + + test('cannot schdule for execution twice', () => { + const alert = new Alert(); + alert.scheduleActions('default', { field: true }); + expect(() => + alert.scheduleActions('default', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); +}); + +describe('scheduleActionsWithSubGroup()', () => { + test('makes hasScheduledActions() return true', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.hasScheduledActions()).toEqual(true); + }); + + test('makes isThrottled() return true when throttled and subgroup is the same', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('makes isThrottled() return false when throttled and subgroup is the different', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'prev-subgroup', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('make isThrottled() return false when throttled expired', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(120000); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('makes getScheduledActionOptions() return given options', () => { + const alert = new Alert({ + state: { foo: true }, + meta: {}, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + subgroup: 'subgroup', + context: { field: true }, + state: { otherField: true }, + }); + }); + + test('cannot schdule for execution twice', () => { + const alert = new Alert(); + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); + + test('cannot schdule for execution twice with different subgroups', () => { + const alert = new Alert(); + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); + + test('cannot schdule for execution twice whether there are subgroups', () => { + const alert = new Alert(); + alert.scheduleActions('default', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); +}); + +describe('replaceState()', () => { + test('replaces previous state', () => { + const alert = new Alert({ + state: { foo: true }, + }); + alert.replaceState({ bar: true }); + expect(alert.getState()).toEqual({ bar: true }); + alert.replaceState({ baz: true }); + expect(alert.getState()).toEqual({ baz: true }); + }); +}); + +describe('updateLastScheduledActions()', () => { + test('replaces previous lastScheduledActions', () => { + const alert = new Alert({ + meta: {}, + }); + alert.updateLastScheduledActions('default'); + expect(alert.toJSON()).toEqual({ + state: {}, + meta: { + lastScheduledActions: { + date: new Date().toISOString(), + group: 'default', + }, + }, + }); + }); +}); + +describe('toJSON', () => { + test('only serializes state and meta', () => { + const alertInstance = new Alert( + { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + } + ); + expect(JSON.stringify(alertInstance)).toEqual( + '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' + ); + }); +}); + +describe('toRaw', () => { + test('returns unserialised underlying state and meta', () => { + const raw = { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }; + const alertInstance = new Alert( + raw + ); + expect(alertInstance.toRaw()).toEqual(raw); + }); +}); diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerting/server/alert/alert.ts similarity index 97% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.ts rename to x-pack/plugins/alerting/server/alert/alert.ts index b41a4e551040c..d34aa68ac1a11 100644 --- a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -27,16 +27,16 @@ interface ScheduledExecutionOptions< state: State; } -export type PublicAlertInstance< +export type PublicAlert< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = DefaultActionGroupId > = Pick< - AlertInstance, + Alert, 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' >; -export class AlertInstance< +export class Alert< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = never diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts similarity index 61% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts rename to x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts index 6518305dcd109..ecb1a10bbac42 100644 --- a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts @@ -6,8 +6,8 @@ */ import sinon from 'sinon'; -import { AlertInstance } from './alert_instance'; -import { createAlertInstanceFactory } from './create_alert_instance_factory'; +import { Alert } from './alert'; +import { createAlertFactory } from './create_alert_factory'; let clock: sinon.SinonFakeTimers; @@ -17,9 +17,9 @@ beforeAll(() => { beforeEach(() => clock.reset()); afterAll(() => clock.restore()); -test('creates new instances for ones not passed in', () => { - const alertInstanceFactory = createAlertInstanceFactory({}); - const result = alertInstanceFactory('1'); +test('creates new alerts for ones not passed in', () => { + const alertFactory = createAlertFactory({ alerts: {} }); + const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` Object { "meta": Object {}, @@ -28,15 +28,17 @@ test('creates new instances for ones not passed in', () => { `); }); -test('reuses existing instances', () => { - const alertInstance = new AlertInstance({ +test('reuses existing alerts', () => { + const alert = new Alert({ state: { foo: true }, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); - const alertInstanceFactory = createAlertInstanceFactory({ - '1': alertInstance, + const alertFactory = createAlertFactory({ + alerts: { + '1': alert, + }, }); - const result = alertInstanceFactory('1'); + const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` Object { "meta": Object { @@ -52,11 +54,11 @@ test('reuses existing instances', () => { `); }); -test('mutates given instances', () => { - const alertInstances = {}; - const alertInstanceFactory = createAlertInstanceFactory(alertInstances); - alertInstanceFactory('1'); - expect(alertInstances).toMatchInlineSnapshot(` +test('mutates given alerts', () => { + const alerts = {}; + const alertFactory = createAlertFactory({ alerts }); + alertFactory.create('1'); + expect(alerts).toMatchInlineSnapshot(` Object { "1": Object { "meta": Object {}, diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts new file mode 100644 index 0000000000000..07f4dbc7b20ea --- /dev/null +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertInstanceContext, AlertInstanceState } from '../types'; +import { Alert } from './alert'; + +export interface CreateAlertFactoryOpts< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +> { + alerts: Record>; +} + +export function createAlertFactory< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +>({ alerts }: CreateAlertFactoryOpts) { + return { + create: (id: string): Alert => { + if (!alerts[id]) { + alerts[id] = new Alert(); + } + + return alerts[id]; + }, + }; +} diff --git a/x-pack/plugins/alerting/server/alert_instance/index.ts b/x-pack/plugins/alerting/server/alert/index.ts similarity index 57% rename from x-pack/plugins/alerting/server/alert_instance/index.ts rename to x-pack/plugins/alerting/server/alert/index.ts index 7b5dd064c5dca..5e1a9ee626b57 100644 --- a/x-pack/plugins/alerting/server/alert_instance/index.ts +++ b/x-pack/plugins/alerting/server/alert/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export type { PublicAlertInstance } from './alert_instance'; -export { AlertInstance } from './alert_instance'; -export { createAlertInstanceFactory } from './create_alert_instance_factory'; +export type { PublicAlert } from './alert'; +export { Alert } from './alert'; +export { createAlertFactory } from './create_alert_factory'; diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts deleted file mode 100644 index 68fed6aa7d3fd..0000000000000 --- a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts +++ /dev/null @@ -1,604 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import sinon from 'sinon'; -import { AlertInstance } from './alert_instance'; -import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; - -let clock: sinon.SinonFakeTimers; - -beforeAll(() => { - clock = sinon.useFakeTimers(); -}); -beforeEach(() => clock.reset()); -afterAll(() => clock.restore()); - -describe('hasScheduledActions()', () => { - test('defaults to false', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.hasScheduledActions()).toEqual(false); - }); - - test('returns true when scheduleActions is called', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); -}); - -describe('isThrottled', () => { - test(`should throttle when group didn't change and throttle period is still active`, () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(30000); - alertInstance.scheduleActions('default'); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(30000); - alertInstance.scheduleActions('default'); - expect(alertInstance.isThrottled('15s')).toEqual(false); - }); - - test(`shouldn't throttle when group changes`, () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(5000); - alertInstance.scheduleActions('other-group'); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); -}); - -describe('scheduledActionGroupOrSubgroupHasChanged()', () => { - test('should be false if no last scheduled and nothing scheduled', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group and subgroup does not change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be true if no last scheduled and has scheduled action', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change', () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActions('penguin'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change and subgroup does change', () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('penguin', 'fish'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does not change and subgroup does change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'fish'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); -}); - -describe('getScheduledActionOptions()', () => { - test('defaults to undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); - }); -}); - -describe('unscheduleActions()', () => { - test('makes hasScheduledActions() return false', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.hasScheduledActions()).toEqual(true); - alertInstance.unscheduleActions(); - expect(alertInstance.hasScheduledActions()).toEqual(false); - }); - - test('makes getScheduledActionOptions() return undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - context: {}, - state: {}, - }); - alertInstance.unscheduleActions(); - expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); - }); -}); - -describe('getState()', () => { - test('returns state passed to constructor', () => { - const state = { foo: true }; - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state }); - expect(alertInstance.getState()).toEqual(state); - }); -}); - -describe('scheduleActions()', () => { - test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true }, meta: {} }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default', { field: true }); - expect(() => - alertInstance.scheduleActions('default', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - -describe('scheduleActionsWithSubGroup()', () => { - test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'prev-subgroup', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true }, meta: {} }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - subgroup: 'subgroup', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice with different subgroups', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice whether there are subgroups', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - -describe('replaceState()', () => { - test('replaces previous state', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true } }); - alertInstance.replaceState({ bar: true }); - expect(alertInstance.getState()).toEqual({ bar: true }); - alertInstance.replaceState({ baz: true }); - expect(alertInstance.getState()).toEqual({ baz: true }); - }); -}); - -describe('updateLastScheduledActions()', () => { - test('replaces previous lastScheduledActions', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ meta: {} }); - alertInstance.updateLastScheduledActions('default'); - expect(alertInstance.toJSON()).toEqual({ - state: {}, - meta: { - lastScheduledActions: { - date: new Date().toISOString(), - group: 'default', - }, - }, - }); - }); -}); - -describe('toJSON', () => { - test('only serializes state and meta', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - expect(JSON.stringify(alertInstance)).toEqual( - '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' - ); - }); -}); - -describe('toRaw', () => { - test('returns unserialised underlying state and meta', () => { - const raw = { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }; - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(raw); - expect(alertInstance.toRaw()).toEqual(raw); - }); -}); diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts deleted file mode 100644 index 2faaff157fd82..0000000000000 --- a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts +++ /dev/null @@ -1,23 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AlertInstanceContext, AlertInstanceState } from '../types'; -import { AlertInstance } from './alert_instance'; - -export function createAlertInstanceFactory< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext, - ActionGroupIds extends string ->(alertInstances: Record>) { - return (id: string): AlertInstance => { - if (!alertInstances[id]) { - alertInstances[id] = new AlertInstance(); - } - - return alertInstances[id]; - }; -} diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 36ee1f8ee9676..63e8df5488895 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -32,7 +32,7 @@ export type { export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; export type { PluginSetupContract, PluginStartContract } from './plugin'; export type { FindResult } from './rules_client'; -export type { PublicAlertInstance as AlertInstance } from './alert_instance'; +export type { PublicAlert as Alert } from './alert'; export { parseDuration } from './lib'; export { getEsErrorMessage } from './lib/errors'; export type { diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index c4702f796ad8e..afbc3ef9cec43 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -7,7 +7,7 @@ import { rulesClientMock } from './rules_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { AlertInstance } from './alert_instance'; +import { Alert } from './alert'; import { elasticsearchServiceMock, savedObjectsClientMock, @@ -37,30 +37,33 @@ const createStartMock = () => { export type AlertInstanceMock< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext -> = jest.Mocked>; -const createAlertInstanceFactoryMock = < - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext ->() => { - const mock = { - hasScheduledActions: jest.fn(), - isThrottled: jest.fn(), - getScheduledActionOptions: jest.fn(), - unscheduleActions: jest.fn(), - getState: jest.fn(), - scheduleActions: jest.fn(), - replaceState: jest.fn(), - updateLastScheduledActions: jest.fn(), - toJSON: jest.fn(), - toRaw: jest.fn(), - }; +> = jest.Mocked>; + +const createAlertFactoryMock = { + create: < + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext + >() => { + const mock = { + hasScheduledActions: jest.fn(), + isThrottled: jest.fn(), + getScheduledActionOptions: jest.fn(), + unscheduleActions: jest.fn(), + getState: jest.fn(), + scheduleActions: jest.fn(), + replaceState: jest.fn(), + updateLastScheduledActions: jest.fn(), + toJSON: jest.fn(), + toRaw: jest.fn(), + }; - // support chaining - mock.replaceState.mockReturnValue(mock); - mock.unscheduleActions.mockReturnValue(mock); - mock.scheduleActions.mockReturnValue(mock); + // support chaining + mock.replaceState.mockReturnValue(mock); + mock.unscheduleActions.mockReturnValue(mock); + mock.scheduleActions.mockReturnValue(mock); - return mock as unknown as AlertInstanceMock; + return mock as unknown as AlertInstanceMock; + }, }; const createAbortableSearchClientMock = () => { @@ -82,11 +85,11 @@ const createAlertServicesMock = < InstanceState extends AlertInstanceState = AlertInstanceState, InstanceContext extends AlertInstanceContext = AlertInstanceContext >() => { - const alertInstanceFactoryMock = createAlertInstanceFactoryMock(); + const alertFactoryMockCreate = createAlertFactoryMock.create(); return { - alertInstanceFactory: jest - .fn>, [string]>() - .mockReturnValue(alertInstanceFactoryMock), + alertFactory: { + create: jest.fn().mockReturnValue(alertFactoryMockCreate), + }, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => true, @@ -97,7 +100,7 @@ const createAlertServicesMock = < export type AlertServicesMock = ReturnType; export const alertsMock = { - createAlertInstanceFactory: createAlertInstanceFactoryMock, + createAlertFactory: createAlertFactoryMock, createSetup: createSetupMock, createStart: createStartMock, createAlertServices: createAlertServicesMock, diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 9100772a806e8..63e35583bc9a1 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -22,23 +22,23 @@ import { import { esKuery } from '../../../../../src/plugins/data/server'; import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; import { - Alert, - PartialAlert, + Alert as Rule, + PartialAlert as PartialRule, RawRule, RuleTypeRegistry, - AlertAction, + AlertAction as RuleAction, IntervalSchedule, - SanitizedAlert, + SanitizedAlert as SanitizedRule, RuleTaskState, AlertSummary, - AlertExecutionStatusValues, - AlertNotifyWhenType, - AlertTypeParams, + AlertExecutionStatusValues as RuleExecutionStatusValues, + AlertNotifyWhenType as RuleNotifyWhenType, + AlertTypeParams as RuleTypeParams, ResolvedSanitizedRule, - AlertWithLegacyId, + AlertWithLegacyId as RuleWithLegacyId, SanitizedRuleWithLegacyId, - PartialAlertWithLegacyId, - RawAlertInstance, + PartialAlertWithLegacyId as PartialRuleWithLegacyId, + RawAlertInstance as RawAlert, } from '../types'; import { validateRuleTypeParams, ruleExecutionStatusFromRaw, getAlertNotifyWhenType } from '../lib'; import { @@ -74,7 +74,7 @@ import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField, validateOperationOnAttributes } from './lib'; import { getRuleExecutionStatusPending } from '../lib/rule_execution_status'; -import { AlertInstance } from '../alert_instance'; +import { Alert } from '../alert'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object'; import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; @@ -82,7 +82,7 @@ import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: string[]; } -type NormalizedAlertAction = Omit; +type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = | { apiKeysEnabled: false } | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; @@ -174,16 +174,16 @@ export interface AggregateResult { ruleMutedStatus?: { muted: number; unmuted: number }; } -export interface FindResult { +export interface FindResult { page: number; perPage: number; total: number; - data: Array>; + data: Array>; } -export interface CreateOptions { +export interface CreateOptions { data: Omit< - Alert, + Rule, | 'id' | 'createdBy' | 'updatedBy' @@ -202,7 +202,7 @@ export interface CreateOptions { }; } -export interface UpdateOptions { +export interface UpdateOptions { id: string; data: { name: string; @@ -211,7 +211,7 @@ export interface UpdateOptions { actions: NormalizedAlertAction[]; params: Params; throttle: string | null; - notifyWhen: AlertNotifyWhenType | null; + notifyWhen: RuleNotifyWhenType | null; }; } @@ -248,7 +248,7 @@ export class RulesClient { private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; private readonly auditLogger?: AuditLogger; private readonly eventLogger?: IEventLogger; - private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; + private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; constructor({ ruleTypeRegistry, @@ -286,10 +286,10 @@ export class RulesClient { this.eventLogger = eventLogger; } - public async create({ + public async create({ data, options, - }: CreateOptions): Promise> { + }: CreateOptions): Promise> { const id = options?.id || SavedObjectsUtils.generateId(); try { @@ -432,7 +432,7 @@ export class RulesClient { ); } - public async get({ + public async get({ id, includeLegacyId = false, excludeFromPublicApi = false, @@ -440,7 +440,7 @@ export class RulesClient { id: string; includeLegacyId?: boolean; excludeFromPublicApi?: boolean; - }): Promise | SanitizedRuleWithLegacyId> { + }): Promise | SanitizedRuleWithLegacyId> { const result = await this.unsecuredSavedObjectsClient.get('alert', id); try { await this.authorization.ensureAuthorized({ @@ -475,7 +475,7 @@ export class RulesClient { ); } - public async resolve({ + public async resolve({ id, includeLegacyId, }: { @@ -612,7 +612,7 @@ export class RulesClient { }); } - public async find({ + public async find({ options: { fields, ...options } = {}, excludeFromPublicApi = false, }: { options?: FindOptions; excludeFromPublicApi?: boolean } = {}): Promise> { @@ -762,7 +762,7 @@ export class RulesClient { }, }; - for (const key of AlertExecutionStatusValues) { + for (const key of RuleExecutionStatusValues) { placeholder.alertExecutionStatus[key] = 0; } @@ -783,7 +783,7 @@ export class RulesClient { }; // Fill missing keys with zeroes - for (const key of AlertExecutionStatusValues) { + for (const key of RuleExecutionStatusValues) { if (!ret.alertExecutionStatus.hasOwnProperty(key)) { ret.alertExecutionStatus[key] = 0; } @@ -878,10 +878,10 @@ export class RulesClient { return removeResult; } - public async update({ + public async update({ id, data, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise> { return await retryIfConflicts( this.logger, `rulesClient.update('${id}')`, @@ -889,10 +889,10 @@ export class RulesClient { ); } - private async updateWithOCC({ + private async updateWithOCC({ id, data, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise> { let alertSavedObject: SavedObject; try { @@ -974,10 +974,10 @@ export class RulesClient { return updateResult; } - private async updateAlert( + private async updateAlert( { id, data }: UpdateOptions, { attributes, version }: SavedObject - ): Promise> { + ): Promise> { const ruleType = this.ruleTypeRegistry.get(attributes.alertTypeId); // Validate @@ -1048,7 +1048,7 @@ export class RulesClient { throw e; } - return this.getPartialAlertFromRaw( + return this.getPartialRuleFromRaw( id, ruleType, updatedObject.attributes, @@ -1332,12 +1332,12 @@ export class RulesClient { try { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(attributes.scheduledTaskId), - attributes as unknown as SanitizedAlert + attributes as unknown as SanitizedRule ); - const recoveredAlertInstances = mapValues, AlertInstance>( + const recoveredAlertInstances = mapValues, Alert>( state.alertInstances ?? {}, - (rawAlertInstance) => new AlertInstance(rawAlertInstance) + (rawAlertInstance) => new Alert(rawAlertInstance) ); const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances); @@ -1568,7 +1568,7 @@ export class RulesClient { } private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId ); @@ -1636,7 +1636,7 @@ export class RulesClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId ); @@ -1751,22 +1751,22 @@ export class RulesClient { ...omit(action, 'actionRef'), id: reference.id, }; - }) as Alert['actions']; + }) as Rule['actions']; } - private getAlertFromRaw( + private getAlertFromRaw( id: string, ruleTypeId: string, rawRule: RawRule, references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false - ): Alert | AlertWithLegacyId { + ): Rule | RuleWithLegacyId { const ruleType = this.ruleTypeRegistry.get(ruleTypeId); // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawRule, it is safe // to cast the result to an Alert - const res = this.getPartialAlertFromRaw( + const res = this.getPartialRuleFromRaw( id, ruleType, rawRule, @@ -1776,13 +1776,13 @@ export class RulesClient { ); // include to result because it is for internal rules client usage if (includeLegacyId) { - return res as AlertWithLegacyId; + return res as RuleWithLegacyId; } // exclude from result because it is an internal variable - return omit(res, ['legacyId']) as Alert; + return omit(res, ['legacyId']) as Rule; } - private getPartialAlertFromRaw( + private getPartialRuleFromRaw( id: string, ruleType: UntypedNormalizedRuleType, { @@ -1801,7 +1801,7 @@ export class RulesClient { references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false - ): PartialAlert | PartialAlertWithLegacyId { + ): PartialRule | PartialRuleWithLegacyId { const rule = { id, notifyWhen, @@ -1820,8 +1820,8 @@ export class RulesClient { }; return includeLegacyId - ? ({ ...rule, legacyId } as PartialAlertWithLegacyId) - : (rule as PartialAlert); + ? ({ ...rule, legacyId } as PartialRuleWithLegacyId) + : (rule as PartialRule); } private async validateActions( @@ -1873,8 +1873,8 @@ export class RulesClient { } private async extractReferences< - Params extends AlertTypeParams, - ExtractedParams extends AlertTypeParams + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams >( ruleType: UntypedNormalizedRuleType, ruleActions: NormalizedAlertAction[], @@ -1909,8 +1909,8 @@ export class RulesClient { } private injectReferencesIntoParams< - Params extends AlertTypeParams, - ExtractedParams extends AlertTypeParams + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams >( ruleId: string, ruleType: UntypedNormalizedRuleType, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 402cc3951d39b..b5a98af23d74b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -309,7 +309,7 @@ describe('Task Runner', () => { }, ] `); - expect(call.services.alertInstanceFactory).toBeTruthy(); + expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); @@ -427,8 +427,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices - .alertInstanceFactory('1') + executorServices.alertFactory + .create('1') .scheduleActionsWithSubGroup('default', 'subDefault'); } ); @@ -708,7 +708,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -934,8 +934,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -991,7 +991,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1192,7 +1192,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1268,8 +1268,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices - .alertInstanceFactory('1') + executorServices.alertFactory + .create('1') .scheduleActionsWithSubGroup('default', 'subgroup1'); } ); @@ -1350,7 +1350,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1672,7 +1672,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -2080,10 +2080,10 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); // create an instance, but don't schedule any actions, so it doesn't go active - executorServices.alertInstanceFactory('3'); + executorServices.alertFactory.create('3'); } ); const taskRunner = new TaskRunner( @@ -2186,7 +2186,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -2297,7 +2297,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const date = new Date().toISOString(); @@ -3692,8 +3692,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -4006,8 +4006,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -4251,8 +4251,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -5035,7 +5035,7 @@ describe('Task Runner', () => { }, ] `); - expect(call.services.alertInstanceFactory).toBeTruthy(); + expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 785a68e1a24b9..9b77ec7f8dc72 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -15,7 +15,7 @@ import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; import { createExecutionHandler, ExecutionHandler } from './create_execution_handler'; -import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; +import { Alert as CreatedAlert, createAlertFactory } from '../alert'; import { validateRuleTypeParams, executionStatusFromState, @@ -285,7 +285,7 @@ export class TaskRunner< async executeAlert( alertId: string, - alert: AlertInstance, + alert: CreatedAlert, executionHandler: ExecutionHandler ) { const { @@ -333,8 +333,8 @@ export class TaskRunner< const alerts = mapValues< Record, - AlertInstance - >(alertRawInstances, (rawAlert) => new AlertInstance(rawAlert)); + CreatedAlert + >(alertRawInstances, (rawAlert) => new CreatedAlert(rawAlert)); const originalAlerts = cloneDeep(alerts); const originalAlertIds = new Set(Object.keys(originalAlerts)); @@ -358,11 +358,13 @@ export class TaskRunner< executionId: this.executionId, services: { ...services, - alertInstanceFactory: createAlertInstanceFactory< + alertFactory: createAlertFactory< InstanceState, InstanceContext, WithoutReservedActionGroups - >(alerts), + >({ + alerts, + }), shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(), shouldStopExecution: () => this.cancelled, search: createAbortableEsClientFactory({ @@ -420,11 +422,11 @@ export class TaskRunner< // Cleanup alerts that are no longer scheduling actions to avoid over populating the alertInstances object const alertsWithScheduledActions = pickBy( alerts, - (alert: AlertInstance) => alert.hasScheduledActions() + (alert: CreatedAlert) => alert.hasScheduledActions() ); const recoveredAlerts = pickBy( alerts, - (alert: AlertInstance, id) => + (alert: CreatedAlert, id) => !alert.hasScheduledActions() && originalAlertIds.has(id) ); @@ -478,7 +480,7 @@ export class TaskRunner< const alertsToExecute = notifyWhen === 'onActionGroupChange' ? Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, AlertInstance]) => { + ([alertName, alert]: [string, CreatedAlert]) => { const shouldExecuteAction = alert.scheduledActionGroupOrSubgroupHasChanged(); if (!shouldExecuteAction) { this.logger.debug( @@ -489,7 +491,7 @@ export class TaskRunner< } ) : Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, AlertInstance]) => { + ([alertName, alert]: [string, CreatedAlert]) => { const throttled = alert.isThrottled(throttle); const muted = mutedAlertIdsSet.has(alertName); const shouldExecuteAction = !throttled && !muted; @@ -506,7 +508,7 @@ export class TaskRunner< const allTriggeredActions = await Promise.all( alertsToExecute.map( - ([alertId, alert]: [string, AlertInstance]) => + ([alertId, alert]: [string, CreatedAlert]) => this.executeAlert(alertId, alert, executionHandler) ) ); @@ -533,7 +535,7 @@ export class TaskRunner< triggeredActions, alertTypeState: updatedRuleTypeState || undefined, alertInstances: mapValues< - Record>, + Record>, RawAlertInstance >(alertsWithScheduledActions, (alert) => alert.toRaw()), }; @@ -910,9 +912,9 @@ interface TrackAlertDurationsParams< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext > { - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; } function trackAlertDurations< @@ -967,9 +969,9 @@ interface GenerateNewAndRecoveredAlertEventsParams< > { eventLogger: IEventLogger; executionId: string; - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; ruleId: string; ruleLabel: string; namespace: string | undefined; @@ -1117,7 +1119,7 @@ interface ScheduleActionsForRecoveredAlertsParams< > { logger: Logger; recoveryActionGroup: ActionGroup; - recoveredAlerts: Dictionary>; + recoveredAlerts: Dictionary>; executionHandler: ExecutionHandler; mutedAlertIdsSet: Set; ruleLabel: string; @@ -1173,8 +1175,8 @@ interface LogActiveAndRecoveredAlertsParams< RecoveryActionGroupId extends string > { logger: Logger; - activeAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + activeAlerts: Dictionary>; + recoveredAlerts: Dictionary>; ruleLabel: string; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 47f888fc71136..f4b67935f7249 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -359,7 +359,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); // setting cancelAlertsOnRuleTimeout to false here @@ -393,7 +393,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); // setting cancelAlertsOnRuleTimeout for ruleType to false here @@ -427,7 +427,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index de6649bb44891..9d6302774f889 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,7 +7,7 @@ import type { IRouter, RequestHandlerContext, SavedObjectReference } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PublicAlertInstance } from './alert_instance'; +import { PublicAlert } from './alert'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -74,9 +74,9 @@ export interface AlertServices< InstanceContext extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = never > extends Services { - alertInstanceFactory: ( - id: string - ) => PublicAlertInstance; + alertFactory: { + create: (id: string) => PublicAlert; + }; shouldWriteAlerts: () => boolean; shouldStopExecution: () => boolean; search: IAbortableClusterClient; diff --git a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts index 2d98c09096f5e..01aa64b85f720 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts @@ -39,7 +39,7 @@ describe('Error count alert', () => { ); await executor({ params }); - expect(services.alertInstanceFactory).not.toBeCalled(); + expect(services.alertFactory.create).not.toBeCalled(); }); it('sends alerts with service name and environment for those that exceeded the threshold', async () => { @@ -138,7 +138,7 @@ describe('Error count alert', () => { 'apm.error_rate_foo_env-foo-2', 'apm.error_rate_bar_env-bar', ].forEach((instanceName) => - expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) ); expect(scheduleActions).toHaveBeenCalledTimes(3); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts index 889fe3c16596e..41bb5126646fc 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -32,7 +32,7 @@ describe('Transaction duration anomaly alert', () => { services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); it('ml jobs are not available', async () => { @@ -59,7 +59,7 @@ describe('Transaction duration anomaly alert', () => { services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); it('anomaly is less than threshold', async () => { @@ -110,7 +110,7 @@ describe('Transaction duration anomaly alert', () => { expect( services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); }); @@ -183,9 +183,9 @@ describe('Transaction duration anomaly alert', () => { await executor({ params }); - expect(services.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(services.alertFactory.create).toHaveBeenCalledTimes(1); - expect(services.alertInstanceFactory).toHaveBeenCalledWith( + expect(services.alertFactory.create).toHaveBeenCalledWith( 'apm.transaction_duration_anomaly_foo_development_type-foo' ); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts index b0a99377c2989..64540e144d8a8 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts @@ -46,7 +46,7 @@ describe('Transaction error rate alert', () => { ); await executor({ params }); - expect(services.alertInstanceFactory).not.toBeCalled(); + expect(services.alertFactory.create).not.toBeCalled(); }); it('sends alerts for services that exceeded the threshold', async () => { @@ -117,12 +117,12 @@ describe('Transaction error rate alert', () => { await executor({ params }); - expect(services.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(services.alertFactory.create).toHaveBeenCalledTimes(1); - expect(services.alertInstanceFactory).toHaveBeenCalledWith( + expect(services.alertFactory.create).toHaveBeenCalledWith( 'apm.transaction_error_rate_foo_type-foo_env-foo' ); - expect(services.alertInstanceFactory).not.toHaveBeenCalledWith( + expect(services.alertFactory.create).not.toHaveBeenCalledWith( 'apm.transaction_error_rate_bar_type-bar_env-bar' ); diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index a8610bbcc8d37..f881b4476fe22 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -42,7 +42,7 @@ export const createRuleTypeMocks = () => { savedObjectsClient: { get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), }, - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, alertWithLifecycle: jest.fn(), logger: loggerMock, shouldWriteAlerts: () => true, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 9301f17f4d99c..df79091612254 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -16,10 +16,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { - AlertInstance as Alert, - AlertTypeState as RuleTypeState, -} from '../../../../../alerting/server'; +import { Alert, AlertTypeState as RuleTypeState } from '../../../../../alerting/server'; import { AlertStates, InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 90f9c508e1038..8f0809f581ad0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -422,7 +422,7 @@ describe('Log threshold executor', () => { processUngroupedResults( results, ruleParams, - alertsMock.createAlertInstanceFactory, + alertsMock.createAlertFactory.create, alertUpdaterMock ); // First call, second argument @@ -486,7 +486,7 @@ describe('Log threshold executor', () => { processGroupByResults( results, ruleParams, - alertsMock.createAlertInstanceFactory, + alertsMock.createAlertFactory.create, alertUpdaterMock ); expect(alertUpdaterMock.mock.calls.length).toBe(2); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 73d7d1bf95363..5eedaac5f020a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -16,7 +16,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { ActionGroup, ActionGroupIdsOf, - AlertInstance as Alert, + Alert, AlertInstanceContext as AlertContext, AlertInstanceState as AlertState, AlertTypeState as RuleTypeState, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts index f762d694a59e7..0fb2ff87fd02c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -83,7 +83,7 @@ export const createMetricAnomalyExecutor = typical, influencers, } = first(data as MappedAnomalyHit[])!; - const alert = services.alertInstanceFactory(`${nodeType}-${metric}`); + const alert = services.alertFactory.create(`${nodeType}-${metric}`); alert.scheduleActions(FIRED_ACTIONS_ID, { alertState: stateToAlertMessage[AlertStates.ALERT], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index b3c4de9658eda..57001d8cbdb1a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -840,9 +840,9 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId }); const alertInstances = new Map(); -services.alertInstanceFactory.mockImplementation((instanceID: string) => { +services.alertFactory.create.mockImplementation((instanceID: string) => { const newAlertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), + instance: alertsMock.createAlertFactory.create(), actionQueue: [], state: {}, }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index f16b8a8135a37..9fbbe26fba126 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -15,10 +15,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { - AlertInstance as Alert, - AlertTypeState as RuleTypeState, -} from '../../../../../alerting/server'; +import { Alert, AlertTypeState as RuleTypeState } from '../../../../../alerting/server'; import { AlertStates, Comparator } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { InfraBackendLibs } from '../../infra_types'; diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index e30ea01b27cb5..68a86a927ac1a 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -139,7 +139,7 @@ export function registerAnomalyDetectionAlertType({ if (executionResult) { const alertInstanceName = executionResult.name; - const alertInstance = services.alertInstanceFactory(alertInstanceName); + const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_SCORE_MATCH_GROUP_ID, executionResult); } }, diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 5fd21d5372d23..1173f92930128 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -159,7 +159,7 @@ export function registerJobsMonitoringRuleType({ ); executionResult.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = services.alertInstanceFactory(alertInstanceName); + const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_DETECTION_JOB_REALTIME_ISSUE, context); }); } diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index d13e6d9ed7f9b..0c48fed40ee34 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -10,11 +10,16 @@ import { i18n } from '@kbn/i18n'; import { RuleType, AlertExecutorOptions, - AlertInstance, + Alert, RulesClient, AlertServices, } from '../../../alerting/server'; -import { Alert, AlertTypeParams, RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; +import { + Alert as Rule, + AlertTypeParams, + RawAlertInstance, + SanitizedAlert, +} from '../../../alerting/common'; import { ActionsClient } from '../../../actions/server'; import { AlertState, @@ -121,7 +126,7 @@ export class BaseRule { }); if (existingRuleData.total > 0) { - return existingRuleData.data[0] as Alert; + return existingRuleData.data[0] as Rule; } const ruleActions = []; @@ -272,7 +277,7 @@ export class BaseRule { for (const node of nodes) { const newAlertStates: AlertNodeState[] = []; // quick fix for now so that non node level alerts will use the cluster id - const instance = services.alertInstanceFactory( + const instance = services.alertFactory.create( node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid ); @@ -331,7 +336,7 @@ export class BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, instanceState: AlertInstanceState | AlertState | unknown, item: AlertData | unknown, cluster?: AlertCluster | unknown diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts index ed4ba69b8e254..d4f9284b40f8f 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts @@ -116,13 +116,15 @@ describe('CCRReadExceptionsRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 705d0c6b9c87f..e072602d6b711 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -21,7 +21,7 @@ import { CommonAlertFilter, CCRReadExceptionsStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CCR_READ_EXCEPTIONS, RULE_DETAILS } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -209,7 +209,7 @@ export class CCRReadExceptionsRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts index 85030657825c4..ec4c15afe6731 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts @@ -81,13 +81,15 @@ describe('ClusterHealthRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index b8810196c833a..a40fafc65d636 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -18,7 +18,7 @@ import { AlertClusterHealth, AlertInstanceState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CLUSTER_HEALTH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -111,7 +111,7 @@ export class ClusterHealthRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts index bcd2c0cbb5810..cf2e0f29ddbc3 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts @@ -83,13 +83,15 @@ describe('CpuUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index fa4b64fd997c3..08a5cdb6c2780 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -22,7 +22,7 @@ import { CommonAlertParams, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -145,7 +145,7 @@ export class CpuUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts index daaded1c18c80..c08d32c395c1b 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts @@ -96,13 +96,15 @@ describe('DiskUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index 1e06f0649d107..a52a2fd79d654 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -22,7 +22,7 @@ import { AlertDiskUsageNodeStats, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -152,7 +152,7 @@ export class DiskUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts index 4531c5f0f1ffc..560ab805a236c 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts @@ -85,13 +85,15 @@ describe('ElasticsearchVersionMismatchAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index 9d89f827f9b10..43f5be14538b6 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -87,7 +87,7 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts index b4444c9088073..b136d2c71f065 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts @@ -88,13 +88,15 @@ describe('KibanaVersionMismatchRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 24182c4a545d3..4e7a688b92ca9 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_KIBANA_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -97,7 +97,7 @@ export class KibanaVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts index 0460064b4f7c5..a82b87cfe8a97 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts @@ -96,13 +96,15 @@ describe('LargeShardSizeRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 92be43b9c06c0..fbcf557a1f6f5 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -21,7 +21,7 @@ import { CommonAlertFilter, IndexShardSizeStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_LARGE_SHARD_SIZE, RULE_DETAILS } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -149,7 +149,7 @@ export class LargeShardSizeRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index 86a6f666fcf87..0a69ee68ffeba 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -86,13 +86,15 @@ describe('LicenseExpirationRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index 3a837a125a523..ad13ca9c56dfa 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -20,7 +20,7 @@ import { AlertLicense, AlertLicenseState, } from '../../common/types/alerts'; -import { AlertExecutorOptions, AlertInstance } from '../../../alerting/server'; +import { AlertExecutorOptions, Alert } from '../../../alerting/server'; import { RULE_LICENSE_EXPIRATION, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -143,7 +143,7 @@ export class LicenseExpirationRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts index 857a9bf5bfa79..b7790f81caa3e 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts @@ -86,13 +86,15 @@ describe('LogstashVersionMismatchRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index ee3e5838d7d35..bca82de1a5fae 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_LOGSTASH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -87,7 +87,7 @@ export class LogstashVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts index 6e7aff2ae8fb4..785b1013304cd 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts @@ -83,13 +83,15 @@ describe('MemoryUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 06ecf4bb450c8..62f790b1eb6d0 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -22,7 +22,7 @@ import { AlertMemoryUsageNodeStats, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_MEMORY_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -158,7 +158,7 @@ export class MemoryUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts index a8a96a61a4b25..b70bfe4bfb375 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts @@ -87,13 +87,15 @@ describe('MissingMonitoringDataRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index fa7cbe009712a..9002855e2b67f 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -19,7 +19,7 @@ import { CommonAlertFilter, AlertNodeState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; @@ -137,7 +137,7 @@ export class MissingMonitoringDataRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts index 3e24df3a6ef15..3704e909101e9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts @@ -137,13 +137,15 @@ describe('NodesChangedAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 82cf91e91b52a..3b14cf2428889 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -19,7 +19,7 @@ import { AlertInstanceState, AlertNodesChangedState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_NODES_CHANGED, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; @@ -174,7 +174,7 @@ export class NodesChangedRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index 0cca5eb81c95f..ca1b78a62646a 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -20,10 +20,10 @@ import { AlertState, AlertThreadPoolRejectionsStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; -import { Alert, RawAlertInstance } from '../../../alerting/common'; +import { Alert as Rule, RawAlertInstance } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; import { Globals } from '../static_globals'; @@ -47,7 +47,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { } constructor( - sanitizedRule: Alert | undefined = undefined, + sanitizedRule: Rule | undefined = undefined, public readonly id: string, public readonly threadPoolType: string, public readonly name: string, @@ -176,7 +176,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { }; } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts index 63a02088b9b65..45f8caacbcd41 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts @@ -89,13 +89,15 @@ describe('ThreadpoolSearchRejectionsRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts index da4c7ffaeffa0..47f6704eae70f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts @@ -89,13 +89,15 @@ describe('ThreadpoolWriteRejectionsAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 9ae3dff28b2ae..aea27787af080 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -13,7 +13,7 @@ import { v4 } from 'uuid'; import { difference } from 'lodash'; import { AlertExecutorOptions, - AlertInstance, + Alert, AlertInstanceContext, AlertInstanceState, AlertTypeParams, @@ -62,7 +62,7 @@ export type LifecycleAlertService< > = (alert: { id: string; fields: ExplicitAlertFields; -}) => AlertInstance; +}) => Alert; export interface LifecycleAlertServices< InstanceState extends AlertInstanceState = never, @@ -143,7 +143,7 @@ export const createLifecycleExecutor = > ): Promise> => { const { - services: { alertInstanceFactory, shouldWriteAlerts }, + services: { alertFactory, shouldWriteAlerts }, state: previousState, } = options; @@ -165,7 +165,7 @@ export const createLifecycleExecutor = > = { alertWithLifecycle: ({ id, fields }) => { currentAlerts[id] = fields; - return alertInstanceFactory(id); + return alertFactory.create(id); }, }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 3b9d8904c89b8..baa60664dea57 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -66,10 +66,12 @@ function createRule(shouldWriteAlerts: boolean = true) { const scheduleActions = jest.fn(); - const alertInstanceFactory = () => { - return { - scheduleActions, - } as any; + const alertFactory = { + create: () => { + return { + scheduleActions, + } as any; + }, }; return { @@ -107,7 +109,7 @@ function createRule(shouldWriteAlerts: boolean = true) { updatedBy: 'updatedBy', }, services: { - alertInstanceFactory, + alertFactory, savedObjectsClient: {} as any, scopedClusterClient: {} as any, shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts index 37b4847bc9c69..5513aaf532522 100644 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts @@ -34,5 +34,5 @@ export const createLifecycleAlertServicesMock = < >( alertServices: AlertServices ): LifecycleAlertServices => ({ - alertWithLifecycle: ({ id }) => alertServices.alertInstanceFactory(id), + alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), }); diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 08b1b0a8ecbf2..3d880988182b1 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -67,8 +67,7 @@ export const createDefaultAlertExecutorOptions = < params, spaceId: 'SPACE_ID', services: { - alertInstanceFactory: alertsMock.createAlertServices() - .alertInstanceFactory, + alertFactory: alertsMock.createAlertServices().alertFactory, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts index 1d97b7a39779a..ae253dfa3438c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts @@ -136,9 +136,9 @@ describe('legacyRules_notification_alert_type', () => { ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -163,9 +163,9 @@ describe('legacyRules_notification_alert_type', () => { ) ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -192,9 +192,9 @@ describe('legacyRules_notification_alert_type', () => { ) ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -204,7 +204,7 @@ describe('legacyRules_notification_alert_type', () => { ); }); - it('should not call alertInstanceFactory if signalsCount was 0', async () => { + it('should not call alertFactory.create if signalsCount was 0', async () => { const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', @@ -218,7 +218,7 @@ describe('legacyRules_notification_alert_type', () => { await alert.executor(payload); - expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); }); it('should call scheduleActions if signalsCount was greater than 0', async () => { @@ -237,9 +237,9 @@ describe('legacyRules_notification_alert_type', () => { await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.replaceState).toHaveBeenCalledWith( expect.objectContaining({ signals_count: 100 }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts index 6a5a9478681f3..62d187bd3ea0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts @@ -119,7 +119,7 @@ export const legacyRulesNotificationAlertType = ({ ); if (signalsCount !== 0) { - const alertInstance = services.alertInstanceFactory(alertId); + const alertInstance = services.alertFactory.create(alertId); scheduleNotificationActions({ alertInstance, signalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts index b40f6c6f8a72d..eebda81fd63f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts @@ -54,7 +54,7 @@ describe('schedule_notification_actions', () => { }; it('Should schedule actions with unflatted and legacy context', () => { - const alertInstance = alertServices.alertInstanceFactory(alertId); + const alertInstance = alertServices.alertFactory.create(alertId); const signals = [sampleThresholdAlert._source, sampleThresholdAlert._source]; scheduleNotificationActions({ alertInstance, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 9b20b031eea0f..394e431203a24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -6,7 +6,7 @@ */ import { mapKeys, snakeCase } from 'lodash/fp'; -import { AlertInstance } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/server'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import { RuleParams } from '../schemas/rule_schemas'; import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; @@ -46,7 +46,7 @@ const formatAlertsForNotificationActions = (alerts: unknown[]): unknown[] => { }; interface ScheduleNotificationActions { - alertInstance: AlertInstance; + alertInstance: Alert; signalsCount: number; resultsLink: string; ruleParams: NotificationRuleTypeParams; @@ -59,7 +59,7 @@ export const scheduleNotificationActions = ({ resultsLink = '', ruleParams, signals, -}: ScheduleNotificationActions): AlertInstance => +}: ScheduleNotificationActions): Alert => alertInstance .replaceState({ signals_count: signalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 964df3c91eb08..b5dffa7b34c14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -82,7 +82,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -107,7 +107,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -137,7 +137,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -166,7 +166,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -197,7 +197,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -235,7 +235,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -271,7 +271,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -313,7 +313,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -375,7 +375,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -435,7 +435,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -497,7 +497,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -559,7 +559,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index cab590b3e2513..2399962ad281e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, SavedObject, Logger } from 'src/core/server'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import { AlertInstance } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/server'; import { RuleParams } from '../schemas/rule_schemas'; import { deconflictSignalsAndResults, getNotificationResultsLink } from '../notifications/utils'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants'; @@ -26,7 +26,7 @@ interface ScheduleThrottledNotificationActionsOptions { outputIndex: RuleParams['outputIndex']; ruleId: RuleParams['ruleId']; esClient: ElasticsearchClient; - alertInstance: AlertInstance; + alertInstance: Alert; notificationRuleParams: NotificationRuleTypeParams; signals: unknown[]; logger: Logger; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index ed0aa04b6a08c..8d89bc66b2041 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -34,7 +34,7 @@ import { } from '../../../../../../alerting/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ExecutorType } from '../../../../../../alerting/server/types'; -import { AlertInstance } from '../../../../../../alerting/server'; +import { Alert } from '../../../../../../alerting/server'; import { ConfigType } from '../../../../config'; import { alertInstanceFactoryStub } from '../../signals/preview/alert_instance_factory_stub'; import { CreateRuleOptions, CreateSecurityRuleTypeWrapperProps } from '../../rule_types/types'; @@ -140,12 +140,14 @@ export const previewRulesRoute = async ( ruleTypeName: string, params: TParams, shouldWriteAlerts: () => boolean, - alertInstanceFactory: ( - id: string - ) => Pick< - AlertInstance, - 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' - > + alertFactory: { + create: ( + id: string + ) => Pick< + Alert, + 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' + >; + } ) => { let statePreview = runState as TState; @@ -178,7 +180,7 @@ export const previewRulesRoute = async ( services: { shouldWriteAlerts, shouldStopExecution: () => false, - alertInstanceFactory, + alertFactory, // Just use es client always for preview search: context.core.elasticsearch.client, savedObjectsClient: context.core.savedObjects.client, @@ -223,7 +225,7 @@ export const previewRulesRoute = async ( queryAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'threshold': @@ -236,7 +238,7 @@ export const previewRulesRoute = async ( thresholdAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'threat_match': @@ -249,7 +251,7 @@ export const previewRulesRoute = async ( threatMatchAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'eql': @@ -260,7 +262,7 @@ export const previewRulesRoute = async ( eqlAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'machine_learning': @@ -271,7 +273,7 @@ export const previewRulesRoute = async ( mlAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 3774930204ae6..3d96e3bb77907 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -76,7 +76,7 @@ export const createRuleTypeMocks = ( search: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: mockSavedObjectsClient, scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, findAlerts: jest.fn(), // TODO: does this stay? alertWithPersistence: jest.fn(), logger: loggerMock, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index e2fc5442d4c80..00244832d0191 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -315,7 +315,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: alertId, @@ -329,7 +329,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = logger, }); } else if (createdSignalsCount) { - const alertInstance = services.alertInstanceFactory(alertId); + const alertInstance = services.alertFactory.create(alertId); scheduleNotificationActions({ alertInstance, signalsCount: createdSignalsCount, @@ -371,7 +371,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: completeRule.alertId, @@ -403,7 +403,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: completeRule.alertId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts index d09314312c78d..7cc709bbe8994 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts @@ -12,7 +12,7 @@ import { AlertTypeState, } from '../../../../../../alerting/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertInstance } from '../../../../../../alerting/server/alert_instance'; +import { Alert } from '../../../../../../alerting/server/alert'; export const alertInstanceFactoryStub = < TParams extends RuleParams, @@ -27,13 +27,13 @@ export const alertInstanceFactoryStub = < return {} as unknown as TInstanceState; }, replaceState(state: TInstanceState) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); }, scheduleActions(actionGroup: TActionGroupIds, alertcontext: TInstanceContext) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); @@ -43,7 +43,7 @@ export const alertInstanceFactoryStub = < subgroup: string, alertcontext: TInstanceContext ) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index c0d05c44201fb..307496e2be391 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -178,7 +178,7 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); expect(result).toMatchInlineSnapshot(` Object { @@ -257,8 +257,8 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith(ConditionMetAlertInstanceId); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -328,7 +328,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward latestTimestamp: new Date(previousTimestamp).toISOString(), @@ -410,7 +410,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -488,7 +488,7 @@ describe('alertType', () => { }; const result = await alertType.executor(executorOptions); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -518,7 +518,7 @@ describe('alertType', () => { state: result as EsQueryAlertState, }); const existingInstance: AlertInstanceMock = - alertServices.alertInstanceFactory.mock.results[1].value; + alertServices.alertFactory.create.mock.results[1].value; expect(existingInstance.replaceState).toHaveBeenCalledWith({ latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), dateStart: expect.any(String), @@ -601,7 +601,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -685,7 +685,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 9dca9e9c3fc61..6a1fcc6a3d7bb 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -160,7 +160,7 @@ export function getAlertType(logger: Logger): RuleType< > ) { const { alertId, name, services, params, state } = options; - const { alertInstanceFactory, search } = services; + const { alertFactory, search } = services; const previousTimestamp = state.latestTimestamp; const abortableEsClient = search.asCurrentUser; @@ -255,7 +255,7 @@ export function getAlertType(logger: Logger): RuleType< }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(ConditionMetAlertInstanceId); + const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); alertInstance // store the params we would need to recreate the query that led to this alert instance .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index ecd08d3dc432f..aca79e29cd3e5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -87,11 +87,11 @@ export function transformResults( export function getActiveEntriesAndGenerateAlerts( prevLocationMap: Map, currLocationMap: Map, - alertInstanceFactory: AlertServices< + alertFactory: AlertServices< GeoContainmentInstanceState, GeoContainmentInstanceContext, typeof ActionGroupId - >['alertInstanceFactory'], + >['alertFactory'], shapesIdsNamesMap: Record, currIntervalEndTime: Date ) { @@ -113,7 +113,7 @@ export function getActiveEntriesAndGenerateAlerts( }; const alertInstanceId = `${entityName}-${context.containingBoundaryName}`; if (shapeLocationId !== OTHER_CATEGORY) { - alertInstanceFactory(alertInstanceId).scheduleActions(ActionGroupId, context); + alertFactory.create(alertInstanceId).scheduleActions(ActionGroupId, context); } }); @@ -189,7 +189,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMap, currLocationMap, - services.alertInstanceFactory, + services.alertFactory, shapesIdsNamesMap, currIntervalEndTime ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 8b78441d174b2..dc633e298490c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -20,9 +20,9 @@ import { OTHER_CATEGORY } from '../es_query_builder'; import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type'; import type { GeoContainmentParams } from '../alert_type'; -const alertInstanceFactory = - (contextKeys: unknown[], testAlertActionArr: unknown[]) => (instanceId: string) => { - const alertInstance = alertsMock.createAlertInstanceFactory< +const alertFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => ({ + create: (instanceId: string) => { + const alertInstance = alertsMock.createAlertFactory.create< GeoContainmentInstanceState, GeoContainmentInstanceContext >(); @@ -39,7 +39,8 @@ const alertInstanceFactory = } ); return alertInstance; - }; + }, +}); describe('geo_containment', () => { describe('transformResults', () => { @@ -253,7 +254,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -278,7 +279,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithIdenticalEntityEntry, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -317,7 +318,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithNonIdenticalEntityEntry, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -340,7 +341,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -373,7 +374,7 @@ describe('geo_containment', () => { getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithThreeMore, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -410,7 +411,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -442,7 +443,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -514,7 +515,7 @@ describe('geo_containment', () => { const alertServicesWithSearchMock: AlertServicesMock = { ...alertsMock.createAlertServices(), // @ts-ignore - alertInstanceFactory: alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory: alertFactory(contextKeys, testAlertActionArr), scopedClusterClient: { asCurrentUser: { // @ts-ignore @@ -538,6 +539,7 @@ describe('geo_containment', () => { it('should query for shapes if state does not contain shapes', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -557,6 +559,7 @@ describe('geo_containment', () => { it('should not query for shapes if state contains shapes', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -575,6 +578,7 @@ describe('geo_containment', () => { it('should carry through shapes filters in state to next call unmodified', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -610,6 +614,7 @@ describe('geo_containment', () => { ], }; const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index f6b1c4a3a3b0a..e55ce6e3a3aba 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -203,7 +203,7 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith('all documents'); + expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents'); }); it('should ensure a null result does not fire actions', async () => { @@ -269,7 +269,7 @@ describe('alertType', () => { }, }); - expect(customAlertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); it('should ensure an undefined result does not fire actions', async () => { @@ -335,6 +335,6 @@ describe('alertType', () => { }, }); - expect(customAlertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index e31744e770462..0eb2810626ac3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -134,7 +134,7 @@ export function getAlertType( options: AlertExecutorOptions ) { const { alertId, name, services, params } = options; - const { alertInstanceFactory, search } = services; + const { alertFactory, search } = services; const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { @@ -208,7 +208,7 @@ export function getAlertType( conditions: humanFn, }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(instanceId); + const alertInstance = alertFactory.create(instanceId); alertInstance.scheduleActions(ActionGroupId, actionContext); logger.debug(`scheduled actionGroup: ${JSON.stringify(actionContext)}`); } diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts index f33afc2b54285..8b9897a0e9436 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts @@ -100,7 +100,7 @@ export function getTransformHealthRuleType(): RuleType< isExportable: true, async executor(options) { const { - services: { scopedClusterClient, alertInstanceFactory }, + services: { scopedClusterClient, alertFactory }, params, } = options; @@ -112,7 +112,7 @@ export function getTransformHealthRuleType(): RuleType< if (executionResult.length > 0) { executionResult.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = alertInstanceFactory(alertInstanceName); + const alertInstance = alertFactory.create(alertInstanceName); alertInstance.scheduleActions(TRANSFORM_ISSUE, context); }); } diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts index 8236af03de85c..e662160bef3e0 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts @@ -16,7 +16,7 @@ import { commonStateTranslations, tlsTranslations } from './translations'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { AlertInstanceContext } from '../../../../alerting/common'; -import { AlertInstance } from '../../../../alerting/server'; +import { Alert } from '../../../../alerting/server'; import { savedObjectsAdapter } from '../saved_objects/saved_objects'; import { createUptimeESClient } from '../lib'; @@ -28,7 +28,7 @@ import { export type ActionGroupIds = ActionGroupIdsOf; -type TLSAlertInstance = AlertInstance, AlertInstanceContext, ActionGroupIds>; +type TLSAlertInstance = Alert, AlertInstanceContext, ActionGroupIds>; interface TlsAlertState { count: number; @@ -113,10 +113,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_s }, isExportable: true, minimumLicenseRequired: 'basic', - async executor({ - services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient }, - state, - }) { + async executor({ services: { alertFactory, scopedClusterClient, savedObjectsClient }, state }) { const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); const uptimeEsClient = createUptimeESClient({ @@ -156,7 +153,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_s 'd' ) .valueOf(); - const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id); + const alertInstance: TLSAlertInstance = alertFactory.create(TLS_LEGACY.id); const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); alertInstance.replaceState({ ...updateState(state, foundCerts), diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index a99de22181766..b1ad23170ae07 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -122,7 +122,7 @@ async function alwaysFiringExecutor(alertExecutorOptions: any) { } if (group) { - const instance = services.alertInstanceFactory('1').replaceState({ instanceStateValue: true }); + const instance = services.alertFactory.create('1').replaceState({ instanceStateValue: true }); if (subgroup) { instance.scheduleActionsWithSubGroup(group, subgroup, { @@ -177,8 +177,8 @@ function getCumulativeFiringAlertType() { const runCount = (state.runCount || 0) + 1; times(runCount, (index) => { - services - .alertInstanceFactory(`instance-${index}`) + services.alertFactory + .create(`instance-${index}`) .replaceState({ instanceStateValue: true }) .scheduleActions(group); }); @@ -446,13 +446,13 @@ function getPatternFiringAlertType() { for (const [instanceId, instancePattern] of Object.entries(pattern)) { const scheduleByPattern = instancePattern[patternIndex]; if (scheduleByPattern === true) { - services.alertInstanceFactory(instanceId).scheduleActions('default', { + services.alertFactory.create(instanceId).scheduleActions('default', { ...EscapableStrings, deep: DeepContextVariables, }); } else if (typeof scheduleByPattern === 'string') { - services - .alertInstanceFactory(instanceId) + services.alertFactory + .create(instanceId) .scheduleActionsWithSubGroup('default', scheduleByPattern); } } @@ -538,7 +538,7 @@ function getLongRunningPatternRuleType(cancelAlertsOnRuleTimeout: boolean = true return {}; } - services.alertInstanceFactory('alert').scheduleActions('default', {}); + services.alertFactory.create('alert').scheduleActions('default', {}); // run long if pattern says to if (pattern[globalPatternIndex++] === true) { diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index de0fb1829c2b1..2cd6b50a2062f 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -54,8 +54,8 @@ export const alwaysFiringAlertType: RuleType< const { services, state, params } = alertExecutorOptions; (params.instances || []).forEach((instance: { id: string; state: any }) => { - services - .alertInstanceFactory(instance.id) + services.alertFactory + .create(instance.id) .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) .scheduleActions('default'); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts index ebef251984cd6..89f15705beb59 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts @@ -177,7 +177,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid producer: 'observability.test', }, services: { - alertInstanceFactory: sinon.stub(), + alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, } as unknown as RuleExecutorOptions< From 8dffa53561730448f263d949f312330813d375b7 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 7 Feb 2022 17:24:08 -0500 Subject: [PATCH 026/161] [Dashboard] Remove Deprecated References (#121925) Remove index pattern references. Replace data filter imports with imports from es-query package --- src/plugins/dashboard/kibana.json | 1 + .../public/application/dashboard_router.tsx | 4 +- .../hooks/use_dashboard_app_state.test.tsx | 16 ++--- .../hooks/use_dashboard_app_state.ts | 19 +++--- .../lib/dashboard_control_group.ts | 2 +- .../application/lib/diff_dashboard_state.ts | 2 +- .../dashboard/public/application/lib/index.ts | 2 +- .../lib/load_saved_dashboard_state.ts | 4 +- .../public/application/lib/save_dashboard.ts | 7 +-- .../lib/sync_dashboard_container_input.ts | 11 +--- ...tterns.ts => sync_dashboard_data_views.ts} | 59 ++++++++++--------- .../get_dashboard_list_item_link.test.ts | 4 +- .../test_helpers/make_default_services.ts | 4 +- .../application/top_nav/dashboard_top_nav.tsx | 2 +- src/plugins/dashboard/public/locator.test.ts | 4 +- src/plugins/dashboard/public/locator.ts | 8 ++- src/plugins/dashboard/public/plugin.tsx | 13 ++-- .../dashboard/public/services/data_views.ts | 9 +++ src/plugins/dashboard/public/types.ts | 17 +++--- .../dashboard/public/url_generator.test.ts | 4 +- .../saved_objects/dashboard_migrations.ts | 6 +- .../move_filters_to_query.test.ts | 6 +- .../replace_index_pattern_reference.ts | 4 +- src/plugins/dashboard/tsconfig.json | 1 + 24 files changed, 113 insertions(+), 96 deletions(-) rename src/plugins/dashboard/public/application/lib/{sync_dashboard_index_patterns.ts => sync_dashboard_data_views.ts} (56%) create mode 100644 src/plugins/dashboard/public/services/data_views.ts diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 683a1a551f81d..0130d4a5f8118 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -8,6 +8,7 @@ "version": "kibana", "requiredPlugins": [ "data", + "dataViews", "embeddable", "controls", "inspector", diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index ae16527b64440..05d663bdac265 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -110,7 +110,7 @@ export async function mountApp({ uiSettings: coreStart.uiSettings, scopedHistory: () => scopedHistory, screenshotModeService: screenshotMode, - indexPatterns: dataStart.indexPatterns, + dataViews: dataStart.dataViews, savedQueryService: dataStart.query.savedQueries, savedObjectsClient: coreStart.savedObjects.client, savedDashboards: dashboardStart.getSavedDashboardLoader(), @@ -212,7 +212,7 @@ export async function mountApp({ .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false) ); if (!hasEmbeddableIncoming) { - dataStart.indexPatterns.clearCache(); + dataStart.dataViews.clearCache(); } // dispatch synthetic hash change event to update hash history objects diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 0ef21fca26f29..039a600d153b2 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -24,13 +24,15 @@ import { EmbeddableFactory, ViewMode } from '../../services/embeddable'; import { dashboardStateStore, setDescription, setViewMode } from '../state'; import { DashboardContainerServices } from '../embeddable/dashboard_container'; import { createKbnUrlStateStorage, defer } from '../../../../kibana_utils/public'; -import { Filter, IIndexPattern, IndexPatternsContract } from '../../services/data'; import { useDashboardAppState, UseDashboardStateProps } from './use_dashboard_app_state'; import { getSampleDashboardInput, getSavedDashboardMock, makeDefaultServices, } from '../test_helpers'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; +import type { Filter } from '@kbn/es-query'; interface SetupEmbeddableFactoryReturn { finalizeEmbeddableCreation: () => void; @@ -56,12 +58,10 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({ const createDashboardAppStateServices = () => { const defaults = makeDefaultServices(); - const indexPatterns = {} as IndexPatternsContract; - const defaultIndexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; - indexPatterns.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); - indexPatterns.getDefault = jest - .fn() - .mockImplementation(() => Promise.resolve(defaultIndexPattern)); + const dataViews = {} as DataViewsContract; + const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView; + dataViews.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); + dataViews.getDefault = jest.fn().mockImplementation(() => Promise.resolve(defaultDataView)); const data = dataPluginMock.createStartContract(); data.query.filterManager.getUpdates$ = jest.fn().mockImplementation(() => of(void 0)); @@ -71,7 +71,7 @@ const createDashboardAppStateServices = () => { .fn() .mockImplementation(() => of(void 0)); - return { ...defaults, indexPatterns, data }; + return { ...defaults, dataViews, data }; }; const setupEmbeddableFactory = ( diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 8c58eab0ded83..2ce1c87252d38 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -15,6 +15,7 @@ import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { DashboardConstants } from '../..'; import { ViewMode } from '../../services/embeddable'; import { useKibana } from '../../services/kibana_react'; +import { DataView } from '../../services/data_views'; import { getNewDashboardTitle } from '../../dashboard_strings'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; @@ -30,7 +31,7 @@ import { tryDestroyDashboardContainer, syncDashboardContainerInput, savedObjectToDashboardState, - syncDashboardIndexPatterns, + syncDashboardDataViews, syncDashboardFilterState, loadSavedDashboardState, buildDashboardContainer, @@ -81,7 +82,7 @@ export const useDashboardAppState = ({ core, chrome, embeddable, - indexPatterns, + dataViews, usageCollection, savedDashboards, initializerContext, @@ -121,7 +122,7 @@ export const useDashboardAppState = ({ search, history, embeddable, - indexPatterns, + dataViews, notifications, kibanaVersion, savedDashboards, @@ -234,11 +235,11 @@ export const useDashboardAppState = ({ /** * Start syncing index patterns between the Query Service and the Dashboard Container. */ - const indexPatternsSubscription = syncDashboardIndexPatterns({ + const dataViewsSubscription = syncDashboardDataViews({ dashboardContainer, - indexPatterns: dashboardBuildContext.indexPatterns, - onUpdateIndexPatterns: (newIndexPatterns) => - setDashboardAppState((s) => ({ ...s, indexPatterns: newIndexPatterns })), + dataViews: dashboardBuildContext.dataViews, + onUpdateDataViews: (newDataViews: DataView[]) => + setDashboardAppState((s) => ({ ...s, dataViews: newDataViews })), }); /** @@ -339,7 +340,7 @@ export const useDashboardAppState = ({ stopWatchingAppStateInUrl(); stopSyncingDashboardFilterState(); lastSavedSubscription.unsubscribe(); - indexPatternsSubscription.unsubscribe(); + dataViewsSubscription.unsubscribe(); tryDestroyDashboardContainer(dashboardContainer); setDashboardAppState((state) => ({ ...state, @@ -368,7 +369,7 @@ export const useDashboardAppState = ({ usageCollection, scopedHistory, notifications, - indexPatterns, + dataViews, kibanaVersion, embeddable, docTitle, diff --git a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts index 90d5a67c3da47..0d1eb3537377f 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts @@ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; import deepEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators'; import { DashboardContainer } from '..'; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index 264c8fcb1de2e..729b0d06f4ab8 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -8,7 +8,7 @@ import { xor, omit, isEmpty } from 'lodash'; import fastIsEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter, isFilterPinned } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, isFilterPinned } from '@kbn/es-query'; import { DashboardContainerInput } from '../..'; import { controlGroupInputIsEqual } from './dashboard_control_group'; diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index 58f962591b67c..eab3604ff841b 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -18,7 +18,7 @@ export { DashboardSessionStorage } from './dashboard_session_storage'; export { loadSavedDashboardState } from './load_saved_dashboard_state'; export { attemptLoadDashboardByTitle } from './load_dashboard_by_title'; export { syncDashboardFilterState } from './sync_dashboard_filter_state'; -export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns'; +export { syncDashboardDataViews } from './sync_dashboard_data_views'; export { syncDashboardContainerInput } from './sync_dashboard_container_input'; export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 03a03842c0e66..45eda98dcc498 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -28,7 +28,7 @@ export const loadSavedDashboardState = async ({ query, history, notifications, - indexPatterns, + dataViews, savedDashboards, usageCollection, savedDashboardId, @@ -51,7 +51,7 @@ export const loadSavedDashboardState = async ({ notifications.toasts.addWarning(getDashboard60Warning()); return; } - await indexPatterns.ensureDefaultDataView(); + await dataViews.ensureDefaultDataView(); try { const savedDashboard = (await savedDashboards.get({ id: savedDashboardId, diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 5a699eb116401..0be2211d4c2fc 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -8,6 +8,7 @@ import _ from 'lodash'; +import { isFilterPinned } from '@kbn/es-query'; import { convertTimeToUTCString } from '.'; import { NotificationsStart } from '../../services/core'; import { DashboardSavedObject } from '../../saved_dashboards'; @@ -16,7 +17,7 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { dashboardSaveToastStrings } from '../../dashboard_strings'; import { getHasTaggingCapabilitiesGuard } from './dashboard_tagging'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; -import { RefreshInterval, TimefilterContract, esFilters } from '../../services/data'; +import { RefreshInterval, TimefilterContract } from '../../services/data'; import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; import { DashboardSessionStorage } from './dashboard_session_storage'; import { serializeControlGroupToDashboardSavedObject } from './dashboard_control_group'; @@ -81,9 +82,7 @@ export const saveDashboard = async ({ savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; // only save unpinned filters - const unpinnedFilters = savedDashboard - .getFilters() - .filter((filter) => !esFilters.isFilterPinned(filter)); + const unpinnedFilters = savedDashboard.getFilters().filter((filter) => !isFilterPinned(filter)); savedDashboard.searchSource.setField('filter', unpinnedFilters); try { diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts index 0fa7487390cd8..d3930cb5c0621 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts @@ -10,8 +10,9 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; import { debounceTime, tap } from 'rxjs/operators'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { DashboardContainer } from '../embeddable'; -import { esFilters, Filter, Query } from '../../services/data'; +import { Query } from '../../services/data'; import { DashboardConstants, DashboardSavedObject } from '../..'; import { setControlGroupState, @@ -96,13 +97,7 @@ export const applyContainerChangesToState = ({ return; } const { filterManager } = query; - if ( - !esFilters.compareFilters( - input.filters, - filterManager.getFilters(), - esFilters.COMPARE_ALL_OPTIONS - ) - ) { + if (!compareFilters(input.filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS)) { // Add filters modifies the object passed to it, hence the clone deep. filterManager.addFilters(_.cloneDeep(input.filters)); applyFilters(latestState.query, input.filters); diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts similarity index 56% rename from src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts rename to src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts index 5460ef7b00037..63cecaa76fb2f 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts @@ -13,48 +13,51 @@ import { distinctUntilChanged, switchMap, filter, mapTo, map } from 'rxjs/operat import { DashboardContainer } from '..'; import { isErrorEmbeddable } from '../../services/embeddable'; -import { IndexPattern, IndexPatternsContract } from '../../services/data'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; -interface SyncDashboardIndexPatternsProps { +interface SyncDashboardDataViewsProps { dashboardContainer: DashboardContainer; - indexPatterns: IndexPatternsContract; - onUpdateIndexPatterns: (newIndexPatterns: IndexPattern[]) => void; + dataViews: DataViewsContract; + onUpdateDataViews: (newDataViews: DataView[]) => void; } -export const syncDashboardIndexPatterns = ({ +export const syncDashboardDataViews = ({ dashboardContainer, - indexPatterns, - onUpdateIndexPatterns, -}: SyncDashboardIndexPatternsProps) => { - const updateIndexPatternsOperator = pipe( + dataViews, + onUpdateDataViews, +}: SyncDashboardDataViewsProps) => { + const updateDataViewsOperator = pipe( filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), - map((container: DashboardContainer): IndexPattern[] | undefined => { - let panelIndexPatterns: IndexPattern[] = []; + map((container: DashboardContainer): DataView[] | undefined => { + let panelDataViews: DataView[] = []; Object.values(container.getChildIds()).forEach((id) => { const embeddableInstance = container.getChild(id); if (isErrorEmbeddable(embeddableInstance)) return; - const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; - if (!embeddableIndexPatterns) return; - panelIndexPatterns.push(...embeddableIndexPatterns); + const embeddableDataViews = ( + embeddableInstance.getOutput() as { indexPatterns: DataView[] } + ).indexPatterns; + if (!embeddableDataViews) return; + panelDataViews.push(...embeddableDataViews); }); if (container.controlGroup) { - panelIndexPatterns.push(...(container.controlGroup.getOutput().dataViews ?? [])); + panelDataViews.push(...(container.controlGroup.getOutput().dataViews ?? [])); } - panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); + panelDataViews = uniqBy(panelDataViews, 'id'); /** * If no index patterns have been returned yet, and there is at least one embeddable which * hasn't yet loaded, defer the loading of the default index pattern by returning undefined. */ if ( - panelIndexPatterns.length === 0 && + panelDataViews.length === 0 && Object.keys(container.getOutput().embeddableLoaded).length > 0 && Object.values(container.getOutput().embeddableLoaded).some((value) => value === false) ) { return; } - return panelIndexPatterns; + return panelDataViews; }), distinctUntilChanged((a, b) => deepEqual( @@ -63,17 +66,17 @@ export const syncDashboardIndexPatterns = ({ ) ), // using switchMap for previous task cancellation - switchMap((panelIndexPatterns?: IndexPattern[]) => { + switchMap((panelDataViews?: DataView[]) => { return new Observable((observer) => { - if (!panelIndexPatterns) return; - if (panelIndexPatterns.length > 0) { + if (!panelDataViews) return; + if (panelDataViews.length > 0) { if (observer.closed) return; - onUpdateIndexPatterns(panelIndexPatterns); + onUpdateDataViews(panelDataViews); observer.complete(); } else { - indexPatterns.getDefault().then((defaultIndexPattern) => { + dataViews.getDefault().then((defaultDataView) => { if (observer.closed) return; - onUpdateIndexPatterns([defaultIndexPattern as IndexPattern]); + onUpdateDataViews([defaultDataView as DataView]); observer.complete(); }); } @@ -81,11 +84,11 @@ export const syncDashboardIndexPatterns = ({ }) ); - const indexPatternSources = [dashboardContainer.getOutput$()]; + const dataViewSources = [dashboardContainer.getOutput$()]; if (dashboardContainer.controlGroup) - indexPatternSources.push(dashboardContainer.controlGroup.getOutput$()); + dataViewSources.push(dashboardContainer.controlGroup.getOutput$()); - return combineLatest(indexPatternSources) - .pipe(mapTo(dashboardContainer), updateIndexPatternsOperator) + return combineLatest(dataViewSources) + .pipe(mapTo(dashboardContainer), updateDataViewsOperator) .subscribe(); }; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index 36b8b57cfdbd8..a6f80c157bee8 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -8,10 +8,10 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; -import { esFilters } from '../../../../data/public'; import { createHashHistory } from 'history'; import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { FilterStateStore } from '@kbn/es-query'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; @@ -118,7 +118,7 @@ describe('when global filters change', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ]; diff --git a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts index 616fe56102df9..656f5672e38c0 100644 --- a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts +++ b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts @@ -13,7 +13,7 @@ import { UrlForwardingStart } from '../../../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../services/navigation'; import { DashboardAppServices, DashboardAppCapabilities } from '../../types'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; -import { IndexPatternsContract, SavedQueryService } from '../../services/data'; +import { DataViewsContract, SavedQueryService } from '../../services/data'; import { savedObjectsPluginMock } from '../../../../saved_objects/public/mocks'; import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; @@ -83,7 +83,7 @@ export function makeDefaultServices(): DashboardAppServices { savedObjectsClient: core.savedObjects.client, dashboardCapabilities: defaultCapabilities, data: dataPluginMock.createStartContract(), - indexPatterns: {} as IndexPatternsContract, + dataViews: {} as DataViewsContract, savedQueryService: {} as SavedQueryService, scopedHistory: () => ({} as ScopedHistory), setHeaderActionMenu: (mountPoint) => {}, diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 005d40a90f38f..eb251ad41f62b 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -525,7 +525,7 @@ export function DashboardTopNav({ showDatePicker, showFilterBar, setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu, - indexPatterns: dashboardAppState.indexPatterns, + indexPatterns: dashboardAppState.dataViews, showSaveQuery: dashboardCapabilities.saveQuery, useDefaultBehaviors: true, savedQuery: state.savedQuery, diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts index f3f5aec9f478c..11ec16908b811 100644 --- a/src/plugins/dashboard/public/locator.test.ts +++ b/src/plugins/dashboard/public/locator.test.ts @@ -9,7 +9,7 @@ import { DashboardAppLocatorDefinition } from './locator'; import { hashedItemStore } from '../../kibana_utils/public'; import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { FilterStateStore } from '@kbn/es-query'; describe('dashboard locator', () => { beforeEach(() => { @@ -79,7 +79,7 @@ describe('dashboard locator', () => { }, query: { query: 'hi' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index b6655e246de36..42efb521cf6e5 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -8,11 +8,11 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { flow } from 'lodash'; -import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import { type Filter } from '@kbn/es-query'; +import type { TimeRange, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import type { SavedDashboardPanel } from '../common/types'; import type { RawDashboardState } from './types'; -import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; @@ -152,12 +152,14 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition( '_g', cleanEmptyKeys({ time: params.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), + filters: filters?.filter((f) => isFilterPinned(f)), refreshInterval: params.refreshInterval, }), { useHash }, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 7f784d43c0cb7..6554520fca101 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,7 +33,7 @@ import { UiActionsSetup, UiActionsStart } from './services/ui_actions'; import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; -import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from './services/data'; +import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { @@ -253,10 +253,13 @@ export class DashboardPlugin filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), - })) + map(async ({ state }) => { + const { isFilterPinned } = await import('@kbn/es-query'); + return { + ...state, + filters: state.filters?.filter(isFilterPinned), + }; + }) ), }, ], diff --git a/src/plugins/dashboard/public/services/data_views.ts b/src/plugins/dashboard/public/services/data_views.ts new file mode 100644 index 0000000000000..4fb2bbaf08503 --- /dev/null +++ b/src/plugins/dashboard/public/services/data_views.ts @@ -0,0 +1,9 @@ +/* + * 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 * from '../../../data_views/public'; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index b7b146aeba348..4de07974203a7 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -17,22 +17,25 @@ import type { KibanaExecutionContext, } from 'kibana/public'; import { History } from 'history'; +import type { Filter } from '@kbn/es-query'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; -import { Query, Filter, IndexPattern, RefreshInterval, TimeRange } from './services/data'; -import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; + +import { DataView } from './services/data_views'; import { SharePluginStart } from './services/share'; import { EmbeddableStart } from './services/embeddable'; import { DashboardSessionStorage } from './application/lib'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { UsageCollectionSetup } from './services/usage_collection'; import { NavigationPublicPluginStart } from './services/navigation'; +import { Query, RefreshInterval, TimeRange } from './services/data'; import { DashboardPanelState, SavedDashboardPanel } from '../common/types'; import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss'; -import { DataPublicPluginStart, IndexPatternsContract } from './services/data'; +import { DataPublicPluginStart, DataViewsContract } from './services/data'; +import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; import { SavedObjectLoader, SavedObjectsStart } from './services/saved_objects'; -import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { ScreenshotModePluginStart } from './services/screenshot_mode'; +import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { DashboardContainer, DashboardSavedObject } from '.'; import { VisualizationsStart } from '../../visualizations/public'; import { DashboardAppLocatorParams } from './locator'; @@ -102,7 +105,7 @@ export interface DashboardContainerInput extends ContainerInput { */ export interface DashboardAppState { hasUnsavedChanges?: boolean; - indexPatterns?: IndexPattern[]; + dataViews?: DataView[]; updateLastSavedState?: () => void; resetToLastSavedState?: () => void; savedDashboard?: DashboardSavedObject; @@ -119,7 +122,7 @@ export interface DashboardAppState { export type DashboardBuildContext = Pick< DashboardAppServices, | 'embeddable' - | 'indexPatterns' + | 'dataViews' | 'savedDashboards' | 'usageCollection' | 'initializerContext' @@ -198,7 +201,7 @@ export interface DashboardAppServices { savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; visualizations: VisualizationsStart; - indexPatterns: IndexPatternsContract; + dataViews: DataViewsContract; usageCollection?: UsageCollectionSetup; navigation: NavigationPublicPluginStart; dashboardCapabilities: DashboardAppCapabilities; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 9a1204f116c7f..f1035d7cc1389 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -9,8 +9,8 @@ import { createDashboardUrlGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters, Filter } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; +import { type Filter, FilterStateStore } from '@kbn/es-query'; const APP_BASE_PATH: string = 'xyz/app/dashboards'; @@ -99,7 +99,7 @@ describe('dashboard url generator', () => { }, query: { query: 'hi' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index e0cd410ce5e8f..ed8f87ad9b51b 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -25,7 +25,7 @@ import { convertSavedDashboardPanelToPanelState, } from '../../common/embeddable/embeddable_saved_object_converters'; import { SavedObjectEmbeddableInput } from '../../../embeddable/common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; import { mergeMigrationFunctionMaps, MigrateFunction, @@ -49,7 +49,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; doc.references.push({ name: searchSource.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: searchSource.index, }); delete searchSource.index; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; doc.references.push({ name: filterRow.meta.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); delete filterRow.meta.index; diff --git a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 8980bd1903323..4000bed0c28ac 100644 --- a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { esFilters, Filter } from 'src/plugins/data/public'; +import { FilterStateStore, Filter } from '@kbn/es-query'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, query: {}, - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, }; const queryFilter: Pre600FilterQuery = { @@ -27,7 +27,7 @@ test('Migrates an old filter query into the query field', () => { expect(newSearchSource).toEqual({ filter: [ { - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, meta: { alias: '', disabled: false, diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts index ddd1c45841b9c..e2ea076de7743 100644 --- a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts +++ b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts @@ -7,14 +7,14 @@ */ import type { SavedObjectMigrationFn } from 'kibana/server'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; export const replaceIndexPatternReference: SavedObjectMigrationFn = (doc) => ({ ...doc, references: Array.isArray(doc.references) ? doc.references.map((reference) => { if (reference.type === 'index_pattern') { - reference.type = INDEX_PATTERN_SAVED_OBJECT_TYPE; + reference.type = DATA_VIEW_SAVED_OBJECT_TYPE; } return reference; }) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 680d06780543a..55049447aee57 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../core/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../controls/tsconfig.json" }, From ca10263ed64462c8bd739560162358ef44856535 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:26:51 -0800 Subject: [PATCH 027/161] [DOCS] Fixes links in Update docs (#124892) * [DOCS] Fixes links in Update docs * [DOCS] Fixes another formatting error --- docs/redirects.asciidoc | 6 +- .../setup/upgrade/upgrade-migrations.asciidoc | 60 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index ff6ccbd6fab36..163fed04578ef 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -386,12 +386,12 @@ This content has moved. Refer to <>. This content has moved. Refer to <>. -[role="exclude" logging-configuration-changes] -== Logging configuration changes +[role="exclude",id="logging-configuration-changes"] +== Logging configuration changes This content has moved. Refer to <>. -[role="exclude" upgrade-migrations] +[role="exclude",id="upgrade-migrations"] == Upgrade migrations This content has moved. Refer to <>. diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index fc921f9118bdf..7136011a4f8f8 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -15,11 +15,11 @@ WARNING: The following instructions assumes {kib} is using the default index nam [[upgrade-migrations-process]] ==== Background -Saved objects are stored in two indices: +Saved objects are stored in two indices: * `.kibana_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. * `.kibana_task_manager_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. - + The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date saved object indices. @@ -29,18 +29,18 @@ The first time a newer {kib} starts, it will first perform an upgrade migration [options="header"] |======================= |Upgrading from version | Outdated index (alias) -| 6.0.0 through 6.4.x | `.kibana` +| 6.0.0 through 6.4.x | `.kibana` `.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) | 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) -| 7.4.0 through 7.11.x -| `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) `.kibana_task_manager_N` (`.kibana_task_manager` alias) |======================= ==== Upgrading multiple {kib} instances -When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. @@ -64,13 +64,15 @@ Error: Unable to complete saved object migrations for the [.kibana] index. Pleas Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] -------------------------------------------- -See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. - +Instructions to work around this issue are in https://github.com/elastic/kibana/issues/95321[this GitHub issue]. + [float] ===== Corrupt saved objects We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. -Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. +Saved objects that were corrupted through manual editing or integrations will cause migration +failures with a log message like `Unable to migrate the corrupt Saved Object document ...`. +Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. For example, given the following error message: @@ -101,7 +103,7 @@ DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab -------------------------------------------- . Restart {kib}. - ++ In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**. Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. @@ -112,15 +114,16 @@ Matching index templates which specify `settings.refresh_interval` or `mappings` Prevention: narrow down the index patterns of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. -Note: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` and index pattern `.kibana`. This index template will not interfere and does not need to be changed or removed. +NOTE: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` +and uses an index pattern of `.kibana`. This index template will not interfere and does not need to be changed or removed. [float] ===== An unhealthy {es} cluster Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensure that your cluster has: - * enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices - * sufficient heap size - * a "green" cluster status + * Enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices + * Sufficient heap size + * A "green" cluster status [float] ===== Different versions of {kib} connected to the same {es} index @@ -134,20 +137,32 @@ For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to +perform the migration again once the process has restarted. Do not delete any saved objects indices to +attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and +later does not require deleting any indices to release a failed migration lock. -If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. +If upgrade migrations fail repeatedly, follow the advice in +<>. +Once the root cause for the migration failure has been addressed, +{kib} will automatically retry the migration without any further intervention. +If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. +If you've followed the advice in <> +and <> and +{kib} is still not able to upgrade successfully, you might choose to rollback {kib} until +you're able to identify and fix the root cause. -WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + +WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with +your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be +rolled back to be compatible with the previous {kib} version. [float] ===== Rollback by restoring a backup snapshot: @@ -164,8 +179,11 @@ In order to rollback after a failed upgrade migration, the saved object indices 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. -3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` -4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. For example, if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. +If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. +Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. +For example. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. 5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. From 912f4b910f978bd23860a3ddc6c41faf32644152 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 7 Feb 2022 17:38:10 -0500 Subject: [PATCH 028/161] [Fleet] do not allow empty name for agent policy (#124853) --- .../agent_policy/create_package_policy_page/index.tsx | 7 ++++++- .../plugins/fleet/server/types/models/agent_policy.ts | 8 +++++++- .../apis/agent_policy/agent_policy.ts | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 743ff40ecf5e6..98e96ce598561 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -206,7 +206,12 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { updatedAgentPolicy: NewAgentPolicy ) => { if (selectedTab === SelectedPolicyTab.NEW) { - if (!updatedAgentPolicy.name || !updatedAgentPolicy.namespace) { + if ( + !updatedAgentPolicy.name || + updatedAgentPolicy.name.trim() === '' || + !updatedAgentPolicy.namespace || + updatedAgentPolicy.namespace.trim() === '' + ) { setHasAgentPolicyError(true); } else { setHasAgentPolicyError(false); diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index e1398aea63634..d15d73fca7332 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -11,9 +11,15 @@ import { agentPolicyStatuses, dataTypes } from '../../../common'; import { PackagePolicySchema, NamespaceSchema } from './package_policy'; +function validateNonEmptyString(val: string) { + if (val.trim() === '') { + return 'Invalid empty string'; + } +} + export const AgentPolicyBaseSchema = { id: schema.maybe(schema.string()), - name: schema.string({ minLength: 1 }), + name: schema.string({ minLength: 1, validate: validateNonEmptyString }), namespace: NamespaceSchema, description: schema.maybe(schema.string()), is_managed: schema.maybe(schema.boolean()), diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index e00ea43a02406..417e0c76a9e6b 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -101,6 +101,17 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); + it('should return a 400 with an empty name', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: ' ', + namespace: 'default', + }) + .expect(400); + }); + it('should return a 400 with an invalid namespace', async () => { await supertest .post(`/api/fleet/agent_policies`) From 1f0f399d6534cf04ff94fc14d207a97589001d3d Mon Sep 17 00:00:00 2001 From: smnschneider <95302847+smnschneider@users.noreply.github.com> Date: Mon, 7 Feb 2022 23:38:58 +0100 Subject: [PATCH 029/161] [Fleet] Change error message for allowAgentUpgradeSourceUri (#124706) --- x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts | 2 +- x-pack/test/fleet_api_integration/apis/agents/upgrade.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index bf386e7f463a7..472f7378028bc 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -133,7 +133,7 @@ export const checkVersionIsSame = (version: string, kibanaVersion: string) => { const checkSourceUriAllowed = (sourceUri?: string) => { if (sourceUri && !appContextService.getConfig()?.developer?.allowAgentUpgradeSourceUri) { throw new Error( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); } }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 8901c3166ca14..57e57a6524b0e 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -174,7 +174,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { @@ -591,7 +591,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); From eb5ab3e23e667df438449f2974095d1d7024ab8f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 7 Feb 2022 16:51:00 -0600 Subject: [PATCH 030/161] [cloud first testing] Purge vault when deployment is removed (#124761) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/scripts/steps/cloud/purge.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js index 0eccb55cef830..b14a3be8f8daf 100644 --- a/.buildkite/scripts/steps/cloud/purge.js +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -50,6 +50,9 @@ for (const deployment of deploymentsToPurge) { console.log(`Scheduling deployment for deletion: ${deployment.name} / ${deployment.id}`); try { execSync(`ecctl deployment shutdown --force '${deployment.id}'`, { stdio: 'inherit' }); + execSync(`vault delete secret/kibana-issues/dev/cloud-deploy/${deployment.name}`, { + stdio: 'inherit', + }); } catch (ex) { console.error(ex.toString()); } From 5538967ba4b555842bc72cc4d601e6ef772a8265 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 7 Feb 2022 18:03:17 -0500 Subject: [PATCH 031/161] [Uptime] add synthetics service sync errors (#124051) * uptime - add synthetics sync errors * update imports * update content * adjust content and flow * update toasts * Update x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx * adjust tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitor_management/locations.ts | 21 ++++++ x-pack/plugins/uptime/e2e/journeys/index.ts | 2 +- .../e2e/journeys/monitor_name.journey.ts | 7 +- .../e2e/page_objects/monitor_management.tsx | 2 +- .../action_bar/action_bar.tsx | 71 ++++++++++++++---- .../action_bar/action_bar_errors.test.tsx | 75 +++++++++++++++++++ .../monitor_management/mocks/index.ts | 8 ++ .../monitor_management/mocks/locations.ts | 35 +++++++++ .../public/state/api/monitor_management.ts | 3 +- .../synthetics_service/service_api_client.ts | 10 ++- 10 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index 6cef41347bbf6..26e3d726a10c0 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -29,6 +29,26 @@ export const ServiceLocationCodec = t.interface({ url: t.string, }); +export const ServiceLocationErrors = t.array( + t.intersection([ + t.interface({ + locationId: t.string, + error: t.interface({ + reason: t.string, + status: t.number, + }), + }), + t.partial({ + failed_monitors: t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), + }), + ]) +); + export const ServiceLocationsCodec = t.array(ServiceLocationCodec); export const isServiceLocationInvalid = (location: ServiceLocation) => @@ -42,3 +62,4 @@ export type ManifestLocation = t.TypeOf; export type ServiceLocation = t.TypeOf; export type ServiceLocations = t.TypeOf; export type ServiceLocationsApiResponse = t.TypeOf; +export type ServiceLocationErrors = t.TypeOf; diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index ce197d574aa15..fe8a4960eac12 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,8 +7,8 @@ export * from './data_view_permissions'; export * from './uptime.journey'; -export * from './monitor_management.journey'; export * from './step_duration.journey'; export * from './alerts'; export * from './read_only_user'; export * from './monitor_name.journey'; +export * from './monitor_management.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts index beb84a9a003a2..456d219adef05 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts @@ -12,7 +12,7 @@ * 2.0. */ -import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { journey, step, expect, after, before, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { byTestId } from './utils'; @@ -23,6 +23,11 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => await uptime.waitForLoadingToFinish(); }); + after(async () => { + await uptime.navigateToMonitorManagement(); + await uptime.deleteMonitor(); + }); + step('Go to monitor-management', async () => { await uptime.navigateToMonitorManagement(); }); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index 057ce21ec5100..fd877708f2bce 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -88,7 +88,7 @@ export function monitorManagementPageProvider({ } else { await page.click('text=Save monitor'); } - return await this.findByTestSubj('uptimeAddMonitorSuccess'); + return await this.findByText('Monitor added successfully.'); }, async fillCodeEditor(value: string) { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 8c9dc7ffe6275..314347331b5b3 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -17,8 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -28,6 +29,10 @@ import { SyntheticsMonitor } from '../../../../common/runtime_types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TestRun } from '../test_now_mode/test_now_mode'; +import { monitorManagementListSelector } from '../../../state/selectors'; + +import { kibanaService } from '../../../state/kibana_service'; + export interface ActionBarProps { monitor: SyntheticsMonitor; isValid: boolean; @@ -39,11 +44,11 @@ export interface ActionBarProps { export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: ActionBarProps) => { const { monitorId } = useParams<{ monitorId: string }>(); const { basePath } = useContext(UptimeSettingsContext); + const { locations } = useSelector(monitorManagementListSelector); const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false); const [isSaving, setIsSaving] = useState(false); - - const { notifications } = useKibana(); + const [isSuccessful, setIsSuccessful] = useState(false); const { data, status } = useFetcher(() => { if (!isSaving || !isValid) { @@ -55,6 +60,9 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti }); }, [monitor, monitorId, isValid, isSaving]); + const hasErrors = data && Object.keys(data).length; + const loading = status === FETCH_STATUS.LOADING; + const handleOnSave = useCallback(() => { if (onSave) { onSave(); @@ -75,23 +83,57 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti setIsSaving(false); } if (status === FETCH_STATUS.FAILURE) { - notifications.toasts.danger({ - title:

{MONITOR_FAILURE_LABEL}

, + kibanaService.toasts.addDanger({ + title: MONITOR_FAILURE_LABEL, toastLifeTimeMs: 3000, }); - } else if (status === FETCH_STATUS.SUCCESS) { - notifications.toasts.success({ - title: ( -

- {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL} -

- ), + } else if (status === FETCH_STATUS.SUCCESS && !hasErrors && !loading) { + kibanaService.toasts.addSuccess({ + title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, toastLifeTimeMs: 3000, }); + setIsSuccessful(true); + } else if (hasErrors && !loading) { + Object.values(data).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+

+ {status + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ + ), + toastLifeTimeMs: 30000, + }); + }); + setIsSuccessful(true); } - }, [data, status, notifications.toasts, isSaving, isValid, monitorId]); + }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); - return status === FETCH_STATUS.SUCCESS ? ( + return isSuccessful ? ( ) : ( @@ -191,7 +233,6 @@ const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( } ); -// TODO: Discuss error states with product const MONITOR_FAILURE_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.monitorFailureMessage', { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx new file mode 100644 index 0000000000000..f217631bfe33d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { FETCH_STATUS } from '../../../../../observability/public'; +import { + DataStream, + HTTPFields, + ScheduleUnit, + SyntheticsMonitor, +} from '../../../../common/runtime_types'; +import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher'; +import * as kibana from '../../../state/kibana_service'; +import { ActionBar } from './action_bar'; +import { mockLocationsState } from '../mocks'; + +jest.mock('../../../state/kibana_service', () => ({ + ...jest.requireActual('../../../state/kibana_service'), + kibanaService: { + toasts: { + addWarning: jest.fn(), + }, + }, +})); + +const monitor: SyntheticsMonitor = { + name: 'test-monitor', + schedule: { + unit: ScheduleUnit.MINUTES, + number: '2', + }, + urls: 'https://elastic.co', + type: DataStream.HTTP, +} as unknown as HTTPFields; + +describe(' Service Errors', () => { + let useFetcher: jest.SpyInstance; + const toast = jest.fn(); + + beforeEach(() => { + useFetcher?.mockClear(); + useFetcher = spyOnUseFetcher({}); + }); + + it('Handles service errors', async () => { + jest.spyOn(kibana.kibanaService.toasts, 'addWarning').mockImplementation(toast); + useFetcher.mockReturnValue({ + data: [ + { locationId: 'us_central', error: { reason: 'Invalid config', status: 400 } }, + { locationId: 'us_central', error: { reason: 'Cannot schedule', status: 500 } }, + ], + status: FETCH_STATUS.SUCCESS, + refetch: () => {}, + }); + render(, { state: mockLocationsState }); + userEvent.click(screen.getByText('Save monitor')); + + await waitFor(() => { + expect(toast).toBeCalledTimes(2); + expect(toast).toBeCalledWith( + expect.objectContaining({ + title: 'Unable to sync monitor config', + toastLifeTimeMs: 30000, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts new file mode 100644 index 0000000000000..1ec4437601d57 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './locations'; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts new file mode 100644 index 0000000000000..b4f23bed097cb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const mockLocation = { + label: 'US Central', + id: 'us_central', + geo: { + lat: 1, + lon: 1, + }, + url: 'url', +}; + +export const mockLocationsState = { + monitorManagementList: { + locations: [mockLocation], + list: { + monitors: [], + perPage: 10, + page: 1, + total: 0, + }, + error: { + serviceLocations: null, + monitorList: null, + }, + loading: { + serviceLocations: false, + monitorList: false, + }, + }, +}; diff --git a/x-pack/plugins/uptime/public/state/api/monitor_management.ts b/x-pack/plugins/uptime/public/state/api/monitor_management.ts index ec2806907baa1..206ba07dc4c23 100644 --- a/x-pack/plugins/uptime/public/state/api/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/api/monitor_management.ts @@ -13,6 +13,7 @@ import { ServiceLocations, SyntheticsMonitor, ServiceLocationsApiResponseCodec, + ServiceLocationErrors, } from '../../../common/runtime_types'; import { SyntheticsMonitorSavedObject } from '../../../common/types'; import { apiService } from './utils'; @@ -23,7 +24,7 @@ export const setMonitor = async ({ }: { monitor: SyntheticsMonitor; id?: string; -}): Promise => { +}): Promise => { if (id) { return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); } else { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 596a64b4d359a..1e82ef77e083b 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -11,7 +11,11 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '../../../../../../src/core/server'; -import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; +import { + MonitorFields, + ServiceLocations, + ServiceLocationErrors, +} from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; import { ServiceConfig } from '../../../common/config'; @@ -109,7 +113,7 @@ export class ServiceAPIClient { }); }; - const pushErrors: Array<{ locationId: string; error: Error }> = []; + const pushErrors: ServiceLocationErrors = []; const promises: Array> = []; @@ -128,7 +132,7 @@ export class ServiceAPIClient { ); }), catchError((err) => { - pushErrors.push({ locationId: id, error: err }); + pushErrors.push({ locationId: id, error: err.response?.data }); this.logger.error(err); // we don't want to throw an unhandled exception here return of(true); From be1be06522050d5ea4727441155470809ca0f008 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 7 Feb 2022 17:11:17 -0600 Subject: [PATCH 032/161] [build] Improve error message for dependencies in cloud docker image build (#124890) * [build] Improve error message for dependencies in cloud docker image build * cleanup --- .../build/tasks/download_cloud_dependencies.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts index 1207594304e64..6ecc09c21ddce 100644 --- a/src/dev/build/tasks/download_cloud_dependencies.ts +++ b/src/dev/build/tasks/download_cloud_dependencies.ts @@ -36,12 +36,19 @@ export const DownloadCloudDependencies: Task = { let buildId = ''; if (!config.isRelease) { - const manifest = await Axios.get( - `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest` - ); - buildId = manifest.data.build.build_id; + const manifestUrl = `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest`; + try { + const manifest = await Axios.get(manifestUrl); + buildId = manifest.data.build.build_id; + } catch (e) { + log.error( + `Unable to find Elastic artifacts for ${config.getBuildVersion()} at ${manifestUrl}.` + ); + throw e; + } } await del([config.resolveFromRepo('.beats')]); + await downloadBeat('metricbeat', buildId); await downloadBeat('filebeat', buildId); }, From 729b49d113bcbe22c84498e0d093341864ee86e8 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:04:07 -0800 Subject: [PATCH 033/161] [DOCS] Removes Upgrade Assistant doc (#124894) * [DOCS] Removes Upgrade Assistant doc from 8.x * [DOCS] Removes Upgrade Assistant from Stack Management page * Update docs/redirects.asciidoc Co-authored-by: James Rodewig Co-authored-by: James Rodewig --- .../upgrade-assistant/index.asciidoc | 26 ------------------- docs/redirects.asciidoc | 5 ++++ docs/user/management.asciidoc | 6 ----- 3 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 docs/management/upgrade-assistant/index.asciidoc diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc deleted file mode 100644 index ccd3f41b9d886..0000000000000 --- a/docs/management/upgrade-assistant/index.asciidoc +++ /dev/null @@ -1,26 +0,0 @@ -[role="xpack"] -[[upgrade-assistant]] -== Upgrade Assistant - -The Upgrade Assistant helps you prepare for your upgrade -to the next major version of the Elastic Stack. -To access the assistant, open the main menu and go to *Stack Management > Upgrade Assistant*. - -The assistant identifies deprecated settings in your configuration, -enables you to see if you are using deprecated features, -and guides you through the process of resolving issues. - -If you have indices that were created prior to 7.0, -you can use the assistant to reindex them so they can be accessed from 8.0+. - -IMPORTANT: To see the most up-to-date deprecation information before -upgrading to 8.0, upgrade to the latest {prev-major-last} release. - -For more information about upgrading, -refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] - -[discrete] -=== Required permissions - -The `manage` cluster privilege is required to access the *Upgrade assistant*. -Additional privileges may be needed to perform certain actions. \ No newline at end of file diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 163fed04578ef..0ca518c3a8788 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -395,3 +395,8 @@ This content has moved. Refer to <>. == Upgrade migrations This content has moved. Refer to <>. + +[role="exclude",id="upgrade-assistant"] +== Upgrade Assistant + +This content has moved. Refer to {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant]. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index e682f7372f817..6c309d56f2294 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -167,10 +167,6 @@ set the timespan for notification messages, and much more. the full list of features that are included in your license, see the https://www.elastic.co/subscriptions[subscription page]. -| <> -| Identify the issues that you need to address before upgrading to the -next major version of {es}, and then reindex, if needed. - |=== @@ -197,6 +193,4 @@ include::{kib-repo-dir}/spaces/index.asciidoc[] include::{kib-repo-dir}/management/managing-tags.asciidoc[] -include::{kib-repo-dir}/management/upgrade-assistant/index.asciidoc[] - include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] From 4394293e217b5c814e64c56c64048debd13f4b6d Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 7 Feb 2022 19:45:56 -0500 Subject: [PATCH 034/161] [Dashboard] Remove URL Generator (#121832) * Remove deprecated and unused dashboard URL generator code Co-authored-by: Steph Milovic --- .../lib/build_dashboard_container.ts | 2 +- .../get_dashboard_list_item_link.test.ts | 4 +- .../listing/get_dashboard_list_item_link.ts | 7 +- .../dashboard/public/dashboard_constants.ts | 1 + src/plugins/dashboard/public/index.ts | 10 +- src/plugins/dashboard/public/plugin.tsx | 45 +-- .../dashboard/public/services/share.ts | 6 +- .../dashboard/public/url_generator.test.ts | 356 ------------------ src/plugins/dashboard/public/url_generator.ts | 170 --------- .../use_risky_hosts_dashboard_button_href.ts | 12 +- .../use_risky_hosts_dashboard_links.tsx | 54 +-- 11 files changed, 50 insertions(+), 617 deletions(-) delete mode 100644 src/plugins/dashboard/public/url_generator.test.ts delete mode 100644 src/plugins/dashboard/public/url_generator.ts diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index 1dd39cc3e5ba9..5752a6445d2a9 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -31,7 +31,7 @@ import { } from '../../services/embeddable'; type BuildDashboardContainerProps = DashboardBuildContext & { - data: DashboardAppServices['data']; // the whole data service is required here because it is required by getUrlGeneratorState + data: DashboardAppServices['data']; // the whole data service is required here because it is required by getLocatorParams savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; incomingEmbeddable?: EmbeddablePackageState; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index a6f80c157bee8..ce9535e549446 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -9,9 +9,9 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; import { FilterStateStore } from '@kbn/es-query'; +import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../dashboard_constants'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts index 2f19924d45982..8af3f2a10666f 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts @@ -9,8 +9,11 @@ import { ApplicationStart } from 'kibana/public'; import { QueryState } from '../../../../data/public'; import { setStateToKbnUrl } from '../../../../kibana_utils/public'; -import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { + DashboardConstants, + createDashboardEditUrl, + GLOBAL_STATE_STORAGE_KEY, +} from '../../dashboard_constants'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; export const getDashboardListItemLink = ( diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 9063b279c25f2..88fbc3b30392f 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -9,6 +9,7 @@ import type { ControlStyle } from '../../controls/public'; export const DASHBOARD_STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DashboardConstants = { LANDING_PAGE_PATH: '/list', diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index f25a92275d723..bff2d4d79108c 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -16,15 +16,7 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export type { - DashboardSetup, - DashboardStart, - DashboardUrlGenerator, - DashboardFeatureFlagConfig, -} from './plugin'; - -export type { DashboardUrlGeneratorState } from './url_generator'; -export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator'; +export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export type { DashboardAppLocator, DashboardAppLocatorParams } from './locator'; export type { DashboardSavedObject } from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 6554520fca101..2f63062ccf60c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,7 @@ import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; -import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; +import { SharePluginSetup, SharePluginStart } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { ScreenshotModePluginSetup, @@ -70,29 +70,15 @@ import { CopyToDashboardAction, DashboardCapabilities, } from './application'; -import { - createDashboardUrlGenerator, - DASHBOARD_APP_URL_GENERATOR, - DashboardUrlGeneratorState, -} from './url_generator'; import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; import { SpacesPluginStart } from './services/spaces'; -declare module '../../share/public' { - export interface UrlGeneratorStateMapping { - [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export type DashboardUrlGenerator = UrlGeneratorContract; - export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } @@ -134,15 +120,6 @@ export interface DashboardStart { getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; - /** - * @deprecated Use dashboard locator instead. Dashboard locator is available - * under `.locator` key. This dashboard URL generator will be removed soon. - * - * ```ts - * plugins.dashboard.locator.getLocation({ ... }); - * ``` - */ - dashboardUrlGenerator?: DashboardUrlGenerator; locator?: DashboardAppLocator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; } @@ -157,11 +134,6 @@ export class DashboardPlugin private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; - - /** - * @deprecated Use locator instead. - */ - private dashboardUrlGenerator?: DashboardUrlGenerator; private locator?: DashboardAppLocator; public setup( @@ -178,20 +150,6 @@ export class DashboardPlugin ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); - const startServices = core.getStartServices(); - - if (share) { - this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( - createDashboardUrlGenerator(async () => { - const [coreStart, , selfStart] = await startServices; - return { - appBasePath: coreStart.application.getUrlForApp('dashboards'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - savedDashboardLoader: selfStart.getSavedDashboardLoader(), - }; - }) - ); - } const getPlaceholderEmbeddableStartServices = async () => { const [coreStart] = await core.getStartServices(); @@ -458,7 +416,6 @@ export class DashboardPlugin factory: dashboardContainerFactory as DashboardContainerFactory, }); }, - dashboardUrlGenerator: this.dashboardUrlGenerator, locator: this.locator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, }; diff --git a/src/plugins/dashboard/public/services/share.ts b/src/plugins/dashboard/public/services/share.ts index 7ed9b86571596..77a9f44a3cf00 100644 --- a/src/plugins/dashboard/public/services/share.ts +++ b/src/plugins/dashboard/public/services/share.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -export type { - SharePluginStart, - SharePluginSetup, - UrlGeneratorContract, -} from '../../../share/public'; +export type { SharePluginStart, SharePluginSetup } from '../../../share/public'; export { downloadMultipleAs } from '../../../share/public'; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts deleted file mode 100644 index f1035d7cc1389..0000000000000 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ /dev/null @@ -1,356 +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 { createDashboardUrlGenerator } from './url_generator'; -import { hashedItemStore } from '../../kibana_utils/public'; -import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { type Filter, FilterStateStore } from '@kbn/es-query'; - -const APP_BASE_PATH: string = 'xyz/app/dashboards'; - -const createMockDashboardLoader = ( - dashboardToFilters: { - [dashboardId: string]: () => Filter[]; - } = {} -) => { - return { - get: async (dashboardId: string) => { - return { - searchSource: { - getField: (field: string) => { - if (field === 'filter') - return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; - throw new Error( - `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` - ); - }, - }, - }; - }, - } as SavedObjectLoader; -}; - -describe('dashboard url generator', () => { - beforeEach(() => { - // @ts-ignore - hashedItemStore.storage = mockStorage; - }); - - test('creates a link to a saved dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({}); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/create?_a=()&_g=()"`); - }); - - test('creates a link with global time range set up', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - }, - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, - }, - ], - query: { query: 'bye', language: 'kuery' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('searchSessionId', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [], - query: { query: 'bye', language: 'kuery' }, - searchSessionId: '__sessionSearchId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` - ); - }); - - test('savedQuery', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - savedQuery: '__savedQueryId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` - ); - expect(url).toContain('__savedQueryId__'); - }); - - test('panels', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - panels: [{ fakePanelContent: 'fakePanelContent' } as any], - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` - ); - }); - - test('if no useHash setting is given, uses the one was start services', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a false useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: true, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a true useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: false, - }); - expect(url.indexOf('relative')).toBeGreaterThan(1); - }); - - describe('preserving saved filters', () => { - const savedFilter1 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter1' }, - }; - - const savedFilter2 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter2' }, - }; - - const appliedFilter = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'appliedfilter' }, - }; - - test('attaches filters from destination dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - ['dashboard2']: () => [savedFilter2], - }), - }) - ); - - const urlToDashboard1 = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); - expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); - - const urlToDashboard2 = await generator.createUrl!({ - dashboardId: 'dashboard2', - filters: [appliedFilter], - }); - - expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); - expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test("doesn't fail if can't retrieve filters from destination dashboard", async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => { - throw new Error('Not found'); - }, - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test('can enforce empty filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [], - preserveSavedFilters: false, - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` - ); - }); - - test('no filters in result url if no filters applied', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - }); - expect(url).not.toEqual(expect.stringContaining('filters')); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/view/dashboard1?_a=()&_g=()"`); - }); - - test('can turn off preserving filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - preserveSavedFilters: false, - }); - - expect(urlWithPreservedFiltersTurnedOff).not.toEqual( - expect.stringContaining('query:savedfilter1') - ); - expect(urlWithPreservedFiltersTurnedOff).toEqual( - expect.stringContaining('query:appliedfilter') - ); - }); - }); -}); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts deleted file mode 100644 index 5c0cd32ee5a16..0000000000000 --- a/src/plugins/dashboard/public/url_generator.ts +++ /dev/null @@ -1,170 +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 { - TimeRange, - Filter, - Query, - esFilters, - QueryState, - RefreshInterval, -} from '../../data/public'; -import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../share/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { ViewMode } from '../../embeddable/public'; -import { DashboardConstants } from './dashboard_constants'; -import { SavedDashboardPanel } from '../common/types'; - -export const STATE_STORAGE_KEY = '_a'; -export const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; - -/** - * @deprecated Use dashboard locator instead. - */ -export interface DashboardUrlGeneratorState { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - /** - * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has a query saved with it, this will _replace_ that query. - */ - query?: Query; - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * View mode of the dashboard. - */ - viewMode?: ViewMode; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: SavedDashboardPanel[]; - - /** - * Saved query ID - */ - savedQuery?: string; -} - -/** - * @deprecated Use dashboard locator instead. - */ -export const createDashboardUrlGenerator = ( - getStartServices: () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; - savedDashboardLoader: SavedObjectLoader; - }> -): UrlGeneratorsDefinition => ({ - id: DASHBOARD_APP_URL_GENERATOR, - createUrl: async (state) => { - const startServices = await getStartServices(); - const useHash = state.useHash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - const hash = state.dashboardId ? `view/${state.dashboardId}` : `create`; - - const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { - if (state.preserveSavedFilters === false) return []; - if (!state.dashboardId) return []; - try { - const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); - return dashboard?.searchSource?.getField('filter') ?? []; - } catch (e) { - // in case dashboard is missing, built the url without those filters - // dashboard app will handle redirect to landing page with toast message - return []; - } - }; - - const cleanEmptyKeys = (stateObj: Record) => { - Object.keys(stateObj).forEach((key) => { - if (stateObj[key] === undefined) { - delete stateObj[key]; - } - }); - return stateObj; - }; - - // leave filters `undefined` if no filters was applied - // in this case dashboard will restore saved filters on its own - const filters = state.filters && [ - ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), - ...state.filters, - ]; - - let url = setStateToKbnUrl( - STATE_STORAGE_KEY, - cleanEmptyKeys({ - query: state.query, - filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), - viewMode: state.viewMode, - panels: state.panels, - savedQuery: state.savedQuery, - }), - { useHash }, - `${appBasePath}#/${hash}` - ); - - url = setStateToKbnUrl( - GLOBAL_STATE_STORAGE_KEY, - cleanEmptyKeys({ - time: state.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), - refreshInterval: state.refreshInterval, - }), - { useHash }, - url - ); - - if (state.searchSessionId) { - url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; - } - - return url; - }, -}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts index 555ae7544180b..5bc2087dc63ab 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts @@ -16,13 +16,15 @@ export const DASHBOARD_REQUEST_BODY = { }; export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { - const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; - const savedObjectsClient = useKibana().services.savedObjects.client; + const { + dashboard, + savedObjects: { client: savedObjectsClient }, + } = useKibana().services; const [buttonHref, setButtonHref] = useState(); useEffect(() => { - if (createDashboardUrl && savedObjectsClient) { + if (dashboard?.locator && savedObjectsClient) { savedObjectsClient.find(DASHBOARD_REQUEST_BODY).then( async (DashboardsSO?: { savedObjects?: Array<{ @@ -31,7 +33,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrl = await createDashboardUrl({ + const dashboardUrl = await dashboard?.locator?.getUrl({ dashboardId: DashboardsSO.savedObjects[0].id, timeRange: { to, @@ -43,7 +45,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { } ); } - }, [createDashboardUrl, from, savedObjectsClient, to]); + }, [dashboard, from, savedObjectsClient, to]); return { buttonHref, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx index 002dc18227f6d..5b8bf180da1f8 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx @@ -14,40 +14,48 @@ export const useRiskyHostsDashboardLinks = ( from: string, listItems: LinkPanelListItem[] ) => { - const createDashboardUrl = useKibana().services.dashboard?.locator?.getLocation; + const { dashboard } = useKibana().services; + const dashboardId = useRiskyHostsDashboardId(); const [listItemsWithLinks, setListItemsWithLinks] = useState([]); useEffect(() => { let cancelled = false; const createLinks = async () => { - if (createDashboardUrl && dashboardId) { + if (dashboard?.locator && dashboardId) { const dashboardUrls = await Promise.all( - listItems.map((listItem) => - createDashboardUrl({ - dashboardId, - timeRange: { - to, - from, - }, - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': listItem.title } }, - }, - ], - }) + listItems.reduce( + (acc: Array>, listItem) => + dashboard && dashboard.locator + ? [ + ...acc, + dashboard.locator.getUrl({ + dashboardId, + timeRange: { + to, + from, + }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { match_phrase: { 'host.name': listItem.title } }, + }, + ], + }), + ] + : acc, + [] ) ); - if (!cancelled) { + if (!cancelled && dashboardUrls.length) { setListItemsWithLinks( listItems.map((item, i) => ({ ...item, - path: dashboardUrls[i] as unknown as string, + path: dashboardUrls[i], })) ); } @@ -59,7 +67,7 @@ export const useRiskyHostsDashboardLinks = ( return () => { cancelled = true; }; - }, [createDashboardUrl, dashboardId, from, listItems, to]); + }, [dashboard, dashboardId, from, listItems, to]); return { listItemsWithLinks }; }; From 97230e94310ceaa0f364f43ed577efadd34c9766 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Mon, 7 Feb 2022 19:51:49 -0500 Subject: [PATCH 035/161] [App Search] New modal to crawl select domains (#124195) --- .../crawl_select_domains_modal.scss | 4 + .../crawl_select_domains_modal.test.tsx | 98 +++++++++++++++ .../crawl_select_domains_modal.tsx | 109 ++++++++++++++++ .../crawl_select_domains_modal_logic.test.ts | 100 +++++++++++++++ .../crawl_select_domains_modal_logic.ts | 67 ++++++++++ .../simplified_selectable.test.tsx | 118 ++++++++++++++++++ .../simplified_selectable.tsx | 90 +++++++++++++ .../crawler_status_indicator.test.tsx | 9 +- .../crawler_status_indicator.tsx | 20 +-- .../start_crawl_context_menu.test.tsx | 76 +++++++++++ .../start_crawl_context_menu.tsx | 79 ++++++++++++ .../components/crawler/crawler_logic.test.ts | 40 +++++- .../components/crawler/crawler_logic.ts | 14 ++- .../crawler/crawler_overview.test.tsx | 7 ++ .../components/crawler/crawler_overview.tsx | 2 + .../crawler/crawler_single_domain.test.tsx | 7 ++ .../crawler/crawler_single_domain.tsx | 2 + .../server/routes/app_search/crawler.test.ts | 13 ++ .../server/routes/app_search/crawler.ts | 7 ++ 19 files changed, 840 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss new file mode 100644 index 0000000000000..09abf97829be4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss @@ -0,0 +1,4 @@ +.crawlSelectDomainsModal { + width: 50rem; + max-width: 90%; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx new file mode 100644 index 0000000000000..79898d9f15e9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiModal, EuiModalFooter, EuiButton, EuiButtonEmpty } from '@elastic/eui'; + +import { rerender } from '../../../../../test_helpers'; + +import { CrawlSelectDomainsModal } from './crawl_select_domains_modal'; +import { SimplifiedSelectable } from './simplified_selectable'; + +const MOCK_VALUES = { + // CrawlerLogic + domains: [{ url: 'https://www.elastic.co' }, { url: 'https://www.swiftype.com' }], + // CrawlSelectDomainsModalLogic + selectedDomainUrls: ['https://www.elastic.co'], + isModalVisible: true, +}; + +const MOCK_ACTIONS = { + // CrawlSelectDomainsModalLogic + hideModal: jest.fn(), + onSelectDomainUrls: jest.fn(), + // CrawlerLogic + startCrawl: jest.fn(), +}; + +describe('CrawlSelectDomainsModal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + + wrapper = shallow(); + }); + + it('is empty when the modal is hidden', () => { + setMockValues({ + ...MOCK_VALUES, + isModalVisible: false, + }); + + rerender(wrapper); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders as a modal when visible', () => { + expect(wrapper.is(EuiModal)).toBe(true); + }); + + it('can be closed', () => { + expect(wrapper.prop('onClose')).toEqual(MOCK_ACTIONS.hideModal); + expect(wrapper.find(EuiModalFooter).find(EuiButtonEmpty).prop('onClick')).toEqual( + MOCK_ACTIONS.hideModal + ); + }); + + it('allows the user to select domains', () => { + expect(wrapper.find(SimplifiedSelectable).props()).toEqual({ + options: ['https://www.elastic.co', 'https://www.swiftype.com'], + selectedOptions: ['https://www.elastic.co'], + onChange: MOCK_ACTIONS.onSelectDomainUrls, + }); + }); + + describe('submit button', () => { + it('is disabled when no domains are selected', () => { + setMockValues({ + ...MOCK_VALUES, + selectedDomainUrls: [], + }); + + rerender(wrapper); + + expect(wrapper.find(EuiModalFooter).find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('starts a crawl and hides the modal', () => { + wrapper.find(EuiModalFooter).find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: MOCK_VALUES.selectedDomainUrls, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx new file mode 100644 index 0000000000000..211266a779df9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; +import { SimplifiedSelectable } from './simplified_selectable'; + +import './crawl_select_domains_modal.scss'; + +export const CrawlSelectDomainsModal: React.FC = () => { + const { domains } = useValues(CrawlerLogic); + const domainUrls = domains.map((domain) => domain.url); + + const crawlSelectDomainsModalLogic = CrawlSelectDomainsModalLogic({ domains }); + const { isDataLoading, isModalVisible, selectedDomainUrls } = useValues( + crawlSelectDomainsModalLogic + ); + const { hideModal, onSelectDomainUrls } = useActions(crawlSelectDomainsModalLogic); + + const { startCrawl } = useActions(CrawlerLogic); + + if (!isModalVisible) { + return null; + } + + return ( + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.modalHeaderTitle', + { + defaultMessage: 'Crawl select domains', + } + )} + + + 0 ? 'accent' : 'subdued'} + > + {selectedDomainUrls.length} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.selectedDescriptor', + { + defaultMessage: 'selected', + } + )} + + + + + + + + {CANCEL_BUTTON_LABEL} + { + startCrawl({ domain_allowlist: selectedDomainUrls }); + }} + disabled={selectedDomainUrls.length === 0} + isLoading={isDataLoading} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.startCrawlButtonLabel', + { + defaultMessage: 'Apply and crawl now', + } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts new file mode 100644 index 0000000000000..ef6ef4d09fadb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; + +describe('CrawlSelectDomainsModalLogic', () => { + const { mount } = new LogicMounter(CrawlSelectDomainsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(CrawlSelectDomainsModalLogic.values).toEqual({ + isDataLoading: false, + isModalVisible: false, + selectedDomainUrls: [], + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + CrawlSelectDomainsModalLogic.actions.hideModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + + describe('showModal', () => { + it('shows the modal', () => { + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(true); + }); + + it('resets the selected options', () => { + mount({ + selectedDomainUrls: ['https://www.elastic.co', 'https://www.swiftype.com'], + }); + + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([]); + }); + }); + + describe('onSelectDomainUrls', () => { + it('saves the urls', () => { + mount({ + selectedDomainUrls: [], + }); + + CrawlSelectDomainsModalLogic.actions.onSelectDomainUrls([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + }); + }); + + describe('[CrawlerLogic.actionTypes.startCrawl]', () => { + it('enables loading state', () => { + mount({ + isDataLoading: false, + }); + + CrawlerLogic.actions.startCrawl(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(true); + }); + }); + + describe('[CrawlerLogic.actionTypes.onStartCrawlRequestComplete]', () => { + it('disables loading state and hides the modal', () => { + mount({ + isDataLoading: true, + isModalVisible: true, + }); + + CrawlerLogic.actions.onStartCrawlRequestComplete(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(false); + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts new file mode 100644 index 0000000000000..088950cbffd3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts @@ -0,0 +1,67 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlerDomain } from '../../types'; + +export interface CrawlSelectDomainsLogicProps { + domains: CrawlerDomain[]; +} + +export interface CrawlSelectDomainsLogicValues { + isDataLoading: boolean; + isModalVisible: boolean; + selectedDomainUrls: string[]; +} + +export interface CrawlSelectDomainsModalLogicActions { + hideModal(): void; + onSelectDomainUrls(domainUrls: string[]): { domainUrls: string[] }; + showModal(): void; +} + +export const CrawlSelectDomainsModalLogic = kea< + MakeLogicType< + CrawlSelectDomainsLogicValues, + CrawlSelectDomainsModalLogicActions, + CrawlSelectDomainsLogicProps + > +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawl_select_domains_modal'], + actions: () => ({ + hideModal: true, + onSelectDomainUrls: (domainUrls) => ({ domainUrls }), + showModal: true, + }), + reducers: () => ({ + isDataLoading: [ + false, + { + [CrawlerLogic.actionTypes.startCrawl]: () => true, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + selectedDomainUrls: [ + [], + { + showModal: () => [], + onSelectDomainUrls: (_, { domainUrls }) => domainUrls, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx new file mode 100644 index 0000000000000..a90259f8dac3c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSelectable, EuiSelectableList, EuiSelectableSearch } from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { SimplifiedSelectable } from './simplified_selectable'; + +describe('SimplifiedSelectable', () => { + let wrapper: ShallowWrapper; + + const MOCK_ON_CHANGE = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = shallow( + + ); + }); + + it('combines the options and selected options', () => { + expect(wrapper.find(EuiSelectable).prop('options')).toEqual([ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + }); + + it('passes newly selected options to the callback', () => { + wrapper.find(EuiSelectable).simulate('change', [ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + + expect(MOCK_ON_CHANGE).toHaveBeenCalledWith(['cat', 'fish']); + }); + + describe('select all button', () => { + it('it is disabled when all options are already selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="SelectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="SelectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith(['cat', 'dog', 'fish']); + }); + }); + + describe('deselect all button', () => { + it('it is disabled when all no options are selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="DeselectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="DeselectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith([]); + }); + }); + + it('renders a search bar and selectable list', () => { + const fullRender = mountWithIntl( + + ); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx new file mode 100644 index 0000000000000..07ede1c59971a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSelectable } from '@elastic/eui'; +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + options: string[]; + selectedOptions: string[]; + onChange(selectedOptions: string[]): void; +} + +export interface OptionMap { + [key: string]: boolean; +} + +export const SimplifiedSelectable: React.FC = ({ options, selectedOptions, onChange }) => { + const selectedOptionsMap: OptionMap = selectedOptions.reduce( + (acc, selectedOption) => ({ + ...acc, + [selectedOption]: true, + }), + {} + ); + + const selectableOptions: Array> = options.map((option) => ({ + label: option, + checked: selectedOptionsMap[option] ? 'on' : undefined, + })); + + return ( + <> + + + onChange(options)} + disabled={selectedOptions.length === options.length} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.selectAllButtonLabel', + { + defaultMessage: 'Select all', + } + )} + + + + onChange([])} + disabled={selectedOptions.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.deselectAllButtonLabel', + { + defaultMessage: 'Deselect all', + } + )} + + + + { + onChange( + newSelectableOptions.filter((option) => option.checked).map((option) => option.label) + ); + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx index c46c360934d0b..cc8b1891838b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx @@ -16,6 +16,7 @@ import { EuiButton } from '@elastic/eui'; import { CrawlerDomain, CrawlerStatus } from '../../types'; import { CrawlerStatusIndicator } from './crawler_status_indicator'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; const MOCK_VALUES = { @@ -72,9 +73,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Start a crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); @@ -87,9 +86,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Retry crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx index c02e45f02c407..d750cf100202f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerStatus } from '../../types'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; export const CrawlerStatusIndicator: React.FC = () => { const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); - const { startCrawl, stopCrawl } = useActions(CrawlerLogic); + const { stopCrawl } = useActions(CrawlerLogic); const disabledButton = ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', { @@ -40,26 +41,27 @@ export const CrawlerStatusIndicator: React.FC = () => { switch (mostRecentCrawlRequestStatus) { case CrawlerStatus.Success: return ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', + + /> ); case CrawlerStatus.Failed: case CrawlerStatus.Canceled: return ( - - {i18n.translate( + + /> ); case CrawlerStatus.Pending: case CrawlerStatus.Suspended: diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx new file mode 100644 index 0000000000000..6d9f1cd7be64b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { ReactWrapper, shallow } from 'enzyme'; + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiResizeObserver, +} from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { StartCrawlContextMenu } from './start_crawl_context_menu'; + +const MOCK_ACTIONS = { + startCrawl: jest.fn(), + showModal: jest.fn(), +}; + +describe('StartCrawlContextMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + }); + + it('is initially closed', () => { + const wrapper = shallow(); + + expect(wrapper.is(EuiPopover)).toBe(true); + expect(wrapper.prop('isOpen')).toEqual(false); + }); + + describe('user actions', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + wrapper = mountWithIntl(); + + wrapper.find(EuiButton).simulate('click'); + + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); + + it('can be opened', () => { + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + expect(menuItems.length).toEqual(2); + }); + + it('can start crawls', () => { + menuItems.at(0).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalled(); + }); + + it('can open a modal to start a crawl with selected domains', () => { + menuItems.at(1).simulate('click'); + + expect(MOCK_ACTIONS.showModal).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx new file mode 100644 index 0000000000000..1182a845bd4f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from '../crawl_select_domains_modal/crawl_select_domains_modal_logic'; + +interface Props { + menuButtonLabel?: string; + fill?: boolean; +} + +export const StartCrawlContextMenu: React.FC = ({ menuButtonLabel, fill }) => { + const { startCrawl } = useActions(CrawlerLogic); + const { showModal: showCrawlSelectDomainsModal } = useActions(CrawlSelectDomainsModalLogic); + + const [isPopoverOpen, setPopover] = useState(false); + + const togglePopover = () => setPopover(!isPopoverOpen); + + const closePopover = () => setPopover(false); + + return ( + + {menuButtonLabel} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + closePopover(); + startCrawl(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlAllDomainsMenuLabel', + { + defaultMessage: 'Crawl all domains on this engine', + } + )} + , + { + closePopover(); + showCrawlSelectDomainsModal(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlSelectDomainsMenuLabel', + { + defaultMessage: 'Crawl select domains', + } + )} + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index e622798e688ab..59ec64c69d5a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -226,7 +226,7 @@ describe('CrawlerLogic', () => { CrawlerStatus.Running, CrawlerStatus.Canceling, ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { + it(`creates a new timeout for most recent crawl request status ${status}`, async () => { jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); http.get.mockReturnValueOnce( Promise.resolve({ @@ -260,6 +260,27 @@ describe('CrawlerLogic', () => { expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); + + it('clears the timeout if no events are active', async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + events: [ + { + status: CrawlerStatus.Failed, + crawl_config: {}, + }, + ], + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + }); }); it('calls flashApiErrors when there is an error on the request for crawler data', async () => { @@ -276,23 +297,36 @@ describe('CrawlerLogic', () => { describe('startCrawl', () => { describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawler data', async () => { + it('creates a new crawl request, fetches latest crawler data, then marks the request complete', async () => { jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.startCrawl(); await nextTick(); expect(http.post).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/crawl_requests' + '/internal/app_search/engines/some-engine/crawler/crawl_requests', + { body: JSON.stringify({ overrides: {} }) } ); expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); }); }); itShowsServerErrorAsFlashMessage(http.post, () => { CrawlerLogic.actions.startCrawl(); }); + + it('marks the request complete even after an error', async () => { + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); + http.post.mockReturnValueOnce(Promise.reject()); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); + }); }); describe('stopCrawl', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 08a01af67ece6..d68dbc59f06d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -48,7 +48,8 @@ interface CrawlerActions { fetchCrawlerData(): void; onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - startCrawl(): void; + onStartCrawlRequestComplete(): void; + startCrawl(overrides?: object): { overrides?: object }; stopCrawl(): void; } @@ -60,7 +61,8 @@ export const CrawlerLogic = kea>({ fetchCrawlerData: true, onCreateNewTimeout: (timeoutId) => ({ timeoutId }), onReceiveCrawlerData: (data) => ({ data }), - startCrawl: () => null, + onStartCrawlRequestComplete: true, + startCrawl: (overrides) => ({ overrides }), stopCrawl: () => null, }, reducers: { @@ -135,15 +137,19 @@ export const CrawlerLogic = kea>({ actions.createNewTimeoutForCrawlerData(POLLING_DURATION_ON_FAILURE); } }, - startCrawl: async () => { + startCrawl: async ({ overrides = {} }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; try { - await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`); + await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`, { + body: JSON.stringify({ overrides }), + }); actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); + } finally { + actions.onStartCrawlRequestComplete(); } }, stopCrawl: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 4d72b854bddfb..509346542ae13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -23,6 +23,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -215,4 +216,10 @@ describe('CrawlerOverview', () => { expect(wrapper.find(AddDomainFormErrors)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index c68e75790f073..f1f25dfb4dc55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -24,6 +24,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -138,6 +139,7 @@ export const CrawlerOverview: React.FC = () => { )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index ed445b923ea2a..addf4093a167b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { getPageHeaderActions } from '../../../test_helpers'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -92,4 +93,10 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index a4b2a9709cd62..63b9c3f080ec2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -17,6 +17,7 @@ import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -78,6 +79,7 @@ export const CrawlerSingleDomain: React.FC = () => { + ); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index c9212bca322d7..fe225f62d1dce 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -141,6 +141,19 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); + it('validates correctly with overrides', () => { + const request = { + params: { name: 'some-engine' }, + body: { overrides: { domain_allowlist: [] } }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with empty overrides', () => { + const request = { params: { name: 'some-engine' }, body: { overrides: {} } }; + mockRouter.shouldValidate(request); + }); + it('fails validation without name', () => { const request = { params: {} }; mockRouter.shouldThrow(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f0fdc5c16098b..5adffe1ff3ee5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -63,6 +63,13 @@ export function registerCrawlerRoutes({ params: schema.object({ name: schema.string(), }), + body: schema.object({ + overrides: schema.maybe( + schema.object({ + domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }), }, }, enterpriseSearchRequestHandler.createRequest({ From 74f0395c4b99bb06b6d76feac40fde2f79d5aaf9 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 8 Feb 2022 08:28:33 +0200 Subject: [PATCH 036/161] [Lens] Display heatmap on suggestions (#124542) * [Lens] Disaply heatmap on suggestions * Fixes test * disable heatmap suggestions for date histogram buckets * Remove comments * Enable it for multibucket charts * Apply PR comments * Lower the score for date histogram bucket, fix the bug with the heatmap suggestions Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/heatmap_component.tsx | 9 +- .../heatmap_visualization/suggestions.test.ts | 154 +++++++++++++++++- .../heatmap_visualization/suggestions.ts | 31 ++-- 3 files changed, 173 insertions(+), 21 deletions(-) diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 3e6e06de31c62..c1e026064fdfb 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -544,10 +544,15 @@ export const HeatmapComponent: FC = memo( yAxisLabelName={yAxisColumn?.name} xAxisTitle={args.gridConfig.isXAxisTitleVisible ? xAxisTitle : undefined} yAxisTitle={args.gridConfig.isYAxisTitleVisible ? yAxisTitle : undefined} - xAxisLabelFormatter={(v) => `${xValuesFormatter.convert(v) ?? ''}`} + xAxisLabelFormatter={(v) => + args.gridConfig.isXAxisLabelVisible ? `${xValuesFormatter.convert(v)}` : '' + } yAxisLabelFormatter={ yAxisColumn - ? (v) => `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}` + ? (v) => + args.gridConfig.isYAxisLabelVisible + ? `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}` + : '' : undefined } /> diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts index 34907c2e93c63..4980adf52e995 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts @@ -293,14 +293,12 @@ describe('heatmap suggestions', () => { title: 'Heat map', hide: true, previewIcon: 'empty', - score: 0.3, + score: 0, }, ]); }); - }); - describe('shows suggestions', () => { - test('when at least one axis and value accessor are available', () => { + test('when at least one axis has a date histogram', () => { expect( getSuggestions({ table: { @@ -357,21 +355,95 @@ describe('heatmap suggestions', () => { }, }, title: 'Heat map', - // Temp hide all suggestions while heatmap is in beta hide: true, previewIcon: 'empty', + score: 0.3, + }, + ]); + }); + }); + + describe('shows suggestions', () => { + test('when at least one axis and value accessor are available', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + }, + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: 'heatmap', + xAccessor: 'number-column', + valueAccessor: 'metric-column', + gridConfig: { + type: HEATMAP_GRID_FUNCTION, + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + legend: { + isVisible: true, + position: Position.Right, + type: LEGEND_FUNCTION, + }, + }, + title: 'Heat map', + hide: false, + previewIcon: 'empty', score: 0.6, }, ]); }); - test('when complete configuration has been resolved', () => { + test('when there is a date histogram and a second bucket dimension', () => { expect( getSuggestions({ table: { layerId: 'first', isMultiRow: true, columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, { columnId: 'date-column', operation: { @@ -390,6 +462,71 @@ describe('heatmap suggestions', () => { label: 'Metric', }, }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: 'heatmap', + yAccessor: 'date-column', + xAccessor: 'number-column', + valueAccessor: 'metric-column', + gridConfig: { + type: HEATMAP_GRID_FUNCTION, + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + legend: { + isVisible: true, + position: Position.Right, + type: LEGEND_FUNCTION, + }, + }, + title: 'Heat map', + hide: false, + previewIcon: 'empty', + score: 0.3, + }, + ]); + }); + + test('when complete configuration has been resolved', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + }, + }, { columnId: 'group-column', operation: { @@ -414,7 +551,7 @@ describe('heatmap suggestions', () => { layerId: 'first', layerType: layerTypes.DATA, shape: 'heatmap', - xAccessor: 'date-column', + xAccessor: 'number-column', yAccessor: 'group-column', valueAccessor: 'metric-column', gridConfig: { @@ -432,8 +569,7 @@ describe('heatmap suggestions', () => { }, }, title: 'Heat map', - // Temp hide all suggestions while heatmap is in beta - hide: true, + hide: false, previewIcon: 'empty', score: 0.9, }, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts index fac07d322e037..52c7a1bfd6d26 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts @@ -58,9 +58,17 @@ export const getSuggestions: Visualization['getSugges * Hide for: * - reduced and reorder tables * - tables with just a single bucket dimension + * - tables with only date histogram */ + const hasOnlyDatehistogramBuckets = + metrics.length === 1 && + groups.length > 0 && + groups.every((group) => group.operation.dataType === 'date'); const hide = - table.changeType === 'reduced' || table.changeType === 'reorder' || isSingleBucketDimension; + table.changeType === 'reduced' || + table.changeType === 'reorder' || + isSingleBucketDimension || + hasOnlyDatehistogramBuckets; const newState: HeatmapVisualizationState = { shape: CHART_SHAPES.HEATMAP, @@ -74,8 +82,8 @@ export const getSuggestions: Visualization['getSugges gridConfig: { type: HEATMAP_GRID_FUNCTION, isCellLabelVisible: false, - isYAxisLabelVisible: true, - isXAxisLabelVisible: true, + isYAxisLabelVisible: state?.gridConfig?.isYAxisLabelVisible ?? true, + isXAxisLabelVisible: state?.gridConfig?.isXAxisLabelVisible ?? true, isYAxisTitleVisible: state?.gridConfig?.isYAxisTitleVisible ?? false, isXAxisTitleVisible: state?.gridConfig?.isXAxisTitleVisible ?? false, }, @@ -93,11 +101,15 @@ export const getSuggestions: Visualization['getSugges newState.xAccessor = histogram[0]?.columnId || ordinal[0]?.columnId; newState.yAccessor = groups.find((g) => g.columnId !== newState.xAccessor)?.columnId; - if (newState.xAccessor) { - score += 0.3; - } - if (newState.yAccessor) { - score += 0.3; + const hasDatehistogram = groups.some((group) => group.operation.dataType === 'date'); + + if (!hasDatehistogram) { + if (newState.xAccessor) { + score += 0.3; + } + if (newState.yAccessor) { + score += 0.3; + } } return [ @@ -106,8 +118,7 @@ export const getSuggestions: Visualization['getSugges title: i18n.translate('xpack.lens.heatmap.heatmapLabel', { defaultMessage: 'Heat map', }), - // Temp hide all suggestions while heatmap is in beta - hide: true || hide, + hide, previewIcon: 'empty', score: Number(score.toFixed(1)), }, From a248af003df81341ac66da2647a2755bac0eef6d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 8 Feb 2022 10:22:21 +0100 Subject: [PATCH 037/161] [ML] Functional tests for the Overview page and side nav (#124793) * update side nav tests for full ml access * ML nodes tests * assert empty states * assert getting started callout * assert panels with data * assert read ml access * waitForDatePickerIndicatorLoaded * add missing step * fix typo * rename variable --- .../components/ml_page/side_nav.tsx | 5 +- .../application/components/stats_bar/stat.tsx | 4 +- .../components/getting_started_callout.tsx | 7 ++- .../nodes_overview/nodes_list.tsx | 5 +- .../apps/ml/permissions/full_ml_access.ts | 61 ++++++++++++++++--- .../apps/ml/permissions/read_ml_access.ts | 33 ++++++++-- .../test/functional/services/ml/common_ui.ts | 4 ++ x-pack/test/functional/services/ml/index.ts | 3 + .../functional/services/ml/ml_nodes_list.ts | 41 +++++++++++++ .../test/functional/services/ml/navigation.ts | 24 ++++++++ .../functional/services/ml/overview_page.ts | 30 +++++++++ 11 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/functional/services/ml/ml_nodes_list.ts diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index 872337f71fe82..39672b14bf836 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -111,6 +111,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { }), pathId: ML_PAGES.SINGLE_METRIC_VIEWER, disabled: disableLinks, + testSubj: 'mlMainTab singleMetricViewer', }, { id: 'settings', @@ -185,7 +186,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { defaultMessage: 'File', }), disabled: false, - testSubj: 'mlMainTab dataVisualizer fileDatavisualizer', + testSubj: 'mlMainTab fileDataVisualizer', }, { id: 'data_view_datavisualizer', @@ -194,7 +195,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { defaultMessage: 'Data View', }), disabled: false, - testSubj: 'mlMainTab dataVisualizer dataViewDatavisualizer', + testSubj: 'mlMainTab indexDataVisualizer', }, ], }, diff --git a/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx b/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx index e08e75143447c..ae04a7a3b2448 100644 --- a/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx +++ b/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx @@ -11,6 +11,7 @@ export interface StatsBarStat { label: string; value: number; show?: boolean; + 'data-test-subj'?: string; } interface StatProps { stat: StatsBarStat; @@ -19,7 +20,8 @@ interface StatProps { export const Stat: FC = ({ stat }) => { return ( - {stat.label}: {stat.value} + {stat.label}:{' '} + {stat.value} ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx index f9d734f7b84d5..457bca80ee2c0 100644 --- a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx @@ -31,6 +31,7 @@ export const GettingStartedCallout: FC = () => { return ( <> { />

- + = ({ compactView = false }) => { label: i18n.translate('xpack.ml.trainedModels.nodesList.totalAmountLabel', { defaultMessage: 'Total machine learning nodes', }), + 'data-test-subj': 'mlTotalNodesCount', }, }; }, [items]); @@ -189,7 +190,7 @@ export const NodesList: FC = ({ compactView = false }) => { } return ( - <> +

{nodesStats && ( @@ -218,6 +219,6 @@ export const NodesList: FC = ({ compactView = false }) => { data-test-subj={isLoading ? 'mlNodesTable loading' : 'mlNodesTable loaded'} />
- +
); }; diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 71c0d101943b9..c038aeba608bd 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -14,6 +14,7 @@ import { USER } from '../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); + const browser = getService('browser'); const testUsers = [ { user: USER.ML_POWERUSER, discoverAvailable: true }, @@ -44,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertKibanaNavMLEntryExists(); }); - it('should display tabs in the ML app correctly', async () => { + it('should display side nav in the ML app correctly', async () => { await ml.testExecution.logTestStep('should load the ML app'); await ml.navigation.navigateToMl(); @@ -52,33 +53,60 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertOverviewTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Anomaly Detection" tab' + 'should display the enabled "Anomaly Detection" section correctly' ); await ml.navigation.assertAnomalyDetectionTabEnabled(true); + await ml.navigation.assertAnomalyExplorerNavItemEnabled(true); + await ml.navigation.assertSingleMetricViewerNavItemEnabled(true); + await ml.navigation.assertSettingsTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Data Frame Analytics" tab' + 'should display the enabled "Data Frame Analytics" section' ); await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); - await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Model Management" section' + ); + await ml.navigation.assertTrainedModelsNavItemEnabled(true); + await ml.navigation.assertNodesNavItemEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); - await ml.navigation.assertSettingsTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Data Visualizer" section' + ); + await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.navigation.assertFileDataVisualizerNavItemEnabled(true); + await ml.navigation.assertIndexDataVisualizerNavItemEnabled(true); }); it('should display elements on ML Overview page correctly', async () => { await ml.testExecution.logTestStep('should load the ML overview page'); await ml.navigation.navigateToOverview(); - await ml.testExecution.logTestStep('should display enabled AD create job button'); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.testExecution.logTestStep('should display a welcome callout'); + await ml.overviewPage.assertGettingStartedCalloutVisible(true); + await ml.overviewPage.dismissGettingStartedCallout(); + + await ml.testExecution.logTestStep('should display ML Nodes panel'); + await ml.mlNodesPanel.assertNodeOverviewPanel(); + + await ml.testExecution.logTestStep('should display Anomaly Detection empty state'); + await ml.overviewPage.assertADEmptyStateExists(); await ml.overviewPage.assertADCreateJobButtonExists(); await ml.overviewPage.assertADCreateJobButtonEnabled(true); - await ml.testExecution.logTestStep('should display enabled DFA create job button'); + await ml.testExecution.logTestStep('should display DFA empty state'); + await ml.overviewPage.assertDFAEmptyStateExists(); await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should persist the getting started callout state after refresh' + ); + await browser.refresh(); + await ml.overviewPage.assertGettingStartedCalloutVisible(false); }); }); } @@ -164,6 +192,21 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the Overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display ML Nodes panel'); + await ml.mlNodesPanel.assertNodeOverviewPanel(); + + await ml.testExecution.logTestStep('should display Anomaly Detection panel'); + await ml.overviewPage.assertAdJobsOverviewPanelExist(); + + await ml.testExecution.logTestStep('should display DFA panel'); + await ml.overviewPage.assertDFAJobsOverviewPanelExist(); + }); + it('should display elements on Anomaly Detection page correctly', async () => { await ml.testExecution.logTestStep('should load the AD job management page'); await ml.navigation.navigateToMl(); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 9abb30548b0eb..fd9cb2cb4c79e 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -52,20 +52,30 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertOverviewTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Anomaly Detection" tab' + 'should display the enabled "Anomaly Detection" section correctly' ); await ml.navigation.assertAnomalyDetectionTabEnabled(true); + await ml.navigation.assertAnomalyExplorerNavItemEnabled(true); + await ml.navigation.assertSingleMetricViewerNavItemEnabled(true); + await ml.navigation.assertSettingsTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Data Frame Analytics" tab' + 'should display the enabled "Data Frame Analytics" section' ); await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); - await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Model Management" section' + ); + await ml.navigation.assertTrainedModelsNavItemEnabled(true); + await ml.navigation.assertNodesNavItemEnabled(false); - await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); - await ml.navigation.assertSettingsTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Data Visualizer" section' + ); + await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.navigation.assertFileDataVisualizerNavItemEnabled(true); + await ml.navigation.assertIndexDataVisualizerNavItemEnabled(true); }); it('should display elements on ML Overview page correctly', async () => { @@ -73,11 +83,22 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToOverview(); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.testExecution.logTestStep('should display a welcome callout'); + await ml.overviewPage.assertGettingStartedCalloutVisible(true); + await ml.overviewPage.dismissGettingStartedCallout(); + + await ml.testExecution.logTestStep('should not display ML Nodes panel'); + await ml.mlNodesPanel.assertNodesOverviewPanelExists(false); + await ml.testExecution.logTestStep('should display disabled AD create job button'); + await ml.overviewPage.assertADEmptyStateExists(); await ml.overviewPage.assertADCreateJobButtonExists(); await ml.overviewPage.assertADCreateJobButtonEnabled(false); await ml.testExecution.logTestStep('should display disabled DFA create job button'); + await ml.overviewPage.assertDFAEmptyStateExists(); await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(false); }); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index cb9ef179f0626..d6b75f53578a8 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -334,5 +334,9 @@ export function MachineLearningCommonUIProvider({ await PageObjects.spaceSelector.goToSpecificSpace(spaceId); await PageObjects.spaceSelector.expectHomePage(spaceId); }, + + async waitForDatePickerIndicatorLoaded() { + await testSubjects.waitForEnabled('superDatePickerApplyTimeButton'); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 4b48e4c0269eb..f7fd5efefda33 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -54,6 +54,7 @@ import { MachineLearningDashboardEmbeddablesProvider } from './dashboard_embedda import { TrainedModelsProvider } from './trained_models'; import { TrainedModelsTableProvider } from './trained_models_table'; import { MachineLearningJobAnnotationsProvider } from './job_annotations_table'; +import { MlNodesPanelProvider } from './ml_nodes_list'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -124,6 +125,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const swimLane = SwimLaneProvider(context); const trainedModels = TrainedModelsProvider(context, api, commonUI); const trainedModelsTable = TrainedModelsTableProvider(context); + const mlNodesPanel = MlNodesPanelProvider(context); return { anomaliesTable, @@ -173,5 +175,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { testResources, trainedModels, trainedModelsTable, + mlNodesPanel, }; } diff --git a/x-pack/test/functional/services/ml/ml_nodes_list.ts b/x-pack/test/functional/services/ml/ml_nodes_list.ts new file mode 100644 index 0000000000000..37cd4143e26cc --- /dev/null +++ b/x-pack/test/functional/services/ml/ml_nodes_list.ts @@ -0,0 +1,41 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MlNodesPanelProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertNodesOverviewPanelExists(expectPanelExits: boolean = true) { + if (expectPanelExits) { + await testSubjects.existOrFail('mlNodesOverviewPanel'); + } else { + await testSubjects.missingOrFail('mlNodesOverviewPanel'); + } + }, + + async assertNodesListLoaded() { + await testSubjects.existOrFail('mlNodesTable loaded', { timeout: 5000 }); + }, + + async assertMlNodesCount(minCount: number = 1) { + const actualCount = parseInt(await testSubjects.getVisibleText('mlTotalNodesCount'), 10); + expect(actualCount).to.not.be.lessThan( + minCount, + `Total ML nodes count should be at least '${minCount}' (got '${actualCount}')` + ); + }, + + async assertNodeOverviewPanel() { + await this.assertNodesOverviewPanelExists(); + await this.assertNodesListLoaded(); + await this.assertMlNodesCount(); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index c11721453d10f..6bf753926c72a 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -106,14 +106,38 @@ export function MachineLearningNavigationProvider({ await this.assertTabEnabled('~mlMainTab & ~anomalyDetection', expectedValue); }, + async assertAnomalyExplorerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~anomalyExplorer', expectedValue); + }, + + async assertSingleMetricViewerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~singleMetricViewer', expectedValue); + }, + async assertDataFrameAnalyticsTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~dataFrameAnalytics', expectedValue); }, + async assertTrainedModelsNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~trainedModels', expectedValue); + }, + + async assertNodesNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~nodesOverview', expectedValue); + }, + async assertDataVisualizerTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~dataVisualizer', expectedValue); }, + async assertFileDataVisualizerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~fileDataVisualizer', expectedValue); + }, + + async assertIndexDataVisualizerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~indexDataVisualizer', expectedValue); + }, + async assertSettingsTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~settings', expectedValue); }, diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts index 8fc04dfa29b18..5f02edde0f310 100644 --- a/x-pack/test/functional/services/ml/overview_page.ts +++ b/x-pack/test/functional/services/ml/overview_page.ts @@ -13,6 +13,24 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC const testSubjects = getService('testSubjects'); return { + async assertGettingStartedCalloutVisible(expectVisible: boolean = true) { + if (expectVisible) { + await testSubjects.existOrFail('mlGettingStartedCallout'); + } else { + await testSubjects.missingOrFail('mlGettingStartedCallout'); + } + }, + + async dismissGettingStartedCallout() { + await this.assertGettingStartedCalloutVisible(true); + await testSubjects.click('mlDismissGettingStartedCallout'); + await this.assertGettingStartedCalloutVisible(false); + }, + + async assertADEmptyStateExists() { + await testSubjects.existOrFail('mlAnomalyDetectionEmptyState'); + }, + async assertADCreateJobButtonExists() { await testSubjects.existOrFail('mlCreateNewJobButton'); }, @@ -27,6 +45,14 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC ); }, + async assertAdJobsOverviewPanelExist() { + await testSubjects.existOrFail('mlOverviewTableAnomalyDetection'); + }, + + async assertDFAEmptyStateExists() { + await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound'); + }, + async assertDFACreateJobButtonExists() { await testSubjects.existOrFail('mlAnalyticsCreateFirstButton'); }, @@ -41,6 +67,10 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC ); }, + async assertDFAJobsOverviewPanelExist() { + await testSubjects.existOrFail('mlOverviewTableAnalytics'); + }, + async assertJobSyncRequiredWarningExists() { await testSubjects.existOrFail('mlJobSyncRequiredWarning', { timeout: 5000 }); }, From c299aabcb32af15d34734258b7b76a5b0d990ecc Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 8 Feb 2022 13:02:15 +0300 Subject: [PATCH 038/161] TSVB needs to display better UX message when default index pattern is non-time based (#124341) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_types/timeseries/common/errors.ts | 10 ++++++++++ .../server/lib/vis_data/get_interval_and_timefield.ts | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_types/timeseries/common/errors.ts b/src/plugins/vis_types/timeseries/common/errors.ts index 8acb8f201a780..d3836fd0cd2a7 100644 --- a/src/plugins/vis_types/timeseries/common/errors.ts +++ b/src/plugins/vis_types/timeseries/common/errors.ts @@ -58,6 +58,16 @@ export class AggNotSupportedError extends UIError { } } +export class TimeFieldNotSpecifiedError extends UIError { + constructor() { + super( + i18n.translate('visTypeTimeseries.errors.timeFieldNotSpecifiedError', { + defaultMessage: 'Time field is required to visualize the data', + }) + ); + } +} + export const filterCannotBeAppliedErrorMessage = i18n.translate( 'visTypeTimeseries.filterCannotBeAppliedError', { diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts index 7c17f003dfbab..af6eb44affabc 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import { AUTO_INTERVAL } from '../../../common/constants'; import { validateField } from '../../../common/fields_utils'; import { validateInterval } from '../../../common/validate_interval'; +import { TimeFieldNotSpecifiedError } from '../../../common/errors'; import type { FetchedIndexPattern, Panel, Series } from '../../../common/types'; @@ -34,7 +35,11 @@ export function getIntervalAndTimefield( } if (panel.use_kibana_indexes) { - validateField(timeField!, index); + if (timeField) { + validateField(timeField, index); + } else { + throw new TimeFieldNotSpecifiedError(); + } } let interval = panel.interval; From e312c36e4cf67dad48a58fdcd947eab022934315 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 8 Feb 2022 06:13:06 -0500 Subject: [PATCH 039/161] [Security Solution] Remove a data fetching hook from the add to timeline action component (#124331) * Fetch alert ecs data in actions.tsx and not a hook in every table row * Add error handling and tests for theshold timelines * Fix bad merge * Remove unused imports * Actually remove unused file * Remove usage of alertIds and dead code from cases * Add basic sanity tests that ensure no extra network calls are being made * Remove unused operator * Remove unused imports * Remove unused mock --- .../public/components/__mock__/timeline.tsx | 2 - .../components/case_view/case_view_page.tsx | 3 - .../components/timeline_context/index.tsx | 1 - .../public/components/user_actions/types.ts | 1 - .../public/cases/pages/index.tsx | 14 -- .../public/common/lib/kibana/services.ts | 6 +- .../components/alerts_table/actions.test.tsx | 194 ++++++++++++++---- .../components/alerts_table/actions.tsx | 135 ++++++++---- .../investigate_in_timeline_action.test.tsx | 81 ++++++++ .../investigate_in_timeline_action.tsx | 3 - .../use_investigate_in_timeline.test.tsx | 82 ++++++++ .../use_investigate_in_timeline.tsx | 59 +++--- .../components/take_action_dropdown/index.tsx | 6 - .../alerts/use_fetch_ecs_alerts_data.ts | 83 -------- .../side_panel/event_details/footer.test.tsx | 131 ++++++++++++ .../side_panel/event_details/footer.tsx | 22 +- .../timeline/body/actions/index.tsx | 3 - .../t_grid/body/control_columns/checkbox.tsx | 6 +- 18 files changed, 579 insertions(+), 253 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx diff --git a/x-pack/plugins/cases/public/components/__mock__/timeline.tsx b/x-pack/plugins/cases/public/components/__mock__/timeline.tsx index 0aeda0f08302d..d576b0ef1732c 100644 --- a/x-pack/plugins/cases/public/components/__mock__/timeline.tsx +++ b/x-pack/plugins/cases/public/components/__mock__/timeline.tsx @@ -24,8 +24,6 @@ export const timelineIntegrationMock = { useInsertTimeline: jest.fn(), }, ui: { - renderInvestigateInTimelineActionComponent: () => - mockTimelineComponent('investigate-in-timeline'), renderTimelineDetailsPanel: () => mockTimelineComponent('timeline-details-panel'), }, }; diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index c0dc4fa4d95e9..235c8eabc9e59 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -356,9 +356,6 @@ export const CaseViewPage = React.memo( isLoadingUserActions={isLoadingUserActions} onShowAlertDetails={onShowAlertDetails} onUpdateField={onUpdateField} - renderInvestigateInTimelineActionComponent={ - timelineUi?.renderInvestigateInTimelineActionComponent - } statusActionButton={ userCanCrud ? ( UseInsertTimelineReturn; }; ui?: { - renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; renderTimelineDetailsPanel?: () => JSX.Element; }; } diff --git a/x-pack/plugins/cases/public/components/user_actions/types.ts b/x-pack/plugins/cases/public/components/user_actions/types.ts index 80657cc90cba9..dece59ec1eb42 100644 --- a/x-pack/plugins/cases/public/components/user_actions/types.ts +++ b/x-pack/plugins/cases/public/components/user_actions/types.ts @@ -28,7 +28,6 @@ export interface UserActionTreeProps { onRuleDetailsClick?: RuleDetailsNavigation['onClick']; onShowAlertDetails: (alertId: string, index: string) => void; onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; - renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; statusActionButton: JSX.Element | null; updateCase: (newCase: Case) => void; useFetchAlertData: (alertIds: string[]) => [boolean, Record]; diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 08878b5615257..1f02ff88b19bd 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -12,7 +12,6 @@ import { TimelineId } from '../../../common/types/timeline'; import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to'; -import * as i18n from './translations'; import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana'; import { APP_ID, CASES_PATH, SecurityPageName } from '../../../common/constants'; import { timelineActions } from '../../timelines/store/timeline'; @@ -25,7 +24,6 @@ import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useInsertTimeline } from '../components/use_insert_timeline'; import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline'; import { DetailsPanel } from '../../timelines/components/side_panel'; -import { InvestigateInTimelineAction } from '../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { useFetchAlertData } from './use_fetch_alert_data'; const TimelineDetailsPanel = () => { @@ -44,17 +42,6 @@ const TimelineDetailsPanel = () => { ); }; -const InvestigateInTimelineActionComponent = (alertIds: string[]) => { - return ( - - ); -}; - const CaseContainerComponent: React.FC = () => { const { cases: casesUi } = useKibana().services; const { getAppUrl, navigateTo } = useNavigation(); @@ -163,7 +150,6 @@ const CaseContainerComponent: React.FC = () => { useInsertTimeline, }, ui: { - renderInvestigateInTimelineActionComponent: InvestigateInTimelineActionComponent, renderTimelineDetailsPanel: TimelineDetailsPanel, }, }, diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts index 364eea3f8d98b..30bd3d6e59896 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts @@ -8,7 +8,8 @@ import { CoreStart } from '../../../../../../../src/core/public'; import { StartPlugins } from '../../../types'; -type GlobalServices = Pick & Pick; +type GlobalServices = Pick & + Pick; export class KibanaServices { private static kibanaVersion?: string; @@ -19,8 +20,9 @@ export class KibanaServices { data, kibanaVersion, uiSettings, + notifications, }: GlobalServices & { kibanaVersion: string }) { - this.services = { data, http, uiSettings }; + this.services = { data, http, uiSettings, notifications }; this.kibanaVersion = kibanaVersion; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index ad95f89c850f6..b1226e5b59190 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -29,11 +29,18 @@ import type { ISearchStart } from '../../../../../../../src/plugins/data/public' import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { getTimelineTemplate } from '../../../timelines/containers/api'; import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { KibanaServices } from '../../../common/lib/kibana'; +import { + DEFAULT_FROM_MOMENT, + DEFAULT_TO_MOMENT, +} from '../../../common/utils/default_date_settings'; jest.mock('../../../timelines/containers/api', () => ({ getTimelineTemplate: jest.fn(), })); +jest.mock('../../../common/lib/kibana'); + describe('alert actions', () => { const anchor = '2020-03-01T17:59:46.349Z'; const unix = moment(anchor).valueOf(); @@ -41,6 +48,9 @@ describe('alert actions', () => { let updateTimelineIsLoading: UpdateTimelineLoading; let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; + let mockKibanaServices: jest.Mock; + let fetchMock: jest.Mock; + let toastMock: jest.Mock; beforeEach(() => { // jest carries state between mocked implementations when using @@ -52,6 +62,14 @@ describe('alert actions', () => { createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; + mockKibanaServices = KibanaServices.get as jest.Mock; + + fetchMock = jest.fn(); + toastMock = jest.fn(); + mockKibanaServices.mockReturnValue({ + http: { fetch: fetchMock }, + notifications: { toasts: { addError: toastMock } }, + }); searchStrategyClient = { ...dataPluginMock.createStartContract().search, @@ -418,6 +436,59 @@ describe('alert actions', () => { }); describe('determineToAndFrom', () => { + const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ + ...mockAADEcsDataWithAlert, + kibana: { + alert: { + ...mockAADEcsDataWithAlert.kibana?.alert, + rule: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule, + parameters: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, + threshold: { + field: ['destination.ip'], + value: 1, + }, + }, + name: ['mock threshold rule'], + saved_id: [], + type: ['threshold'], + uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + timeline_id: undefined, + timeline_title: undefined, + }, + threshold_result: { + count: 99, + from: '2021-01-10T21:11:45.839Z', + cardinality: [ + { + field: 'source.ip', + value: 1, + }, + ], + terms: [ + { + field: 'destination.ip', + value: 1, + }, + ], + }, + }, + }, + }); + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: { + hits: [ + { + _id: ecsDataMockWithNoTemplateTimeline[0]._id, + _index: 'mock', + _source: ecsDataMockWithNoTemplateTimeline[0], + }, + ], + }, + }); + }); test('it uses ecs.Data.timestamp if one is provided', () => { const ecsDataMock: Ecs = { ...mockEcsDataWithAlert, @@ -438,47 +509,6 @@ describe('alert actions', () => { }); test('it uses original_time and threshold_result.from for threshold alerts', async () => { - const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ - ...mockAADEcsDataWithAlert, - kibana: { - alert: { - ...mockAADEcsDataWithAlert.kibana?.alert, - rule: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule, - parameters: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, - threshold: { - field: ['destination.ip'], - value: 1, - }, - }, - name: ['mock threshold rule'], - saved_id: [], - type: ['threshold'], - uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], - timeline_id: undefined, - timeline_title: undefined, - }, - threshold_result: { - count: 99, - from: '2021-01-10T21:11:45.839Z', - cardinality: [ - { - field: 'source.ip', - value: 1, - }, - ], - terms: [ - { - field: 'destination.ip', - value: 1, - }, - ], - }, - }, - }, - }); - const expectedFrom = '2021-01-10T21:11:45.839Z'; const expectedTo = '2021-01-10T21:12:45.839Z'; @@ -525,4 +555,86 @@ describe('alert actions', () => { }); }); }); + + describe('show toasts when data is malformed', () => { + const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ + ...mockAADEcsDataWithAlert, + kibana: { + alert: { + ...mockAADEcsDataWithAlert.kibana?.alert, + rule: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule, + parameters: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, + threshold: { + field: ['destination.ip'], + value: 1, + }, + }, + name: ['mock threshold rule'], + saved_id: [], + type: ['threshold'], + uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + timeline_id: undefined, + timeline_title: undefined, + }, + threshold_result: { + count: 99, + from: '2021-01-10T21:11:45.839Z', + cardinality: [ + { + field: 'source.ip', + value: 1, + }, + ], + terms: [ + { + field: 'destination.ip', + value: 1, + }, + ], + }, + }, + }, + }); + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: 'not correctly formed doc', + }); + }); + test('renders a toast and calls create timeline with basic defaults', async () => { + const expectedFrom = DEFAULT_FROM_MOMENT.toISOString(); + const expectedTo = DEFAULT_TO_MOMENT.toISOString(); + const timelineProps = { + ...defaultTimelineProps, + timeline: { + ...defaultTimelineProps.timeline, + dataProviders: [], + dateRange: { + start: expectedFrom, + end: expectedTo, + }, + description: '', + kqlQuery: { + filterQuery: null, + }, + resolveTimelineConfig: undefined, + }, + from: expectedFrom, + to: expectedTo, + }; + + delete timelineProps.ruleNote; + + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMockWithNoTemplateTimeline, + updateTimelineIsLoading, + searchStrategyClient, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(timelineProps); + expect(toastMock).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index c12133089e02a..46e439d38f81e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -21,6 +21,7 @@ import { ALERT_RULE_PARAMETERS, } from '@kbn/rule-data-utils'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALERT_ORIGINAL_TIME, ALERT_GROUP_ID, @@ -64,6 +65,13 @@ import { QueryOperator, } from '../../../timelines/components/timeline/data_providers/data_provider'; import { getTimelineTemplate } from '../../../timelines/containers/api'; +import { KibanaServices } from '../../../common/lib/kibana'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants'; +import { buildAlertsQuery, formatAlertToEcsSignal } from '../../../common/utils/alerts'; +import { + DEFAULT_FROM_MOMENT, + DEFAULT_TO_MOMENT, +} from '../../../common/utils/default_date_settings'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { @@ -177,7 +185,7 @@ export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggr return thresholdEcsData.reduce( (outerAcc, thresholdData) => { const threshold = - getField(thresholdData, ALERT_RULE_PARAMETERS).threshold ?? + getField(thresholdData, `${ALERT_RULE_PARAMETERS}.threshold`) ?? thresholdData.signal?.rule?.threshold; const thresholdResult: { @@ -384,47 +392,102 @@ const buildEqlDataProviderOrFilter = ( return { filters: [], dataProviders: [] }; }; -const createThresholdTimeline = ( +const createThresholdTimeline = async ( ecsData: Ecs, createTimeline: ({ from, timeline, to }: CreateTimelineProps) => void, noteContent: string, templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] } ) => { - const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(ecsData); - const params = getField(ecsData, ALERT_RULE_PARAMETERS); - const filters = getFiltersFromRule(params.filters ?? ecsData.signal?.rule?.filters) ?? []; - const language = params.language ?? ecsData.signal?.rule?.language ?? 'kuery'; - const query = params.query ?? ecsData.signal?.rule?.query ?? ''; - const indexNames = params.index ?? ecsData.signal?.rule?.index ?? []; - - return createTimeline({ - from: thresholdFrom, - notes: null, - timeline: { - ...timelineDefaults, - description: `_id: ${ecsData._id}`, - filters: templateValues.filters ?? filters, - dataProviders: templateValues.dataProviders ?? dataProviders, - id: TimelineId.active, - indexNames, - dateRange: { - start: thresholdFrom, - end: thresholdTo, - }, - eventType: 'all', - kqlQuery: { - filterQuery: { - kuery: { - kind: language, - expression: templateValues.query ?? query, + try { + const alertResponse = await KibanaServices.get().http.fetch< + estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> + >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + method: 'POST', + body: JSON.stringify(buildAlertsQuery([ecsData._id])), + }); + const formattedAlertData = + alertResponse?.hits.hits.reduce((acc, { _id, _index, _source = {} }) => { + return [ + ...acc, + { + ...formatAlertToEcsSignal(_source), + _id, + _index, + timestamp: _source['@timestamp'], + }, + ]; + }, []) ?? []; + const alertDoc = formattedAlertData[0]; + const params = getField(alertDoc, ALERT_RULE_PARAMETERS); + const filters = getFiltersFromRule(params.filters ?? alertDoc.signal?.rule?.filters) ?? []; + const language = params.language ?? alertDoc.signal?.rule?.language ?? 'kuery'; + const query = params.query ?? alertDoc.signal?.rule?.query ?? ''; + const indexNames = params.index ?? alertDoc.signal?.rule?.index ?? []; + + const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(alertDoc); + return createTimeline({ + from: thresholdFrom, + notes: null, + timeline: { + ...timelineDefaults, + description: `_id: ${alertDoc._id}`, + filters: templateValues.filters ?? filters, + dataProviders: templateValues.dataProviders ?? dataProviders, + id: TimelineId.active, + indexNames, + dateRange: { + start: thresholdFrom, + end: thresholdTo, + }, + eventType: 'all', + kqlQuery: { + filterQuery: { + kuery: { + kind: language, + expression: templateValues.query ?? query, + }, + serializedQuery: templateValues.query ?? query, }, - serializedQuery: templateValues.query ?? query, }, }, - }, - to: thresholdTo, - ruleNote: noteContent, - }); + to: thresholdTo, + ruleNote: noteContent, + }); + } catch (error) { + const { toasts } = KibanaServices.get().notifications; + toasts.addError(error, { + toastMessage: i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.createThresholdTimelineFailure', + { + defaultMessage: 'Failed to create timeline for document _id: {id}', + values: { id: ecsData._id }, + } + ), + title: i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.createThresholdTimelineFailureTitle', + { + defaultMessage: 'Failed to create theshold alert timeline', + } + ), + }); + const from = DEFAULT_FROM_MOMENT.toISOString(); + const to = DEFAULT_TO_MOMENT.toISOString(); + return createTimeline({ + from, + notes: null, + timeline: { + ...timelineDefaults, + id: TimelineId.active, + indexNames: [], + dateRange: { + start: from, + end: to, + }, + eventType: 'all', + }, + to, + }); + } }; export const sendAlertToTimelineAction = async ({ @@ -492,7 +555,7 @@ export const sendAlertToTimelineAction = async ({ ); // threshold with template if (isThresholdRule(ecsData)) { - createThresholdTimeline(ecsData, createTimeline, noteContent, { + return createThresholdTimeline(ecsData, createTimeline, noteContent, { filters, query, dataProviders, @@ -550,7 +613,7 @@ export const sendAlertToTimelineAction = async ({ }); } } else if (isThresholdRule(ecsData)) { - createThresholdTimeline(ecsData, createTimeline, noteContent, {}); + return createThresholdTimeline(ecsData, createTimeline, noteContent, {}); } else { let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id); if (isEqlRuleWithGroupId(ecsData)) { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx new file mode 100644 index 0000000000000..24433e2f2ca99 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { fireEvent, render, act } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; +import { Ecs } from '../../../../../common/ecs'; +import * as actions from '../actions'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import type { SendAlertToTimelineActionProps } from '../types'; +import { InvestigateInTimelineAction } from './investigate_in_timeline_action'; + +const ecsRowData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../actions'); + +const props = { + ecsRowData, + onInvestigateInTimelineAlertClick: () => {}, + ariaLabel: 'test', +}; + +describe('use investigate in timeline hook', () => { + let mockSendAlertToTimeline: jest.SpyInstance, [SendAlertToTimelineActionProps]>; + + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + test('it creates a component and click handler', () => { + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('send-alert-to-timeline-button')).toBeTruthy(); + }); + test('it calls sendAlertToTimelineAction once on click, not on mount', () => { + const wrapper = render( + + + + ); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(0); + act(() => { + fireEvent.click(wrapper.getByTestId('send-alert-to-timeline-button')); + }); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index bca04dcf37a5b..b8d8232cb613c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -19,21 +19,18 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline'; interface InvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; ariaLabel?: string; - alertIds?: string[]; buttonType?: 'text' | 'icon'; onInvestigateInTimelineAlertClick?: () => void; } const InvestigateInTimelineActionComponent: React.FC = ({ ariaLabel = ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, - alertIds, ecsRowData, buttonType, onInvestigateInTimelineAlertClick, }) => { const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ ecsRowData, - alertIds, onInvestigateInTimelineAlertClick, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx new file mode 100644 index 0000000000000..fc413a6f4f814 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { fireEvent, render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; +import { Ecs } from '../../../../../common/ecs'; +import { useInvestigateInTimeline } from './use_investigate_in_timeline'; +import * as actions from '../actions'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import type { SendAlertToTimelineActionProps } from '../types'; + +const ecsRowData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../actions'); + +const props = { + ecsRowData, + onInvestigateInTimelineAlertClick: () => {}, +}; + +describe('use investigate in timeline hook', () => { + let mockSendAlertToTimeline: jest.SpyInstance, [SendAlertToTimelineActionProps]>; + + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + test('it creates a component and click handler', () => { + const { result } = renderHook(() => useInvestigateInTimeline(props), { + wrapper: TestProviders, + }); + expect(result.current.investigateInTimelineActionItems).toBeTruthy(); + expect(typeof result.current.investigateInTimelineAlertClick).toBe('function'); + }); + + describe('the click handler calls createTimeline once and only once', () => { + test('runs 0 times on render, once on click', async () => { + const { result } = renderHook(() => useInvestigateInTimeline(props), { + wrapper: TestProviders, + }); + const component = result.current.investigateInTimelineActionItems[0]; + const { getByTestId } = render(component); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(0); + act(() => { + fireEvent.click(getByTestId('investigate-in-timeline-action-item')); + }); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index c1cbe657415a6..301395eb5b963 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -6,32 +6,27 @@ */ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { isEmpty } from 'lodash'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { Ecs } from '../../../../../common/ecs'; -import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { sendAlertToTimelineAction } from '../actions'; import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; +import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline'; import { CreateTimelineProps } from '../types'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useFetchEcsAlertsData } from '../../../containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; interface UseInvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; - nonEcsRowData?: TimelineNonEcsData[]; - alertIds?: string[] | null | undefined; onInvestigateInTimelineAlertClick?: () => void; } export const useInvestigateInTimeline = ({ ecsRowData, - alertIds, onInvestigateInTimelineAlertClick, }: UseInvestigateInTimelineActionProps) => { const { @@ -54,8 +49,14 @@ export const useInvestigateInTimeline = ({ [dispatch] ); + const clearActiveTimeline = useCreateTimeline({ + timelineId: TimelineId.active, + timelineType: TimelineType.default, + }); + const createTimeline = useCallback( ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + clearActiveTimeline(); updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); dispatchUpdateTimeline(dispatch)({ duplicate: true, @@ -72,27 +73,14 @@ export const useInvestigateInTimeline = ({ ruleNote, })(); }, - [dispatch, filterManager, updateTimelineIsLoading] + [dispatch, filterManager, updateTimelineIsLoading, clearActiveTimeline] ); - const showInvestigateInTimelineAction = alertIds != null; - const { isLoading: isFetchingAlertEcs, alertsEcsData } = useFetchEcsAlertsData({ - alertIds, - skip: alertIds == null, - }); - const investigateInTimelineAlertClick = useCallback(async () => { if (onInvestigateInTimelineAlertClick) { onInvestigateInTimelineAlertClick(); } - if (!isEmpty(alertsEcsData) && alertsEcsData !== null) { - await sendAlertToTimelineAction({ - createTimeline, - ecsData: alertsEcsData, - searchStrategyClient, - updateTimelineIsLoading, - }); - } else if (ecsRowData != null) { + if (ecsRowData != null) { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsRowData, @@ -101,7 +89,6 @@ export const useInvestigateInTimeline = ({ }); } }, [ - alertsEcsData, createTimeline, ecsRowData, onInvestigateInTimelineAlertClick, @@ -109,22 +96,22 @@ export const useInvestigateInTimeline = ({ updateTimelineIsLoading, ]); - const investigateInTimelineActionItems = showInvestigateInTimelineAction - ? [ - - {ACTION_INVESTIGATE_IN_TIMELINE} - , - ] - : []; + const investigateInTimelineActionItems = useMemo( + () => [ + + {ACTION_INVESTIGATE_IN_TIMELINE} + , + ], + [ecsRowData, investigateInTimelineAlertClick] + ); return { investigateInTimelineActionItems, investigateInTimelineAlertClick, - showInvestigateInTimelineAction, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index d04f6c5d7d510..8ad76c70247bf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -8,7 +8,6 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; -import { isEmpty } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; @@ -81,10 +80,6 @@ export const TakeActionDropdown = React.memo( [detailsData] ); - const alertIds = useMemo( - () => (isEmpty(actionsData.eventId) ? null : [actionsData.eventId]), - [actionsData.eventId] - ); const isEvent = actionsData.eventKind === 'event'; const isAgentEndpoint = useMemo(() => ecsData?.agent?.type?.includes('endpoint'), [ecsData]); @@ -156,7 +151,6 @@ export const TakeActionDropdown = React.memo( }); const { investigateInTimelineActionItems } = useInvestigateInTimeline({ - alertIds, ecsRowData: ecsData, onInvestigateInTimelineAlertClick: closePopoverHandler, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts deleted file mode 100644 index c459fab89a25e..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts +++ /dev/null @@ -1,83 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useEffect, useState } from 'react'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { isEmpty } from 'lodash'; - -import { Ecs } from '../../../../../common/ecs'; - -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; -import { KibanaServices } from '../../../../common/lib/kibana'; -import { buildAlertsQuery, formatAlertToEcsSignal } from '../../../../common/utils/alerts'; - -export const useFetchEcsAlertsData = ({ - alertIds, - skip, - onError, -}: { - alertIds?: string[] | null | undefined; - skip?: boolean; - onError?: (e: Error) => void; -}): { isLoading: boolean | null; alertsEcsData: Ecs[] | null } => { - const [isLoading, setIsLoading] = useState(null); - const [alertsEcsData, setAlertEcsData] = useState(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchAlert = async () => { - try { - setIsLoading(true); - const alertResponse = await KibanaServices.get().http.fetch< - estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> - >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { - method: 'POST', - body: JSON.stringify(buildAlertsQuery(alertIds ?? [])), - }); - - setAlertEcsData( - alertResponse?.hits.hits.reduce( - (acc, { _id, _index, _source = {} }) => [ - ...acc, - { - ...formatAlertToEcsSignal(_source), - _id, - _index, - timestamp: _source['@timestamp'], - }, - ], - [] - ) ?? [] - ); - } catch (e) { - if (isSubscribed) { - if (onError) { - onError(e as Error); - } - } - } - if (isSubscribed) { - setIsLoading(false); - } - }; - - if (!isEmpty(alertIds) && !skip) { - fetchAlert(); - } - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [alertIds, onError, skip]); - - return { - isLoading, - alertsEcsData, - }; -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx new file mode 100644 index 0000000000000..71d6f6253010d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { EventDetailsFooter } from './footer'; +import '../../../../common/mock/match_media'; +import { TestProviders } from '../../../../common/mock'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { Ecs } from '../../../../../common/ecs'; +import { mockAlertDetailsData } from '../../../../common/components/event_details/__mocks__'; +import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; + +const ecsData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => { + return { + ...detail, + isObjectArray: false, + }; +}) as TimelineEventsDetailsItem[]; + +jest.mock('../../../../../common/endpoint/service/host_isolation/utils', () => { + return { + isIsolationSupported: jest.fn().mockReturnValue(true), + }; +}); + +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_host_isolation_status', + () => { + return { + useHostIsolationStatus: jest.fn().mockReturnValue({ + loading: false, + isIsolated: false, + agentStatus: 'healthy', + }), + }; + } +); + +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); + +jest.mock('../../../../detections/components/user_info', () => ({ + useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), +})); +jest.mock('../../../../common/lib/kibana'); +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', + () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), + }) +); +jest.mock('../../../../cases/components/use_insert_timeline'); + +jest.mock('../../../../common/utils/endpoint_alert_check', () => { + return { + isAlertFromEndpointAlert: jest.fn().mockReturnValue(true), + isAlertFromEndpointEvent: jest.fn().mockReturnValue(true), + }; +}); +jest.mock( + '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline', + () => { + return { + useInvestigateInTimeline: jest.fn().mockReturnValue({ + investigateInTimelineActionItems: [
], + investigateInTimelineAlertClick: () => {}, + }), + }; + } +); +jest.mock('../../../../detections/components/alerts_table/actions'); + +const defaultProps = { + timelineId: TimelineId.test, + loadingEventDetails: false, + detailsEcsData: ecsData, + isHostIsolationPanelOpen: false, + handleOnEventClosed: jest.fn(), + onAddIsolationStatusClick: jest.fn(), + expandedEvent: { eventId: ecsData._id, indexName: '' }, + detailsData: mockAlertDetailsDataWithIsObject, +}; + +describe('event details footer component', () => { + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('it renders the take action dropdown', () => { + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('take-action-dropdown-btn')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index 6f46111656871..86b23594c947a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { find, get, isEmpty } from 'lodash/fp'; +import { find } from 'lodash/fp'; import { connect, ConnectedProps } from 'react-redux'; import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; @@ -19,7 +19,6 @@ import { useEventFilterModal } from '../../../../detections/components/alerts_ta import { getFieldValue } from '../../../../detections/components/host_isolation/helpers'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../../common/ecs'; -import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; import { inputsModel, inputsSelectors, State } from '../../../../common/store'; interface EventDetailsFooterProps { @@ -82,11 +81,6 @@ export const EventDetailsFooterComponent = React.memo( [detailsData] ); - const eventIds = useMemo( - () => (isEmpty(expandedEvent?.eventId) ? null : [expandedEvent?.eventId]), - [expandedEvent?.eventId] - ); - const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); }; @@ -113,21 +107,15 @@ export const EventDetailsFooterComponent = React.memo( const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } = useEventFilterModal(); - const { alertsEcsData } = useFetchEcsAlertsData({ - alertIds: eventIds, - skip: expandedEvent?.eventId == null, - }); - - const ecsData = detailsEcsData ?? get(0, alertsEcsData); return ( <> - {ecsData && ( + {detailsEcsData && ( )} - {isAddEventFilterModalOpen && ecsData != null && ( - + {isAddEventFilterModalOpen && detailsEcsData != null && ( + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 75ca399bf52d4..8be6200d1e84a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -48,7 +48,6 @@ const ActionsComponent: React.FC = ({ ariaRowindex, checked, columnValues, - data, ecsData, eventId, eventIdToNoteIds, @@ -68,7 +67,6 @@ const ActionsComponent: React.FC = ({ const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const alertIds = useMemo(() => [ecsData._id], [ecsData]); const onPinEvent: OnPinEvent = useCallback( (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), @@ -167,7 +165,6 @@ const ActionsComponent: React.FC = ({ )} diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx index 77a761edebd49..b1a16cf5b3abd 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx @@ -7,7 +7,6 @@ import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; import type { ActionProps, HeaderActionProps } from '../../../../../common/types'; import * as i18n from './translations'; @@ -19,10 +18,7 @@ export const RowCheckBox = ({ columnValues, disabled, loadingEventIds, - data, }: ActionProps) => { - const ruleProducers = data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? []; - const ruleProducer = ruleProducers[0]; const handleSelectEvent = useCallback( (event: React.ChangeEvent) => { if (!disabled) { @@ -39,7 +35,7 @@ export const RowCheckBox = ({ ) : ( Date: Tue, 8 Feb 2022 13:36:19 +0100 Subject: [PATCH 040/161] [Lens] fix Formula to Quick functions does not preserve custom formatting (#124840) * [Lens] fix transitioning from Formula to Quick functions does not preserve chosen format * linter --- .../operations/layer_helpers.test.ts | 45 +++++++++++++++++++ .../operations/layer_helpers.ts | 1 + 2 files changed, 46 insertions(+) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index dad1250b39e14..1b432c4a34add 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2119,6 +2119,51 @@ describe('state_helpers', () => { }, }); }); + + it('should carry over a custom formatting when transitioning from a managed reference', () => { + const actual = replaceColumn({ + layer: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'MY CUSTOM LABEL', + customLabel: true, + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: 'average(bytes)', + format: { + id: 'number', + params: { decimals: 2 }, + }, + }, + references: [], + } as FormulaIndexPatternColumn, + }, + }, + indexPattern, + columnId: 'col1', + op: 'average', + field: indexPattern.fields[2], // bytes field + visualizationGroups: [], + shouldResetLabel: undefined, + }); + + expect(actual.columns.col1).toEqual( + expect.objectContaining({ + params: { + format: { + id: 'number', + params: { decimals: 2 }, + }, + }, + }) + ); + }); }); it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 7985500798b38..438d728b7df1f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -422,6 +422,7 @@ export function replaceColumn({ op, field, visualizationGroups, + incompleteParams: previousColumn, }); // if the formula label is not the default one, propagate it to the new operation From 3f702c1fd46ce408daad24295eaee0c61ed7a5af Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 8 Feb 2022 14:33:53 +0100 Subject: [PATCH 041/161] [Lens] Make top values work for custom numeric formatters (#124566) * :bug: Make top values work with custom formatter * :bug: Make custom formatter work with multi-terms formatter * :bug: simplify parentFormat logic and add more tests * :ok_hand: Integrate feedback * :sparkles: Add migration for top values formatting Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../format_column/format_column.test.ts | 30 +++ .../format_column/format_column_fn.ts | 38 +++- .../droppable/droppable.test.ts | 3 + .../operations/definitions/terms/index.tsx | 29 ++- .../definitions/terms/terms.test.tsx | 176 ++++++++++++++++-- .../operations/definitions/terms/types.ts | 4 - .../server/migrations/common_migrations.ts | 31 +++ .../saved_object_migrations.test.ts | 75 ++++++++ .../migrations/saved_object_migrations.ts | 8 +- .../plugins/lens/server/migrations/types.ts | 31 ++- 10 files changed, 399 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index 4d53f96c71fd8..17192103efaae 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -159,6 +159,36 @@ describe('format_column', () => { params: { pattern: '0,0.00000', }, + pattern: '0,0.00000', + }, + }); + }); + + it('should support multi-fields formatters', async () => { + datatable.columns[0].meta.params = { + id: 'previousWrapper', + params: { id: 'myMultiFieldFormatter', paramsPerField: [{ id: 'number' }] }, + }; + const result = await fn(datatable, { + columnId: 'test', + format: 'number', + decimals: 5, + parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'wrapper', + params: { + wrapperParam: 123, + id: 'number', + paramsPerField: [ + { + id: 'number', + params: { + pattern: '0,0.00000', + }, + pattern: '0,0.00000', + }, + ], }, }); }); diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts index 37540ee0950af..93b54e777b645 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -43,18 +43,48 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( const parentFormatId = parsedParentFormat.id; const parentFormatParams = parsedParentFormat.params ?? {}; - if (!parentFormatId) { + // Be careful here to check for undefined custom format + const isDuplicateParentFormatter = parentFormatId === col.meta.params?.id && format == null; + if (!parentFormatId || isDuplicateParentFormatter) { return col; } if (format && supportedFormats[format]) { + const customParams = { + pattern: supportedFormats[format].decimalsToPattern(decimals), + }; + // Some parent formatters are multi-fields and wrap the custom format into a "paramsPerField" + // property. Here the format is passed to this property to make it work properly + if (col.meta.params?.params?.paramsPerField?.length) { + return withParams(col, { + id: parentFormatId, + params: { + ...col.meta.params?.params, + id: format, + ...parentFormatParams, + // some wrapper formatters require params to be flatten out (i.e. terms) while others + // require them to be in the params property (i.e. ranges) + // so for now duplicate + paramsPerField: col.meta.params?.params?.paramsPerField.map( + (f: { id: string | undefined; params: Record | undefined }) => ({ + ...f, + params: { ...f.params, ...customParams }, + ...customParams, + }) + ), + }, + }); + } return withParams(col, { id: parentFormatId, params: { + ...col.meta.params?.params, id: format, - params: { - pattern: supportedFormats[format].decimalsToPattern(decimals), - }, + // some wrapper formatters require params to be flatten out (i.e. terms) while others + // require them to be in the params property (i.e. ranges) + // so for now duplicate + ...customParams, + params: customParams, ...parentFormatParams, }, }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 3871715cf31e5..002fec786d7e6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -2167,6 +2167,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, orderDirection: 'desc', size: 10, + parentFormat: { id: 'terms' }, }, }, col3: testState.layers.first.columns.col3, @@ -2257,6 +2258,7 @@ describe('IndexPatternDimensionEditorPanel', () => { type: 'alphabetical', }, orderDirection: 'desc', + parentFormat: { id: 'terms' }, size: 10, }, }, @@ -2267,6 +2269,7 @@ describe('IndexPatternDimensionEditorPanel', () => { filter: undefined, operationType: 'unique_count', sourceField: 'src', + timeShift: undefined, dataType: 'number', params: undefined, scale: 'ratio', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index be286532ad75b..4c656d15f197f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -81,6 +81,11 @@ function isScriptedField(fieldName: string | IndexPatternField, indexPattern?: I return fieldName.scripted; } +// It is not always possible to know if there's a numeric field, so just ignore it for now +function getParentFormatter(params: Partial) { + return { id: params.secondaryFields?.length ? 'multi_terms' : 'terms' }; +} + const idPrefix = htmlIdGenerator()(); const DEFAULT_SIZE = 3; // Elasticsearch limit @@ -124,9 +129,18 @@ export const termsOperation: OperationDefinition targetColumn.sourceField !== f), + // remove the sourceField + secondaryFields.delete(targetColumn.sourceField); + + const secondaryFieldsList: string[] = [...secondaryFields]; + const ret: Partial = { + secondaryFields: secondaryFieldsList, + parentFormat: getParentFormatter({ + ...targetColumn.params, + secondaryFields: secondaryFieldsList, + }), }; + return ret; }, canAddNewField: ({ targetColumn, sourceColumn, field, indexPattern }) => { // first step: collect the fields from the targetColumn @@ -222,6 +236,7 @@ export const termsOperation: OperationDefinition { + (fields: string[]) => { const column = layer.columns[columnId] as TermsIndexPatternColumn; + const secondaryFields = fields.length > 1 ? fields.slice(1) : undefined; updateLayer({ ...layer, columns: { @@ -364,7 +381,11 @@ export const termsOperation: OperationDefinition 1 ? fields.slice(1) : undefined, + secondaryFields, + parentFormat: getParentFormatter({ + ...column.params, + secondaryFields, + }), }, }, } as Record, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index fb3943289437d..cfdab76c9b6d9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; -import { EuiButtonGroup, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; +import { EuiButtonGroup, EuiComboBox, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; import type { IUiSettingsClient, SavedObjectsClientContract, @@ -39,6 +39,16 @@ jest.mock('@elastic/eui', () => { }; }); +// Need to mock the debounce call to test some FieldInput behaviour +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + const uiSettingsMock = {} as IUiSettingsClient; const defaultProps = { @@ -224,15 +234,18 @@ describe('terms', () => { }, orderDirection: 'asc', format: { id: 'number', params: { decimals: 0 } }, + parentFormat: { id: 'terms' }, }, }; const indexPattern = createMockedIndexPattern(); - const newStringField = indexPattern.fields.find((i) => i.name === 'source')!; + const newStringField = indexPattern.getFieldByName('source')!; const column = termsOperation.onFieldChange(oldColumn, newStringField); expect(column).toHaveProperty('dataType', 'string'); expect(column).toHaveProperty('sourceField', 'source'); expect(column.params.format).toBeUndefined(); + // Preserve the parentFormat as it will be ignored down the line if not required + expect(column.params.parentFormat).toEqual({ id: 'terms' }); }); it('should remove secondary fields when a new field is passed', () => { @@ -253,7 +266,7 @@ describe('terms', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newStringField = indexPattern.fields.find((i) => i.name === 'source')!; + const newStringField = indexPattern.getFieldByName('source')!; const column = termsOperation.onFieldChange(oldColumn, newStringField); expect(column.params.secondaryFields).toBeUndefined(); @@ -277,13 +290,59 @@ describe('terms', () => { }, }; const indexPattern = createMockedIndexPattern(); - const sanemStringField = indexPattern.fields.find((i) => i.name === 'bytes')!; + const newNumericField = indexPattern.getFieldByName('bytes')!; - const column = termsOperation.onFieldChange(oldColumn, sanemStringField, { + const column = termsOperation.onFieldChange(oldColumn, newNumericField, { secondaryFields: ['dest', 'geo.src'], }); expect(column.params.secondaryFields).toEqual(expect.arrayContaining(['dest', 'geo.src'])); }); + + it('should reassign the parentFormatter on single field change', () => { + const oldColumn: TermsIndexPatternColumn = { + operationType: 'terms', + sourceField: 'bytes', + label: 'Top values of bytes', + isBucketed: true, + dataType: 'number', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + format: { id: 'number', params: { decimals: 0 } }, + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('memory')!; + + const column = termsOperation.onFieldChange(oldColumn, newNumberField); + expect(column.params.parentFormat).toEqual({ id: 'terms' }); + }); + + it('should reassign the parentFormatter on multiple fields change', () => { + const oldColumn: TermsIndexPatternColumn = { + operationType: 'terms', + sourceField: 'bytes', + label: 'Top values of bytes', + isBucketed: true, + dataType: 'number', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + format: { id: 'number', params: { decimals: 0 } }, + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('memory')!; + + const column = termsOperation.onFieldChange(oldColumn, newNumberField); + expect(column.params.parentFormat).toEqual({ id: 'terms' }); + }); }); describe('getPossibleOperationForField', () => { @@ -516,6 +575,23 @@ describe('terms', () => { }); expect(termsColumn.params).toEqual(expect.objectContaining({ size: 5 })); }); + + it('should set a parentFormat as "terms" if a numeric field is passed', () => { + const termsColumn = termsOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + field: { + aggregatable: true, + searchable: true, + type: 'number', + name: 'numericTest', + displayName: 'test', + }, + }); + expect(termsColumn.params).toEqual( + expect.objectContaining({ parentFormat: { id: 'terms' } }) + ); + }); }); describe('onOtherColumnChanged', () => { @@ -1364,6 +1440,47 @@ describe('terms', () => { instance.find('[data-test-subj="indexPattern-terms-add-field"]').first().prop('isDisabled') ).toBeTruthy(); }); + + it('should update the parentFormatter on transition between single to multi terms', () => { + const updateLayerSpy = jest.fn(); + const existingFields = getExistingFields(); + const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields); + + let instance = mount( + + ); + // add a new field + act(() => { + instance.find('[data-test-subj="indexPattern-terms-add-field"]').first().simulate('click'); + }); + instance = instance.update(); + + act(() => { + instance.find(EuiComboBox).last().prop('onChange')!([ + { value: { type: 'field', field: 'bytes' }, label: 'bytes' }, + ]); + }); + + expect(updateLayerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + params: expect.objectContaining({ + parentFormat: { id: 'multi_terms' }, + }), + }), + }), + }) + ); + }); }); describe('param editor', () => { @@ -2046,7 +2163,10 @@ describe('terms', () => { sourceColumn: createMultiTermsColumn(['bytes', 'memory']), indexPattern: defaultProps.indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should return existing multiterms with only new fields from source column', () => { @@ -2056,7 +2176,10 @@ describe('terms', () => { sourceColumn: createMultiTermsColumn(['bytes', 'dest']), indexPattern: defaultProps.indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should return existing multiterms with only multiple new fields from source column', () => { @@ -2066,7 +2189,10 @@ describe('terms', () => { sourceColumn: createMultiTermsColumn(['dest', 'bytes', 'memory']), indexPattern: defaultProps.indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should append field to multiterms', () => { @@ -2079,7 +2205,10 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['bytes']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should not append scripted field to multiterms', () => { @@ -2092,7 +2221,7 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: [] }); + ).toEqual({ secondaryFields: [], parentFormat: { id: 'terms' } }); }); it('should add both sourceColumn and field (as last term) to the targetColumn', () => { @@ -2105,7 +2234,10 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'memory', 'bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'memory', 'bytes']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should not add sourceColumn field if it has only scripted field', () => { @@ -2118,7 +2250,27 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes']), + parentFormat: { id: 'multi_terms' }, + }); + }); + + it('should assign a parent formatter if a custom formatter is present', () => { + const indexPattern = createMockedIndexPattern(); + + const targetColumn = createMultiTermsColumn(['source', 'dest']); + targetColumn.params.format = { id: 'bytes', params: { decimals: 2 } }; + expect( + termsOperation.getParamsForMultipleFields?.({ + targetColumn, + sourceColumn: createMultiTermsColumn(['scripted']), + indexPattern, + }) + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest']), + parentFormat: { id: 'multi_terms' }, + }); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts index 584893f182666..1284870327653 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts @@ -30,10 +30,6 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { }; parentFormat?: { id: string; - params?: { - id?: string; - template?: string; - }; }; }; } diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 87edc94fd1ae6..39eed3cbc2a35 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -250,3 +250,34 @@ export const getLensFilterMigrations = ( state: { ...lensDoc.attributes.state, filters: migrate(lensDoc.attributes.state.filters) }, }, })); + +export const fixLensTopValuesCustomFormatting = (attributes: LensDocShape810): LensDocShape810 => { + const newAttributes = cloneDeep(attributes); + const datasourceLayers = newAttributes.state.datasourceStates.indexpattern.layers || {}; + (newAttributes as LensDocShape810).state.datasourceStates.indexpattern.layers = + Object.fromEntries( + Object.entries(datasourceLayers).map(([layerId, layer]) => { + return [ + layerId, + { + ...layer, + columns: Object.fromEntries( + Object.entries(layer.columns).map(([columnId, column]) => { + if (column.operationType === 'terms') { + return [ + columnId, + { + ...column, + params: { ...column.params, parentFormat: { id: 'terms' } }, + }, + ]; + } + return [columnId, column]; + }) + ), + }, + ]; + }) + ); + return newAttributes as LensDocShape810; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 11572b7a1f7d9..5cd63b2786fe4 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -1704,4 +1704,79 @@ describe('Lens migrations', () => { }, }); }); + + describe('8.1.0 add parentFormat to terms operation', () => { + const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: null, + state: { + datasourceMetaData: { + filterableIndexPatterns: [], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'logstash-*', + layers: { + '2': { + columns: { + '3': { + dataType: 'string', + isBucketed: true, + label: 'Top values of geoip.country_iso_code', + operationType: 'terms', + params: {}, + scale: 'ordinal', + sourceField: 'geoip.country_iso_code', + }, + '4': { + label: 'Anzahl der Aufnahmen', + dataType: 'number', + operationType: 'count', + sourceField: 'Aufnahmen', + isBucketed: false, + scale: 'ratio', + }, + '5': { + label: 'Sum of bytes', + dataType: 'numver', + operationType: 'sum', + sourceField: 'bytes', + isBucketed: false, + scale: 'ratio', + }, + }, + columnOrder: ['3', '4', '5'], + }, + }, + }, + }, + visualization: {}, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }, + } as unknown as SavedObjectUnsanitizedDoc; + + it('should change field for count operations but not for others, not changing the vis', () => { + const result = migrations['8.1.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + + expect( + Object.values( + result.attributes.state.datasourceStates.indexpattern.layers['2'].columns + ).find(({ operationType }) => operationType === 'terms') + ).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ parentFormat: { id: 'terms' } }), + }) + ); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 8e7d555b33694..2617fb42bce09 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -39,6 +39,7 @@ import { getLensFilterMigrations, getLensCustomVisualizationMigrations, commonRenameRecordsField, + fixLensTopValuesCustomFormatting, } from './common_migrations'; interface LensDocShapePre710 { @@ -458,6 +459,11 @@ const renameRecordsField: SavedObjectMigrationFn = (doc) => { + const newDoc = cloneDeep(doc); + return { ...newDoc, attributes: fixLensTopValuesCustomFormatting(newDoc.attributes) }; +}; + const lensMigrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -471,7 +477,7 @@ const lensMigrations: SavedObjectMigrationMap = { '7.14.0': removeTimezoneDateHistogramParam, '7.15.0': addLayerTypeToVisualization, '7.16.0': moveDefaultReversedPaletteToCustom, - '8.1.0': flow(renameFilterReferences, renameRecordsField), + '8.1.0': flow(renameFilterReferences, renameRecordsField, addParentFormatter), }; export const getAllMigrations = ( diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index 11c23d98dab37..7cbb2052dbfff 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -200,9 +200,38 @@ export interface LensDocShape715 { export type LensDocShape810 = Omit< LensDocShape715, - 'filters' + 'filters' | 'state' > & { filters: Filter[]; + state: Omit & { + datasourceStates: { + indexpattern: Omit & { + layers: Record< + string, + Omit< + LensDocShape715['state']['datasourceStates']['indexpattern']['layers'][string], + 'columns' + > & { + columns: Record< + string, + | { + operationType: 'terms'; + params: { + secondaryFields?: string[]; + }; + [key: string]: unknown; + } + | { + operationType: OperationTypePost712; + params: Record; + [key: string]: unknown; + } + >; + } + >; + }; + }; + }; }; export type VisState716 = From ca9c004f1a247b018f1e4b38fcab5e4a5e988b4f Mon Sep 17 00:00:00 2001 From: Tobias Stadler Date: Tue, 8 Feb 2022 15:04:25 +0100 Subject: [PATCH 042/161] Always allow internal urls in Vega (#124705) --- ...-core-public.iexternalurl.isinternalurl.md | 24 +++++++++++++++++++ .../kibana-plugin-core-public.iexternalurl.md | 1 + .../public/http/external_url_service.test.ts | 17 +++++++++++++ src/core/public/http/external_url_service.ts | 24 +++++++++++++++---- src/core/public/http/http_service.mock.ts | 1 + src/core/public/http/types.ts | 7 ++++++ src/core/public/public.api.md | 1 + .../dashboard_empty_screen.test.tsx.snap | 3 +++ .../__snapshots__/home.test.tsx.snap | 3 +++ .../__snapshots__/flyout.test.tsx.snap | 1 + ...telemetry_management_section.test.tsx.snap | 1 + .../vega/public/vega_view/vega_base_view.js | 2 +- .../public/lib/url_drilldown.test.ts | 4 ++++ 13 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md new file mode 100644 index 0000000000000..396e5586f1fed --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) > [isInternalUrl](./kibana-plugin-core-public.iexternalurl.isinternalurl.md) + +## IExternalUrl.isInternalUrl() method + +Determines if the provided URL is an internal url. + +Signature: + +```typescript +isInternalUrl(relativeOrAbsoluteUrl: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| relativeOrAbsoluteUrl | string | | + +Returns: + +boolean + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md index 5a598281c7be7..d0d4e6a3a4464 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md @@ -16,5 +16,6 @@ export interface IExternalUrl | Method | Description | | --- | --- | +| [isInternalUrl(relativeOrAbsoluteUrl)](./kibana-plugin-core-public.iexternalurl.isinternalurl.md) | Determines if the provided URL is an internal url. | | [validateUrl(relativeOrAbsoluteUrl)](./kibana-plugin-core-public.iexternalurl.validateurl.md) | Determines if the provided URL is a valid location to send users. Validation is based on the configured allow list in kibana.yml.If the URL is valid, then a URL will be returned. Otherwise, this will return null. | diff --git a/src/core/public/http/external_url_service.test.ts b/src/core/public/http/external_url_service.test.ts index ee757c5046760..4ce3709ff6366 100644 --- a/src/core/public/http/external_url_service.test.ts +++ b/src/core/public/http/external_url_service.test.ts @@ -73,6 +73,23 @@ const internalRequestScenarios = [ ]; describe('External Url Service', () => { + describe('#isInternalUrl', () => { + const { setup } = setupService({ + location: new URL('https://example.com/app/management?q=1&bar=false#some-hash'), + serverBasePath: '', + policy: [], + }); + + it('internal request', () => { + expect(setup.isInternalUrl('/')).toBeTruthy(); + expect(setup.isInternalUrl('https://example.com/')).toBeTruthy(); + }); + + it('external request', () => { + expect(setup.isInternalUrl('https://elastic.co/')).toBeFalsy(); + }); + }); + describe('#validateUrl', () => { describe('internal requests with a server base path', () => { const serverBasePath = '/base-path'; diff --git a/src/core/public/http/external_url_service.ts b/src/core/public/http/external_url_service.ts index 166e167b3b994..0fb1c85d48257 100644 --- a/src/core/public/http/external_url_service.ts +++ b/src/core/public/http/external_url_service.ts @@ -50,20 +50,33 @@ function normalizeProtocol(protocol: string) { return protocol.endsWith(':') ? protocol.slice(0, -1).toLowerCase() : protocol.toLowerCase(); } +const createIsInternalUrlValidation = ( + location: Pick, + serverBasePath: string +) => { + return function isInternallUrl(next: string) { + const base = new URL(location.href); + const url = new URL(next, base); + + return ( + url.origin === base.origin && + (!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`)) + ); + }; +}; + const createExternalUrlValidation = ( rules: IExternalUrlPolicy[], location: Pick, serverBasePath: string ) => { + const isInternalUrl = createIsInternalUrlValidation(location, serverBasePath); + return function validateExternalUrl(next: string) { const base = new URL(location.href); const url = new URL(next, base); - const isInternalURL = - url.origin === base.origin && - (!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`)); - - if (isInternalURL) { + if (isInternalUrl(next)) { return url; } @@ -90,6 +103,7 @@ export class ExternalUrlService implements CoreService { const { policy } = injectedMetadata.getExternalUrlConfig(); return { + isInternalUrl: createIsInternalUrlValidation(location, serverBasePath), validateUrl: createExternalUrlValidation(policy, location, serverBasePath), }; } diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index fff99d84a76a6..bfd81a1003736 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -36,6 +36,7 @@ const createServiceMock = ({ isAnonymous: jest.fn(), }, externalUrl: { + isInternalUrl: jest.fn(), validateUrl: jest.fn(), }, addLoadingCountSource: jest.fn(), diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 876799765ea1c..afe1d653c599c 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -110,6 +110,13 @@ export interface IBasePath { * @public */ export interface IExternalUrl { + /** + * Determines if the provided URL is an internal url. + * + * @param relativeOrAbsoluteUrl + */ + isInternalUrl(relativeOrAbsoluteUrl: string): boolean; + /** * Determines if the provided URL is a valid location to send users. * Validation is based on the configured allow list in kibana.yml. diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index db132e267807e..d8cf4706ceb16 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -686,6 +686,7 @@ export interface IBasePath { // @public export interface IExternalUrl { + isInternalUrl(relativeOrAbsoluteUrl: string): boolean; validateUrl(relativeOrAbsoluteUrl: string): URL | null; } diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 70a21438754bd..e416dced4f8a1 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -19,6 +19,7 @@ exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -343,6 +344,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -706,6 +708,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index 373fc8ea59b6f..ab6ad1b6cc0c5 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap @@ -396,6 +396,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -464,6 +465,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -533,6 +535,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 9651b8658032e..c816bbe65d4f1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -182,6 +182,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 0edad23d3312b..cccbd07fe14b3 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -280,6 +280,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index ce6454ef0a0e1..a87c8318e319c 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -207,7 +207,7 @@ export class VegaBaseView { const vegaLoader = loader(); const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); vegaLoader.sanitize = async (uri, options) => { - if (uri.bypassToken === bypassToken) { + if (uri.bypassToken === bypassToken || this._externalUrl.isInternalUrl(uri)) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. uri = uri.url; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index c4eee1282de87..c8d187549a66b 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -68,6 +68,10 @@ const mockNavigateToUrl = jest.fn(() => Promise.resolve()); class TextExternalUrl implements IExternalUrl { constructor(private readonly isCorrect: boolean = true) {} + public isInternalUrl(url: string): boolean { + return false; + } + public validateUrl(url: string): URL | null { return this.isCorrect ? new URL(url) : null; } From 815685f26487dc668595527a7156e722e12a142e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 8 Feb 2022 17:05:16 +0300 Subject: [PATCH 043/161] [TSVB] Lucene query on dashboard level is not respected for annotations request (#124802) Closes: #124693 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../annotations/query.test.ts | 439 ++++++++++++++++++ .../request_processors/annotations/query.ts | 23 +- .../request_processors/table/query.test.ts | 340 ++++++++++++++ .../request_processors/table/query.ts | 13 +- 4 files changed, 796 insertions(+), 19 deletions(-) create mode 100644 src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts create mode 100644 src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts new file mode 100644 index 0000000000000..83271d492718b --- /dev/null +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts @@ -0,0 +1,439 @@ +/* + * 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 { query } from './query'; + +import type { + AnnotationsRequestProcessorsFunction, + AnnotationsRequestProcessorsParams, +} from './types'; +import { DefaultSearchCapabilities } from '../../../search_strategies/capabilities/default_search_capabilities'; + +describe('query', () => { + let req: AnnotationsRequestProcessorsParams['req']; + let panel: AnnotationsRequestProcessorsParams['panel']; + let annotation: AnnotationsRequestProcessorsParams['annotation']; + let esQueryConfig: AnnotationsRequestProcessorsParams['esQueryConfig']; + let annotationIndex: AnnotationsRequestProcessorsParams['annotationIndex']; + let capabilities: AnnotationsRequestProcessorsParams['capabilities']; + let uiSettings: AnnotationsRequestProcessorsParams['uiSettings']; + + const next = jest.fn((x) => x) as unknown as ReturnType< + ReturnType + >; + + beforeEach(() => { + req = { + body: { + timerange: { + timezone: 'Europe/Minsk', + min: '2022-01-29T22:03:02.317Z', + max: '2022-02-07T09:00:00.000Z', + }, + }, + } as AnnotationsRequestProcessorsParams['req']; + panel = {} as AnnotationsRequestProcessorsParams['panel']; + annotation = { + time_field: 'fooField', + } as AnnotationsRequestProcessorsParams['annotation']; + annotationIndex = { + indexPattern: undefined, + indexPatternString: 'foo*', + }; + capabilities = { + getValidTimeInterval: jest.fn((x) => x), + } as unknown as DefaultSearchCapabilities; + uiSettings = { + get: jest.fn().mockResolvedValue(100), + } as unknown as AnnotationsRequestProcessorsParams['uiSettings']; + }); + + test('should set "size" to 0', async () => { + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.size).toBe(0); + }); + + test('should apply global query (Lucene)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'lucene', + }, + ]; + + annotation.ignore_global_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "query_string": Object { + "query": "hour_of_day : 1", + }, + }, + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global query (KQL)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + annotation.ignore_global_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global filters', async () => { + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + annotation.ignore_global_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "referer", + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should add panel filters and merge it with global one', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + panel.filter = { + query: 'agent : 2', + language: 'kuery', + }; + + annotation.ignore_global_filters = 0; + annotation.ignore_panel_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "agent": "2", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should ignore global and panel filters/queries ', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + panel.filter = { + query: 'agent : 2', + language: 'kuery', + }; + + annotation.ignore_global_filters = 1; + annotation.ignore_panel_filters = 1; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should add annotation query ', async () => { + annotation.query_string = { + query: 'hour_of_day : 1', + language: 'kuery', + }; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); +}); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts index 1400702a47fd5..eaf2c5ae2e7bf 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts @@ -34,14 +34,12 @@ export const query: AnnotationsRequestProcessorsFunction = ({ const { bucketSize } = getBucketSize(req, 'auto', capabilities, barTargetUiSettings); const { from, to } = getTimerange(req); - doc.size = 0; const queries = !annotation.ignore_global_filters ? req.body.query : []; const filters = !annotation.ignore_global_filters ? req.body.filters : []; + const esQuery = buildEsQuery(indexPattern, queries, filters, esQueryConfig); - doc.query = buildEsQuery(indexPattern, queries, filters, esQueryConfig); - - const boolFilters: unknown[] = [ - { + if (timeField) { + esQuery.bool.must.push({ range: { [timeField]: { gte: from.toISOString(), @@ -49,25 +47,28 @@ export const query: AnnotationsRequestProcessorsFunction = ({ format: 'strict_date_optional_time', }, }, - }, - ]; + }); + } if (annotation.query_string) { - boolFilters.push(buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig)); + esQuery.bool.must.push( + buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig) + ); } if (!annotation.ignore_panel_filters && panel.filter) { - boolFilters.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); + esQuery.bool.must.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); } if (annotation.fields) { const fields = annotation.fields.split(/[,\s]+/) || []; fields.forEach((field) => { - boolFilters.push({ exists: { field } }); + esQuery.bool.must.push({ exists: { field } }); }); } - overwrite(doc, 'query.bool.must', boolFilters); + overwrite(doc, 'size', 0); + overwrite(doc, 'query', esQuery); return next(doc); }; diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts new file mode 100644 index 0000000000000..013a5f0314d2d --- /dev/null +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts @@ -0,0 +1,340 @@ +/* + * 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 { query } from './query'; + +import type { TableRequestProcessorsFunction, TableRequestProcessorsParams } from './types'; + +describe('query', () => { + let req: TableRequestProcessorsParams['req']; + let panel: TableRequestProcessorsParams['panel']; + let seriesIndex: TableRequestProcessorsParams['seriesIndex']; + let buildSeriesMetaParams: TableRequestProcessorsParams['buildSeriesMetaParams']; + + const next = jest.fn((x) => x) as unknown as ReturnType< + ReturnType + >; + + beforeEach(() => { + req = { + body: { + timerange: { + timezone: 'Europe/Minsk', + min: '2022-01-29T22:03:02.317Z', + max: '2022-02-07T09:00:00.000Z', + }, + }, + } as TableRequestProcessorsParams['req']; + panel = {} as TableRequestProcessorsParams['panel']; + seriesIndex = { + indexPattern: undefined, + indexPatternString: 'foo*', + }; + buildSeriesMetaParams = jest.fn().mockResolvedValue({ timeField: 'fooField' }); + }); + + test('should set "size" to 0', async () => { + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.size).toBe(0); + }); + + test('should apply global query (Lucene)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'lucene', + }, + ]; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "query_string": Object { + "query": "hour_of_day : 1", + }, + }, + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global query (KQL)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global filters', async () => { + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "referer", + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should add panel filters and merge it with global one', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + panel.filter = { + query: 'agent : 2', + language: 'kuery', + }; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "agent": "2", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should ignore global filters/queries in case is panel.ignore_global_filter = 1 ', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + panel.ignore_global_filter = 1; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); +}); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts index d92aa1317b971..1036a4f8a105c 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts @@ -17,14 +17,10 @@ export const query: TableRequestProcessorsFunction = const { timeField } = await buildSeriesMetaParams(); const { from, to } = getTimerange(req); const indexPattern = seriesIndex.indexPattern || undefined; - - doc.size = 0; - const queries = !panel.ignore_global_filter ? req.body.query : []; const filters = !panel.ignore_global_filter ? req.body.filters : []; - doc.query = buildEsQuery(indexPattern, queries, filters, esQueryConfig); - const boolFilters: unknown[] = []; + const esQuery = buildEsQuery(indexPattern, queries, filters, esQueryConfig); if (timeField) { const timerange = { @@ -37,13 +33,14 @@ export const query: TableRequestProcessorsFunction = }, }; - boolFilters.push(timerange); + esQuery.bool.must.push(timerange); } if (panel.filter) { - boolFilters.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); + esQuery.bool.must.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); } - overwrite(doc, 'query.bool.must', boolFilters); + overwrite(doc, 'size', 0); + overwrite(doc, 'query', esQuery); return next(doc); }; From ee9f01e925eb6572e84d9845ea52e12ea43679bf Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 8 Feb 2022 15:12:25 +0100 Subject: [PATCH 044/161] Unskip test scripted fields preview (#124358) * unskip test and change we how assert for expected states * REVERT - added .only for flaky test runner * remove .only Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../management/_scripted_fields_preview.js | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js index a442b521d5d98..b6c941fe21d0a 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -13,8 +13,15 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - // FLAKY: https://github.com/elastic/kibana/issues/118981 - describe.skip('scripted fields preview', () => { + const scriptResultToJson = (scriptResult) => { + try { + return JSON.parse(scriptResult); + } catch (e) { + expect().fail(`Could JSON.parse script result: "${scriptResult}"`); + } + }; + + describe('scripted fields preview', () => { before(async function () { await browser.setWindowSize(1200, 800); await PageObjects.settings.navigateTo(); @@ -46,7 +53,15 @@ export default function ({ getService, getPageObjects }) { const scriptResults = await PageObjects.settings.executeScriptedField( `doc['bytes'].value * 2` ); - expect(scriptResults.replace(/\s/g, '')).to.contain('"myScriptedField":[6196'); + const [ + { + _id, + [SCRIPTED_FIELD_NAME]: { [0]: scriptedField }, + }, + ] = scriptResultToJson(scriptResults); + expect(_id).to.be.a('string'); + expect(scriptedField).to.be.a('number'); + expect(scriptedField).to.match(/[0-9]+/); }); it('should display additional fields', async function () { @@ -54,7 +69,9 @@ export default function ({ getService, getPageObjects }) { `doc['bytes'].value * 2`, ['bytes'] ); - expect(scriptResults.replace(/\s/g, '')).to.contain('"bytes":3098'); + const [{ _id, bytes }] = scriptResultToJson(scriptResults); + expect(_id).to.be.a('string'); + expect(bytes).to.be.a('number'); }); }); } From df6d386d5087a250ac5eec31edddb830c16c4d21 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 8 Feb 2022 09:14:01 -0500 Subject: [PATCH 045/161] [Fleet] Fix docker registry timeout in integration tests (#124889) --- .../server/integration_tests/docker_registry_helper.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts index bb34dc3258d05..902be3aa35bcd 100644 --- a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts +++ b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts @@ -12,6 +12,8 @@ import fetch from 'node-fetch'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const DOCKER_START_TIMEOUT = 5 * 60 * 1000; // 5 minutes + export function useDockerRegistry() { const packageRegistryPort = process.env.FLEET_PACKAGE_REGISTRY_PORT || '8081'; @@ -32,8 +34,9 @@ export function useDockerRegistry() { isExited = true; }); - let retries = 0; - while (!isExited && retries++ <= 20) { + const startedAt = Date.now(); + + while (!isExited && Date.now() - startedAt <= DOCKER_START_TIMEOUT) { try { const res = await fetch(`http://localhost:${packageRegistryPort}/`); if (res.status === 200) { From b8abe763c9b13b805baf157465489aac5060590c Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Tue, 8 Feb 2022 08:44:50 -0600 Subject: [PATCH 046/161] split date_nested kbn_archive out of es_archive (#124646) * split kbn_archive out of es_archive * add missing unload of kbn_archive Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/discover/_date_nested.ts | 5 +++++ .../apps/discover/_saved_queries.ts | 4 ++++ .../apps/discover/_search_on_page_load.ts | 6 +++++- .../es_archiver/date_nested/data.json | 21 ++----------------- .../es_archiver/date_nested/mappings.json | 9 +++++++- .../fixtures/kbn_archiver/date_nested.json | 15 +++++++++++++ 6 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 test/functional/fixtures/kbn_archiver/date_nested.json diff --git a/test/functional/apps/discover/_date_nested.ts b/test/functional/apps/discover/_date_nested.ts index 8297d84832ff6..83b9bdd44a5be 100644 --- a/test/functional/apps/discover/_date_nested.ts +++ b/test/functional/apps/discover/_date_nested.ts @@ -13,10 +13,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const security = getService('security'); + const kibanaServer = getService('kibanaServer'); describe('timefield is a date in a nested field', function () { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested.json' + ); await security.testUser.setRoles(['kibana_admin', 'kibana_date_nested']); await PageObjects.common.navigateToApp('discover'); }); @@ -24,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async function unloadMakelogs() { await security.testUser.restoreDefaults(); await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); }); it('should show an error message', async function () { diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index b7d19807e563e..5a8b14545508c 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -42,6 +42,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested.json' + ); await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); @@ -53,6 +56,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); await PageObjects.common.unsetTime(); diff --git a/test/functional/apps/discover/_search_on_page_load.ts b/test/functional/apps/discover/_search_on_page_load.ts index 277d2e72d729f..0198881e981b8 100644 --- a/test/functional/apps/discover/_search_on_page_load.ts +++ b/test/functional/apps/discover/_search_on_page_load.ts @@ -43,6 +43,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // and load a set of data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested.json' + ); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.common.navigateToApp('discover'); @@ -50,7 +53,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); - await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); + await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/functional/fixtures/es_archiver/date_nested/data.json b/test/functional/fixtures/es_archiver/date_nested/data.json index bb623f93627c7..e2ffb210d1208 100644 --- a/test/functional/fixtures/es_archiver/date_nested/data.json +++ b/test/functional/fixtures/es_archiver/date_nested/data.json @@ -1,30 +1,13 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:date-nested", - "index": ".kibana", - "source": { - "index-pattern": { - "fields":"[]", - "timeFieldName": "nested.timestamp", - "title": "date-nested" - }, - "type": "index-pattern" - } - } -} - - { "type": "doc", "value": { "id": "date-nested-1", "index": "date-nested", "source": { - "message" : "test", + "message": "test", "nested": { "timestamp": "2021-06-30T12:00:00.123Z" } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/date_nested/mappings.json b/test/functional/fixtures/es_archiver/date_nested/mappings.json index f30e5863f4f8b..b3f995cae173d 100644 --- a/test/functional/fixtures/es_archiver/date_nested/mappings.json +++ b/test/functional/fixtures/es_archiver/date_nested/mappings.json @@ -1,6 +1,8 @@ { "type": "index", "value": { + "aliases": { + }, "index": "date-nested", "mappings": { "properties": { @@ -8,6 +10,11 @@ "type": "text" }, "nested": { + "properties": { + "timestamp": { + "type": "date" + } + }, "type": "nested" } } @@ -19,4 +26,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/date_nested.json b/test/functional/fixtures/kbn_archiver/date_nested.json new file mode 100644 index 0000000000000..c015a5b0bbd62 --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/date_nested.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "fields": "[]", + "timeFieldName": "nested.timestamp", + "title": "date-nested" + }, + "coreMigrationVersion": "8.2.0", + "id": "date-nested", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzIyLDFd" +} \ No newline at end of file From a88756adb6e508b22f033faca29cca335eccf080 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Tue, 8 Feb 2022 15:52:33 +0100 Subject: [PATCH 047/161] [Fleet] Create package policy tests (#124697) * unit tests for create package policy submit * unit tests for create package policy submit * added tests for vars * added tests for navigate * tests for step_define_package_policy * added tests for step_configure_package * extracted duplicated max package name fn to common * added tests for step_select_hosts --- x-pack/plugins/fleet/common/services/index.ts | 1 + .../common/services/max_package_name.test.ts | 39 ++ .../fleet/common/services/max_package_name.ts | 20 + .../create_package_policy_page/index.test.tsx | 454 +++++++++++++++++- .../step_configure_package.test.tsx | 153 ++++++ .../step_define_package_policy.test.tsx | 202 ++++++++ .../step_define_package_policy.tsx | 17 +- .../step_select_hosts.test.tsx | 157 ++++++ .../fleet/server/services/package_policy.ts | 13 +- 9 files changed, 1025 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugins/fleet/common/services/max_package_name.test.ts create mode 100644 x-pack/plugins/fleet/common/services/max_package_name.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 7698308270fff..472047f6f496e 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -35,3 +35,4 @@ export { export { normalizeHostsForAgents } from './hosts_utils'; export { splitPkgKey } from './split_pkg_key'; +export { getMaxPackageName } from './max_package_name'; diff --git a/x-pack/plugins/fleet/common/services/max_package_name.test.ts b/x-pack/plugins/fleet/common/services/max_package_name.test.ts new file mode 100644 index 0000000000000..f2631257d7e3f --- /dev/null +++ b/x-pack/plugins/fleet/common/services/max_package_name.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { getMaxPackageName } from '.'; + +describe('get max package policy name', () => { + it('should return index 1 when no policies', () => { + const name = getMaxPackageName('apache', []); + expect(name).toEqual('apache-1'); + }); + + it('should return index 1 when policies with other name', () => { + const name = getMaxPackageName('apache', [{ name: 'package' } as any]); + expect(name).toEqual('apache-1'); + }); + + it('should return index 2 when policies 1 exists', () => { + const name = getMaxPackageName('apache', [{ name: 'apache-1' } as any]); + expect(name).toEqual('apache-2'); + }); + + it('should return index 11 when policy 10 is max', () => { + const name = getMaxPackageName('apache', [ + { name: 'apache-10' } as any, + { name: 'apache-9' } as any, + { name: 'package' } as any, + ]); + expect(name).toEqual('apache-11'); + }); + + it('should return index 1 when policies undefined', () => { + const name = getMaxPackageName('apache'); + expect(name).toEqual('apache-1'); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/max_package_name.ts b/x-pack/plugins/fleet/common/services/max_package_name.ts new file mode 100644 index 0000000000000..6078d4e6b7bbf --- /dev/null +++ b/x-pack/plugins/fleet/common/services/max_package_name.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getMaxPackageName(packageName: string, packagePolicies?: Array<{ name: string }>) { + // Retrieve highest number appended to package policy name and increment it by one + const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`); + + const maxPkgPolicyName = Math.max( + ...(packagePolicies ?? []) + .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) + .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)), + 0 + ); + + return `${packageName}-${maxPkgPolicyName + 1}`; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx index b8bae0cb1f541..7a15276afbcd1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx @@ -5,19 +5,152 @@ * 2.0. */ -import { Route } from 'react-router-dom'; +import { Route, useLocation, useHistory } from 'react-router-dom'; import React from 'react'; -import { act } from 'react-test-renderer'; +import { fireEvent, act, waitFor } from '@testing-library/react'; import type { MockedFleetStartServices, TestRenderer } from '../../../../../mock'; import { createFleetTestRendererMock } from '../../../../../mock'; import { FLEET_ROUTING_PATHS, pagePathGetters, PLUGIN_ID } from '../../../constants'; import type { CreatePackagePolicyRouteState } from '../../../types'; +import { + sendCreatePackagePolicy, + sendCreateAgentPolicy, + sendGetAgentStatus, + useIntraAppState, + useStartServices, +} from '../../../hooks'; + import { CreatePackagePolicyPage } from './index'; +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), + sendGetStatus: jest + .fn() + .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), + sendGetAgentStatus: jest.fn().mockResolvedValue({ data: { results: { total: 0 } } }), + useGetAgentPolicies: jest.fn().mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any), + sendGetOneAgentPolicy: jest.fn().mockResolvedValue({ + data: { item: { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' } }, + }), + useGetPackageInfoByKey: jest.fn().mockReturnValue({ + data: { + item: { + name: 'nginx', + title: 'Nginx', + version: '1.3.0', + release: 'ga', + description: 'Collect logs and metrics from Nginx HTTP servers with Elastic Agent.', + policy_templates: [ + { + name: 'nginx', + title: 'Nginx logs and metrics', + description: 'Collect logs and metrics from Nginx instances', + inputs: [ + { + type: 'logfile', + title: 'Collect logs from Nginx instances', + description: 'Collecting Nginx access and error logs', + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'nginx.access', + title: 'Nginx access logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/access.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx access logs', + description: 'Collect Nginx access logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'access', + }, + ], + latestVersion: '1.3.0', + removable: true, + keepPoliciesUpToDate: false, + status: 'not_installed', + }, + }, + isLoading: false, + }), + sendCreatePackagePolicy: jest + .fn() + .mockResolvedValue({ data: { item: { id: 'policy-1', inputs: [] } } }), + sendCreateAgentPolicy: jest.fn().mockResolvedValue({ + data: { item: { id: 'agent-policy-2', name: 'Agent policy 2', namespace: 'default' } }, + }), + useIntraAppState: jest.fn().mockReturnValue({}), + useStartServices: jest.fn().mockReturnValue({ + application: { navigateToApp: jest.fn() }, + notifications: { + toasts: { + addError: jest.fn(), + addSuccess: jest.fn(), + }, + }, + docLinks: { + links: { + fleet: {}, + }, + }, + http: { + basePath: { + get: () => 'http://localhost:5620', + prepend: (url: string) => 'http://localhost:5620' + url, + }, + }, + chrome: { + docTitle: { + change: jest.fn(), + }, + setBreadcrumbs: jest.fn(), + }, + }), + }; +}); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn().mockReturnValue({ search: '' }), + useHistory: jest.fn().mockReturnValue({ + push: jest.fn(), + }), +})); + describe('when on the package policy create page', () => { - const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-0.3.7' })[1]; + const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-1.3.0' })[1]; let testRenderer: TestRenderer; let renderResult: ReturnType; @@ -47,6 +180,8 @@ describe('when on the package policy create page', () => { pathname: createPageUrlPath, state: expectedRouteState, }); + + (useIntraAppState as jest.MockedFunction).mockReturnValue(expectedRouteState); }); describe('and the cancel Link or Button is clicked', () => { @@ -67,16 +202,325 @@ describe('when on the package policy create page', () => { }); }); - it('should use custom "cancel" URL', () => { + test('should use custom "cancel" URL', () => { expect(cancelLink.href).toBe(expectedRouteState.onCancelUrl); expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl); }); }); }); + + describe('submit page', () => { + const newPackagePolicy = { + description: '', + enabled: true, + inputs: [ + { + enabled: true, + policy_template: 'nginx', + streams: [ + { + data_stream: { + dataset: 'nginx.access', + type: 'logs', + }, + enabled: true, + vars: { + paths: { + type: 'text', + value: ['/var/log/nginx/access.log*'], + }, + }, + }, + ], + type: 'logfile', + }, + ], + name: 'nginx-1', + namespace: 'default', + output_id: '', + package: { + name: 'nginx', + title: 'Nginx', + version: '1.3.0', + }, + policy_id: 'agent-policy-1', + vars: undefined, + }; + + test('should create package policy on submit when query param agent policy id is set', async () => { + (useLocation as jest.MockedFunction).mockImplementationOnce(() => ({ + search: 'policyId=agent-policy-1', + })); + + render(); + + let saveBtn: HTMLElement; + + await waitFor(() => { + saveBtn = renderResult.getByText(/Save and continue/).closest('button')!; + expect(saveBtn).not.toBeDisabled(); + }); + + await act(async () => { + fireEvent.click(saveBtn); + }); + + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + policy_id: 'agent-policy-1', + }); + expect(sendCreateAgentPolicy as jest.MockedFunction).not.toHaveBeenCalled(); + + await waitFor(() => { + expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); + }); + }); + + describe('on save navigate', () => { + async function setupSaveNavigate(routeState: any) { + (useIntraAppState as jest.MockedFunction).mockReturnValue(routeState); + render(); + + await act(async () => { + fireEvent.click(renderResult.getByText('Existing hosts')!); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + await act(async () => { + fireEvent.click( + renderResult.getByText(/Add Elastic Agent to your hosts/).closest('button')! + ); + }); + } + + test('should navigate to save navigate path if set', async () => { + const routeState = { + onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }], + }; + + await setupSaveNavigate(routeState); + + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, { + path: '/save/url/here', + }); + }); + + test('should navigate to save navigate path with query param if set', async () => { + const mockUseLocation = useLocation as jest.MockedFunction; + mockUseLocation.mockReturnValue({ + search: 'policyId=agent-policy-1', + }); + + const routeState = { + onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }], + }; + + await setupSaveNavigate(routeState); + + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, { + path: '/policies/agent-policy-1', + }); + + mockUseLocation.mockReturnValue({ + search: '', + }); + }); + + test('should navigate to save navigate app if set', async () => { + const routeState = { + onSaveNavigateTo: [PLUGIN_ID], + }; + await setupSaveNavigate(routeState); + + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID); + }); + + test('should set history if no routeState', async () => { + await setupSaveNavigate({}); + + expect(useHistory().push).toHaveBeenCalledWith('/policies/agent-policy-1'); + }); + }); + + describe('without query param', () => { + beforeEach(() => { + render(); + + (sendCreateAgentPolicy as jest.MockedFunction).mockClear(); + (sendCreatePackagePolicy as jest.MockedFunction).mockClear(); + }); + + test('should create agent policy before creating package policy on submit when new hosts is selected', async () => { + await waitFor(() => { + renderResult.getByDisplayValue('Agent policy 2'); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + expect(sendCreateAgentPolicy as jest.MockedFunction).toHaveBeenCalledWith( + { + description: '', + monitoring_enabled: ['logs', 'metrics'], + name: 'Agent policy 2', + namespace: 'default', + }, + { withSysMonitoring: true } + ); + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + policy_id: 'agent-policy-2', + }); + + await waitFor(() => { + expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); + }); + }); + + test('should disable submit button on invalid form with empty agent policy name', async () => { + await act(async () => { + fireEvent.change(renderResult.getByLabelText('New agent policy name'), { + target: { value: '' }, + }); + }); + + renderResult.getByText( + 'Your integration policy has errors. Please fix them before saving.' + ); + expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled(); + }); + + test('should not show modal if agent policy has agents', async () => { + (sendGetAgentStatus as jest.MockedFunction).mockResolvedValueOnce({ + data: { results: { total: 1 } }, + }); + + await act(async () => { + fireEvent.click(renderResult.getByText('Existing hosts')!); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + await waitFor(() => { + expect(renderResult.getByText('This action will update 1 agent')).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click( + renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')! + ); + }); + + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalled(); + }); + + describe('create package policy with existing agent policy', () => { + beforeEach(async () => { + await act(async () => { + fireEvent.click(renderResult.getByText('Existing hosts')!); + }); + }); + + test('should creating package policy with existing host', async () => { + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + expect(sendCreateAgentPolicy as jest.MockedFunction).not.toHaveBeenCalled(); + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + policy_id: 'agent-policy-1', + }); + + await waitFor(() => { + expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); + }); + }); + + test('should disable submit button on invalid form with empty name', async () => { + await act(async () => { + fireEvent.change(renderResult.getByLabelText('Integration name'), { + target: { value: '' }, + }); + }); + + renderResult.getByText( + 'Your integration policy has errors. Please fix them before saving.' + ); + expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled(); + }); + + test('should disable submit button on invalid form with empty package var', async () => { + await act(async () => { + fireEvent.click(renderResult.getByLabelText('Show logfile inputs')); + }); + + await act(async () => { + fireEvent.change(renderResult.getByDisplayValue('/var/log/nginx/access.log*'), { + target: { value: '' }, + }); + }); + + renderResult.getByText( + 'Your integration policy has errors. Please fix them before saving.' + ); + expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled(); + }); + + test('should submit form with changed package var', async () => { + await act(async () => { + fireEvent.click(renderResult.getByLabelText('Show logfile inputs')); + }); + + await act(async () => { + fireEvent.change(renderResult.getByDisplayValue('/var/log/nginx/access.log*'), { + target: { value: '/path/to/log' }, + }); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + inputs: [ + { + ...newPackagePolicy.inputs[0], + streams: [ + { + ...newPackagePolicy.inputs[0].streams[0], + vars: { + paths: { + type: 'text', + value: ['/path/to/log'], + }, + }, + }, + ], + }, + ], + }); + }); + }); + }); + }); }); const mockApiCalls = (http: MockedFleetStartServices['http']) => { - http.get.mockImplementation(async (path) => { + http.get.mockImplementation(async (path: any) => { + if (path === '/api/fleet/agents/setup') { + return Promise.resolve({ data: { results: { total: 0 } } }); + } + if (path === '/api/fleet/package_policies') { + return Promise.resolve({ data: { items: [] } }); + } const err = new Error(`API [GET ${path}] is not MOCKED!`); // eslint-disable-next-line no-console console.log(err); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx new file mode 100644 index 0000000000000..543747307908e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; + +import type { TestRenderer } from '../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../mock'; +import type { NewPackagePolicy, PackageInfo } from '../../../types'; + +import { StepConfigurePackagePolicy } from './step_configure_package'; + +describe('StepConfigurePackage', () => { + let packageInfo: PackageInfo; + let packagePolicy: NewPackagePolicy; + const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => { + packagePolicy = { + ...val, + ...packagePolicy, + }; + }); + + const validationResults = { name: null, description: null, namespace: null, inputs: {} }; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + )); + + beforeEach(() => { + packageInfo = { + name: 'nginx', + title: 'Nginx', + version: '1.3.0', + release: 'ga', + description: 'Collect logs and metrics from Nginx HTTP servers with Elastic Agent.', + format_version: '', + owner: { github: '' }, + assets: {} as any, + policy_templates: [ + { + name: 'nginx', + title: 'Nginx logs and metrics', + description: 'Collect logs and metrics from Nginx instances', + inputs: [ + { + type: 'logfile', + title: 'Collect logs from Nginx instances', + description: 'Collecting Nginx access and error logs', + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'nginx.access', + title: 'Nginx access logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/access.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx access logs', + description: 'Collect Nginx access logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'access', + }, + ], + latestVersion: '1.3.0', + removable: true, + keepPoliciesUpToDate: false, + status: 'not_installed', + }; + packagePolicy = { + name: 'nginx-1', + description: 'desc', + namespace: 'default', + policy_id: '', + enabled: true, + output_id: '', + inputs: [ + { + type: 'logfile', + policy_template: 'nginx', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { + paths: { value: ['/var/log/nginx/access.log*'], type: 'text' }, + tags: { value: ['nginx-access'], type: 'text' }, + preserve_original_event: { value: false, type: 'bool' }, + processors: { type: 'yaml' }, + }, + }, + ], + }, + ], + }; + testRenderer = createFleetTestRendererMock(); + }); + + it('should show nothing to configure if no matching integration', () => { + packageInfo.policy_templates = []; + render(); + + waitFor(() => { + expect(renderResult.getByText('Nothing to configure')).toBeInTheDocument(); + }); + }); + + it('should show inputs of policy templates and update package policy with input enabled: false', async () => { + render(); + + waitFor(() => { + expect(renderResult.getByText('Collect logs from Nginx instances')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(renderResult.getByRole('switch')); + }); + expect(mockUpdatePackagePolicy.mock.calls[0][0].inputs[0].enabled).toEqual(false); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx new file mode 100644 index 0000000000000..a15692b718a32 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; + +import type { TestRenderer } from '../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../mock'; +import type { AgentPolicy, NewPackagePolicy, PackageInfo } from '../../../types'; + +import { useGetPackagePolicies } from '../../../hooks'; + +import { StepDefinePackagePolicy } from './step_define_package_policy'; + +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useGetPackagePolicies: jest.fn().mockReturnValue({ + data: { + items: [{ name: 'nginx-1' }, { name: 'other-policy' }], + }, + isLoading: false, + }), + useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), + sendGetStatus: jest + .fn() + .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), + }; +}); + +describe('StepDefinePackagePolicy', () => { + const packageInfo: PackageInfo = { + name: 'apache', + version: '1.0.0', + description: '', + format_version: '', + release: 'ga', + owner: { github: '' }, + title: 'Apache', + latestVersion: '', + assets: {} as any, + status: 'not_installed', + vars: [ + { + show_user: true, + name: 'Show user var', + type: 'string', + default: 'showUserVarVal', + }, + { + required: true, + name: 'Required var', + type: 'bool', + }, + { + name: 'Advanced var', + type: 'bool', + default: true, + }, + ], + }; + const agentPolicy: AgentPolicy = { + id: 'agent-policy-1', + namespace: 'ns', + name: 'Agent policy 1', + is_managed: false, + status: 'active', + updated_at: '', + updated_by: '', + revision: 1, + package_policies: [], + }; + let packagePolicy: NewPackagePolicy; + const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => { + packagePolicy = { + ...val, + ...packagePolicy, + }; + }); + + const validationResults = { name: null, description: null, namespace: null, inputs: {} }; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + )); + + beforeEach(() => { + packagePolicy = { + name: '', + description: 'desc', + namespace: 'default', + policy_id: '', + enabled: true, + output_id: '', + inputs: [], + }; + testRenderer = createFleetTestRendererMock(); + }); + + describe('default API response', () => { + beforeEach(() => { + render(); + }); + + it('should set index 1 name to package policy on init if no package policies exist for this package', () => { + waitFor(() => { + expect(renderResult.getByDisplayValue('apache-1')).toBeInTheDocument(); + expect(renderResult.getByDisplayValue('desc')).toBeInTheDocument(); + }); + + expect(mockUpdatePackagePolicy.mock.calls[0]).toEqual([ + { + description: 'desc', + enabled: true, + inputs: [], + name: 'apache-1', + namespace: 'default', + policy_id: 'agent-policy-1', + output_id: '', + package: { + name: 'apache', + title: 'Apache', + version: '1.0.0', + }, + vars: { + 'Advanced var': { + type: 'bool', + value: true, + }, + 'Required var': { + type: 'bool', + value: undefined, + }, + 'Show user var': { + type: 'string', + value: 'showUserVarVal', + }, + }, + }, + ]); + expect(mockUpdatePackagePolicy.mock.calls[1]).toEqual([ + { + namespace: 'ns', + policy_id: 'agent-policy-1', + }, + ]); + }); + + it('should display vars coming from package policy', async () => { + waitFor(() => { + expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); + expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Required var'); + expect(renderResult.getByText('Required var is required')).toHaveAttribute( + 'class', + 'euiFormErrorText' + ); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText('Advanced options').closest('button')!); + }); + + waitFor(() => { + expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Advanced var'); + }); + }); + }); + + it('should set incremented name if other package policies exist', () => { + (useGetPackagePolicies as jest.MockedFunction).mockReturnValueOnce({ + data: { + items: [ + { name: 'apache-1' }, + { name: 'apache-2' }, + { name: 'apache-9' }, + { name: 'apache-10' }, + ], + }, + isLoading: false, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByDisplayValue('apache-11')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 5afe87901d1b2..7fcddf4439557 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -27,7 +27,7 @@ import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../services import { Loading } from '../../../components'; import { useStartServices, useGetPackagePolicies } from '../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; -import { SO_SEARCH_LIMIT } from '../../../../../../common'; +import { SO_SEARCH_LIMIT, getMaxPackageName } from '../../../../../../common'; import { isAdvancedVar } from './services'; import type { PackagePolicyValidationResults } from './services'; @@ -99,20 +99,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ // If package has changed, create shell package policy with input&stream values based on package info if (currentPkgKey !== pkgKey) { - // Retrieve highest number appended to package policy name and increment it by one - const pkgPoliciesNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const pkgPoliciesWithMatchingNames = packagePolicyData?.items - ? packagePolicyData.items - .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) - .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)) - .sort((a, b) => a - b) - : []; - - const incrementedName = `${packageInfo.name}-${ - pkgPoliciesWithMatchingNames.length - ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 - : 1 - }`; + const incrementedName = getMaxPackageName(packageInfo.name, packagePolicyData?.items); updatePackagePolicy( packageToPackagePolicy( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx new file mode 100644 index 0000000000000..0c9f450e83dae --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; + +import type { TestRenderer } from '../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../mock'; + +import { useGetAgentPolicies } from '../../../hooks'; +import type { AgentPolicy, PackageInfo } from '../../../types'; + +import { StepSelectHosts } from './step_select_hosts'; + +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useGetAgentPolicies: jest.fn(), + }; +}); + +describe('StepSelectHosts', () => { + const packageInfo: PackageInfo = { + name: 'apache', + version: '1.0.0', + description: '', + format_version: '', + release: 'ga', + owner: { github: '' }, + title: 'Apache', + latestVersion: '', + assets: {} as any, + status: 'not_installed', + vars: [], + }; + const agentPolicy: AgentPolicy = { + id: 'agent-policy-1', + namespace: 'default', + name: 'Agent policy 1', + is_managed: false, + status: 'active', + updated_at: '', + updated_by: '', + revision: 1, + package_policies: [], + }; + const newAgentPolicy = { + name: '', + namespace: 'default', + }; + const validation = {}; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + )); + beforeEach(() => { + testRenderer = createFleetTestRendererMock(); + }); + + it('should display create form when no agent policies', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByText('Agent policy 1')).toBeInTheDocument(); + }); + expect(renderResult.queryByRole('tablist')).not.toBeInTheDocument(); + }); + + it('should display tabs with New hosts selected when agent policies exist', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + expect(renderResult.getByText('Agent policy 2')).toBeInTheDocument(); + }); + expect(renderResult.getByText('New hosts').closest('button')).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + + it('should display dropdown with agent policy selected when Existing hosts selected', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(renderResult.getByText('Existing hosts').closest('button')!); + }); + + expect(renderResult.getAllByRole('option').length).toEqual(1); + expect(renderResult.getByText('Agent policy 1').closest('select')).toBeInTheDocument(); + }); + + it('should display dropdown without preselected value when Existing hosts selected with mulitple agent policies', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [ + { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }, + { id: 'agent-policy-2', name: 'Agent policy 2', namespace: 'default' }, + ], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(renderResult.getByText('Existing hosts').closest('button')!); + }); + + expect(renderResult.getAllByRole('option').length).toEqual(2); + waitFor(() => { + expect(renderResult.getByText('An agent policy is required.')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 840642492b262..69d7a81a24efd 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -29,6 +29,7 @@ import { validatePackagePolicy, validationHasErrors, SO_SEARCH_LIMIT, + getMaxPackageName, } from '../../common'; import type { DeletePackagePoliciesResponse, @@ -1398,15 +1399,5 @@ export async function incrementPackageName( kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${packageName}"`, }); - // Retrieve highest number appended to package policy name and increment it by one - const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`); - - const maxPkgPolicyName = Math.max( - ...(packagePolicyData?.items ?? []) - .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) - .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)), - 0 - ); - - return `${packageName}-${maxPkgPolicyName + 1}`; + return getMaxPackageName(packageName, packagePolicyData?.items); } From 535acb782ce0b5d6f5ba10c76880e748d76e884e Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Tue, 8 Feb 2022 08:22:46 -0700 Subject: [PATCH 048/161] [Observability][RAC] Set display names for columns and fix reason message (#124570) * [Observability][RAC] Set display names for columns and fix reason message bug * Adding missing file * Adding a way to add additional fields to fetch to the timeline query --- .../pages/alerts/components/parse_alert.ts | 3 +- .../alerts_table_t_grid/add_display_names.ts | 29 +++++++++++++++++++ .../alerts_table_t_grid.tsx | 29 +++++++++++++++++-- .../components/t_grid/standalone/index.tsx | 9 ++++-- 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts b/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts index 2c07aed15122e..6577e022a2a6d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts +++ b/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts @@ -11,6 +11,7 @@ import { ALERT_STATUS_ACTIVE, ALERT_RULE_TYPE_ID, ALERT_RULE_NAME, + ALERT_REASON, } from '@kbn/rule-data-utils'; import type { TopAlert } from '../'; import { experimentalRuleFieldMap } from '../../../../../rule_registry/common/assets/field_maps/experimental_rule_field_map'; @@ -38,7 +39,7 @@ export const parseAlert = const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsedFields[ALERT_RULE_NAME] ?? '', + reason: parsedFields[ALERT_REASON] ?? parsedFields[ALERT_RULE_NAME] ?? '', ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts new file mode 100644 index 0000000000000..d22daeba10ecb --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.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 { ALERT_DURATION, ALERT_REASON, ALERT_STATUS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { EuiDataGridColumn } from '@elastic/eui'; +import { translations } from '../../../../config'; +import type { ColumnHeaderOptions } from '../../../../../../timelines/common'; + +export const addDisplayNames = ( + column: Pick & + ColumnHeaderOptions +) => { + if (column.id === ALERT_REASON) { + return { ...column, displayAsText: translations.alertsTable.reasonColumnDescription }; + } + if (column.id === ALERT_DURATION) { + return { ...column, displayAsText: translations.alertsTable.durationColumnDescription }; + } + if (column.id === ALERT_STATUS) { + return { ...column, displayAsText: translations.alertsTable.statusColumnDescription }; + } + if (column.id === TIMESTAMP) { + return { ...column, displayAsText: translations.alertsTable.lastUpdatedColumnDescription }; + } + return column; +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index f39a1be586d5f..d419fbee1d34e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -10,7 +10,18 @@ * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. * This way plugins can do targeted imports to reduce the final code bundle */ -import { ALERT_DURATION, ALERT_REASON, ALERT_STATUS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_UUID, + TIMESTAMP, + ALERT_START, +} from '@kbn/rule-data-utils'; import { EuiButtonIcon, @@ -53,6 +64,7 @@ import { LazyAlertsFlyout } from '../../../..'; import { parseAlert } from '../../components/parse_alert'; import { CoreStart } from '../../../../../../../../src/core/public'; import { translations, paths } from '../../../../config'; +import { addDisplayNames } from './add_display_names'; const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.alert.tableState'; @@ -361,7 +373,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { casesOwner: observabilityFeatureId, casePermissions, type, - columns: tGridState?.columns ?? columns, + columns: (tGridState?.columns ?? columns).map(addDisplayNames), deletedEventIds, disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, end: rangeTo, @@ -390,7 +402,18 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { sortDirection, }, ], - + queryFields: [ + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_UUID, + ALERT_START, + TIMESTAMP, + ], leadingControlColumns, trailingControlColumns, unit: (totalAlerts: number) => translations.alertsTable.showingAlertsTitle(totalAlerts), diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index 593278788e01c..0ee83bea1bc67 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -121,6 +121,7 @@ export interface TGridStandaloneProps { data?: DataPublicPluginStart; unit?: (total: number) => React.ReactNode; showCheckboxes?: boolean; + queryFields?: string[]; } const TGridStandaloneComponent: React.FC = ({ @@ -155,6 +156,7 @@ const TGridStandaloneComponent: React.FC = ({ data, unit, showCheckboxes = true, + queryFields = [], }) => { const dispatch = useDispatch(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; @@ -166,7 +168,7 @@ const TGridStandaloneComponent: React.FC = ({ const { itemsPerPage: itemsPerPageStore, itemsPerPageOptions: itemsPerPageOptionsStore, - queryFields, + queryFields: queryFieldsFromState, sort: sortStore, title, } = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? '')); @@ -203,9 +205,9 @@ const TGridStandaloneComponent: React.FC = ({ (acc, c) => (c.linkField != null ? [...acc, c.id, c.linkField] : [...acc, c.id]), [] ), - ...(queryFields ?? []), + ...(queryFieldsFromState ?? []), ], - [columnsHeader, queryFields] + [columnsHeader, queryFieldsFromState] ); const sortField = useMemo( @@ -335,6 +337,7 @@ const TGridStandaloneComponent: React.FC = ({ sort, loadingText, unit, + queryFields, }) ); // eslint-disable-next-line react-hooks/exhaustive-deps From befefc334793a1d6121e427ba6c14323eed61e9e Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 8 Feb 2022 16:32:10 +0100 Subject: [PATCH 049/161] FTR - check ES security before creating system_indices_superuser (#124948) This PR adds a check if ES security is enabled before creating the system_indices_superuser in the security service. --- test/common/services/security/system_indices_user.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/common/services/security/system_indices_user.ts b/test/common/services/security/system_indices_user.ts index c1ab6b1e0abfa..2546fbeafffa7 100644 --- a/test/common/services/security/system_indices_user.ts +++ b/test/common/services/security/system_indices_user.ts @@ -25,6 +25,16 @@ export async function createSystemIndicesUser(ctx: FtrProviderContext) { const es = createEsClientForFtrConfig(config); + // There are cases where the test config file doesn't have security disabled + // but tests are still executed on ES without security. Checking this case + // by trying to fetch the users list. + try { + await es.security.getUser(); + } catch (error) { + log.debug('Could not fetch users, assuming security is disabled'); + return; + } + log.debug('===============creating system indices role and user==============='); await es.security.putRole({ From e7ab1e51c7f33a90009694de7a7fe810d95194b0 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 8 Feb 2022 10:43:44 -0500 Subject: [PATCH 050/161] skip failing test suite (#124938) --- x-pack/test/functional/apps/ml/permissions/full_ml_access.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index c038aeba608bd..e41f1491f98af 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { { user: USER.ML_POWERUSER_SPACES, discoverAvailable: false }, ]; - describe('for user with full ML access', function () { + // Failing: See https://github.com/elastic/kibana/issues/124938 + describe.skip('for user with full ML access', function () { this.tags(['skipFirefox', 'mlqa']); describe('with no data loaded', function () { From 9a63bb813038b407d297f826b189e18b92c9da52 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 8 Feb 2022 10:31:28 -0600 Subject: [PATCH 051/161] [ci] Fix state check for purge cloud deployments (#124985) Pull request state from GitHub returns 'OPEN' if a pull request is open. This updates the capitalization when branching from state --- .buildkite/scripts/steps/cloud/purge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js index b14a3be8f8daf..336f7daf736ae 100644 --- a/.buildkite/scripts/steps/cloud/purge.js +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -26,7 +26,7 @@ for (const deployment of prDeployments) { const lastCommit = pullRequest.commits.slice(-1)[0]; const lastCommitTimestamp = new Date(lastCommit.committedDate).getTime() / 1000; - if (pullRequest.state !== 'open') { + if (pullRequest.state !== 'OPEN') { console.log(`Pull Request #${prNumber} is no longer open, will delete associated deployment`); deploymentsToPurge.push(deployment); } else if (!pullRequest.labels.filter((label) => label.name === 'ci:deploy-cloud')) { From e5678ffc5e3702932fbd175e9a5606b46fb32259 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 8 Feb 2022 10:54:46 -0600 Subject: [PATCH 052/161] [ML] Fix permission check for Discover/data view redirect from Anomaly detection explorer page (#124408) * Add permission check for data view redirect * Add checks if data view doesn't exist & tooltip hints * Remove console.error cause they are redundant * Not show link if no access to Discover * Fix duplicates * Fix duplicates, text Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/anomalies_table/links_menu.tsx | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index acca495a9c900..00ed14081c9be 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -15,6 +15,7 @@ import { EuiContextMenuPanel, EuiPopover, EuiProgress, + EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -57,25 +58,33 @@ interface LinksMenuProps { export const LinksMenuUI = (props: LinksMenuProps) => { const [openInDiscoverUrl, setOpenInDiscoverUrl] = useState(); + const [discoverUrlError, setDiscoverUrlError] = useState(); const isCategorizationAnomalyRecord = isCategorizationAnomaly(props.anomaly); const closePopover = props.onItemClick; const kibana = useMlKibana(); + const { + services: { share, application }, + } = kibana; useEffect(() => { let unmounted = false; const generateDiscoverUrl = async () => { - const { - services: { share }, - } = kibana; const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); if (!discoverLocator) { - // eslint-disable-next-line no-console - console.error('No locator for Discover detected'); + const discoverLocatorMissing = i18n.translate( + 'xpack.ml.anomaliesTable.linksMenu.discoverLocatorMissingErrorMessage', + { + defaultMessage: 'No locator for Discover detected', + } + ); + + setDiscoverUrlError(discoverLocatorMissing); + return; } @@ -83,7 +92,20 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const index = job.datafeed_config.indices[0]; const interval = props.interval; - const dataViewId = (await getDataViewIdFromName(index)) || index; + const dataViewId = await getDataViewIdFromName(index); + + // If data view doesn't exist for some reasons + if (!dataViewId) { + const autoGeneratedDiscoverLinkError = i18n.translate( + 'xpack.ml.anomaliesTable.linksMenu.autoGeneratedDiscoverLinkErrorMessage', + { + defaultMessage: `Unable to link to Discover; no data view exists for index '{index}'`, + values: { index }, + } + ); + + setDiscoverUrlError(autoGeneratedDiscoverLinkError); + } const record = props.anomaly.source; const earliestMoment = moment(record.timestamp).startOf(interval); @@ -243,9 +265,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { }; const viewSeries = async () => { - const { - services: { share }, - } = kibana; const mlLocator = share.url.locators.get(ML_APP_LOCATOR); const record = props.anomaly.source; @@ -501,22 +520,31 @@ export const LinksMenuUI = (props: LinksMenuProps) => { }); } - if (!isCategorizationAnomalyRecord) { + if (application.capabilities.discover?.show && !isCategorizationAnomalyRecord) { // Add item from the start, but disable it during the URL generation. - const isLoading = openInDiscoverUrl === undefined; + const isLoading = discoverUrlError === undefined && openInDiscoverUrl === undefined; items.push( - + {discoverUrlError ? ( + + + + ) : ( + + )} {isLoading ? : null} ); From 6f71ffcd0ed65f9e4654c5ea5548baf71031faec Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Tue, 8 Feb 2022 17:08:23 +0000 Subject: [PATCH 053/161] [Security Solution][Detections] fixes UX issue with discoverability of new features on the Rules page (#124343) - Added tour with 2 steps - Sorting/filtering for in memory table - New Bulk Edit actions - Created context with tour management, which wraps rules table - Results of user tour's journey is saved in localStorage - Disabled tour for Cypress tests **Note:** Text in second step was changed to "You can now bulk update index patterns and tags for multiple custom rules at once." It's not reflected on screenshots. On screenshots and video present older text: "You can now bulk update index patterns and tags for multiple rules at once.", where word `custom` is absent ## UI ### First step Screenshot 2022-02-03 at 16 54 20 ### Second step Screenshot 2022-02-03 at 16 54 29 Eui Tour: https://elastic.github.io/eui/#/display/tour ## Screen recording https://user-images.githubusercontent.com/92328789/152391296-0b03b299-270a-4d96-9adf-d98bc6405b4f.mov ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) --- .../security_solution/common/constants.ts | 3 + .../security_solution/cypress/tasks/login.ts | 29 +++- .../utility_bar/utility_bar_action.tsx | 11 +- .../all/bulk_actions/bulk_edit_flyout.tsx | 2 +- .../detection_engine/rules/all/index.test.tsx | 7 +- .../detection_engine/rules/all/index.tsx | 2 +- .../rules/all/optional_eui_tour_step.tsx | 29 ++++ .../rules/all/rules_feature_tour_context.tsx | 141 ++++++++++++++++++ .../rules/all/rules_table_toolbar.tsx | 33 +++- .../rules/all/utility_bar.tsx | 35 +++-- .../pages/detection_engine/rules/index.tsx | 138 ++++++++--------- .../detection_engine/rules/translations.ts | 44 ++++++ 12 files changed, 378 insertions(+), 96 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 44e73cd8d1a8f..41dc63a2a48ec 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -422,3 +422,6 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG_PREFIX = `${APP_ID}:limitedConcurrenc */ export const RULES_TABLE_MAX_PAGE_SIZE = 100; export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAGE_SIZE]; + +export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY = + 'securitySolution.rulesManagementPage.newFeaturesTour.v8.1'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index ad6ad0486e518..349f2aaf32732 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -9,6 +9,7 @@ import * as yaml from 'js-yaml'; import Url, { UrlObject } from 'url'; import { ROLES } from '../../common/test'; +import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../common/constants'; import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; import { hostDetailsUrl, LOGOUT_URL } from '../urls/navigation'; @@ -284,6 +285,21 @@ export const getEnvAuth = (): User => { } }; +/** + * Saves in localStorage rules feature tour config with deactivated option + * It prevents tour to appear during tests and cover UI elements + * @param window - browser's window object + */ +const disableRulesFeatureTour = (window: Window) => { + const tourConfig = { + isTourActive: false, + }; + window.localStorage.setItem( + RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY, + JSON.stringify(tourConfig) + ); +}; + /** * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing @@ -301,6 +317,7 @@ export const loginAndWaitForPage = ( if (onBeforeLoadCallback) { onBeforeLoadCallback(win); } + disableRulesFeatureTour(win); }, } ); @@ -315,13 +332,17 @@ export const waitForPage = (url: string) => { export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => { login(role); - cy.visit(role ? getUrlWithRoute(role, url) : url); + cy.visit(role ? getUrlWithRoute(role, url) : url, { + onBeforeLoad: disableRulesFeatureTour, + }); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; export const loginWithUserAndWaitForPageWithoutDateRange = (url: string, user: User) => { loginWithUser(user); - cy.visit(constructUrlWithUser(user, url)); + cy.visit(constructUrlWithUser(user, url), { + onBeforeLoad: disableRulesFeatureTour, + }); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; @@ -329,7 +350,9 @@ export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`; login(role); - cy.visit(role ? getUrlWithRoute(role, route) : route); + cy.visit(role ? getUrlWithRoute(role, route) : route, { + onBeforeLoad: disableRulesFeatureTour, + }); cy.get('[data-test-subj="headerGlobalNav"]'); cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx index faa4733a0bf3e..aa07a4442fab7 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx @@ -35,11 +35,17 @@ const Popover = React.memo( ownFocus, dataTestSubj, popoverPanelPaddingSize, + onClick, }) => { const [popoverState, setPopoverState] = useState(false); const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]); + const handleLinkIconClick = useCallback(() => { + onClick?.(); + setPopoverState(!popoverState); + }, [popoverState, onClick]); + return ( ( iconSide={iconSide} iconSize={iconSize} iconType={iconType} - onClick={() => setPopoverState(!popoverState)} + onClick={handleLinkIconClick} disabled={disabled} > {children} } - closePopover={() => setPopoverState(false)} + closePopover={closePopover} isOpen={popoverState} repositionOnScroll > @@ -107,6 +113,7 @@ export const UtilityBarAction = React.memo( {popoverContent ? ( void; - onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; editAction: BulkActionEditType; rulesCount: number; tags: string[]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 3b24dda539174..6092ec2a134d1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -13,6 +13,7 @@ import { TestProviders } from '../../../../../common/mock'; import '../../../../../common/mock/formatted_relative'; import '../../../../../common/mock/match_media'; import { AllRules } from './index'; +import { RulesFeatureTourContextProvider } from './rules_feature_tour_context'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../../common/lib/kibana'); @@ -67,7 +68,8 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - + , + { wrappingComponent: RulesFeatureTourContextProvider } ); await waitFor(() => { @@ -90,7 +92,8 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - + , + { wrappingComponent: RulesFeatureTourContextProvider } ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index e8c7742125c74..6bb9927c8ab82 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -45,7 +45,7 @@ export const AllRules = React.memo( return ( <> - + = ({ + children, + stepProps, +}) => { + if (!stepProps) { + return <>{children}; + } + + return ( + + <>{children} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx new file mode 100644 index 0000000000000..6c1d5a0de7a54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useEffect, useMemo, FC } from 'react'; + +import { noop } from 'lodash'; +import { + useEuiTour, + EuiTourState, + EuiStatelessTourStep, + EuiSpacer, + EuiButton, + EuiTourStepProps, +} from '@elastic/eui'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../../../../../common/constants'; +import { useKibana } from '../../../../../common/lib/kibana'; + +import * as i18n from '../translations'; + +export interface RulesFeatureTourContextType { + steps: { + inMemoryTableStepProps: EuiTourStepProps; + bulkActionsStepProps: EuiTourStepProps; + }; + goToNextStep: () => void; + finishTour: () => void; +} + +const TOUR_POPOVER_WIDTH = 360; + +const featuresTourSteps: EuiStatelessTourStep[] = [ + { + step: 1, + title: i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP_TITLE, + content: <>, + stepsTotal: 2, + children: <>, + onFinish: noop, + maxWidth: TOUR_POPOVER_WIDTH, + }, + { + step: 2, + title: i18n.FEATURE_TOUR_BULK_ACTIONS_STEP_TITLE, + content:

{i18n.FEATURE_TOUR_BULK_ACTIONS_STEP}

, + stepsTotal: 2, + children: <>, + onFinish: noop, + anchorPosition: 'rightUp', + maxWidth: TOUR_POPOVER_WIDTH, + }, +]; + +const tourConfig: EuiTourState = { + currentTourStep: 1, + isTourActive: true, + tourPopoverWidth: TOUR_POPOVER_WIDTH, + tourSubtitle: i18n.FEATURE_TOUR_TITLE, +}; + +const RulesFeatureTourContext = createContext(null); + +/** + * Context for new rules features, displayed in demo tour(euiTour) + * It has a common state in useEuiTour, which allows transition from one step to the next, for components within it[context] + * It also stores tour's state in localStorage + */ +export const RulesFeatureTourContextProvider: FC = ({ children }) => { + const { storage } = useKibana().services; + const initialStore = useMemo( + () => ({ + ...tourConfig, + ...(storage.get(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY) ?? tourConfig), + }), + [storage] + ); + + const [stepProps, actions, reducerState] = useEuiTour(featuresTourSteps, initialStore); + + const finishTour = actions.finishTour; + const goToNextStep = actions.incrementStep; + + const inMemoryTableStepProps = useMemo( + () => ({ + ...stepProps[0], + content: ( + <> +

{i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP}

+ + + {i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP_NEXT} + + + ), + }), + [stepProps, goToNextStep] + ); + + useEffect(() => { + const { isTourActive, currentTourStep } = reducerState; + storage.set(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY, { isTourActive, currentTourStep }); + }, [reducerState, storage]); + + const providerValue = useMemo( + () => ({ + steps: { + inMemoryTableStepProps, + bulkActionsStepProps: stepProps[1], + }, + finishTour, + goToNextStep, + }), + [finishTour, goToNextStep, inMemoryTableStepProps, stepProps] + ); + + return ( + + {children} + + ); +}; + +export const useRulesFeatureTourContext = (): RulesFeatureTourContextType => { + const rulesFeatureTourContext = useContext(RulesFeatureTourContext); + invariant( + rulesFeatureTourContext, + 'useRulesFeatureTourContext should be used inside RulesFeatureTourContextProvider' + ); + + return rulesFeatureTourContext; +}; + +export const useRulesFeatureTourContextOptional = (): RulesFeatureTourContextType | null => { + const rulesFeatureTourContext = useContext(RulesFeatureTourContext); + + return rulesFeatureTourContext; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx index 261e14fd1411b..966cb726c8711 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx @@ -10,6 +10,8 @@ import React from 'react'; import styled from 'styled-components'; import { useRulesTableContext } from './rules_table/rules_table_context'; import * as i18n from '../translations'; +import { useRulesFeatureTourContext } from './rules_feature_tour_context'; +import { OptionalEuiTourStep } from './optional_eui_tour_step'; const ToolbarLayout = styled.div` display: grid; @@ -22,6 +24,7 @@ const ToolbarLayout = styled.div` interface RulesTableToolbarProps { activeTab: AllRulesTabs; onTabChange: (tab: AllRulesTabs) => void; + loading: boolean; } export enum AllRulesTabs { @@ -43,12 +46,17 @@ const allRulesTabs = [ ]; export const RulesTableToolbar = React.memo( - ({ onTabChange, activeTab }) => { + ({ onTabChange, activeTab, loading }) => { const { state: { isInMemorySorting }, actions: { setIsInMemorySorting }, } = useRulesTableContext(); + const { + steps: { inMemoryTableStepProps }, + goToNextStep, + } = useRulesFeatureTourContext(); + return ( @@ -64,13 +72,22 @@ export const RulesTableToolbar = React.memo( ))} - - setIsInMemorySorting(e.target.checked)} - /> - + {/* delaying render of tour due to EuiPopover can't react to layout changes + https://github.com/elastic/kibana/pull/124343#issuecomment-1032467614 */} + + + { + if (inMemoryTableStepProps.isStepOpen) { + goToNextStep(); + } + setIsInMemorySorting(e.target.checked); + }} + /> + + ); } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx index 6d9c2f92b214e..a936e84cee00a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx @@ -22,6 +22,9 @@ import { UtilityBarText, } from '../../../../../common/components/utility_bar'; import * as i18n from '../translations'; +import { useRulesFeatureTourContextOptional } from './rules_feature_tour_context'; + +import { OptionalEuiTourStep } from './optional_eui_tour_step'; interface AllRulesUtilityBarProps { canBulkEdit: boolean; @@ -55,6 +58,9 @@ export const AllRulesUtilityBar = React.memo( isBulkActionInProgress, hasDisabledActions, }) => { + // use optional rulesFeatureTourContext as AllRulesUtilityBar can be used outside the context + const featureTour = useRulesFeatureTourContextOptional(); + const handleGetBulkItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element | null => { if (onGetBulkItemsPopoverContent != null) { @@ -134,17 +140,24 @@ export const AllRulesUtilityBar = React.memo( )} {canBulkEdit && ( - - {i18n.BATCH_ACTIONS} - + + { + if (featureTour?.steps?.bulkActionsStepProps?.isStepOpen) { + featureTour?.finishTour(); + } + }} + > + {i18n.BATCH_ACTIONS} + + )} { showExceptionsCheckBox showCheckBox /> - - - - - {loadPrebuiltRulesAndTemplatesButton && ( - {loadPrebuiltRulesAndTemplatesButton} - )} - {reloadPrebuiltRulesAndTemplatesButton && ( - {reloadPrebuiltRulesAndTemplatesButton} - )} - - + + + + + + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} + )} + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} + )} + + + + {i18n.UPLOAD_VALUE_LISTS} + + + + - {i18n.UPLOAD_VALUE_LISTS} + {i18n.IMPORT_RULE} - - - - - {i18n.IMPORT_RULE} - - - - - {i18n.ADD_NEW_RULE} - - - - - {(prePackagedRuleStatus === 'ruleNeedUpdate' || - prePackagedTimelineStatus === 'timelineNeedUpdate') && ( - + + + {i18n.ADD_NEW_RULE} + + +
+ + {(prePackagedRuleStatus === 'ruleNeedUpdate' || + prePackagedTimelineStatus === 'timelineNeedUpdate') && ( + + )} + - )} - - - - + + + ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 1de060c16a97a..386e00fc28d8b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -88,6 +88,50 @@ export const EDIT_PAGE_TITLE = i18n.translate( } ); +export const FEATURE_TOUR_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle', + { + defaultMessage: "What's new", + } +); + +export const FEATURE_TOUR_IN_MEMORY_TABLE_STEP = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.inMemoryTableStepDescription', + { + defaultMessage: + 'The experimental rules table view allows for advanced sorting and filtering capabilities.', + } +); + +export const FEATURE_TOUR_IN_MEMORY_TABLE_STEP_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.inMemoryTableStepTitle', + { + defaultMessage: 'Step 1', + } +); + +export const FEATURE_TOUR_IN_MEMORY_TABLE_STEP_NEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.inMemoryTableStepNextButtonTitle', + { + defaultMessage: 'Ok, got it', + } +); + +export const FEATURE_TOUR_BULK_ACTIONS_STEP_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.bulkActionsStepTitle', + { + defaultMessage: 'Step 2', + } +); + +export const FEATURE_TOUR_BULK_ACTIONS_STEP = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.bulkActionsStepDescription', + { + defaultMessage: + 'You can now bulk update index patterns and tags for multiple custom rules at once.', + } +); + export const REFRESH = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.refreshTitle', { From 370af2e64ba6d74da4e2d8a85d80db8734636016 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Tue, 8 Feb 2022 11:25:58 -0600 Subject: [PATCH 054/161] [Lens] fix saving filters in Lens visualization (#124885) --- .../lens/public/app_plugin/app.test.tsx | 7 +++-- x-pack/plugins/lens/public/app_plugin/app.tsx | 13 ++++++--- .../app_plugin/lens_document_equality.ts | 16 ++++++++++- .../app_plugin/save_modal_container.tsx | 28 ++----------------- x-pack/plugins/lens/public/plugin.ts | 2 +- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 23804a8a6d618..9cf22b2a8fc84 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -678,13 +678,14 @@ describe('Lens App', () => { filters: [pinned, unpinned], }, }); + + const { state: expectedFilters } = services.data.query.filterManager.extract([unpinned]); + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( expect.objectContaining({ savedObjectId: defaultSavedObjectId, title: 'hello there2', - state: expect.objectContaining({ - filters: services.data.query.filterManager.inject([unpinned], []), - }), + state: expect.objectContaining({ filters: expectedFilters }), }), true, { id: '5678', savedObjectId: defaultSavedObjectId } diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 78e739a6324ec..45517298b0432 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -92,9 +92,9 @@ export function App({ () => ({ datasourceMap, visualizationMap, - extractFilterReferences: data.query.filterManager.extract, + extractFilterReferences: data.query.filterManager.extract.bind(data.query.filterManager), }), - [datasourceMap, visualizationMap, data.query.filterManager.extract] + [datasourceMap, visualizationMap, data.query.filterManager] ); const currentDoc = useLensSelector((state) => @@ -153,7 +153,12 @@ export function App({ onAppLeave((actions) => { if ( application.capabilities.visualize.save && - !isLensEqual(persistedDoc, lastKnownDoc, data.query.filterManager.inject, datasourceMap) && + !isLensEqual( + persistedDoc, + lastKnownDoc, + data.query.filterManager.inject.bind(data.query.filterManager), + datasourceMap + ) && (isSaveable || persistedDoc) ) { return actions.confirm( @@ -174,7 +179,7 @@ export function App({ isSaveable, persistedDoc, application.capabilities.visualize.save, - data.query.filterManager.inject, + data.query.filterManager, datasourceMap, ]); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts index 3e833502c0592..59a8aadbe62ec 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts @@ -9,7 +9,7 @@ import { isEqual, intersection, union } from 'lodash'; import { FilterManager } from 'src/plugins/data/public'; import { Document } from '../persistence/saved_object_store'; import { DatasourceMap } from '../types'; -import { injectDocFilterReferences, removePinnedFilters } from './save_modal_container'; +import { removePinnedFilters } from './save_modal_container'; const removeNonSerializable = (obj: Parameters[0]) => JSON.parse(JSON.stringify(obj)); @@ -75,3 +75,17 @@ export const isLensEqual = ( return true; }; + +function injectDocFilterReferences( + injectFilterReferences: FilterManager['inject'], + doc?: Document +) { + if (!doc) return undefined; + return { + ...doc, + state: { + ...doc.state, + filters: injectFilterReferences(doc.state?.filters || [], doc.references), + }, + }; +} diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index e3098904a4b85..7fc03fd2a6551 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -16,7 +16,6 @@ import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; import { Document, checkForDuplicateTitle } from '../persistence'; import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; -import { FilterManager } from '../../../../../src/plugins/data/public'; import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common'; import { trackUiEvent } from '../lens_ui_telemetry'; import type { LensAppState } from '../state_management'; @@ -169,11 +168,10 @@ const redirectToDashboard = ({ const getDocToSave = ( lastKnownDoc: Document, saveProps: SaveProps, - references: SavedObjectReference[], - injectFilterReferences: FilterManager['inject'] + references: SavedObjectReference[] ) => { const docToSave = { - ...injectDocFilterReferences(injectFilterReferences, removePinnedFilters(lastKnownDoc))!, + ...removePinnedFilters(lastKnownDoc)!, references, }; @@ -201,7 +199,6 @@ export const runSaveLensVisualization = async ( ): Promise | undefined> => { const { chrome, - data, initialInput, originatingApp, lastKnownDoc, @@ -242,12 +239,7 @@ export const runSaveLensVisualization = async ( ); } - const docToSave = getDocToSave( - lastKnownDoc, - saveProps, - references, - data.query.filterManager.inject - ); + const docToSave = getDocToSave(lastKnownDoc, saveProps, references); // Required to serialize filters in by value mode until // https://github.com/elastic/kibana/issues/77588 is fixed @@ -358,20 +350,6 @@ export const runSaveLensVisualization = async ( } }; -export function injectDocFilterReferences( - injectFilterReferences: FilterManager['inject'], - doc?: Document -) { - if (!doc) return undefined; - return { - ...doc, - state: { - ...doc.state, - filters: injectFilterReferences(doc.state?.filters || [], doc.references), - }, - }; -} - export function removePinnedFilters(doc?: Document) { if (!doc) return undefined; return { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b3b78ffc4c2e8..8370d093a5b62 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -239,7 +239,7 @@ export class LensPlugin { timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, - injectFilterReferences: data.query.filterManager.inject, + injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager), visualizationMap, indexPatternService: plugins.data.indexPatterns, uiActions: plugins.uiActions, From 30350fc9def38c0adf97ea25a875e23f5900e197 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 8 Feb 2022 18:28:48 +0100 Subject: [PATCH 055/161] [Fleet] Remove unwanted overflow on Integrations screenshots (#124975) --- .../sections/epm/screens/detail/overview/screenshots.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx index d368b07b62e41..a6f11a40bc3b7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx @@ -5,6 +5,7 @@ * 2.0. */ import React, { useState, useMemo, memo } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiPagination } from '@elastic/eui'; @@ -17,6 +18,9 @@ interface ScreenshotProps { packageName: string; version: string; } +const Pagination = styled(EuiPagination)` + max-width: 130px; +`; export const Screenshots: React.FC = memo(({ images, packageName, version }) => { const { toPackageImage } = useLinks(); @@ -48,7 +52,7 @@ export const Screenshots: React.FC = memo(({ images, packageNam - Date: Tue, 8 Feb 2022 18:31:21 +0100 Subject: [PATCH 056/161] Add failed-test and chore labels to Fleet project automation (#124994) --- .github/workflows/add-to-fleet-project.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/add-to-fleet-project.yml b/.github/workflows/add-to-fleet-project.yml index fc5676887f3ae..59b3513c85284 100644 --- a/.github/workflows/add-to-fleet-project.yml +++ b/.github/workflows/add-to-fleet-project.yml @@ -10,7 +10,9 @@ jobs: contains(github.event.issue.labels.*.name, 'Team:Fleet') && ( contains(github.event.issue.labels.*.name, 'technical debt') || contains(github.event.issue.labels.*.name, 'bug') || - contains(github.event.issue.labels.*.name, 'performance') + contains(github.event.issue.labels.*.name, 'performance') || + contains(github.event.issue.labels.*.name, 'failed-test') || + contains(github.event.issue.labels.*.name, 'chore') ) steps: - uses: octokit/graphql-action@v2.x @@ -28,5 +30,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: + # https://github.com/orgs/elastic/projects/763 PROJECT_ID: "PN_kwDOAGc3Zs4AAsH6" + # Token with `write:org` access GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }} From ce094b970aaed6fcd029cf9bd180da0242d8b3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Tue, 8 Feb 2022 18:39:10 +0100 Subject: [PATCH 057/161] [Security Solution][Endpoint] Set horizontal scrollbar to fix an issue with match_any operator (#124516) * Set max width 100% to fix an issue with match_any operator * Adds horizontal scrollbar for large criteria condition values * Adds horizontal scrollbar for large criteria condition values to the whole entry, not only on values Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/criteria_conditions.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx index 048b79c354803..238fe87c05890 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx @@ -146,15 +146,17 @@ export const CriteriaConditions = memo( {entries.map(({ field, type, value, operator, entries: nestedEntries = [] }) => { return (
- {CONDITION_AND}} - value={field} - color="subdued" - /> - +
+ {CONDITION_AND}} + value={field} + color="subdued" + /> + +
{getNestedEntriesContent(type, nestedEntries)}
); From 73cc08e0755bc1a409e3e4a03870e764a3bbf145 Mon Sep 17 00:00:00 2001 From: Tobias Stadler Date: Tue, 8 Feb 2022 18:51:39 +0100 Subject: [PATCH 058/161] Rename Backend to Dependency (#124067) * Rename Backend to Dependency which was missed in #110523 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../waterfall/span_flyout/sticky_span_properties.tsx | 12 ++++++------ x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx index 3067b335f4861..1e8cf5d19e5e0 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx @@ -49,7 +49,7 @@ export function StickySpanProperties({ span, transaction }: Props) { }); const spanName = span.span.name; - const backendName = span.span.destination?.service.resource; + const dependencyName = span.span.destination?.service.resource; const transactionStickyProperties = transaction ? [ @@ -98,13 +98,13 @@ export function StickySpanProperties({ span, transaction }: Props) { ] : []; - const backendStickyProperties = backendName + const dependencyStickyProperties = dependencyName ? [ { label: i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.backendLabel', + 'xpack.apm.transactionDetails.spanFlyout.dependencyLabel', { - defaultMessage: 'Backend', + defaultMessage: 'Dependency', } ), fieldName: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -112,7 +112,7 @@ export function StickySpanProperties({ span, transaction }: Props) { Date: Tue, 8 Feb 2022 15:15:34 -0500 Subject: [PATCH 059/161] [Fleet] Fix preconfiguration error when renaming a preconfigured policy (#124953) * Fix preconfiguration error when renaming a preconfigured policy * Add test + only compare on ID if it's defined on the preconfigured policy Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/services/preconfiguration.test.ts | 50 +++++++++++++++++++ .../fleet/server/services/preconfiguration.ts | 4 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 89b8098f477fd..6d6d641381da2 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -283,6 +283,7 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( describe('policy preconfiguration', () => { beforeEach(() => { + mockedPackagePolicyService.getByIDs.mockReset(); mockedPackagePolicyService.create.mockReset(); mockInstalledPackages.clear(); mockInstallPackageErrors.clear(); @@ -468,6 +469,55 @@ describe('policy preconfiguration', () => { ); }); + it('should not try to recreate preconfigure package policy that has been renamed', async () => { + const soClient = getPutPreconfiguredPackagesMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedPackagePolicyService.getByIDs.mockResolvedValue([ + { name: 'Renamed package policy', id: 'test_package1' } as PackagePolicy, + ]); + + mockConfiguredPolicies.set('test-id', { + name: 'Test policy', + description: 'Test policy description', + unenroll_timeout: 120, + namespace: 'default', + id: 'test-id', + package_policies: [ + { + name: 'test_package1', + id: 'test_package1', + }, + ], + is_managed: true, + } as PreconfiguredAgentPolicy); + + await ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + [ + { + name: 'Test policy', + namespace: 'default', + id: 'test-id', + is_managed: true, + package_policies: [ + { + package: { name: 'test_package' }, + name: 'test_package1', + id: 'test_package1', + }, + ], + }, + ] as PreconfiguredAgentPolicy[], + [{ name: 'test_package', version: '3.0.0' }], + mockDefaultOutput, + DEFAULT_SPACE_ID + ); + + expect(mockedPackagePolicyService.create).not.toBeCalled(); + }); + it('should throw an error when trying to install duplicate packages', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 06d700c3577b5..e9d97856a926f 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -338,7 +338,9 @@ export async function ensurePreconfiguredPackagesAndPolicies( const packagePoliciesToAdd = installedPackagePolicies.filter((installablePackagePolicy) => { return !(agentPolicyWithPackagePolicies?.package_policies as PackagePolicy[]).some( - (packagePolicy) => packagePolicy.name === installablePackagePolicy.name + (packagePolicy) => + (packagePolicy.id !== undefined && packagePolicy.id === installablePackagePolicy.id) || + packagePolicy.name === installablePackagePolicy.name ); }); From 1f3b7f405e7a8bd2c30c1a6e02d03f540b10f088 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 8 Feb 2022 15:19:37 -0500 Subject: [PATCH 060/161] skip failing test suite (#124990) --- test/functional/apps/discover/_saved_queries.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 5a8b14545508c..fe94987289a39 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -36,7 +36,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await queryBar.setQuery('response:200'); }; - describe('saved queries saved objects', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/124990 + describe.skip('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); From 1af9629c58de827d34694d5f7e132ba7abade817 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Feb 2022 20:58:01 +0000 Subject: [PATCH 061/161] chore(NA): splits types from code on @kbn/typed-react-router-config (#124944) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 1 + packages/BUILD.bazel | 1 + .../kbn-typed-react-router-config/BUILD.bazel | 39 +++++++++++++++---- .../package.json | 1 - yarn.lock | 4 ++ 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 9ea3e8335f886..d284cfbc87648 100644 --- a/package.json +++ b/package.json @@ -618,6 +618,7 @@ "@types/kbn__telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module_types", "@types/kbn__test": "link:bazel-bin/packages/kbn-test/npm_module_types", "@types/kbn__test-jest-helpers": "link:bazel-bin/packages/kbn-test-jest-helpers/npm_module_types", + "@types/kbn__typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config/npm_module_types", "@types/kbn__ui-shared-deps-npm": "link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types", "@types/kbn__ui-shared-deps-src": "link:bazel-bin/packages/kbn-ui-shared-deps-src/npm_module_types", "@types/kbn__ui-theme": "link:bazel-bin/packages/kbn-ui-theme/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6421f36bf73b7..02e82476cd88d 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -132,6 +132,7 @@ filegroup( "//packages/kbn-telemetry-tools:build_types", "//packages/kbn-test:build_types", "//packages/kbn-test-jest-helpers:build_types", + "//packages/kbn-typed-react-router-config:build_types", "//packages/kbn-ui-shared-deps-npm:build_types", "//packages/kbn-ui-shared-deps-src:build_types", "//packages/kbn-ui-theme:build_types", diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index 6f4e53e58fff7..62fd6adf5bb26 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-typed-react-router-config" PKG_REQUIRE_NAME = "@kbn/typed-react-router-config" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__typed-react-router-config" SOURCE_FILES = glob( [ @@ -28,23 +29,30 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "//packages/kbn-io-ts-utils", - "@npm//tslib", - "@npm//utility-types", + "@npm//fp-ts", + "@npm//history", "@npm//io-ts", + "@npm//lodash", "@npm//query-string", + "@npm//react", "@npm//react-router-config", "@npm//react-router-dom", + "@npm//tslib", + "@npm//utility-types", ] TYPES_DEPS = [ "//packages/kbn-io-ts-utils:npm_module_types", + "@npm//fp-ts", "@npm//query-string", "@npm//utility-types", + "@npm//@types/history", "@npm//@types/jest", + "@npm//@types/lodash", "@npm//@types/node", + "@npm//@types/react", "@npm//@types/react-router-config", "@npm//@types/react-router-dom", - "@npm//@types/history", ] jsts_transpiler( @@ -86,7 +94,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -105,3 +113,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-typed-react-router-config/package.json b/packages/kbn-typed-react-router-config/package.json index 50c2e4b5d7e89..0f45f63f4ab2d 100644 --- a/packages/kbn-typed-react-router-config/package.json +++ b/packages/kbn-typed-react-router-config/package.json @@ -1,7 +1,6 @@ { "name": "@kbn/typed-react-router-config", "main": "target_node/index.js", - "types": "target_types/index.d.ts", "browser": "target_web/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/yarn.lock b/yarn.lock index ad5df52a1655c..18bb281349a43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6929,6 +6929,10 @@ version "0.0.0" uid "" +"@types/kbn__typed-react-router-config@link:bazel-bin/packages/kbn-typed-react-router-config/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__ui-shared-deps-npm@link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types": version "0.0.0" uid "" From ec323617b5a394686ef45908e82786f99dce6b2a Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 8 Feb 2022 14:37:22 -0700 Subject: [PATCH 062/161] Adds section on Advanced Settings (#124915) Co-authored-by: Kaarina Tungseth --- dev_docs/key_concepts/building_blocks.mdx | 6 + dev_docs/tutorials/advanced_settings.mdx | 288 ++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 dev_docs/tutorials/advanced_settings.mdx diff --git a/dev_docs/key_concepts/building_blocks.mdx b/dev_docs/key_concepts/building_blocks.mdx index 6536019f668cf..61e3a711775c3 100644 --- a/dev_docs/key_concepts/building_blocks.mdx +++ b/dev_docs/key_concepts/building_blocks.mdx @@ -122,6 +122,12 @@ sharing and space isolation, and tags. **Github labels**: `Team:Core`, `Feature:Saved Objects` +## Advanced Settings + + + - + - + - + +`uiSettings` are registered synchronously during `core`'s setup lifecycle phase. This means that once you add a new advanced setting, you cannot change or remove it without . + +### Configuration with Advanced Settings UI + +The `uiSettings` service is the programmatic interface to Kibana's Advanced Settings UI. Kibana plugins use the service to extend Kibana UI Settings Management with custom settings for a plugin. + +Configuration through the Advanced Settings UI is restricted to users authorised to access the Advanced Settings page. Users who don't have permissions to change these values default to using the csettings configuration defined for the space that they are in. The `config` saved object can be shared between spaces. + +### Configuration with UI settings overrides + +When a setting is configured as an override in `kibana.yml`, it overrides any other value store in the `config` saved object. The override applies to Kibana as a whole for all spaces and users, and the option is disabled on the Advanced Settings page. We refer to these as "global" overrides. + +Note: If an override is misconfigured, it fails config validation and prevents Kibana from starting up. Validation is, however, limited to value _type_ and not to _key_ (name). For example, when a plugin registers the `my_plugin_foo: 42` setting , then declares the following override, the config validation fails: + +```kibana.yml +uiSettings.overrides: + my_plugin_foo: "42" +``` +The following override results in a successful config validation: + +```kibana.yml +uiSettings.overrides: + my_pluginFoo: 42 +``` + +### Client side usage + +On the client, the `uiSettings` service is accessible directly from `core` and the client provides plugins access to the `config` entries stored in Elasticsearch. + + + Refer to [the client-side uiSettings service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-public.iuisettingsclient.md) + + +The following is a basic example for using the `uiSettings` service: + +**src/plugins/charts/public/plugin.ts** +```ts +import { Plugin, CoreSetup } from 'kibana/public'; +import { ExpressionsSetup } from '../../expressions/public'; +import { palette, systemPalette } from '../common'; + +import { ThemeService, LegacyColorsService } from './services'; +import { PaletteService } from './services/palettes/service'; +import { ActiveCursor } from './services/active_cursor'; + +export type Theme = Omit; +export type Color = Omit; + +interface SetupDependencies { + expressions: ExpressionsSetup; +} + +/** @public */ +export interface ChartsPluginSetup { + legacyColors: Color; + theme: Theme; + palettes: ReturnType; +} + +/** @public */ +export type ChartsPluginStart = ChartsPluginSetup & { + activeCursor: ActiveCursor; +}; + +/** @public */ +export class ChartsPlugin implements Plugin { + private readonly themeService = new ThemeService(); + private readonly legacyColorsService = new LegacyColorsService(); + private readonly paletteService = new PaletteService(); + private readonly activeCursor = new ActiveCursor(); + + private palettes: undefined | ReturnType; + + public setup(core: CoreSetup, dependencies: SetupDependencies): ChartsPluginSetup { + dependencies.expressions.registerFunction(palette); + dependencies.expressions.registerFunction(systemPalette); + this.themeService.init(core.uiSettings); + this.legacyColorsService.init(core.uiSettings); + this.palettes = this.paletteService.setup(this.legacyColorsService); + + this.activeCursor.setup(); + + return { + legacyColors: this.legacyColorsService, + theme: this.themeService, + palettes: this.palettes, + }; + } + + public start(): ChartsPluginStart { + return { + legacyColors: this.legacyColorsService, + theme: this.themeService, + palettes: this.palettes!, + activeCursor: this.activeCursor, + }; + } +} + +``` + +### Server side usage + +On the server side, `uiSettings` are accessible directly from `core`. The following example shows how to register a new setting with the minimum required schema parameter against which validations are performed on read and write. +The example also shows how plugins can leverage the optional deprecation parameter on registration for handling deprecation notices and renames. The deprecation warnings are rendered in the Advanced Settings UI and should also be added to the Configure Kibana guide. + + + Refer to [the server-side uiSettings service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md) + + +**src/plugins/charts/server/plugin.ts** + +```ts +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { CoreSetup, Plugin } from 'kibana/server'; +import { COLOR_MAPPING_SETTING, LEGACY_TIME_AXIS, palette, systemPalette } from '../common'; +import { ExpressionsServerSetup } from '../../expressions/server'; + +interface SetupDependencies { + expressions: ExpressionsServerSetup; +} + +export class ChartsServerPlugin implements Plugin { + public setup(core: CoreSetup, dependencies: SetupDependencies) { + dependencies.expressions.registerFunction(palette); + dependencies.expressions.registerFunction(systemPalette); + core.uiSettings.register({ + [COLOR_MAPPING_SETTING]: { + name: i18n.translate('charts.advancedSettings.visualization.colorMappingTitle', { + defaultMessage: 'Color mapping', + }), + value: JSON.stringify({ + Count: '#00A69B', + }), + type: 'json', + description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', { + defaultMessage: + 'Maps values to specific colors in charts using the Compatibility palette.', + }), + deprecation: { + message: i18n.translate( + 'charts.advancedSettings.visualization.colorMappingTextDeprecation', + { + defaultMessage: + 'This setting is deprecated and will not be supported in a future version.', + } + ), + docLinksKey: 'visualizationSettings', + }, + category: ['visualization'], + schema: schema.string(), + }, + ... + }); + + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} +``` +For optimal Kibana performance, `uiSettings` are cached. Any changes that require a cache refresh should use the `requiresPageReload` parameter on registration. + +For example, changing the time filter refresh interval triggers a prompt in the UI that the page needs to be refreshed to save the new value: + +**src/plugins/data/server/ui_settings.ts** + +```ts +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import type { DocLinksServiceSetup, UiSettingsParams } from 'kibana/server'; +import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../common'; + +export function getUiSettings( + docLinks: DocLinksServiceSetup +): Record> { + return { + ... + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": false, + "value": 0 + }`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { + defaultMessage: `The timefilter's default refresh interval. The "value" needs to be specified in milliseconds.`, + }), + requiresPageReload: true, + schema: schema.object({ + pause: schema.boolean(), + value: schema.number(), + }), + }, + ... + } +} +``` + +### Registering Migrations +To change or remove a `uiSetting`, you must migrate the whole `config` Saved Object. `uiSettings` migrations are declared directly in the service. + +For example, in 7.9.0, `siem` as renamed to `securitySolution`, and in 8.0.0, `theme:version` was removed: + +**src/core/server/ui_settings/saved_objects/migrations.ts** + +```ts +import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from 'kibana/server'; + +export const migrations = { + '7.9.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({ + ...doc, + ...(doc.attributes && { + attributes: Object.keys(doc.attributes).reduce( + (acc, key) => + key.startsWith('siem:') + ? { + ...acc, + [key.replace('siem', 'securitySolution')]: doc.attributes[key], + } + : { + ...acc, + [key]: doc.attributes[key], + }, + {} + ), + }), + references: doc.references || [], + }), + '7.12.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({...}), + '7.13.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({...}), + '8.0.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({ + ...doc, + ...(doc.attributes && { + attributes: Object.keys(doc.attributes).reduce( + (acc, key) => + [ + // owner: Team:Geo [1] + 'visualization:regionmap:showWarnings', + ... + // owner: Team:Core + ... + 'theme:version', + // owner: Team:AppServices + ... + ].includes(key) + ? { + ...acc, + } + : { + ...acc, + [key]: doc.attributes[key], + }, + {} + ), + }), + references: doc.references || [], + }), + '8.1.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({...}), +}; +``` +[1] Since all `uiSettings` migrations are added to the same migration function, while not required, grouping settings by team is good practice. From 330c3e21acbcf4fc65312e5e39e360466fc02fdd Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 8 Feb 2022 15:41:52 -0600 Subject: [PATCH 063/161] [data views] don't allow single `*` index pattern (#124906) * don't allow single astrisk * Update form_schema.ts * add tests --- .../public/components/form_schema.test.ts | 22 +++++++++++++++++++ .../public/components/form_schema.ts | 17 +++++++++++++- .../data_view_editor/public/shared_imports.ts | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/plugins/data_view_editor/public/components/form_schema.test.ts diff --git a/src/plugins/data_view_editor/public/components/form_schema.test.ts b/src/plugins/data_view_editor/public/components/form_schema.test.ts new file mode 100644 index 0000000000000..b2e1f697843c6 --- /dev/null +++ b/src/plugins/data_view_editor/public/components/form_schema.test.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 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 { singleAstriskValidator } from './form_schema'; +import { ValidationFuncArg } from '../shared_imports'; + +describe('validators', () => { + test('singleAstriskValidator should pass', async () => { + const result = singleAstriskValidator({ value: 'kibana*' } as ValidationFuncArg); + expect(result).toBeUndefined(); + }); + test('singleAstriskValidator should fail', async () => { + const result = singleAstriskValidator({ value: '*' } as ValidationFuncArg); + // returns error + expect(result).toBeDefined(); + }); +}); diff --git a/src/plugins/data_view_editor/public/components/form_schema.ts b/src/plugins/data_view_editor/public/components/form_schema.ts index a6df0c4206d2a..178fedda2de34 100644 --- a/src/plugins/data_view_editor/public/components/form_schema.ts +++ b/src/plugins/data_view_editor/public/components/form_schema.ts @@ -7,9 +7,21 @@ */ import { i18n } from '@kbn/i18n'; -import { fieldValidators } from '../shared_imports'; +import { fieldValidators, ValidationFunc } from '../shared_imports'; import { INDEX_PATTERN_TYPE } from '../types'; +export const singleAstriskValidator = ( + ...args: Parameters +): ReturnType => { + const [{ value, path }] = args; + + const message = i18n.translate('indexPatternEditor.validations.noSingleAstriskPattern', { + defaultMessage: "A single '*' is not an allowed index pattern", + }); + + return value === '*' ? { code: 'ERR_FIELD_MISSING', path, message } : undefined; +}; + export const schema = { title: { label: i18n.translate('indexPatternEditor.editor.form.titleLabel', { @@ -28,6 +40,9 @@ export const schema = { }) ), }, + { + validator: singleAstriskValidator, + }, ], }, timestampField: { diff --git a/src/plugins/data_view_editor/public/shared_imports.ts b/src/plugins/data_view_editor/public/shared_imports.ts index cca695bc9a95e..dd9b8ea2a0e41 100644 --- a/src/plugins/data_view_editor/public/shared_imports.ts +++ b/src/plugins/data_view_editor/public/shared_imports.ts @@ -28,6 +28,7 @@ export type { ValidationFunc, FieldConfig, ValidationConfig, + ValidationFuncArg, } from '../../es_ui_shared/static/forms/hook_form_lib'; export { useForm, From 3249e565a64e5650c4f5c0e2f50df5c9cc5fbe05 Mon Sep 17 00:00:00 2001 From: nastasha-solomon <79124755+nastasha-solomon@users.noreply.github.com> Date: Tue, 8 Feb 2022 16:57:29 -0500 Subject: [PATCH 064/161] [DOCS] Pre-configured connectors can no longer be used within Cases (#123876) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/connectors/pre-configured-connectors.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index 4d304cdd6c5a2..aaef1b673d0b6 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -11,6 +11,8 @@ action are predefined, including the connector name and ID. - Appear in all spaces because they are not saved objects. - Cannot be edited or deleted. +NOTE: Preconfigured connectors cannot be used with cases. + [float] [[preconfigured-connector-example]] ==== Preconfigured connectors example @@ -70,4 +72,4 @@ image::images/pre-configured-connectors-managing.png[Connectors managing tab wit Clicking a preconfigured connector shows the description, but not the configuration. A message indicates that this is a preconfigured connector. [role="screenshot"] -image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] \ No newline at end of file +image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] From 16292de73035bf986eb10428f66611cb33dec3d6 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 8 Feb 2022 17:15:50 -0500 Subject: [PATCH 065/161] move awaitingRemoval control variable (#124913) --- .../public/application/lib/sync_dashboard_url_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts index e0a1526baa473..392b37bb4d8e0 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts @@ -94,13 +94,13 @@ const loadDashboardUrlState = ({ if (!awaitingRemoval) { awaitingRemoval = true; kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => { + awaitingRemoval = false; if (nextUrl.includes(DASHBOARD_STATE_STORAGE_KEY)) { return replaceUrlHashQuery(nextUrl, (query) => { delete query[DASHBOARD_STATE_STORAGE_KEY]; return query; }); } - awaitingRemoval = false; return nextUrl; }, true); } From f85e59dd08103f4b8369b229ac5f06d6e07a6724 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 8 Feb 2022 15:53:20 -0700 Subject: [PATCH 066/161] [Reporting] Set Fields into the SearchSource in Discover's getSharingData (#123412) * [Reporting/CSV] set fields on the searchSource for querying performance * simplify generate_csv * update outdated comment * fix generate_csv test * simplify get_sharing_data * fix api integration tests to match pre-existing snapshots * Apply suggestions from code review Co-authored-by: Michael Dokolin * use config in condition * add more testing * test change clarify Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michael Dokolin --- .../public/utils/get_sharing_data.test.ts | 94 ++++++++++++++++++- .../discover/public/utils/get_sharing_data.ts | 15 ++- .../panel_actions/get_csv_panel_action.tsx | 6 +- .../__snapshots__/generate_csv.test.ts.snap | 6 +- .../generate_csv/generate_csv.test.ts | 87 +++++------------ .../generate_csv/generate_csv.ts | 61 ++---------- .../download_csv_dashboard.ts | 5 + 7 files changed, 150 insertions(+), 124 deletions(-) diff --git a/src/plugins/discover/public/utils/get_sharing_data.test.ts b/src/plugins/discover/public/utils/get_sharing_data.test.ts index aef9bcff15403..cc37599ef12c0 100644 --- a/src/plugins/discover/public/utils/get_sharing_data.test.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.test.ts @@ -11,7 +11,11 @@ import type { DataView } from 'src/plugins/data/common'; import type { DiscoverServices } from '../build_services'; import { dataPluginMock } from '../../../data/public/mocks'; import { createSearchSourceMock } from '../../../data/common/search/search_source/mocks'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + SORT_DEFAULT_ORDER_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../common'; import { indexPatternMock } from '../__mocks__/index_pattern'; import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; @@ -23,6 +27,9 @@ describe('getSharingData', () => { data: dataPluginMock.createStartContract(), uiSettings: { get: (key: string) => { + if (key === SEARCH_FIELDS_FROM_SOURCE) { + return false; + } if (key === SORT_DEFAULT_ORDER_SETTING) { return 'desc'; } @@ -64,6 +71,91 @@ describe('getSharingData', () => { `); }); + test('getSearchSource does not add fields to the searchSource', async () => { + const index = { ...indexPatternMock } as DataView; + index.timeFieldName = 'cool-timefield'; + const searchSourceMock = createSearchSourceMock({ index }); + const { getSearchSource } = await getSharingData(searchSourceMock, {}, services); + expect(getSearchSource()).toMatchInlineSnapshot(` + Object { + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_doc": "desc", + }, + ], + } + `); + }); + + test(`getSearchSource does not add fields to the searchSource with 'discover:searchFieldsFromSource=true'`, async () => { + const originalGet = services.uiSettings.get; + services.uiSettings = { + get: (key: string, ...args: unknown[]) => { + if (key === SEARCH_FIELDS_FROM_SOURCE) { + return true; + } + return originalGet(key, ...args); + }, + } as unknown as IUiSettingsClient; + const index = { ...indexPatternMock } as DataView; + index.timeFieldName = 'cool-timefield'; + const searchSourceMock = createSearchSourceMock({ index }); + const { getSearchSource } = await getSharingData( + searchSourceMock, + { + columns: [ + 'cool-field-1', + 'cool-field-2', + 'cool-field-3', + 'cool-field-4', + 'cool-field-5', + 'cool-field-6', + ], + }, + services + ); + expect(getSearchSource()).toMatchInlineSnapshot(` + Object { + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_doc": "desc", + }, + ], + } + `); + }); + + test('getSearchSource does add fields to the searchSource when columns are selected', async () => { + const index = { ...indexPatternMock } as DataView; + index.timeFieldName = 'cool-timefield'; + const searchSourceMock = createSearchSourceMock({ index }); + const { getSearchSource } = await getSharingData( + searchSourceMock, + { + columns: [ + 'cool-field-1', + 'cool-field-2', + 'cool-field-3', + 'cool-field-4', + 'cool-field-5', + 'cool-field-6', + ], + }, + services + ); + expect(getSearchSource().fields).toStrictEqual([ + 'cool-timefield', + 'cool-field-1', + 'cool-field-2', + 'cool-field-3', + 'cool-field-4', + 'cool-field-5', + 'cool-field-6', + ]); + }); + test('fields have prepended timeField', async () => { const index = { ...indexPatternMock } as DataView; index.timeFieldName = 'cool-timefield'; diff --git a/src/plugins/discover/public/utils/get_sharing_data.ts b/src/plugins/discover/public/utils/get_sharing_data.ts index e14ae252da95e..cd00fc5e3c70e 100644 --- a/src/plugins/discover/public/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.ts @@ -10,7 +10,11 @@ import type { Capabilities } from 'kibana/public'; import type { IUiSettingsClient } from 'kibana/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { Filter, ISearchSource, SerializedSearchSourceFields } from 'src/plugins/data/common'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + SEARCH_FIELDS_FROM_SOURCE, + SORT_DEFAULT_ORDER_SETTING, +} from '../../common'; import type { SavedSearch, SortOrder } from '../services/saved_searches'; import { getSortForSearchSource } from '../components/doc_table'; import { AppState } from '../application/main/services/discover_state'; @@ -72,6 +76,15 @@ export async function getSharingData( searchSource.setField('filter', filter); } + /* + * For downstream querying performance, the searchSource object must have fields set. + * Otherwise, the requests will ask for all fields, even if only a few are really needed. + * Discover does not set fields, since having all fields is needed for the UI. + */ + const useFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE); + if (useFieldsApi && columns.length) { + searchSource.setField('fields', columns); + } return searchSource.getSerializedFields(true); }, columns, diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index b9bb529e93268..255fb91946a13 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -75,11 +75,7 @@ export class ReportingCsvPanelAction implements ActionDefinition public async getSearchSource(savedSearch: SavedSearch, _embeddable: ISearchEmbeddable) { const [{ uiSettings }, { data }] = await this.startServices$.pipe(first()).toPromise(); const { getSharingData } = await loadSharingDataHelpers(); - return await getSharingData( - savedSearch.searchSource, - savedSearch, // TODO: get unsaved state (using embeddable.searchScope): https://github.com/elastic/kibana/issues/43977 - { uiSettings, data } - ); + return await getSharingData(savedSearch.searchSource, savedSearch, { uiSettings, data }); } public isCompatible = async (context: ActionContext) => { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap index f27b0691e58ea..30f7721afe448 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap @@ -12,9 +12,9 @@ exports[`fields from job.columns (7.13+ generated) columns can be top-level fiel " `; -exports[`fields from job.columns (7.13+ generated) empty columns defaults to using searchSource.getFields() 1`] = ` -"product -coconut +exports[`fields from job.columns (7.13+ generated) default column names come from tabify 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",category,product +\\"my-cool-id\\",\\"my-cool-index\\",\\"'-\\",\\"cool, rad\\",coconut " `; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index 60b4cc05448b8..0f6652943da25 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -69,21 +69,6 @@ const mockDataClientSearchDefault = jest.fn().mockImplementation( }, }) ); -const mockSearchSourceGetFieldDefault = jest.fn().mockImplementation((key: string) => { - switch (key) { - case 'fields': - return ['date', 'ip', 'message']; - case 'index': - return { - fields: { - getByName: jest.fn().mockImplementation(() => []), - getByType: jest.fn().mockImplementation(() => []), - }, - metaFields: ['_id', '_index', '_type', '_score'], - getFormatterForField: jest.fn(), - }; - } -}); const mockFieldFormatsRegistry = { deserialize: jest @@ -123,14 +108,26 @@ beforeEach(async () => { }) ); - searchSourceMock.getField = mockSearchSourceGetFieldDefault; + searchSourceMock.getField = jest.fn((key: string) => { + switch (key) { + case 'index': + return { + fields: { + getByName: jest.fn(() => []), + getByType: jest.fn(() => []), + }, + metaFields: ['_id', '_index', '_type', '_score'], + getFormatterForField: jest.fn(), + }; + } + }); }); const logger = createMockLevelLogger(); it('formats an empty search result to CSV content', async () => { const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', 'message'] }), mockConfig, { es: mockEsClient, @@ -170,7 +167,7 @@ it('formats a search result to CSV content', async () => { }) ); const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', 'message'] }), mockConfig, { es: mockEsClient, @@ -193,12 +190,6 @@ it('formats a search result to CSV content', async () => { const HITS_TOTAL = 100; it('calculates the bytes of the content', async () => { - searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { - if (key === 'fields') { - return ['message']; - } - return mockSearchSourceGetFieldDefault(key); - }); mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ rawResponse: { @@ -215,7 +206,7 @@ it('calculates the bytes of the content', async () => { ); const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['message'] }), mockConfig, { es: mockEsClient, @@ -267,7 +258,7 @@ it('warns if max size was reached', async () => { ); const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', 'message'] }), mockConfig, { es: mockEsClient, @@ -321,7 +312,7 @@ it('uses the scrollId to page all the data', async () => { }); const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', 'message'] }), mockConfig, { es: mockEsClient, @@ -361,12 +352,6 @@ it('uses the scrollId to page all the data', async () => { describe('fields from job.searchSource.getFields() (7.12 generated)', () => { it('cells can be multi-value', async () => { - searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { - if (key === 'fields') { - return ['_id', 'sku']; - } - return mockSearchSourceGetFieldDefault(key); - }); mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ rawResponse: { @@ -388,7 +373,7 @@ describe('fields from job.searchSource.getFields() (7.12 generated)', () => { ); const generateCsv = new CsvGenerator( - createMockJob({ searchSource: {} }), + createMockJob({ searchSource: {}, columns: ['_id', 'sku'] }), mockConfig, { es: mockEsClient, @@ -409,12 +394,6 @@ describe('fields from job.searchSource.getFields() (7.12 generated)', () => { }); it('provides top-level underscored fields as columns', async () => { - searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { - if (key === 'fields') { - return ['_id', '_index', 'date', 'message']; - } - return mockSearchSourceGetFieldDefault(key); - }); mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ rawResponse: { @@ -445,6 +424,7 @@ describe('fields from job.searchSource.getFields() (7.12 generated)', () => { fields: ['_id', '_index', '@date', 'message'], filter: [], }, + columns: ['_id', '_index', 'date', 'message'], }), mockConfig, { @@ -468,12 +448,6 @@ describe('fields from job.searchSource.getFields() (7.12 generated)', () => { }); it('sorts the fields when they are to be used as table column names', async () => { - searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { - if (key === 'fields') { - return ['*']; - } - return mockSearchSourceGetFieldDefault(key); - }); mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ rawResponse: { @@ -620,13 +594,7 @@ describe('fields from job.columns (7.13+ generated)', () => { expect(content).toMatchSnapshot(); }); - it('empty columns defaults to using searchSource.getFields()', async () => { - searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { - if (key === 'fields') { - return ['product']; - } - return mockSearchSourceGetFieldDefault(key); - }); + it('default column names come from tabify', async () => { mockDataClient.search = jest.fn().mockImplementation(() => Rx.of({ rawResponse: { @@ -694,7 +662,7 @@ describe('formulas', () => { ); const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', 'message'] }), mockConfig, { es: mockEsClient, @@ -736,15 +704,8 @@ describe('formulas', () => { }) ); - searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { - if (key === 'fields') { - return ['date', 'ip', TEST_FORMULA]; - } - return mockSearchSourceGetFieldDefault(key); - }); - const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', TEST_FORMULA] }), mockConfig, { es: mockEsClient, @@ -797,7 +758,7 @@ describe('formulas', () => { ); const generateCsv = new CsvGenerator( - createMockJob({}), + createMockJob({ columns: ['date', 'ip', 'message'] }), mockConfig, { es: mockEsClient, diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 804fc66c54758..9088423159cf0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -16,8 +16,6 @@ import type { DataView, ISearchSource, ISearchStartSearchSource, - SearchFieldValue, - SearchSourceFields, } from '../../../../../../../src/plugins/data/common'; import { cellHasFormulas, @@ -50,23 +48,7 @@ interface Dependencies { fieldFormatsRegistry: IFieldFormatsRegistry; } -// Function to check if the field name values can be used as the header row -function isPlainStringArray( - fields: SearchFieldValue[] | string | boolean | undefined -): fields is string[] { - let result = true; - if (Array.isArray(fields)) { - fields.forEach((field) => { - if (typeof field !== 'string' || field === '*' || field === '_source') { - result = false; - } - }); - } - return result; -} - export class CsvGenerator { - private _columns?: string[]; private csvContainsFormulas = false; private maxSizeReached = false; private csvRowCount = 0; @@ -136,36 +118,10 @@ export class CsvGenerator { }; } - private getColumns(searchSource: ISearchSource, table: Datatable) { - if (this._columns != null) { - return this._columns; - } - - // if columns is not provided in job params, - // default to use fields/fieldsFromSource from the searchSource to get the ordering of columns - const getFromSearchSource = (): string[] => { - const fieldValues: Pick = { - fields: searchSource.getField('fields'), - fieldsFromSource: searchSource.getField('fieldsFromSource'), - }; - const fieldSource = fieldValues.fieldsFromSource ? 'fieldsFromSource' : 'fields'; - this.logger.debug(`Getting columns from '${fieldSource}' in search source.`); - - const fields = fieldValues[fieldSource]; - // Check if field name values are string[] and if the fields are user-defined - if (isPlainStringArray(fields)) { - return fields; - } - - // Default to using the table column IDs as the fields - const columnIds = table.columns.map((c) => c.id); - // Fields in the API response don't come sorted - they need to be sorted client-side - columnIds.sort(); - return columnIds; - }; - this._columns = this.job.columns?.length ? this.job.columns : getFromSearchSource(); - - return this._columns; + private getColumnsFromTabify(table: Datatable) { + const columnIds = table.columns.map((c) => c.id); + columnIds.sort(); + return columnIds; } private formatCellValues(formatters: Record) { @@ -379,9 +335,12 @@ export class CsvGenerator { break; } - // If columns exists in the job params, use it to order the CSV columns - // otherwise, get the ordering from the searchSource's fields / fieldsFromSource - const columns = this.getColumns(searchSource, table) || []; + let columns: string[]; + if (this.job.columns && this.job.columns.length > 0) { + columns = this.job.columns; + } else { + columns = this.getColumnsFromTabify(table); + } if (first) { first = false; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts b/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts index 9e99f5886894e..6694b5299cd67 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts @@ -233,6 +233,7 @@ export default function ({ getService }: FtrProviderContext) { query: { language: 'kuery', query: '' }, sort: [{ '@timestamp': 'desc' }], }, + columns: ['@timestamp', 'clientip', 'extension'], }) )) as supertest.Response; const { status: resStatus, text: resText, type: resType } = res; @@ -271,6 +272,7 @@ export default function ({ getService }: FtrProviderContext) { query: { language: 'kuery', query: '' }, sort: [{ '@timestamp': 'desc' }], }, + columns: ['@timestamp', 'clientip', 'extension'], }) )) as supertest.Response; const { status: resStatus, text: resText, type: resType } = res; @@ -301,6 +303,7 @@ export default function ({ getService }: FtrProviderContext) { fields: ['date', 'message'], filter: [], }, + columns: ['date', 'message'], }) ); const { status: resStatus, text: resText, type: resType } = res; @@ -322,6 +325,7 @@ export default function ({ getService }: FtrProviderContext) { fields: ['date', 'message'], filter: [], }, + columns: ['date', 'message'], }) ); const { status: resStatus, text: resText, type: resType } = res; @@ -378,6 +382,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], }, + columns: ['name', 'power'], }) )) as supertest.Response; From e4dd6e769c493becefcac1290cd5c4e955b0f64b Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Wed, 9 Feb 2022 01:39:46 +0100 Subject: [PATCH 067/161] Alert doesn't fire action if it's muted or throttled (#124775) * Alert doesn't fire action if it's muted or throttled --- .../server/task_runner/task_runner.test.ts | 124 +++++++++++++++++- .../server/task_runner/task_runner.ts | 52 ++++---- 2 files changed, 148 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index b5a98af23d74b..679531695c31b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -41,6 +41,7 @@ import { omit } from 'lodash'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { ExecuteOptions } from '../../../actions/server/create_execute_function'; +import moment from 'moment'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -978,7 +979,128 @@ describe('Task Runner', () => { } ); - test('actionsPlugin.execute is not called when notifyWhen=onActionGroupChange and alert alert state does not change', async () => { + test.each(ephemeralTestParams)( + 'skips firing actions for active alert if alert is throttled %s', + async (nameExtension, customTaskRunnerFactoryInitializerParams, enqueueFunction) => { + ( + customTaskRunnerFactoryInitializerParams as TaskRunnerFactoryInitializerParamsType + ).actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + customTaskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue( + true + ); + actionsClient.ephemeralEnqueuedExecution.mockResolvedValue(mockRunNowResponse); + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + ruleType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '2': { + meta: { + lastScheduledActions: { date: moment().toISOString(), group: 'default' }, + }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 86400000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + rulesClient.get.mockResolvedValue({ + ...mockedRuleTypeSavedObject, + throttle: '1d', + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, + }, + references: [], + }); + await taskRunner.run(); + // expect(enqueueFunction).toHaveBeenCalledTimes(1); + + const logger = customTaskRunnerFactoryInitializerParams.logger; + // expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).nthCalledWith( + 3, + `skipping scheduling of actions for '2' in rule test:1: 'rule-name': rule is throttled` + ); + } + ); + + test.each(ephemeralTestParams)( + 'skips firing actions for active alert when alert is muted even if notifyWhen === onActionGroupChange %s', + async (nameExtension, customTaskRunnerFactoryInitializerParams, enqueueFunction) => { + customTaskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue( + true + ); + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + ruleType, + mockedTaskInstance, + customTaskRunnerFactoryInitializerParams + ); + rulesClient.get.mockResolvedValue({ + ...mockedRuleTypeSavedObject, + mutedInstanceIds: ['2'], + notifyWhen: 'onActionGroupChange', + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, + }, + references: [], + }); + await taskRunner.run(); + expect(enqueueFunction).toHaveBeenCalledTimes(1); + const logger = customTaskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).nthCalledWith( + 3, + `skipping scheduling of actions for '2' in rule test:1: 'rule-name': rule is muted` + ); + } + ); + + test('actionsPlugin.execute is not called when notifyWhen=onActionGroupChange and alert state does not change', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); ruleType.executor.mockImplementation( diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 9b77ec7f8dc72..51d50c398c6f5 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -477,34 +477,32 @@ export class TaskRunner< triggeredActions = concat(triggeredActions, scheduledActionsForRecoveredAlerts); - const alertsToExecute = - notifyWhen === 'onActionGroupChange' - ? Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, CreatedAlert]) => { - const shouldExecuteAction = alert.scheduledActionGroupOrSubgroupHasChanged(); - if (!shouldExecuteAction) { - this.logger.debug( - `skipping scheduling of actions for '${alertName}' in rule ${ruleLabel}: alert is active but action group has not changed` - ); - } - return shouldExecuteAction; - } - ) - : Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, CreatedAlert]) => { - const throttled = alert.isThrottled(throttle); - const muted = mutedAlertIdsSet.has(alertName); - const shouldExecuteAction = !throttled && !muted; - if (!shouldExecuteAction) { - this.logger.debug( - `skipping scheduling of actions for '${alertName}' in rule ${ruleLabel}: rule is ${ - muted ? 'muted' : 'throttled' - }` - ); - } - return shouldExecuteAction; - } + const alertsToExecute = Object.entries(alertsWithScheduledActions).filter( + ([alertName, alert]: [string, CreatedAlert]) => { + const throttled = alert.isThrottled(throttle); + const muted = mutedAlertIdsSet.has(alertName); + let shouldExecuteAction = true; + + if (throttled || muted) { + shouldExecuteAction = false; + this.logger.debug( + `skipping scheduling of actions for '${alertName}' in rule ${ruleLabel}: rule is ${ + muted ? 'muted' : 'throttled' + }` ); + } else if ( + notifyWhen === 'onActionGroupChange' && + !alert.scheduledActionGroupOrSubgroupHasChanged() + ) { + shouldExecuteAction = false; + this.logger.debug( + `skipping scheduling of actions for '${alertName}' in rule ${ruleLabel}: alert is active but action group has not changed` + ); + } + + return shouldExecuteAction; + } + ); const allTriggeredActions = await Promise.all( alertsToExecute.map( From 86cbc2833c18c7456b751a2c3fa8ed1ca52efac9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 17:51:48 -0700 Subject: [PATCH 068/161] Update dependency core-js to ^3.21.0 (main) (#125018) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d284cfbc87648..978186d22386d 100644 --- a/package.json +++ b/package.json @@ -217,7 +217,7 @@ "constate": "^1.3.2", "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", - "core-js": "^3.20.3", + "core-js": "^3.21.0", "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", diff --git a/yarn.lock b/yarn.lock index 18bb281349a43..cc10bb8f012f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11557,10 +11557,10 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.4, core-js@^3.20.3, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: - version "3.20.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a" - integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== +core-js@^3.0.4, core-js@^3.21.0, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3: + version "3.21.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.0.tgz#f479dbfc3dffb035a0827602dd056839a774aa71" + integrity sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ== core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: version "1.0.2" From 0a8dde3d4341c81f9a15b77aeb914ab8dc5221ce Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 9 Feb 2022 02:44:06 +0000 Subject: [PATCH 069/161] skip flaky suite (#124986) --- test/functional/apps/discover/_saved_queries.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index fe94987289a39..0e821b7006bf9 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -99,7 +99,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('saved query management component functionality', function () { + // FLAKY: https://github.com/elastic/kibana/issues/124986 + describe.skip('saved query management component functionality', function () { before(async () => await setUpQueriesWithFilters()); it('should show the saved query management component when there are no saved queries', async () => { From ecff412e22e6a64da3a82db62df8c8f3818b1642 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 9 Feb 2022 09:45:13 +0100 Subject: [PATCH 070/161] remove tabify performance issue (#124931) --- .../data/common/search/tabify/tabify.test.ts | 38 ++++++++++++++++++- .../data/common/search/tabify/tabify.ts | 6 ++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/plugins/data/common/search/tabify/tabify.test.ts b/src/plugins/data/common/search/tabify/tabify.test.ts index 6cdf9a3547d48..3e1b856de4100 100644 --- a/src/plugins/data/common/search/tabify/tabify.test.ts +++ b/src/plugins/data/common/search/tabify/tabify.test.ts @@ -8,7 +8,7 @@ import { tabifyAggResponse } from './tabify'; import { IndexPattern } from '../..'; -import { AggConfigs, IAggConfig, IAggConfigs } from '../aggs'; +import { AggConfigs, BucketAggParam, IAggConfig, IAggConfigs } from '../aggs'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { metricOnly, threeTermBuckets } from './fixtures/fake_hierarchical_data'; @@ -54,6 +54,42 @@ describe('tabifyAggResponse Integration', () => { expect(resp.columns[0]).toHaveProperty('name', aggConfigs.aggs[0].makeLabel()); }); + describe('scaleMetricValues performance check', () => { + beforeAll(() => { + typesRegistry.get('count').params.push({ + name: 'scaleMetricValues', + default: false, + write: () => {}, + advanced: true, + } as any as BucketAggParam); + }); + test('does not call write if scaleMetricValues is not set', () => { + const aggConfigs = createAggConfigs([{ type: 'count' } as any]); + + const writeMock = jest.fn(); + aggConfigs.getRequestAggs()[0].write = writeMock; + + tabifyAggResponse(aggConfigs, metricOnly, { + metricsAtAllLevels: true, + }); + expect(writeMock).not.toHaveBeenCalled(); + }); + + test('does call write if scaleMetricValues is set', () => { + const aggConfigs = createAggConfigs([ + { type: 'count', params: { scaleMetricValues: true } } as any, + ]); + + const writeMock = jest.fn(() => ({})); + aggConfigs.getRequestAggs()[0].write = writeMock; + + tabifyAggResponse(aggConfigs, metricOnly, { + metricsAtAllLevels: true, + }); + expect(writeMock).toHaveBeenCalled(); + }); + }); + describe('transforms a complex response', () => { let esResp: typeof threeTermBuckets; let aggConfigs: IAggConfigs; diff --git a/src/plugins/data/common/search/tabify/tabify.ts b/src/plugins/data/common/search/tabify/tabify.ts index 5b1247a8f1719..1bdca61d654f7 100644 --- a/src/plugins/data/common/search/tabify/tabify.ts +++ b/src/plugins/data/common/search/tabify/tabify.ts @@ -37,8 +37,10 @@ export function tabifyAggResponse( if (column) { const agg = column.aggConfig; - const aggInfo = agg.write(aggs); - aggScale *= aggInfo.metricScale || 1; + if (agg.getParam('scaleMetricValues')) { + const aggInfo = agg.write(aggs); + aggScale *= aggInfo.metricScale || 1; + } switch (agg.type.type) { case AggGroupNames.Buckets: From 01d479108b295266dd034b4cf35d5d6fb2ceace2 Mon Sep 17 00:00:00 2001 From: Shivindera Singh Date: Wed, 9 Feb 2022 10:13:59 +0100 Subject: [PATCH 071/161] allow filtering index pattern UI for runtime fields (#124114) * allow filtering index pattern UI for runtime fields * fix namespace error * add multi select filters * add fix for failing tests * test fixes --- .../indexed_fields_table.test.tsx.snap | 73 +++++ .../indexed_fields_table.test.tsx | 45 ++- .../indexed_fields_table.tsx | 32 ++- .../scripted_field_table.test.tsx | 8 +- .../scripted_fields_table.tsx | 8 +- .../edit_index_pattern/tabs/tabs.tsx | 261 ++++++++++++++++-- .../edit_index_pattern/tabs/utils.ts | 39 +-- .../apps/management/_index_pattern_filter.js | 2 + .../management/_scripted_fields_filter.js | 2 + test/functional/page_objects/settings_page.ts | 38 ++- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 12 files changed, 430 insertions(+), 82 deletions(-) diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 8b6e0a1682750..a8e8228c34119 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -141,6 +141,24 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should "name": "amount", "type": "long", }, + Object { + "displayName": "runtime", + "excluded": false, + "format": "", + "hasRuntime": true, + "info": Array [], + "isMapped": false, + "isUserEditable": false, + "kbnType": "number", + "name": "runtime", + "runtimeField": Object { + "script": Object { + "source": "emit('Hello');", + }, + "type": "long", + }, + "type": "long", + }, ] } /> @@ -182,6 +200,43 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
`; +exports[`IndexedFieldsTable should filter based on the schema filter 1`] = ` +
+ + +`; + exports[`IndexedFieldsTable should filter based on the type filter 1`] = `
diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 4773dbff38a28..e179050ca7fe2 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -11,6 +11,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { DataViewField, DataView, DataViewType } from 'src/plugins/data_views/public'; import { IndexedFieldsTable } from './indexed_fields_table'; import { getFieldInfo } from '../../utils'; +import { RuntimeField } from 'src/plugins/data_views/common'; jest.mock('@elastic/eui', () => ({ EuiFlexGroup: 'eui-flex-group', @@ -67,11 +68,12 @@ const rollupIndexPattern = { } as unknown as DataView; const mockFieldToIndexPatternField = ( - spec: Record + spec: Record ) => { return new DataViewField(spec as unknown as DataViewField['spec']); }; +const runtimeField: RuntimeField = { type: 'long', script: { source: "emit('Hello');" } }; const fields = [ { name: 'Elastic', @@ -88,6 +90,11 @@ const fields = [ isUserEditable: true, }, { name: 'amount', displayName: 'amount', esTypes: ['long'], isUserEditable: true }, + { + name: 'runtime', + displayName: 'runtime', + runtimeField, + }, ].map(mockFieldToIndexPatternField); describe('IndexedFieldsTable', () => { @@ -100,7 +107,8 @@ describe('IndexedFieldsTable', () => { fieldWildcardMatcher={() => { return () => false; }} - indexedFieldTypeFilter="" + indexedFieldTypeFilter={[]} + schemaFieldTypeFilter={[]} fieldFilter="" /> ).dive(); @@ -120,7 +128,8 @@ describe('IndexedFieldsTable', () => { fieldWildcardMatcher={() => { return () => false; }} - indexedFieldTypeFilter="" + indexedFieldTypeFilter={[]} + schemaFieldTypeFilter={[]} fieldFilter="" /> ).dive(); @@ -141,13 +150,36 @@ describe('IndexedFieldsTable', () => { fieldWildcardMatcher={() => { return () => false; }} - indexedFieldTypeFilter="" + indexedFieldTypeFilter={[]} + schemaFieldTypeFilter={[]} + fieldFilter="" + /> + ).dive(); + + await new Promise((resolve) => process.nextTick(resolve)); + component.setProps({ indexedFieldTypeFilter: ['date'] }); + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should filter based on the schema filter', async () => { + const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( + { + return () => false; + }} + indexedFieldTypeFilter={[]} + schemaFieldTypeFilter={[]} fieldFilter="" /> ).dive(); await new Promise((resolve) => process.nextTick(resolve)); - component.setProps({ indexedFieldTypeFilter: 'date' }); + component.setProps({ schemaFieldTypeFilter: ['runtime'] }); component.update(); expect(component).toMatchSnapshot(); @@ -163,7 +195,8 @@ describe('IndexedFieldsTable', () => { fieldWildcardMatcher={() => { return () => false; }} - indexedFieldTypeFilter="" + indexedFieldTypeFilter={[]} + schemaFieldTypeFilter={[]} fieldFilter="" /> ).dive(); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 667a4e029e02b..07a3bf50aab53 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,7 +19,8 @@ interface IndexedFieldsTableProps { fields: DataViewField[]; indexPattern: DataView; fieldFilter?: string; - indexedFieldTypeFilter?: string; + indexedFieldTypeFilter: string[]; + schemaFieldTypeFilter: string[]; helpers: { editField: (fieldName: string) => void; deleteField: (fieldName: string) => void; @@ -93,7 +94,8 @@ class IndexedFields extends Component props.fieldFilter, (state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => props.indexedFieldTypeFilter, - (fields, fieldFilter, indexedFieldTypeFilter) => { + (state: IndexedFieldsTableState, props: IndexedFieldsTableProps) => props.schemaFieldTypeFilter, + (fields, fieldFilter, indexedFieldTypeFilter, schemaFieldTypeFilter) => { if (fieldFilter) { const normalizedFieldFilter = fieldFilter.toLowerCase(); fields = fields.filter( @@ -103,14 +105,34 @@ class IndexedFields extends Component { - if (indexedFieldTypeFilter === 'conflict' && field.kbnType === 'conflict') { + if (indexedFieldTypeFilter.includes('conflict') && field.kbnType === 'conflict') { + return true; + } + if ( + 'runtimeField' in field && + field.runtimeField?.type && + indexedFieldTypeFilter.includes(field.runtimeField?.type) + ) { return true; } // match one of multiple types on a field - return field.esTypes?.length && field.esTypes?.indexOf(indexedFieldTypeFilter) !== -1; + return ( + field.esTypes?.length && + field.esTypes.filter((val) => indexedFieldTypeFilter.includes(val)).length + ); + }); + } + + if (schemaFieldTypeFilter.length) { + // match fields of schema type + fields = fields.filter((field) => { + return ( + (schemaFieldTypeFilter.includes('runtime') && 'runtimeField' in field) || + (schemaFieldTypeFilter.includes('indexed') && !('runtimeField' in field)) + ); }); } diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index aa74ae8c78fae..169b3673001a1 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -68,6 +68,7 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={[]} /> ).dive(); @@ -86,6 +87,7 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={[]} /> ).dive(); @@ -117,6 +119,7 @@ describe('ScriptedFieldsTable', () => { painlessDocLink={'painlessDoc'} helpers={helpers} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={[]} /> ).dive(); @@ -125,7 +128,7 @@ describe('ScriptedFieldsTable', () => { await component.update(); // Fire `componentWillMount()` await component.update(); // Force update the component post async actions - component.setProps({ scriptedFieldLanguageFilter: 'painless' }); + component.setProps({ scriptedFieldLanguageFilter: ['painless'] }); component.update(); expect(component).toMatchSnapshot(); @@ -142,6 +145,7 @@ describe('ScriptedFieldsTable', () => { painlessDocLink={'painlessDoc'} helpers={helpers} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={[]} /> ).dive(); @@ -162,6 +166,7 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={[]} /> ).dive(); @@ -189,6 +194,7 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + scriptedFieldLanguageFilter={[]} /> ).dive(); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx index 2e3657b23c331..1b9d63e2d2c6a 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -23,7 +23,7 @@ import { useKibana } from '../../../../../../plugins/kibana_react/public'; interface ScriptedFieldsTableProps { indexPattern: DataView; fieldFilter?: string; - scriptedFieldLanguageFilter?: string; + scriptedFieldLanguageFilter: string[]; helpers: { redirectToRoute: Function; getRouteHref?: Function; @@ -92,9 +92,9 @@ class ScriptedFields extends Component field.lang === this.props.scriptedFieldLanguageFilter + if (scriptedFieldLanguageFilter.length) { + languageFilteredFields = fields.filter((field) => + scriptedFieldLanguageFilter.includes(field.lang) ); } diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index f24374a85a9ae..7bf9292fad788 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -9,15 +9,18 @@ import React, { useState, useCallback, useEffect, Fragment, useMemo, useRef } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { + EuiFilterButton, + EuiFilterGroup, EuiFlexGroup, EuiFlexItem, + EuiPopover, EuiTabbedContent, EuiTabbedContentTab, EuiSpacer, EuiFieldSearch, - EuiSelect, - EuiSelectOption, EuiButton, + EuiFilterSelectItem, + FilterChecked, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '../../../../../kibana_utils/public'; @@ -34,7 +37,7 @@ import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../ import { SourceFiltersTable } from '../source_filters_table'; import { IndexedFieldsTable } from '../indexed_fields_table'; import { ScriptedFieldsTable } from '../scripted_fields_table'; -import { getTabs, getPath, convertToEuiSelectOption } from './utils'; +import { getTabs, getPath, convertToEuiFilterOptions } from './utils'; import { getFieldInfo } from '../../utils'; interface TabsProps extends Pick { @@ -44,6 +47,12 @@ interface TabsProps extends Pick { refreshFields: () => void; } +interface FilterItems { + value: string; + name: string; + checked?: FilterChecked; +} + const searchAriaLabel = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.searchAria', { @@ -51,6 +60,10 @@ const searchAriaLabel = i18n.translate( } ); +const filterLabel = i18n.translate('indexPatternManagement.editIndexPattern.fields.filter', { + defaultMessage: 'Field type', +}); + const filterAriaLabel = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.filterAria', { @@ -58,6 +71,45 @@ const filterAriaLabel = i18n.translate( } ); +const schemaFilterLabel = i18n.translate('indexPatternManagement.editIndexPattern.fields.schema', { + defaultMessage: 'Schema type', +}); + +const schemaAriaLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.schemaAria', + { + defaultMessage: 'Filter schema types', + } +); + +const scriptedFieldFilterLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.scriptedFieldFilter', + { + defaultMessage: 'All languages', + } +); + +const scriptedFieldAriaLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.scriptedFieldFilterAria', + { + defaultMessage: 'Filter scripted field languages', + } +); + +const schemaOptionRuntime = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.runtime', + { + defaultMessage: 'Runtime', + } +); + +const schemaOptionIndexed = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.indexed', + { + defaultMessage: 'Indexed', + } +); + const filterPlaceholder = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.filterPlaceholder', { @@ -83,16 +135,53 @@ export function Tabs({ const { application, uiSettings, docLinks, dataViewFieldEditor, overlays, theme } = useKibana().services; const [fieldFilter, setFieldFilter] = useState(''); - const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); - const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState(''); - const [indexedFieldTypes, setIndexedFieldType] = useState([]); - const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); const [syncingStateFunc, setSyncingStateFunc] = useState({ getCurrentTab: () => TAB_INDEXED_FIELDS, }); + const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState([]); + const [isScriptedFieldFilterOpen, setIsScriptedFieldFilterOpen] = useState(false); + const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); + const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState([]); + const [isIndexedFilterOpen, setIsIndexedFilterOpen] = useState(false); + const [indexedFieldTypes, setIndexedFieldTypes] = useState([]); + const [schemaFieldTypeFilter, setSchemaFieldTypeFilter] = useState([]); + const [isSchemaFilterOpen, setIsSchemaFilterOpen] = useState(false); + const [schemaItems, setSchemaItems] = useState([ + { + value: 'runtime', + name: schemaOptionRuntime, + }, + { + value: 'indexed', + name: schemaOptionIndexed, + }, + ]); const closeEditorHandler = useRef<() => void | undefined>(); const { DeleteRuntimeFieldProvider } = dataViewFieldEditor; + const updateFilterItem = ( + items: FilterItems[], + index: number, + updater: (a: FilterItems[]) => void + ) => { + if (!items[index]) { + return; + } + + const newItems = [...items]; + + switch (newItems[index].checked) { + case 'on': + newItems[index].checked = undefined; + break; + + default: + newItems[index].checked = 'on'; + } + + updater(newItems); + }; + const refreshFilters = useCallback(() => { const tempIndexedFieldTypes: string[] = []; const tempScriptedFieldLanguages: string[] = []; @@ -113,10 +202,8 @@ export function Tabs({ } }); - setIndexedFieldType(convertToEuiSelectOption(tempIndexedFieldTypes, 'indexedFiledTypes')); - setScriptedFieldLanguages( - convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages') - ); + setIndexedFieldTypes(convertToEuiFilterOptions(tempIndexedFieldTypes)); + setScriptedFieldLanguages(convertToEuiFilterOptions(tempScriptedFieldLanguages)); }, [indexPattern]); const closeFieldEditor = useCallback(() => { @@ -172,13 +259,92 @@ export function Tabs({ {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( <> - setIndexedFieldTypeFilter(e.target.value)} - data-test-subj="indexedFieldTypeFilterDropdown" - aria-label={filterAriaLabel} - /> + + setIsIndexedFilterOpen(!isIndexedFilterOpen)} + isSelected={isIndexedFilterOpen} + numFilters={indexedFieldTypes.length} + hasActiveFilters={!!indexedFieldTypes.find((item) => item.checked === 'on')} + numActiveFilters={ + indexedFieldTypes.filter((item) => item.checked === 'on').length + } + > + {filterLabel} + + } + isOpen={isIndexedFilterOpen} + closePopover={() => setIsIndexedFilterOpen(false)} + > + {indexedFieldTypes.map((item, index) => ( + { + setIndexedFieldTypeFilter( + item.checked + ? indexedFieldTypeFilter.filter((f) => f !== item.value) + : [...indexedFieldTypeFilter, item.value] + ); + updateFilterItem(indexedFieldTypes, index, setIndexedFieldTypes); + }} + data-test-subj={`indexedFieldTypeFilterDropdown-option-${item.value}${ + item.checked ? '-checked' : '' + }`} + > + {item.name} + + ))} + + setIsSchemaFilterOpen(!isSchemaFilterOpen)} + isSelected={isSchemaFilterOpen} + numFilters={schemaItems.length} + hasActiveFilters={!!schemaItems.find((item) => item.checked === 'on')} + numActiveFilters={ + schemaItems.filter((item) => item.checked === 'on').length + } + > + {schemaFilterLabel} + + } + isOpen={isSchemaFilterOpen} + closePopover={() => setIsSchemaFilterOpen(false)} + > + {schemaItems.map((item, index) => ( + { + setSchemaFieldTypeFilter( + item.checked + ? schemaFieldTypeFilter.filter((f) => f !== item.value) + : [...schemaFieldTypeFilter, item.value] + ); + updateFilterItem(schemaItems, index, setSchemaItems); + }} + data-test-subj={`schemaFieldTypeFilterDropdown-option-${item.value}${ + item.checked ? '-checked' : '' + }`} + > + {item.name} + + ))} + + {userEditPermission && ( @@ -191,12 +357,52 @@ export function Tabs({ )} {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( - setScriptedFieldLanguageFilter(e.target.value)} - data-test-subj="scriptedFieldLanguageFilterDropdown" - /> + + setIsScriptedFieldFilterOpen(!isScriptedFieldFilterOpen)} + isSelected={isScriptedFieldFilterOpen} + numFilters={scriptedFieldLanguages.length} + hasActiveFilters={ + !!scriptedFieldLanguages.find((item) => item.checked === 'on') + } + numActiveFilters={ + scriptedFieldLanguages.filter((item) => item.checked === 'on').length + } + > + {scriptedFieldFilterLabel} + + } + isOpen={isScriptedFieldFilterOpen} + closePopover={() => setIsScriptedFieldFilterOpen(false)} + > + {scriptedFieldLanguages.map((item, index) => ( + { + setScriptedFieldLanguageFilter( + item.checked + ? scriptedFieldLanguageFilter.filter((f) => f !== item.value) + : [...scriptedFieldLanguageFilter, item.value] + ); + updateFilterItem(scriptedFieldLanguages, index, setScriptedFieldLanguages); + }} + data-test-subj={`scriptedFieldLanguageFilterDropdown-option-${item.value}${ + item.checked ? '-checked' : '' + }`} + > + {item.name} + + ))} + + )} @@ -206,8 +412,13 @@ export function Tabs({ fieldFilter, indexedFieldTypeFilter, indexedFieldTypes, + isIndexedFilterOpen, scriptedFieldLanguageFilter, scriptedFieldLanguages, + isScriptedFieldFilterOpen, + schemaItems, + schemaFieldTypeFilter, + isSchemaFilterOpen, openFieldEditor, userEditPermission, ] @@ -230,6 +441,7 @@ export function Tabs({ fieldFilter={fieldFilter} fieldWildcardMatcher={fieldWildcardMatcherDecorated} indexedFieldTypeFilter={indexedFieldTypeFilter} + schemaFieldTypeFilter={schemaFieldTypeFilter} helpers={{ editField: openFieldEditor, deleteField, @@ -289,6 +501,7 @@ export function Tabs({ history, indexPattern, indexedFieldTypeFilter, + schemaFieldTypeFilter, refreshFilters, scriptedFieldLanguageFilter, saveIndexPattern, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts index 0ea8d9d9e28f3..e82722b1a5127 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts @@ -105,36 +105,11 @@ export function getPath(field: DataViewField, indexPattern: DataView) { return `/dataView/${indexPattern?.id}/field/${encodeURIComponent(field.name)}`; } -const allTypesDropDown = i18n.translate( - 'indexPatternManagement.editIndexPattern.fields.allTypesDropDown', - { - defaultMessage: 'All field types', - } -); - -const allLangsDropDown = i18n.translate( - 'indexPatternManagement.editIndexPattern.fields.allLangsDropDown', - { - defaultMessage: 'All languages', - } -); - -export function convertToEuiSelectOption(options: string[], type: string) { - const euiOptions = - options.length > 0 - ? [ - { - value: '', - text: type === 'scriptedFieldLanguages' ? allLangsDropDown : allTypesDropDown, - }, - ] - : []; - return euiOptions.concat( - uniq(options).map((option) => { - return { - value: option, - text: option, - }; - }) - ); +export function convertToEuiFilterOptions(options: string[]) { + return uniq(options).map((option) => { + return { + value: option, + name: option, + }; + }); } diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index 261ba29410a09..3e9d316b59c61 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -44,6 +44,7 @@ export default function ({ getService, getPageObjects }) { expect(fieldType).to.be('keyword'); } }); + await PageObjects.settings.clearFieldTypeFilter('keyword'); await PageObjects.settings.setFieldTypeFilter('long'); @@ -54,6 +55,7 @@ export default function ({ getService, getPageObjects }) { expect(fieldType).to.be('long'); } }); + await PageObjects.settings.clearFieldTypeFilter('long'); }); }); } diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.js index 6ced089936e5e..6a7d414becfe7 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.js @@ -66,6 +66,7 @@ export default function ({ getService, getPageObjects }) { expect(lang).to.be('painless'); } }); + await PageObjects.settings.clearScriptedFieldLanguageFilter('painless'); await PageObjects.settings.setScriptedFieldLanguageFilter('expression'); @@ -76,6 +77,7 @@ export default function ({ getService, getPageObjects }) { expect(lang).to.be('expression'); } }); + await PageObjects.settings.clearScriptedFieldLanguageFilter('expression'); }); }); } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 54728e1db3f55..b1e4aa823821b 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -258,18 +258,44 @@ export class SettingsPageObject extends FtrService { ); } + async clearFieldTypeFilter(type: string) { + await this.testSubjects.clickWhenNotDisabled('indexedFieldTypeFilterDropdown'); + await this.testSubjects.existOrFail('indexedFieldTypeFilterDropdown-popover'); + await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}-checked`); + await this.testSubjects.click(`indexedFieldTypeFilterDropdown-option-${type}-checked`); + await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}`); + await this.browser.pressKeys(this.browser.keys.ESCAPE); + } + async setFieldTypeFilter(type: string) { - await this.find.clickByCssSelector( - 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[value="' + type + '"]' + await this.testSubjects.clickWhenNotDisabled('indexedFieldTypeFilterDropdown'); + await this.testSubjects.existOrFail('indexedFieldTypeFilterDropdown-popover'); + await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}`); + await this.testSubjects.click(`indexedFieldTypeFilterDropdown-option-${type}`); + await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}-checked`); + await this.browser.pressKeys(this.browser.keys.ESCAPE); + } + + async clearScriptedFieldLanguageFilter(type: string) { + await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown'); + await this.testSubjects.existOrFail('scriptedFieldLanguageFilterDropdown-popover'); + await this.testSubjects.existOrFail( + `scriptedFieldLanguageFilterDropdown-option-${type}-checked` ); + await this.testSubjects.click(`scriptedFieldLanguageFilterDropdown-option-${type}-checked`); + await this.testSubjects.existOrFail(`scriptedFieldLanguageFilterDropdown-option-${type}`); + await this.browser.pressKeys(this.browser.keys.ESCAPE); } async setScriptedFieldLanguageFilter(language: string) { - await this.find.clickByCssSelector( - 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[value="' + - language + - '"]' + await this.testSubjects.clickWhenNotDisabled('scriptedFieldLanguageFilterDropdown'); + await this.testSubjects.existOrFail('scriptedFieldLanguageFilterDropdown-popover'); + await this.testSubjects.existOrFail(`scriptedFieldLanguageFilterDropdown-option-${language}`); + await this.testSubjects.click(`scriptedFieldLanguageFilterDropdown-option-${language}`); + await this.testSubjects.existOrFail( + `scriptedFieldLanguageFilterDropdown-option-${language}-checked` ); + await this.browser.pressKeys(this.browser.keys.ESCAPE); } async filterField(name: string) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a86fe5ccd7a64..817c97fb39e48 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4143,8 +4143,6 @@ "indexPatternManagement.editIndexPattern.deleteButton": "削除", "indexPatternManagement.editIndexPattern.deprecation": "スクリプトフィールドは廃止予定です。代わりに{runtimeDocs}を使用してください。", "indexPatternManagement.editIndexPattern.fields.addFieldButtonLabel": "フィールドの追加", - "indexPatternManagement.editIndexPattern.fields.allLangsDropDown": "すべての言語", - "indexPatternManagement.editIndexPattern.fields.allTypesDropDown": "すべてのフィールドタイプ", "indexPatternManagement.editIndexPattern.fields.conflictModal.closeBtn": "閉じる", "indexPatternManagement.editIndexPattern.fields.conflictModal.description": "{fieldName}フィールドの型がインデックス全体で変更され、検索、視覚化、他の分析で使用できない可能性があります。", "indexPatternManagement.editIndexPattern.fields.conflictModal.title": "このフィールドは型が競合しています", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 160da296fd831..726a6f3926be5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3933,8 +3933,6 @@ "indexPatternManagement.editIndexPattern.deleteButton": "删除", "indexPatternManagement.editIndexPattern.deprecation": "脚本字段已弃用。改用 {runtimeDocs}。", "indexPatternManagement.editIndexPattern.fields.addFieldButtonLabel": "添加字段", - "indexPatternManagement.editIndexPattern.fields.allLangsDropDown": "所有语言", - "indexPatternManagement.editIndexPattern.fields.allTypesDropDown": "所有字段类型", "indexPatternManagement.editIndexPattern.fields.conflictModal.closeBtn": "关闭", "indexPatternManagement.editIndexPattern.fields.conflictModal.description": "{fieldName} 字段的类型在不同索引中会有所不同,并且可能无法用于搜索、可视化和其他分析。", "indexPatternManagement.editIndexPattern.fields.conflictModal.title": "此字段存在类型冲突", From 77fe5f0f459aa56a1de01fff387268494bae6fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Wed, 9 Feb 2022 10:24:58 +0100 Subject: [PATCH 072/161] [Unified Observability] Combine series action buttons (#123914) * fix typo * Group series action in a context menu * add remove series inside series actions * Add some basic tests for series actions * Change show/hide series icon * Remove unused import * wrap functions with useCallback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../series_editor/columns/remove_series.tsx | 51 ------ .../columns/series_actions.test.tsx | 54 ++++++ .../series_editor/columns/series_actions.tsx | 157 +++++++++++++----- .../exploratory_view/series_editor/series.tsx | 6 +- 4 files changed, 173 insertions(+), 95 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.test.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx deleted file mode 100644 index 2d38b81e12c9f..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx +++ /dev/null @@ -1,51 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: number; -} - -export function RemoveSeries({ seriesId }: Props) { - const { removeSeries, allSeries } = useSeriesStorage(); - - const onClick = () => { - removeSeries(seriesId); - }; - - const isDisabled = seriesId === 0 && allSeries.length > 1; - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.test.tsx new file mode 100644 index 0000000000000..d855f648f5da2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; +import { SeriesActions } from './series_actions'; +import { mockUxSeries, render } from '../../rtl_helpers'; + +describe('SeriesActions', function () { + it('should contain an edit button', function () { + const { getByLabelText } = render(); + + expect(getByLabelText('Edit series')).toBeInTheDocument(); + }); + + it('should contain an actions button', function () { + const { getByLabelText } = render(); + + expect(getByLabelText('View series actions')).toBeInTheDocument(); + }); + + describe('action context menu', function () { + beforeEach(() => { + render(); + + const actionsButton = screen.getByLabelText('View series actions'); + userEvent.click(actionsButton); + }); + + it('should display the action list when the actions button is clicked', function () { + expect(screen.getByLabelText('Series actions list')).toBeVisible(); + }); + + it('should display a view sample link', function () { + expect(screen.getByLabelText('View sample documents')).toBeVisible(); + }); + + it('should display a hide series link', function () { + expect(screen.getByLabelText('Hide series')).toBeVisible(); + }); + + it('should display a duplicates series link', function () { + expect(screen.getByLabelText('Duplicate series')).toBeVisible(); + }); + + it('should display a remove series link', function () { + expect(screen.getByLabelText('Remove series')).toBeVisible(); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 074a9e1ca6780..22d87e3977abc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -5,10 +5,17 @@ * 2.0. */ -import React from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React, { useState, useCallback } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RemoveSeries } from './remove_series'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesConfig, SeriesUrl } from '../../types'; import { useDiscoverLink } from '../../hooks/use_discover_link'; @@ -22,13 +29,15 @@ interface Props { } export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: Props) { - const { setSeries, allSeries } = useSeriesStorage(); + const { setSeries, removeSeries, allSeries } = useSeriesStorage(); + const [isPopoverOpen, setPopover] = useState(false); const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); const { indexPatterns } = useAppIndexPatternContext(); const indexPattern = indexPatterns?.[series.dataType]; + const deleteDisabled = seriesId === 0 && allSeries.length > 1; const copySeries = () => { let copySeriesId: string = `${series.name}-copy`; @@ -36,6 +45,7 @@ export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: P copySeriesId = copySeriesId + allSeries.length; } setSeries(allSeries.length, { ...series, name: copySeriesId, breakdown: undefined }); + closePopover(); }; const toggleSeries = () => { @@ -44,8 +54,31 @@ export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: P } else { setSeries(seriesId, { ...series, hidden: true }); } + closePopover(); }; + const closePopover = useCallback(() => { + setPopover(false); + }, [setPopover]); + + const onRemoveSeriesClick = useCallback(() => { + removeSeries(seriesId); + closePopover(); + }, [removeSeries, seriesId, closePopover]); + + const changePopoverVisibility = useCallback(() => { + setPopover(!isPopoverOpen); + }, [setPopover, isPopoverOpen]); + + const popoverButton = ( + + ); + return ( @@ -59,45 +92,57 @@ export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: P /> - - - - - - - - - - - - - + + {VIEW_SAMPLE_DOCUMENTS_LABEL} + , + + {series.hidden ? SHOW_SERIES_LABEL : HIDE_SERIES_LABEL} + , + + {COPY_SERIES_LABEL} + , + + {DELETE_SERIES_LABEL} + , + ]} /> - - - - + ); @@ -111,13 +156,43 @@ const HIDE_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.hide' defaultMessage: 'Hide series', }); +const SHOW_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.show', { + defaultMessage: 'Show series', +}); + const COPY_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.clone', { defaultMessage: 'Duplicate series', }); +const DELETE_SERIES_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.removeSeries', + { + defaultMessage: 'Remove series', + } +); + +const DELETE_SERIES_TOOLTIP_LABEL = i18n.translate( + 'xpack.observability.expView.seriesEditor.removeSeriesDisabled', + { + defaultMessage: + 'Main series cannot be removed. Please remove all series below before you can remove this.', + } +); + const VIEW_SAMPLE_DOCUMENTS_LABEL = i18n.translate( 'xpack.observability.seriesEditor.sampleDocuments', { - defaultMessage: 'View sample documents in new tab', + defaultMessage: 'View sample documents', + } +); + +const POPOVER_BUTTON_LABEL = i18n.translate('xpack.observability.seriesEditor.popoverButtonLabel', { + defaultMessage: 'View series actions', +}); + +const ACTIONS_CONTEXT_MENU_LABEL = i18n.translate( + 'xpack.observability.seriesEditor.actionsAriaContextLabel', + { + defaultMessage: 'Series actions list', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx index d320b84c6a684..5678bd61e1d47 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx @@ -47,11 +47,11 @@ export function Series({ item, isExpanded, toggleExpanded }: Props) { seriesId: id, }; - const [isExapndedOnce, setIsExapndedOnce] = useState(false); + const [isExpandedOnce, setIsExpandedOnce] = useState(false); useEffect(() => { if (isExpanded) { - setIsExapndedOnce(true); + setIsExpandedOnce(true); } }, [isExpanded]); @@ -99,7 +99,7 @@ export function Series({ item, isExpanded, toggleExpanded }: Props) { > - {isExapndedOnce && } + {isExpandedOnce && } From 558bcec4c1bfeb12bbb1885a8103b0d315f5b670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Wed, 9 Feb 2022 11:13:06 +0100 Subject: [PATCH 073/161] [ResponseOps] Improves ES query rule type description (#124979) --- .../plugins/stack_alerts/public/alert_types/es_query/index.ts | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts index b6cb8406dbb0a..cf54c5934c026 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts @@ -15,7 +15,7 @@ export function getAlertType(): RuleTypeModel { return { id: '.es-query', description: i18n.translate('xpack.stackAlerts.esQuery.ui.alertType.descriptionText', { - defaultMessage: 'Alert on matches against an Elasticsearch query.', + defaultMessage: 'Alert when matches are found during the latest query run.', }), iconClass: 'logoElastic', documentationUrl: (docLinks) => docLinks.links.alerting.esQuery, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 817c97fb39e48..edd9f2585c42a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25476,7 +25476,6 @@ "xpack.stackAlerts.esQuery.missingEsQueryErrorMessage": "[esQuery]:「query」を含む必要があります", "xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", "xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage": "Elasticsearchクエリアラート'\\{\\{alertName\\}\\}'が有効です。\n\n- 値:\\{\\{context.value\\}\\}\n- 満たされた条件:\\{\\{context.conditions\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- タイムスタンプ:\\{\\{context.date\\}\\}", - "xpack.stackAlerts.esQuery.ui.alertType.descriptionText": "Elasticsearchクエリと一致したときにアラートを発行します。", "xpack.stackAlerts.esQuery.ui.conditionPrompt": "一致数", "xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "前回の{window}でクエリが{count}個のドキュメントと一致しました。", "xpack.stackAlerts.esQuery.ui.queryEditor": "Elasticsearchクエリエディター", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 726a6f3926be5..907498450fb47 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25671,7 +25671,6 @@ "xpack.stackAlerts.esQuery.missingEsQueryErrorMessage": "[esQuery]:必须包含“query”", "xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", "xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage": "Elasticsearch 查询告警“\\{\\{alertName\\}\\}”处于活动状态:\n\n- 值:\\{\\{context.value\\}\\}\n- 满足的条件:\\{\\{context.conditions\\}\\} 超过 \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- 时间戳:\\{\\{context.date\\}\\}", - "xpack.stackAlerts.esQuery.ui.alertType.descriptionText": "匹配 Elasticsearch 查询时告警。", "xpack.stackAlerts.esQuery.ui.conditionPrompt": "当匹配数目", "xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "查询在过去 {window} 匹配 {count} 个文档。", "xpack.stackAlerts.esQuery.ui.queryEditor": "Elasticsearch 查询编辑器", From b2b60ff061ad8eec2ea7b1fae351eb9888937005 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 9 Feb 2022 11:42:53 +0100 Subject: [PATCH 074/161] [Screenshotting] Fix potential race condition when screenshotting (#123820) * extract message from error objects * only warn for 400 and up status codes * naively wait for vis ready after resizing the browser viewport * use a single default viewport size, enable layout to set default page viewport for every page that is created * refactor viewport -> windowSize in chromium args * allow overriding defaults and use new windowSize arg for chromium args * always round page dimension numbers. note: this will break if we ever have a "undefined" set as a key value * added comment * update snapshot to new width value * make defaultViewport a required field on createPage * added comment * style: use async-await rather than .then chaining. also added a comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../browsers/chromium/driver_factory/args.ts | 15 ++-- .../chromium/driver_factory/index.test.ts | 14 ++-- .../browsers/chromium/driver_factory/index.ts | 18 ++++- .../server/layouts/create_layout.ts | 12 +++- .../server/screenshots/index.test.ts | 2 +- .../server/screenshots/index.ts | 71 +++++++++++-------- .../server/screenshots/observable.ts | 24 +++---- 7 files changed, 97 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts index e5985082b3c1c..964b8298151f5 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts @@ -7,7 +7,7 @@ import type { ConfigType } from '../../../config'; -interface Viewport { +interface WindowSize { height: number; width: number; } @@ -16,12 +16,17 @@ type Proxy = ConfigType['browser']['chromium']['proxy']; interface LaunchArgs { userDataDir: string; - viewport?: Viewport; + windowSize?: WindowSize; disableSandbox?: boolean; proxy: Proxy; } -export const args = ({ userDataDir, disableSandbox, viewport, proxy: proxyConfig }: LaunchArgs) => { +export const args = ({ + userDataDir, + disableSandbox, + windowSize, + proxy: proxyConfig, +}: LaunchArgs) => { const flags = [ // Disable built-in Google Translate service '--disable-translate', @@ -50,11 +55,11 @@ export const args = ({ userDataDir, disableSandbox, viewport, proxy: proxyConfig `--mainFrameClipsContent=false`, ]; - if (viewport) { + if (windowSize) { // NOTE: setting the window size does NOT set the viewport size: viewport and window size are different. // The viewport may later need to be resized depending on the position of the clip area. // These numbers come from the job parameters, so this is a close guess. - flags.push(`--window-size=${Math.floor(viewport.width)},${Math.floor(viewport.height)}`); + flags.push(`--window-size=${Math.floor(windowSize.width)},${Math.floor(windowSize.height)}`); } if (proxyConfig.enabled) { diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts index 7d9813928f924..bf8a1786735eb 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts @@ -11,7 +11,7 @@ import { mergeMap, take } from 'rxjs/operators'; import type { Logger } from 'src/core/server'; import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { ConfigType } from '../../../config'; -import { HeadlessChromiumDriverFactory } from '.'; +import { HeadlessChromiumDriverFactory, DEFAULT_VIEWPORT } from '.'; jest.mock('puppeteer'); @@ -70,7 +70,10 @@ describe('HeadlessChromiumDriverFactory', () => { describe('createPage', () => { it('returns browser driver, unexpected process exit observable, and close callback', async () => { await expect( - factory.createPage({ openUrlTimeout: 0 }).pipe(take(1)).toPromise() + factory + .createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }) + .pipe(take(1)) + .toPromise() ).resolves.toEqual( expect.objectContaining({ driver: expect.anything(), @@ -85,7 +88,10 @@ describe('HeadlessChromiumDriverFactory', () => { `Puppeteer Launch mock fail.` ); expect(() => - factory.createPage({ openUrlTimeout: 0 }).pipe(take(1)).toPromise() + factory + .createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }) + .pipe(take(1)) + .toPromise() ).rejects.toThrowErrorMatchingInlineSnapshot( `"Error spawning Chromium browser! Puppeteer Launch mock fail."` ); @@ -94,7 +100,7 @@ describe('HeadlessChromiumDriverFactory', () => { describe('close behaviour', () => { it('does not allow close to be called on the browse more than once', async () => { await factory - .createPage({ openUrlTimeout: 0 }) + .createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }) .pipe( take(1), mergeMap(async ({ close }) => { diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts index 787bd8fbfca99..d26d948beee16 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -7,6 +7,7 @@ import { getDataPath } from '@kbn/utils'; import { spawn } from 'child_process'; +import _ from 'lodash'; import del from 'del'; import fs from 'fs'; import { uniq } from 'lodash'; @@ -36,6 +37,12 @@ import { getMetrics, PerformanceMetrics } from './metrics'; interface CreatePageOptions { browserTimezone?: string; + defaultViewport: { + /** Size in pixels */ + width?: number; + /** Size in pixels */ + height?: number; + }; openUrlTimeout: number; } @@ -110,7 +117,7 @@ export class HeadlessChromiumDriverFactory { userDataDir: this.userDataDir, disableSandbox: this.config.browser.chromium.disableSandbox, proxy: this.config.browser.chromium.proxy, - viewport: DEFAULT_VIEWPORT, + windowSize: DEFAULT_VIEWPORT, // Approximate the default viewport size }); } @@ -118,7 +125,7 @@ export class HeadlessChromiumDriverFactory { * Return an observable to objects which will drive screenshot capture for a page */ createPage( - { browserTimezone, openUrlTimeout }: CreatePageOptions, + { browserTimezone, openUrlTimeout, defaultViewport }: CreatePageOptions, pLogger = this.logger ): Rx.Observable { // FIXME: 'create' is deprecated @@ -139,6 +146,13 @@ export class HeadlessChromiumDriverFactory { ignoreHTTPSErrors: true, handleSIGHUP: false, args: chromiumArgs, + + // We optionally set this at page creation to reduce the chances of + // browser reflow. In most cases only the height needs to be adjusted + // before taking a screenshot. + // NOTE: _.defaults assigns to the target object, so we copy it. + // NOTE NOTE: _.defaults is not the same as { ...DEFAULT_VIEWPORT, ...defaultViewport } + defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT), env: { TZ: browserTimezone, }, diff --git a/x-pack/plugins/screenshotting/server/layouts/create_layout.ts b/x-pack/plugins/screenshotting/server/layouts/create_layout.ts index 29a34a07e696f..fa4b3a40e2c79 100644 --- a/x-pack/plugins/screenshotting/server/layouts/create_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/create_layout.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { map as mapRecord } from 'fp-ts/lib/Record'; import type { LayoutParams } from '../../common/layout'; import { LayoutTypes } from '../../common'; import type { Layout } from '.'; @@ -12,13 +13,20 @@ import { CanvasLayout } from './canvas_layout'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; +/** + * We naively round all numeric values in the object, this will break screenshotting + * if ever a have a non-number set as a value, but this points to an issue + * in the code responsible for creating the dimensions object. + */ +const roundNumbers = mapRecord(Math.round); + export function createLayout({ id, dimensions, selectors, ...config }: LayoutParams): Layout { if (dimensions && id === LayoutTypes.PRESERVE_LAYOUT) { - return new PreserveLayout(dimensions, selectors); + return new PreserveLayout(roundNumbers(dimensions), selectors); } if (dimensions && id === LayoutTypes.CANVAS) { - return new CanvasLayout(dimensions); + return new CanvasLayout(roundNumbers(dimensions)); } // layoutParams is optional as PrintLayout doesn't use it diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts index c49f2289ba959..858e1ae9d6093 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts @@ -376,7 +376,7 @@ describe('Screenshot Observable Pipeline', () => { "height": 1200, "left": 0, "top": 0, - "width": 1800, + "width": 1950, }, "scroll": Object { "x": 0, diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index 363d59ccca950..d7332217e78a5 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -68,38 +68,47 @@ export function getScreenshots( timeouts: { openUrl: openUrlTimeout }, } = options; - return browserDriverFactory.createPage({ browserTimezone, openUrlTimeout }, logger).pipe( - mergeMap(({ driver, unexpectedExit$, metrics$, close }) => { - apmCreatePage?.end(); - metrics$.subscribe(({ cpu, memory }) => { - apmTrans?.setLabel('cpu', cpu, false); - apmTrans?.setLabel('memory', memory, false); - }); - unexpectedExit$.subscribe({ error: () => apmTrans?.end() }); + return browserDriverFactory + .createPage( + { + browserTimezone, + openUrlTimeout, + defaultViewport: { height: layout.height, width: layout.width }, + }, + logger + ) + .pipe( + mergeMap(({ driver, unexpectedExit$, metrics$, close }) => { + apmCreatePage?.end(); + metrics$.subscribe(({ cpu, memory }) => { + apmTrans?.setLabel('cpu', cpu, false); + apmTrans?.setLabel('memory', memory, false); + }); + unexpectedExit$.subscribe({ error: () => apmTrans?.end() }); - const screen = new ScreenshotObservableHandler(driver, logger, layout, options); + const screen = new ScreenshotObservableHandler(driver, logger, layout, options); - return from(options.urls).pipe( - concatMap((url, index) => - screen.setupPage(index, url, apmTrans).pipe( - catchError((error) => { - screen.checkPageIsOpen(); // this fails the job if the browser has closed + return from(options.urls).pipe( + concatMap((url, index) => + screen.setupPage(index, url, apmTrans).pipe( + catchError((error) => { + screen.checkPageIsOpen(); // this fails the job if the browser has closed - logger.error(error); - return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture - }), - takeUntil(unexpectedExit$), - screen.getScreenshots() - ) - ), - take(options.urls.length), - toArray(), - mergeMap((results) => { - // At this point we no longer need the page, close it. - return close().pipe(mapTo({ layout, metrics$, results })); - }) - ); - }), - first() - ); + logger.error(error); + return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture + }), + takeUntil(unexpectedExit$), + screen.getScreenshots() + ) + ), + take(options.urls.length), + toArray(), + mergeMap((results) => { + // At this point we no longer need the page, close it. + return close().pipe(mapTo({ layout, metrics$, results })); + }) + ); + }), + first() + ); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index b77180a9399b1..a238af5bcc25b 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -11,7 +11,7 @@ import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; import type { Logger } from 'src/core/server'; import type { Layout as ScreenshotModeLayout } from 'src/plugins/screenshot_mode/common'; import type { ConditionalHeaders, HeadlessChromiumDriver } from '../browsers'; -import { getChromiumDisconnectedError } from '../browsers'; +import { getChromiumDisconnectedError, DEFAULT_VIEWPORT } from '../browsers'; import type { Layout } from '../layouts'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; import { getElementPositionAndAttributes } from './get_element_position_data'; @@ -107,12 +107,9 @@ interface PageSetupResults { error?: Error; } -const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; -const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; - const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { - const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT; - const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH; + const height = dimensions?.height || DEFAULT_VIEWPORT.height; + const width = dimensions?.width || DEFAULT_VIEWPORT.width; return [ { @@ -130,8 +127,7 @@ const getDefaultElementPosition = (dimensions: { height?: number; width?: number * provided by the browser. */ const getDefaultViewPort = () => ({ - height: DEFAULT_SCREENSHOT_CLIP_HEIGHT, - width: DEFAULT_SCREENSHOT_CLIP_WIDTH, + ...DEFAULT_VIEWPORT, zoom: 1, }); @@ -180,14 +176,14 @@ export class ScreenshotObservableHandler { const waitTimeout = this.options.timeouts.waitForElements; return defer(() => getNumberOfItems(driver, this.logger, waitTimeout, this.layout)).pipe( - mergeMap((itemsCount) => { - // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout + mergeMap(async (itemsCount) => { + // set the viewport to the dimensions from the job, to allow elements to flow into the expected layout const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); - return forkJoin([ - driver.setViewport(viewport, this.logger), - waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout), - ]); + // Set the viewport allowing time for the browser to handle reflow and redraw + // before checking for readiness of visualizations. + await driver.setViewport(viewport, this.logger); + await waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout); }), this.waitUntil(waitTimeout, 'wait for elements') ); From ceb14e68429db6d3bb7ef8336b936f35084f1db7 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Wed, 9 Feb 2022 12:33:39 +0100 Subject: [PATCH 075/161] [Fleet] Test new privileges system via cypress (#124797) * Tests new roles introduced with superuser removal * Use login and roles utilities from security-solution cypress library * Add some more tests * expand tests * Fix failing test * Fix linter check --- .../cypress/integration/fleet_startup.spec.ts | 50 +-- .../integration/integrations_mock.spec.ts | 3 +- .../integration/integrations_real.spec.ts | 3 +- ...ileges_fleet_all_integrations_none.spec.ts | 42 +++ ...ileges_fleet_all_integrations_read.spec.ts | 88 +++++ ...ileges_fleet_none_integrations_all.spec.ts | 40 ++ x-pack/plugins/fleet/cypress/screens/fleet.ts | 11 + .../fleet/cypress/screens/integrations.ts | 2 + .../fleet/cypress/screens/navigation.ts | 3 +- x-pack/plugins/fleet/cypress/tasks/fleet.ts | 59 +++ x-pack/plugins/fleet/cypress/tasks/login.ts | 341 ++++++++++++++++++ .../plugins/fleet/cypress/tasks/navigation.ts | 13 +- .../plugins/fleet/cypress/tasks/privileges.ts | 232 ++++++++++++ .../fleet/public/applications/fleet/app.tsx | 4 +- .../package_policies_table.tsx | 1 + .../fleet_server_missing_privileges.tsx | 4 +- .../package_policy_actions_menu.tsx | 4 + 17 files changed, 847 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts create mode 100644 x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts create mode 100644 x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts create mode 100644 x-pack/plugins/fleet/cypress/tasks/fleet.ts create mode 100644 x-pack/plugins/fleet/cypress/tasks/login.ts create mode 100644 x-pack/plugins/fleet/cypress/tasks/privileges.ts diff --git a/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts b/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts index 83423d62e2a43..5c14ee1df6d4e 100644 --- a/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts @@ -5,47 +5,17 @@ * 2.0. */ -import { AGENTS_TAB, AGENT_POLICIES_TAB, ENROLLMENT_TOKENS_TAB } from '../screens/fleet'; +import { + AGENTS_TAB, + ADD_AGENT_BUTTON_TOP, + AGENT_FLYOUT_CLOSE_BUTTON, + STANDALONE_TAB, +} from '../screens/fleet'; import { cleanupAgentPolicies, unenrollAgent } from '../tasks/cleanup'; +import { verifyPolicy, verifyAgentPackage, navigateToTab } from '../tasks/fleet'; import { FLEET, navigateTo } from '../tasks/navigation'; describe('Fleet startup', () => { - function navigateToTab(tab: string) { - cy.getBySel(tab).click(); - cy.get('.euiBasicTable-loading').should('not.exist'); - } - - function navigateToAgentPolicy(name: string) { - cy.get('.euiLink').contains(name).click(); - cy.get('.euiLoadingSpinner').should('not.exist'); - } - - function navigateToEnrollmentTokens() { - cy.getBySel(ENROLLMENT_TOKENS_TAB).click(); - cy.get('.euiBasicTable-loading').should('not.exist'); - cy.get('.euiButtonIcon--danger'); // wait for trash icon - } - - function verifyPolicy(name: string, integrations: string[]) { - navigateToTab(AGENT_POLICIES_TAB); - - navigateToAgentPolicy(name); - integrations.forEach((integration) => { - cy.get('.euiLink').contains(integration); - }); - - cy.get('.euiButtonEmpty').contains('View all agent policies').click(); - - navigateToEnrollmentTokens(); - - cy.get('.euiTableCellContent').contains(name); - } - - function verifyAgentPackage() { - cy.visit('/app/integrations/installed'); - cy.getBySel('integration-card:epr:elastic_agent'); - } - // skipping Fleet Server enroll, to enable, comment out runner.ts line 23 describe.skip('Fleet Server', () => { it('should display Add agent button and Healthy agent once Fleet Agent page loaded', () => { @@ -77,8 +47,8 @@ describe('Fleet startup', () => { }); it('should create agent policy', () => { - cy.getBySel('addAgentBtnTop').click(); - cy.getBySel('standaloneTab').click(); + cy.getBySel(ADD_AGENT_BUTTON_TOP).click(); + cy.getBySel(STANDALONE_TAB).click(); cy.intercept('POST', '/api/fleet/agent_policies?sys_monitoring=true').as('createAgentPolicy'); @@ -97,7 +67,7 @@ describe('Fleet startup', () => { // verify agent.yml code block has new policy id cy.get('.euiCodeBlock__code').contains(`id: ${agentPolicyId}`); - cy.getBySel('euiFlyoutCloseButton').click(); + cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click(); // verify policy is created and has system package verifyPolicy('Agent policy 1', ['System']); diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts index 080b01458e18f..1b969e1a8ca2e 100644 --- a/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts @@ -7,6 +7,7 @@ import { navigateTo } from '../tasks/navigation'; import { UPDATE_PACKAGE_BTN } from '../screens/integrations'; +import { AGENT_POLICY_SAVE_INTEGRATION } from '../screens/fleet'; describe('Add Integration - Mock API', () => { describe('upgrade package and upgrade package policy', () => { @@ -141,7 +142,7 @@ describe('Add Integration - Mock API', () => { ); cy.getBySel('toastCloseButton').click(); - cy.getBySel('saveIntegration').click(); + cy.getBySel(AGENT_POLICY_SAVE_INTEGRATION).click(); cy.wait('@updateApachePolicy').then((interception) => { expect(interception.request.body.package.version).to.equal(newVersion); diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts index ffb0f14c97a7f..e06b3d3ed5670 100644 --- a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts @@ -24,6 +24,7 @@ import { SETTINGS_TAB, UPDATE_PACKAGE_BTN, } from '../screens/integrations'; +import { ADD_PACKAGE_POLICY_BTN } from '../screens/fleet'; import { cleanupAgentPolicies } from '../tasks/cleanup'; describe('Add Integration - Real API', () => { @@ -75,7 +76,7 @@ describe('Add Integration - Real API', () => { cy.visit(`/app/fleet/policies/${agentPolicyId}`); cy.intercept('GET', '/api/fleet/epm/packages?*').as('packages'); - cy.getBySel('addPackagePolicyButton').click(); + cy.getBySel(ADD_PACKAGE_POLICY_BTN).click(); cy.wait('@packages'); cy.get('.euiLoadingSpinner').should('not.exist'); cy.get('input[placeholder="Search for integrations"]').type('Apache'); diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts new file mode 100644 index 0000000000000..f9ae802d3b426 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts @@ -0,0 +1,42 @@ +/* + * 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 { FLEET } from '../tasks/navigation'; +import { + createUsersAndRoles, + FleetAllIntegrNoneRole, + FleetAllIntegrNoneUser, + deleteUsersAndRoles, +} from '../tasks/privileges'; +import { loginWithUserAndWaitForPage, logout } from '../tasks/login'; + +import { MISSING_PRIVILEGES_TITLE, MISSING_PRIVILEGES_MESSAGE } from '../screens/fleet'; +const rolesToCreate = [FleetAllIntegrNoneRole]; +const usersToCreate = [FleetAllIntegrNoneUser]; + +describe('When the user has All privilege for Fleet but None for integrations', () => { + before(() => { + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + + afterEach(() => { + logout(); + }); + + after(() => { + deleteUsersAndRoles(usersToCreate, rolesToCreate); + }); + + it('Fleet access is blocked with a callout', () => { + loginWithUserAndWaitForPage(FLEET, FleetAllIntegrNoneUser); + cy.getBySel(MISSING_PRIVILEGES_TITLE).should('have.text', 'Permission denied'); + cy.getBySel(MISSING_PRIVILEGES_MESSAGE).should( + 'contain', + 'You are not authorized to access Fleet.' + ); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts new file mode 100644 index 0000000000000..327ba39e65377 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FLEET, INTEGRATIONS, navigateTo } from '../tasks/navigation'; +import { + createUsersAndRoles, + FleetAllIntegrReadRole, + FleetAllIntegrReadUser, + deleteUsersAndRoles, +} from '../tasks/privileges'; +import { loginWithUserAndWaitForPage, logout } from '../tasks/login'; +import { navigateToTab, createAgentPolicy } from '../tasks/fleet'; +import { cleanupAgentPolicies, unenrollAgent } from '../tasks/cleanup'; + +import { + FLEET_SERVER_MISSING_PRIVILEGES_TITLE, + FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE, + ADD_AGENT_BUTTON_TOP, + AGENT_POLICIES_TAB, + AGENT_POLICY_SAVE_INTEGRATION, + ADD_PACKAGE_POLICY_BTN, +} from '../screens/fleet'; +import { ADD_POLICY_BTN, AGENT_POLICY_NAME_LINK } from '../screens/integrations'; + +const rolesToCreate = [FleetAllIntegrReadRole]; +const usersToCreate = [FleetAllIntegrReadUser]; + +describe('When the user has All privilege for Fleet but Read for integrations', () => { + before(() => { + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + + after(() => { + deleteUsersAndRoles(usersToCreate, rolesToCreate); + }); + + afterEach(() => { + logout(); + }); + + describe('When there are agent policies', () => { + before(() => { + navigateTo(FLEET); + createAgentPolicy(); + }); + + it('Some elements in the UI are not enabled', () => { + logout(); + loginWithUserAndWaitForPage(FLEET, FleetAllIntegrReadUser); + navigateToTab(AGENT_POLICIES_TAB); + + cy.getBySel(AGENT_POLICY_NAME_LINK).click(); + cy.getBySel(ADD_PACKAGE_POLICY_BTN).should('be.disabled'); + + cy.get('a[title="system-1"]').click(); + cy.getBySel(AGENT_POLICY_SAVE_INTEGRATION).should('be.disabled'); + }); + + after(() => { + unenrollAgent(); + cleanupAgentPolicies(); + }); + }); + + describe('When there are no agent policies', () => { + it('If fleet server is not set up, Fleet shows a callout', () => { + loginWithUserAndWaitForPage(FLEET, FleetAllIntegrReadUser); + cy.getBySel(FLEET_SERVER_MISSING_PRIVILEGES_TITLE).should('have.text', 'Permission denied'); + cy.getBySel(FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE).should( + 'contain', + 'Fleet Server needs to be set up.' + ); + cy.getBySel(ADD_AGENT_BUTTON_TOP).should('not.be.disabled'); + }); + }); + + describe('Integrations', () => { + it('are visible but cannot be added', () => { + loginWithUserAndWaitForPage(INTEGRATIONS, FleetAllIntegrReadUser); + cy.getBySel('integration-card:epr:apache').click(); + cy.getBySel(ADD_POLICY_BTN).should('be.disabled'); + }); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts new file mode 100644 index 0000000000000..68fcecb76de21 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INTEGRATIONS } from '../tasks/navigation'; +import { + createUsersAndRoles, + FleetNoneIntegrAllRole, + FleetNoneIntegrAllUser, + deleteUsersAndRoles, +} from '../tasks/privileges'; +import { loginWithUserAndWaitForPage, logout } from '../tasks/login'; + +import { ADD_POLICY_BTN } from '../screens/integrations'; + +const rolesToCreate = [FleetNoneIntegrAllRole]; +const usersToCreate = [FleetNoneIntegrAllUser]; + +describe('When the user has All privileges for Integrations but None for for Fleet', () => { + before(() => { + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + + afterEach(() => { + logout(); + }); + + after(() => { + deleteUsersAndRoles(usersToCreate, rolesToCreate); + }); + + it('Integrations are visible but cannot be added', () => { + loginWithUserAndWaitForPage(INTEGRATIONS, FleetNoneIntegrAllUser); + cy.getBySel('integration-card:epr:apache').click(); + cy.getBySel(ADD_POLICY_BTN).should('be.disabled'); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index 4c0bb7cea161e..32ecdc4f5da71 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -6,8 +6,19 @@ */ export const ADD_AGENT_BUTTON = 'addAgentButton'; +export const ADD_AGENT_BUTTON_TOP = 'addAgentBtnTop'; +export const CREATE_POLICY_BUTTON = 'createPolicyBtn'; +export const AGENT_FLYOUT_CLOSE_BUTTON = 'euiFlyoutCloseButton'; export const AGENTS_TAB = 'fleet-agents-tab'; export const AGENT_POLICIES_TAB = 'fleet-agent-policies-tab'; export const ENROLLMENT_TOKENS_TAB = 'fleet-enrollment-tokens-tab'; export const SETTINGS_TAB = 'fleet-settings-tab'; +export const STANDALONE_TAB = 'standaloneTab'; +export const MISSING_PRIVILEGES_TITLE = 'missingPrivilegesPromptTitle'; +export const MISSING_PRIVILEGES_MESSAGE = 'missingPrivilegesPromptMessage'; +export const FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE = 'fleetServerMissingPrivilegesMessage'; +export const FLEET_SERVER_MISSING_PRIVILEGES_TITLE = 'fleetServerMissingPrivilegesTitle'; +export const AGENT_POLICY_SAVE_INTEGRATION = 'saveIntegration'; +export const PACKAGE_POLICY_TABLE_LINK = 'PackagePoliciesTableLink'; +export const ADD_PACKAGE_POLICY_BTN = 'addPackagePolicyButton'; diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts index 3c980723cc4df..dddede9e77f8d 100644 --- a/x-pack/plugins/fleet/cypress/screens/integrations.ts +++ b/x-pack/plugins/fleet/cypress/screens/integrations.ts @@ -11,6 +11,7 @@ export const INTEGRATIONS_CARD = '.euiCard__titleAnchor'; export const INTEGRATION_NAME_LINK = 'integrationNameLink'; export const AGENT_POLICY_NAME_LINK = 'agentPolicyNameLink'; +export const AGENT_ACTIONS_BTN = 'agentActionsBtn'; export const CONFIRM_MODAL_BTN = 'confirmModalConfirmButton'; export const CONFIRM_MODAL_BTN_SEL = `[data-test-subj=${CONFIRM_MODAL_BTN}]`; @@ -19,6 +20,7 @@ export const FLYOUT_CLOSE_BTN_SEL = '[data-test-subj="euiFlyoutCloseButton"]'; export const SETTINGS_TAB = 'tab-settings'; export const POLICIES_TAB = 'tab-policies'; +export const ADVANCED_TAB = 'tab-custom'; export const UPDATE_PACKAGE_BTN = 'updatePackageBtn'; export const LATEST_VERSION = 'latestVersion'; diff --git a/x-pack/plugins/fleet/cypress/screens/navigation.ts b/x-pack/plugins/fleet/cypress/screens/navigation.ts index fee38161b6b2b..76b73711db495 100644 --- a/x-pack/plugins/fleet/cypress/screens/navigation.ts +++ b/x-pack/plugins/fleet/cypress/screens/navigation.ts @@ -5,4 +5,5 @@ * 2.0. */ -export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]'; +export const TOGGLE_NAVIGATION_BTN = 'toggleNavButton'; +export const NAV_APP_LINK = 'collapsibleNavAppLink'; diff --git a/x-pack/plugins/fleet/cypress/tasks/fleet.ts b/x-pack/plugins/fleet/cypress/tasks/fleet.ts new file mode 100644 index 0000000000000..304ab7445d4e4 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/fleet.ts @@ -0,0 +1,59 @@ +/* + * 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 { + AGENT_POLICIES_TAB, + ENROLLMENT_TOKENS_TAB, + ADD_AGENT_BUTTON_TOP, + CREATE_POLICY_BUTTON, + AGENT_FLYOUT_CLOSE_BUTTON, + STANDALONE_TAB, +} from '../screens/fleet'; + +export function createAgentPolicy() { + cy.getBySel(ADD_AGENT_BUTTON_TOP).click(); + cy.getBySel(STANDALONE_TAB).click(); + cy.getBySel(CREATE_POLICY_BUTTON).click(); + cy.getBySel('agentPolicyCreateStatusCallOut').contains('Agent policy created'); + cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click(); +} + +export function navigateToTab(tab: string) { + cy.getBySel(tab).click(); + cy.get('.euiBasicTable-loading').should('not.exist'); +} + +export function navigateToAgentPolicy(name: string) { + cy.get('.euiLink').contains(name).click(); + cy.get('.euiLoadingSpinner').should('not.exist'); +} + +export function navigateToEnrollmentTokens() { + cy.getBySel(ENROLLMENT_TOKENS_TAB).click(); + cy.get('.euiBasicTable-loading').should('not.exist'); + cy.get('.euiButtonIcon--danger'); // wait for trash icon +} + +export function verifyPolicy(name: string, integrations: string[]) { + navigateToTab(AGENT_POLICIES_TAB); + + navigateToAgentPolicy(name); + integrations.forEach((integration) => { + cy.get('.euiLink').contains(integration); + }); + + cy.get('.euiButtonEmpty').contains('View all agent policies').click(); + + navigateToEnrollmentTokens(); + + cy.get('.euiTableCellContent').contains(name); +} + +export function verifyAgentPackage() { + cy.visit('/app/integrations/installed'); + cy.getBySel('integration-card:epr:elastic_agent'); +} diff --git a/x-pack/plugins/fleet/cypress/tasks/login.ts b/x-pack/plugins/fleet/cypress/tasks/login.ts new file mode 100644 index 0000000000000..2df7b88f1607b --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/login.ts @@ -0,0 +1,341 @@ +/* + * 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 Url from 'url'; +import type { UrlObject } from 'url'; + +import * as yaml from 'js-yaml'; + +import type { ROLES } from './privileges'; +import { hostDetailsUrl, LOGOUT_URL } from './navigation'; + +/** + * Credentials in the `kibana.dev.yml` config file will be used to authenticate + * with Kibana when credentials are not provided via environment variables + */ +const KIBANA_DEV_YML_PATH = '../../../config/kibana.dev.yml'; + +/** + * The configuration path in `kibana.dev.yml` to the username to be used when + * authenticating with Kibana. + */ +const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username'; + +/** + * The configuration path in `kibana.dev.yml` to the password to be used when + * authenticating with Kibana. + */ +const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password'; + +/** + * The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the + * username to be used when authenticating with Kibana + */ +const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME'; + +/** + * The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the + * username to be used when authenticating with Kibana + */ +const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; + +/** + * The Kibana server endpoint used for authentication + */ +const LOGIN_API_ENDPOINT = '/internal/security/login'; + +/** + * cy.visit will default to the baseUrl which uses the default kibana test user + * This function will override that functionality in cy.visit by building the baseUrl + * directly from the environment variables set up in x-pack/test/security_solution_cypress/runner.ts + * + * @param role string role/user to log in with + * @param route string route to visit + */ +export const getUrlWithRoute = (role: ROLES, route: string) => { + const url = Cypress.config().baseUrl; + const kibana = new URL(String(url)); + const theUrl = `${Url.format({ + auth: `${role}:changeme`, + username: role, + password: 'changeme', + protocol: kibana.protocol.replace(':', ''), + hostname: kibana.hostname, + port: kibana.port, + } as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`; + cy.log(`origin: ${theUrl}`); + return theUrl; +}; + +interface User { + username: string; + password: string; +} + +/** + * Builds a URL with basic auth using the passed in user. + * + * @param user the user information to build the basic auth with + * @param route string route to visit + */ +export const constructUrlWithUser = (user: User, route: string) => { + const url = Cypress.config().baseUrl; + const kibana = new URL(String(url)); + const hostname = kibana.hostname; + const username = user.username; + const password = user.password; + const protocol = kibana.protocol.replace(':', ''); + const port = kibana.port; + + const path = `${route.startsWith('/') ? '' : '/'}${route}`; + const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`; + const builtUrl = new URL(strUrl); + + cy.log(`origin: ${builtUrl.href}`); + return builtUrl.href; +}; + +export const getCurlScriptEnvVars = () => ({ + ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'), + ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'), + ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'), + KIBANA_URL: Cypress.config().baseUrl, +}); + +export const postRoleAndUser = (role: ROLES) => { + const env = getCurlScriptEnvVars(); + const detectionsRoleScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_role.sh`; + const detectionsRoleJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_role.json`; + const detectionsUserScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_user.sh`; + const detectionsUserJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_user.json`; + + // post the role + cy.exec(`bash ${detectionsRoleScriptPath} ${detectionsRoleJsonPath}`, { + env, + }); + + // post the user associated with the role to elasticsearch + cy.exec(`bash ${detectionsUserScriptPath} ${detectionsUserJsonPath}`, { + env, + }); +}; + +export const deleteRoleAndUser = (role: ROLES) => { + const env = getCurlScriptEnvVars(); + const detectionsUserDeleteScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/delete_detections_user.sh`; + + // delete the role + cy.exec(`bash ${detectionsUserDeleteScriptPath}`, { + env, + }); +}; + +export const loginWithUser = (user: User) => { + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: user.username, + password: user.password, + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(user, LOGIN_API_ENDPOINT), + }); +}; + +export const loginWithRole = async (role: ROLES) => { + postRoleAndUser(role); + const theUrl = Url.format({ + auth: `${role}:changeme`, + username: role, + password: 'changeme', + protocol: Cypress.env('protocol'), + hostname: Cypress.env('hostname'), + port: Cypress.env('configport'), + } as UrlObject); + cy.log(`origin: ${theUrl}`); + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: role, + password: 'changeme', + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: getUrlWithRoute(role, LOGIN_API_ENDPOINT), + }); +}; + +/** + * Authenticates with Kibana using, if specified, credentials specified by + * environment variables. The credentials in `kibana.dev.yml` will be used + * for authentication when the environment variables are unset. + * + * To speed the execution of tests, prefer this non-interactive authentication, + * which is faster than authentication via Kibana's interactive login page. + */ +export const login = (role?: ROLES) => { + if (role != null) { + loginWithRole(role); + } else if (credentialsProvidedByEnvironment()) { + loginViaEnvironmentCredentials(); + } else { + loginViaConfig(); + } +}; + +/** + * Returns `true` if the credentials used to login to Kibana are provided + * via environment variables + */ +const credentialsProvidedByEnvironment = (): boolean => + Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null; + +/** + * Authenticates with Kibana by reading credentials from the + * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` + * environment variables, and POSTing the username and password directly to + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). + */ +const loginViaEnvironmentCredentials = () => { + cy.log( + `Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables` + ); + + // programmatically authenticate without interacting with the Kibana login page + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-env' }, + method: 'POST', + url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, + }); +}; + +/** + * Authenticates with Kibana by reading credentials from the + * `kibana.dev.yml` file and POSTing the username and password directly to + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). + */ +const loginViaConfig = () => { + cy.log( + `Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\`` + ); + + // read the login details from `kibana.dev.yaml` + cy.readFile(KIBANA_DEV_YML_PATH).then((kibanaDevYml) => { + const config = yaml.safeLoad(kibanaDevYml); + + // programmatically authenticate without interacting with the Kibana login page + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: config.elasticsearch.username, + password: config.elasticsearch.password, + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, + }); + }); +}; + +/** + * Get the configured auth details that were used to spawn cypress + * + * @returns the default Elasticsearch username and password for this environment + */ +export const getEnvAuth = (): User => { + if (credentialsProvidedByEnvironment()) { + return { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }; + } else { + let user: User = { username: '', password: '' }; + cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => { + const config = yaml.safeLoad(devYml); + user = { username: config.elasticsearch.username, password: config.elasticsearch.password }; + }); + + return user; + } +}; + +/** + * Authenticates with Kibana, visits the specified `url`, and waits for the + * Kibana global nav to be displayed before continuing + */ +export const loginAndWaitForPage = ( + url: string, + role?: ROLES, + onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void +) => { + login(role); + cy.visit( + `${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`, + { + onBeforeLoad(win) { + if (onBeforeLoadCallback) { + onBeforeLoadCallback(win); + } + }, + } + ); + cy.get('[data-test-subj="headerGlobalNav"]'); +}; +export const waitForPage = (url: string) => { + cy.visit( + `${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))` + ); + cy.get('[data-test-subj="headerGlobalNav"]'); +}; + +export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => { + login(role); + cy.visit(role ? getUrlWithRoute(role, url) : url); + cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); +}; + +export const loginWithUserAndWaitForPage = (url: string, user: User) => { + loginWithUser(user); + cy.visit(constructUrlWithUser(user, url)); + cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); +}; + +export const loginAndWaitForHostDetailsPage = (hostName = 'suricata-iowa') => { + loginAndWaitForPage(hostDetailsUrl(hostName)); + cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist'); +}; + +export const waitForPageWithoutDateRange = (url: string, role?: ROLES) => { + cy.visit(role ? getUrlWithRoute(role, url) : url); + cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); +}; + +export const logout = () => { + cy.visit(LOGOUT_URL); +}; diff --git a/x-pack/plugins/fleet/cypress/tasks/navigation.ts b/x-pack/plugins/fleet/cypress/tasks/navigation.ts index a2dd131b647a6..741a2cf761e8c 100644 --- a/x-pack/plugins/fleet/cypress/tasks/navigation.ts +++ b/x-pack/plugins/fleet/cypress/tasks/navigation.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation'; - export const INTEGRATIONS = 'app/integrations#/'; export const FLEET = 'app/fleet/'; +export const LOGIN_API_ENDPOINT = '/internal/security/login'; +export const LOGOUT_API_ENDPOINT = '/api/security/logout'; +export const LOGIN_URL = '/login'; +export const LOGOUT_URL = '/logout'; + +export const hostDetailsUrl = (hostName: string) => + `/app/security/hosts/${hostName}/authentications`; export const navigateTo = (page: string) => { cy.visit(page); }; - -export const openNavigationFlyout = () => { - cy.get(TOGGLE_NAVIGATION_BTN).click(); -}; diff --git a/x-pack/plugins/fleet/cypress/tasks/privileges.ts b/x-pack/plugins/fleet/cypress/tasks/privileges.ts new file mode 100644 index 0000000000000..edcc8e3749689 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/privileges.ts @@ -0,0 +1,232 @@ +/* + * 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 { constructUrlWithUser, getEnvAuth } from './login'; + +interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +interface UserInfo { + username: string; + full_name: string; + email: string; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} + +// Create roles with allowed combinations of Fleet and Integrations +export const FleetAllIntegrAllRole: Role = { + name: 'fleet_all_int_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + fleetv2: ['all'], + fleet: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const FleetAllIntegrAllUser: User = { + username: 'fleet_all_int_all_user', + password: 'password', + roles: [FleetAllIntegrAllRole.name], +}; + +export const FleetAllIntegrReadRole: Role = { + name: 'fleet_all_int_read_user', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + fleetv2: ['all'], + fleet: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const FleetAllIntegrReadUser: User = { + username: 'fleet_all_int_read_user', + password: 'password', + roles: [FleetAllIntegrReadRole.name], +}; +export const FleetAllIntegrNoneRole: Role = { + name: 'fleet_all_int_none_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + fleetv2: ['all'], + fleet: ['none'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const FleetAllIntegrNoneUser: User = { + username: 'fleet_all_int_none_user', + password: 'password', + roles: [FleetAllIntegrNoneRole.name], +}; +export const FleetNoneIntegrAllRole: Role = { + name: 'fleet_none_int_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + fleetv2: ['none'], + fleet: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const FleetNoneIntegrAllUser: User = { + username: 'fleet_none_int_all_user', + password: 'password', + roles: [FleetNoneIntegrAllRole.name], +}; + +const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + +export enum ROLES { + elastic = 'elastic', +} + +export const createUsersAndRoles = (users: User[], roles: Role[]) => { + const envUser = getEnvAuth(); + for (const role of roles) { + cy.log(`Creating role: ${JSON.stringify(role)}`); + cy.request({ + body: role.privileges, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'PUT', + url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), + }) + .its('status') + .should('eql', 204); + } + + for (const user of users) { + const userInfo = getUserInfo(user); + cy.log(`Creating user: ${JSON.stringify(user)}`); + cy.request({ + body: { + username: user.username, + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), + }) + .its('status') + .should('eql', 200); + } +}; + +export const deleteUsersAndRoles = (users: User[], roles: Role[]) => { + const envUser = getEnvAuth(); + for (const user of users) { + cy.log(`Deleting user: ${JSON.stringify(user)}`); + cy.request({ + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'DELETE', + url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), + failOnStatusCode: false, + }) + .its('status') + .should('oneOf', [204, 404]); + } + + for (const role of roles) { + cy.log(`Deleting role: ${JSON.stringify(role)}`); + cy.request({ + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'DELETE', + url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), + failOnStatusCode: false, + }) + .its('status') + .should('oneOf', [204, 404]); + } +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 9799561970e48..29a491fe0c932 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -88,7 +88,7 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err +

= memo(({ err

} body={ -

+

= ({ { +

{

} body={ -

+

{ setIsActionsMenuOpen(false); @@ -75,6 +76,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ ] : []), , { return ( { From 49ec2c4459edb124da1b793b69e8d3315249e434 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Wed, 9 Feb 2022 13:03:23 +0100 Subject: [PATCH 076/161] Add github action for labeling Fleet issues for QA team (#124949) --- .github/workflows/label-qa-fixed-in.yml | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/label-qa-fixed-in.yml diff --git a/.github/workflows/label-qa-fixed-in.yml b/.github/workflows/label-qa-fixed-in.yml new file mode 100644 index 0000000000000..e1dafa061f623 --- /dev/null +++ b/.github/workflows/label-qa-fixed-in.yml @@ -0,0 +1,78 @@ +name: Add QA labels to Fleet issues +on: + pull_request: + types: + - closed + +jobs: + fetch_issues_to_label: + runs-on: ubuntu-latest + # Only run on PRs that were merged for the Fleet team + if: | + github.event.pull_request.merged_at && + contains(github.event.pull_request.labels.*.name, 'Team:Fleet') + outputs: + matrix: ${{ steps.issues_to_label.outputs.value }} + label_ids: ${{ steps.label_ids.outputs.value }} + steps: + - uses: octokit/graphql-action@v2.x + id: closing_issues + with: + query: | + query closingIssueNumbersQuery($prnumber: Int!) { + repository(owner: "elastic", name: "kibana") { + pullRequest(number: $prnumber) { + closingIssuesReferences(first: 10) { + nodes { + id + labels(first: 20) { + nodes { + id + name + } + } + } + } + } + } + } + prnumber: ${{ github.event.number }} + token: ${{ secrets.GITHUB_TOKEN }} + - uses: sergeysova/jq-action@v2 + id: issues_to_label + with: + # Map to the issues' node id + cmd: echo $CLOSING_ISSUES | jq -c '.repository.pullRequest.closingIssuesReferences.nodes | map(.id)' + multiline: true + env: + CLOSING_ISSUES: ${{ steps.closing_issues.outputs.data }} + - uses: sergeysova/jq-action@v2 + id: label_ids + with: + # Get list of version labels on pull request and map to label's node id, append 'QA:Ready For Testing' id ("MDU6TGFiZWwyNTQ1NjcwOTI4") + cmd: echo $PR_LABELS | jq -c 'map(select(.name | test("v[0-9]+\\.[0-9]+\\.[0-9]+")) | .node_id) + ["MDU6TGFiZWwyNTQ1NjcwOTI4"]' + multiline: true + env: + PR_LABELS: ${{ toJSON(github.event.pull_request.labels) }} + + label_issues: + needs: fetch_issues_to_label + runs-on: ubuntu-latest + # For each issue closed by the PR run this job + strategy: + matrix: + issueNodeId: ${{ fromJSON(needs.fetch_issues_to_label.outputs.matrix) }} + name: Label issue ${{ matrix.issueNodeId }} + steps: + - uses: octokit/graphql-action@v2.x + id: add_labels_to_closed_issue + with: + query: | + mutation add_label($issueid:String!, $labelids:[String!]!) { + addLabelsToLabelable(input: {labelableId: $issueid, labelIds: $labelids}) { + clientMutationId + } + } + issueid: ${{ matrix.issueNodeId }} + labelids: ${{ needs.fetch_issues_to_label.outputs.label_ids }} + token: ${{ secrets.GITHUB_TOKEN }} From ed36a1322bc42dc29005bce36490713ce7029cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Wed, 9 Feb 2022 13:15:06 +0100 Subject: [PATCH 077/161] [Unified observability] Remove uptime team as codeowners from exploratory exploratory view (#125053) --- .github/CODEOWNERS | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3f36f4b67e56b..a8619643d1b2e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -138,13 +138,11 @@ # Uptime /x-pack/plugins/uptime @elastic/uptime /x-pack/plugins/ux @elastic/uptime -/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime /x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime /x-pack/test/functional/apps/uptime @elastic/uptime /x-pack/test/functional/es_archives/uptime @elastic/uptime /x-pack/test/functional/services/uptime @elastic/uptime /x-pack/test/api_integration/apis/uptime @elastic/uptime -/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime # Client Side Monitoring / Uptime (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/public/application/uxApp.tsx @elastic/uptime From 939bc3d01e8a19df738859bc27b9654486efa02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Wed, 9 Feb 2022 13:26:30 +0100 Subject: [PATCH 078/161] [Security Solution][Endpoint] Unify remove artifact from policy message and fix back buttons (#124936) * Updates delete messages for event filters and host isolation exceptions list * Fixes back buttons when click on view full details. Open create dialog by default from artifacts policy view * Fixes ts checks --- .../policy_event_filters_delete_modal.test.tsx | 1 + .../policy_event_filters_delete_modal.tsx | 6 ++++-- .../policy_event_filters_empty_unexisting.tsx | 2 +- .../use_policy_event_filters_empty_hooks.ts | 12 +++++++++--- .../list/policy_event_filters_list.tsx | 5 ++++- .../components/delete_modal.test.tsx | 1 + .../components/delete_modal.tsx | 7 +++++-- .../components/empty_unexisting.tsx | 2 +- .../components/list.test.tsx | 2 +- .../components/list.tsx | 16 +++++++++++++--- ...licy_host_isolation_exceptions_empty_hooks.ts | 12 +++++++++--- .../host_isolation_exceptions_tab.tsx | 2 +- .../policy_trusted_apps_empty_unexisting.tsx | 2 +- .../empty/use_policy_trusted_apps_empty_hooks.ts | 12 +++++++++--- .../layout/policy_trusted_apps_layout.tsx | 2 +- .../list/policy_trusted_apps_list.test.tsx | 2 +- .../list/policy_trusted_apps_list.tsx | 11 +++++++---- 17 files changed, 69 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx index 20522e35e8983..2e00dab303007 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.test.tsx @@ -39,6 +39,7 @@ describe('Policy details event filter delete modal', () => { renderResult = mockedContext.render( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx index eca26a0026dd1..bfa2f09ab9773 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/delete_modal/policy_event_filters_delete_modal.tsx @@ -16,10 +16,12 @@ import { useBulkUpdateEventFilters } from '../hooks'; export const PolicyEventFiltersDeleteModal = ({ policyId, + policyName, exception, onCancel, }: { policyId: string; + policyName: string; exception: ExceptionListItemSchema; onCancel: () => void; }) => { @@ -36,8 +38,8 @@ export const PolicyEventFiltersDeleteModal = ({ text: i18n.translate( 'xpack.securitySolution.endpoint.policy.eventFilters.list.removeDialog.successToastText', { - defaultMessage: '"{exception}" has been removed from policy', - values: { exception: exception.name }, + defaultMessage: '"{eventFilterName}" has been removed from {policyName} policy', + values: { eventFilterName: exception.name, policyName }, } ), }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx index c4b9e778664ef..7976fc8a566da 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unexisting.tsx @@ -16,7 +16,7 @@ interface CommonProps { } export const PolicyEventFiltersEmptyUnexisting = memo(({ policyId, policyName }) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); + const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName, { show: 'create' }); return ( { +export const useGetLinkTo = ( + policyId: string, + policyName: string, + location?: Partial +) => { const { getAppUrl } = useAppUrl(); const { toRoutePath, toRouteUrl } = useMemo(() => { - const path = getEventFiltersListPath(); + const path = getEventFiltersListPath(location); return { toRoutePath: path, toRouteUrl: getAppUrl({ path }), }; - }, [getAppUrl]); + }, [getAppUrl, location]); const policyEventFiltersPath = useMemo(() => getPolicyEventFiltersPath(policyId), [policyId]); const policyEventFilterRouteState = useMemo(() => { @@ -55,5 +60,6 @@ export const useGetLinkTo = (policyId: string, policyName: string) => { return { onClickHandler, toRouteUrl, + state: policyEventFilterRouteState, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx index 5ab6f4bfb0eba..63930610f8aa7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/list/policy_event_filters_list.tsx @@ -33,6 +33,7 @@ import { PolicyEventFiltersDeleteModal } from '../delete_modal'; import { isGlobalPolicyEffected } from '../../../../../components/effected_policy_select/utils'; import { getEventFiltersListPath } from '../../../../../common/routing'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { useGetLinkTo } from '../empty/use_policy_event_filters_empty_hooks'; interface PolicyEventFiltersListProps { policy: ImmutableObject; @@ -47,6 +48,7 @@ export const PolicyEventFiltersList = React.memo(({ const [exceptionItemToDelete, setExceptionItemToDelete] = useState< ExceptionListItemSchema | undefined >(); + const { state } = useGetLinkTo(policy.id, policy.name); const { data: eventFilters, @@ -116,7 +118,7 @@ export const PolicyEventFiltersList = React.memo(({ ), href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), navigateAppId: APP_UI_ID, - navigateOptions: { path: viewUrlPath }, + navigateOptions: { path: viewUrlPath, state }, 'data-test-subj': 'view-full-details-action', }; const item = artifact as ExceptionListItemSchema; @@ -159,6 +161,7 @@ export const PolicyEventFiltersList = React.memo(({ {exceptionItemToDelete && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx index bd9bfbf5d653d..5e750b5599d71 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.test.tsx @@ -43,6 +43,7 @@ describe('Policy details host isolation exceptions delete modal', () => { (renderResult = mockedContext.render( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx index 655107b8f357d..ad8868bf68346 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/delete_modal.tsx @@ -17,10 +17,12 @@ import { updateOneHostIsolationExceptionItem } from '../../../../host_isolation_ export const PolicyHostIsolationExceptionsDeleteModal = ({ policyId, + policyName, exception, onCancel, }: { policyId: string; + policyName: string; exception: ExceptionListItemSchema; onCancel: () => void; }) => { @@ -51,8 +53,9 @@ export const PolicyHostIsolationExceptionsDeleteModal = ({ text: i18n.translate( 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.removeDialog.successToastText', { - defaultMessage: '"{exception}" has been removed from policy', - values: { exception: exception.name }, + defaultMessage: + '"{hostIsolationExceptionName}" has been removed from {policyName} policy', + values: { hostIsolationExceptionName: exception.name, policyName }, } ), }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx index 34246caf7c313..94185904ce6cc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx @@ -16,7 +16,7 @@ export const PolicyHostIsolationExceptionsEmptyUnexisting = ({ }: { policy: PolicyData; }) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policy.id, policy.name); + const { onClickHandler, toRouteUrl } = useGetLinkTo(policy.id, policy.name, { show: 'create' }); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx index 4ad3919fcc563..17e3ace9a6410 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx @@ -52,7 +52,7 @@ describe('Policy details host isolation exceptions tab', () => { ({ history } = mockedContext); render = () => (renderResult = mockedContext.render( - + )); act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx index feee5491da314..3b5244aad30da 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx @@ -33,14 +33,23 @@ import { getCurrentArtifactsLocation } from '../../../store/policy_details/selec import { usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyHostIsolationExceptionsDeleteModal } from './delete_modal'; import { useFetchHostIsolationExceptionsList } from '../../../../host_isolation_exceptions/view/hooks'; - -export const PolicyHostIsolationExceptionsList = ({ policyId }: { policyId: string }) => { +import { useGetLinkTo } from './use_policy_host_isolation_exceptions_empty_hooks'; + +export const PolicyHostIsolationExceptionsList = ({ + policyId, + policyName, +}: { + policyId: string; + policyName: string; +}) => { const history = useHistory(); const { getAppUrl } = useAppUrl(); const privileges = useUserPrivileges().endpointPrivileges; const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); + const { state } = useGetLinkTo(policyId, policyName); + // load the list of policies> const policiesRequest = useGetEndpointSpecificPolicies(); const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); @@ -127,7 +136,7 @@ export const PolicyHostIsolationExceptionsList = ({ policyId }: { policyId: stri ), href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), navigateAppId: APP_UI_ID, - navigateOptions: { path: viewUrlPath }, + navigateOptions: { path: viewUrlPath, state }, 'data-test-subj': 'view-full-details-action', }; @@ -172,6 +181,7 @@ export const PolicyHostIsolationExceptionsList = ({ policyId }: { policyId: stri {exceptionItemToDelete ? ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts index 6aba11a4499ea..494dfd9a7ae08 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts @@ -14,16 +14,21 @@ import { getHostIsolationExceptionsListPath, } from '../../../../../common/routing'; import { APP_UI_ID } from '../../../../../../../common/constants'; +import { HostIsolationExceptionsPageLocation } from '../../../../host_isolation_exceptions/types'; -export const useGetLinkTo = (policyId: string, policyName: string) => { +export const useGetLinkTo = ( + policyId: string, + policyName: string, + location?: Partial +) => { const { getAppUrl } = useAppUrl(); const { toRoutePath, toRouteUrl } = useMemo(() => { - const path = getHostIsolationExceptionsListPath(); + const path = getHostIsolationExceptionsListPath(location); return { toRoutePath: path, toRouteUrl: getAppUrl({ path }), }; - }, [getAppUrl]); + }, [getAppUrl, location]); const policyHostIsolationExceptionsPath = useMemo( () => getPolicyHostIsolationExceptionsPath(policyId), @@ -61,5 +66,6 @@ export const useGetLinkTo = (policyId: string, policyName: string) => { return { onClickHandler, toRouteUrl, + state: policyHostIsolationExceptionsRouteState, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx index 1836997e74c16..f9ec756a8be76 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx @@ -181,7 +181,7 @@ export const PolicyHostIsolationExceptionsTab = ({ policy }: { policy: PolicyDat color="transparent" borderRadius="none" > - + ) : ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx index 1c96398b8347e..1fe834a9fce46 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unexisting.tsx @@ -16,7 +16,7 @@ interface CommonProps { } export const PolicyTrustedAppsEmptyUnexisting = memo(({ policyId, policyName }) => { - const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); + const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName, { show: 'create' }); return ( { +export const useGetLinkTo = ( + policyId: string, + policyName: string, + location?: Partial +) => { const { getAppUrl } = useAppUrl(); const { toRoutePath, toRouteUrl } = useMemo(() => { - const path = getTrustedAppsListPath(); + const path = getTrustedAppsListPath(location); return { toRoutePath: path, toRouteUrl: getAppUrl({ path }), }; - }, [getAppUrl]); + }, [getAppUrl, location]); const policyTrustedAppsPath = useMemo(() => getPolicyTrustedAppsPath(policyId), [policyId]); const policyTrustedAppRouteState = useMemo(() => { @@ -55,5 +60,6 @@ export const useGetLinkTo = (policyId: string, policyName: string) => { return { onClickHandler, toRouteUrl, + state: policyTrustedAppRouteState, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index 83fb3663104a6..dd89cca43c10d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -166,7 +166,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { /> ) ) : displayHeaderAndContent ? ( - + ) : ( )} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index 67b435f4873a1..da304adc2db44 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -87,7 +87,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); waitForAction = appTestContext.middlewareSpy.waitForAction; - componentRenderProps = {}; + componentRenderProps = { policyId: '9f08b220-342d-4c8d-8971-4cf96adcac29', policyName: 'test' }; render = async (waitForLoadedState: boolean = true) => { appTestContext.history.push( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index fa4d4e40b3e52..54dabf87f474b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -22,7 +22,6 @@ import { getPolicyTrustedAppsListPagination, getTrustedAppsAllPoliciesById, isPolicyTrustedAppListLoading, - policyIdFromParams, getCurrentPolicyArtifactsFilter, } from '../../../store/policy_details/selectors'; import { @@ -39,21 +38,23 @@ import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/art import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { useGetLinkTo } from '../empty/use_policy_trusted_apps_empty_hooks'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; export interface PolicyTrustedAppsListProps { hideTotalShowingLabel?: boolean; + policyId: string; + policyName: string; } export const PolicyTrustedAppsList = memo( - ({ hideTotalShowingLabel = false }) => { + ({ hideTotalShowingLabel = false, policyId, policyName }) => { const getTestId = useTestIdGenerator(DATA_TEST_SUBJ); const toasts = useToasts(); const history = useHistory(); const { getAppUrl } = useAppUrl(); const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; - const policyId = usePolicyDetailsSelector(policyIdFromParams); const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); const defaultFilter = usePolicyDetailsSelector(getCurrentPolicyArtifactsFilter); const trustedAppItems = usePolicyDetailsSelector(getPolicyTrustedAppList); @@ -62,6 +63,7 @@ export const PolicyTrustedAppsList = memo( const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById); const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError); const navigateCallback = usePolicyDetailsNavigateCallback(); + const { state } = useGetLinkTo(policyId, policyName); const [isCardExpanded, setCardExpanded] = useState>({}); const [trustedAppsForRemoval, setTrustedAppsForRemoval] = useState([]); @@ -152,7 +154,7 @@ export const PolicyTrustedAppsList = memo( ), href: getAppUrl({ appId: APP_UI_ID, path: viewUrlPath }), navigateAppId: APP_UI_ID, - navigateOptions: { path: viewUrlPath }, + navigateOptions: { path: viewUrlPath, state }, 'data-test-subj': getTestId('viewFullDetailsAction'), }, ]; @@ -201,6 +203,7 @@ export const PolicyTrustedAppsList = memo( isCardExpanded, trustedAppItems, canCreateArtifactsByPolicy, + state, ]); const provideCardProps = useCallback['cardComponentProps']>( From c5454536ad8d9e60544e02b0438c1c0488371e22 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 9 Feb 2022 14:45:50 +0100 Subject: [PATCH 079/161] [Screenshotting] Limit number of concurrently running Chromium instances (#124720) * Convert screenshots capturing function into a service * Update screenshots service to limit the number of concurrent sessions --- .../server/config/schema.test.ts | 2 + .../screenshotting/server/config/schema.ts | 1 + x-pack/plugins/screenshotting/server/mock.ts | 3 +- .../plugins/screenshotting/server/plugin.ts | 19 ++- .../server/screenshots/index.test.ts | 38 ++++-- .../server/screenshots/index.ts | 121 ++++++++++-------- .../screenshotting/server/screenshots/mock.ts | 8 +- .../server/screenshots/semaphore.test.ts | 73 +++++++++++ .../server/screenshots/semaphore.ts | 70 ++++++++++ 9 files changed, 255 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts create mode 100644 x-pack/plugins/screenshotting/server/screenshots/semaphore.ts diff --git a/x-pack/plugins/screenshotting/server/config/schema.test.ts b/x-pack/plugins/screenshotting/server/config/schema.test.ts index 9180f0d180d5f..e25791b0ebad9 100644 --- a/x-pack/plugins/screenshotting/server/config/schema.test.ts +++ b/x-pack/plugins/screenshotting/server/config/schema.test.ts @@ -54,6 +54,7 @@ describe('ConfigSchema', () => { }, ], }, + "poolSize": 1, } `); }); @@ -105,6 +106,7 @@ describe('ConfigSchema', () => { }, ], }, + "poolSize": 1, } `); }); diff --git a/x-pack/plugins/screenshotting/server/config/schema.ts b/x-pack/plugins/screenshotting/server/config/schema.ts index bcf2fa9feead9..7711ffe1ea8e8 100644 --- a/x-pack/plugins/screenshotting/server/config/schema.ts +++ b/x-pack/plugins/screenshotting/server/config/schema.ts @@ -67,6 +67,7 @@ export const ConfigSchema = schema.object({ }), }), }), + poolSize: schema.number({ defaultValue: 1, min: 1 }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/screenshotting/server/mock.ts b/x-pack/plugins/screenshotting/server/mock.ts index 49d69521f2c19..f7d3421ca0f3e 100644 --- a/x-pack/plugins/screenshotting/server/mock.ts +++ b/x-pack/plugins/screenshotting/server/mock.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Logger } from 'src/core/server'; import { createMockBrowserDriverFactory } from './browsers/mock'; import { createMockScreenshots } from './screenshots/mock'; import type { ScreenshottingStart } from '.'; @@ -17,6 +16,6 @@ export function createMockScreenshottingStart(): jest.Mocked getScreenshots(driver, {} as Logger, options)), + getScreenshots: jest.fn((options) => getScreenshots(options)), }; } diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts index c7870322ccdfc..138193815debe 100755 --- a/x-pack/plugins/screenshotting/server/plugin.ts +++ b/x-pack/plugins/screenshotting/server/plugin.ts @@ -17,7 +17,7 @@ import type { import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { ChromiumArchivePaths, HeadlessChromiumDriverFactory, install } from './browsers'; import { ConfigType, createConfig } from './config'; -import { getScreenshots, ScreenshotOptions } from './screenshots'; +import { Screenshots } from './screenshots'; import { getChromiumPackage } from './utils'; interface SetupDeps { @@ -39,7 +39,7 @@ export interface ScreenshottingStart { * @param options Screenshots session options. * @returns Observable with screenshotting results. */ - getScreenshots(options: ScreenshotOptions): ReturnType; + getScreenshots: Screenshots['getScreenshots']; } export class ScreenshottingPlugin implements Plugin { @@ -47,6 +47,7 @@ export class ScreenshottingPlugin implements Plugin; + private screenshots!: Promise; constructor(context: PluginInitializerContext) { this.logger = context.logger.get(); @@ -65,12 +66,20 @@ export class ScreenshottingPlugin implements Plugin { this.logger.error('Error in screenshotting setup, it may not function properly.'); this.logger.error(error); }); + this.screenshots = (async () => { + const browserDriverFactory = await this.browserDriverFactory; + const logger = this.logger.get('screenshot'); + + return new Screenshots(browserDriverFactory, logger, this.config); + })(); + // Already handled in `browserDriverFactory` + this.screenshots.catch(() => {}); + return {}; } @@ -79,8 +88,8 @@ export class ScreenshottingPlugin implements Plugin from(this.browserDriverFactory).pipe(switchMap((factory) => factory.diagnose())), getScreenshots: (options) => - from(this.browserDriverFactory).pipe( - switchMap((factory) => getScreenshots(factory, this.logger.get('screenshot'), options)) + from(this.screenshots).pipe( + switchMap((screenshots) => screenshots.getScreenshots(options)) ), }; } diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts index 858e1ae9d6093..eae7a6a5bc031 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts @@ -7,12 +7,13 @@ import { of, throwError, NEVER } from 'rxjs'; import type { Logger } from 'src/core/server'; +import type { ConfigType } from '../config'; import { createMockBrowserDriver, createMockBrowserDriverFactory } from '../browsers/mock'; import type { HeadlessChromiumDriverFactory } from '../browsers'; import * as Layouts from '../layouts/create_layout'; import { createMockLayout } from '../layouts/mock'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; -import { getScreenshots, ScreenshotOptions } from '.'; +import { Screenshots, ScreenshotOptions } from '.'; /* * Tests @@ -23,6 +24,7 @@ describe('Screenshot Observable Pipeline', () => { let layout: ReturnType; let logger: jest.Mocked; let options: ScreenshotOptions; + let screenshots: Screenshots; beforeEach(async () => { driver = createMockBrowserDriver(); @@ -45,6 +47,7 @@ describe('Screenshot Observable Pipeline', () => { }, urls: ['/welcome/home/start/index.htm'], } as unknown as typeof options; + screenshots = new Screenshots(driverFactory, logger, { poolSize: 1 } as ConfigType); jest.spyOn(Layouts, 'createLayout').mockReturnValue(layout); @@ -56,7 +59,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const result = await getScreenshots(driverFactory, logger, options).toPromise(); + const result = await screenshots.getScreenshots(options).toPromise(); expect(result).toHaveProperty('results'); expect(result.results).toMatchInlineSnapshot(` @@ -112,10 +115,12 @@ describe('Screenshot Observable Pipeline', () => { it('pipelines multiple urls into', async () => { driver.screenshot.mockResolvedValue(Buffer.from('some screenshots')); - const result = await getScreenshots(driverFactory, logger, { - ...options, - urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], - }).toPromise(); + const result = await screenshots + .getScreenshots({ + ...options, + urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], + }) + .toPromise(); expect(result).toHaveProperty('results'); expect(result.results).toMatchInlineSnapshot(` @@ -245,10 +250,15 @@ describe('Screenshot Observable Pipeline', () => { driver.waitForSelector.mockImplementation((selectorArg: string) => { throw new Error('Mock error!'); }); - const result = await getScreenshots(driverFactory, logger, { - ...options, - urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php3'], - }).toPromise(); + const result = await screenshots + .getScreenshots({ + ...options, + urls: [ + '/welcome/home/start/index2.htm', + '/welcome/home/start/index.php3?page=./home.php3', + ], + }) + .toPromise(); expect(result).toHaveProperty('results'); expect(result.results).toMatchInlineSnapshot(` @@ -351,9 +361,9 @@ describe('Screenshot Observable Pipeline', () => { }) ); - await expect( - getScreenshots(driverFactory, logger, options).toPromise() - ).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); + await expect(screenshots.getScreenshots(options).toPromise()).rejects.toMatchInlineSnapshot( + `"Instant timeout has fired!"` + ); }); it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { @@ -362,7 +372,7 @@ describe('Screenshot Observable Pipeline', () => { ); layout.getViewport = () => null; - const result = await getScreenshots(driverFactory, logger, options).toPromise(); + const result = await screenshots.getScreenshots(options).toPromise(); expect(result).toHaveProperty('results'); expect(result.results).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index d7332217e78a5..a43fd4549e482 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -19,11 +19,13 @@ import { } from 'rxjs/operators'; import type { Logger } from 'src/core/server'; import { LayoutParams } from '../../common'; +import type { ConfigType } from '../config'; import type { HeadlessChromiumDriverFactory, PerformanceMetrics } from '../browsers'; import { createLayout } from '../layouts'; import type { Layout } from '../layouts'; import { ScreenshotObservableHandler } from './observable'; import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable'; +import { Semaphore } from './semaphore'; export interface ScreenshotOptions extends ScreenshotObservableOptions { layout: LayoutParams; @@ -51,64 +53,73 @@ const DEFAULT_SETUP_RESULT = { timeRange: null, }; -export function getScreenshots( - browserDriverFactory: HeadlessChromiumDriverFactory, - logger: Logger, - options: ScreenshotOptions -): Observable { - const apmTrans = apm.startTransaction('screenshot-pipeline', 'screenshotting'); - const apmCreateLayout = apmTrans?.startSpan('create-layout', 'setup'); - const layout = createLayout(options.layout); - logger.debug(`Layout: width=${layout.width} height=${layout.height}`); - apmCreateLayout?.end(); +export class Screenshots { + private semaphore: Semaphore; - const apmCreatePage = apmTrans?.startSpan('create-page', 'wait'); - const { - browserTimezone, - timeouts: { openUrl: openUrlTimeout }, - } = options; + constructor( + private readonly browserDriverFactory: HeadlessChromiumDriverFactory, + private readonly logger: Logger, + { poolSize }: ConfigType + ) { + this.semaphore = new Semaphore(poolSize); + } - return browserDriverFactory - .createPage( - { - browserTimezone, - openUrlTimeout, - defaultViewport: { height: layout.height, width: layout.width }, - }, - logger - ) - .pipe( - mergeMap(({ driver, unexpectedExit$, metrics$, close }) => { - apmCreatePage?.end(); - metrics$.subscribe(({ cpu, memory }) => { - apmTrans?.setLabel('cpu', cpu, false); - apmTrans?.setLabel('memory', memory, false); - }); - unexpectedExit$.subscribe({ error: () => apmTrans?.end() }); + getScreenshots(options: ScreenshotOptions): Observable { + const apmTrans = apm.startTransaction('screenshot-pipeline', 'screenshotting'); + const apmCreateLayout = apmTrans?.startSpan('create-layout', 'setup'); + const layout = createLayout(options.layout); + this.logger.debug(`Layout: width=${layout.width} height=${layout.height}`); + apmCreateLayout?.end(); - const screen = new ScreenshotObservableHandler(driver, logger, layout, options); + const apmCreatePage = apmTrans?.startSpan('create-page', 'wait'); + const { + browserTimezone, + timeouts: { openUrl: openUrlTimeout }, + } = options; - return from(options.urls).pipe( - concatMap((url, index) => - screen.setupPage(index, url, apmTrans).pipe( - catchError((error) => { - screen.checkPageIsOpen(); // this fails the job if the browser has closed + return this.browserDriverFactory + .createPage( + { + browserTimezone, + openUrlTimeout, + defaultViewport: { height: layout.height, width: layout.width }, + }, + this.logger + ) + .pipe( + this.semaphore.acquire(), + mergeMap(({ driver, unexpectedExit$, metrics$, close }) => { + apmCreatePage?.end(); + metrics$.subscribe(({ cpu, memory }) => { + apmTrans?.setLabel('cpu', cpu, false); + apmTrans?.setLabel('memory', memory, false); + }); + unexpectedExit$.subscribe({ error: () => apmTrans?.end() }); - logger.error(error); - return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture - }), - takeUntil(unexpectedExit$), - screen.getScreenshots() - ) - ), - take(options.urls.length), - toArray(), - mergeMap((results) => { - // At this point we no longer need the page, close it. - return close().pipe(mapTo({ layout, metrics$, results })); - }) - ); - }), - first() - ); + const screen = new ScreenshotObservableHandler(driver, this.logger, layout, options); + + return from(options.urls).pipe( + concatMap((url, index) => + screen.setupPage(index, url, apmTrans).pipe( + catchError((error) => { + screen.checkPageIsOpen(); // this fails the job if the browser has closed + + this.logger.error(error); + return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture + }), + takeUntil(unexpectedExit$), + screen.getScreenshots() + ) + ), + take(options.urls.length), + toArray(), + mergeMap((results) => { + // At this point we no longer need the page, close it. + return close().pipe(mapTo({ layout, metrics$, results })); + }) + ); + }), + first() + ); + } } diff --git a/x-pack/plugins/screenshotting/server/screenshots/mock.ts b/x-pack/plugins/screenshotting/server/screenshots/mock.ts index edef9c9044c9a..c4b5707243136 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/mock.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/mock.ts @@ -7,11 +7,11 @@ import { of, NEVER } from 'rxjs'; import { createMockLayout } from '../layouts/mock'; -import type { getScreenshots, ScreenshotResult } from '.'; +import type { Screenshots, ScreenshotResult } from '.'; -export function createMockScreenshots(): jest.Mocked<{ getScreenshots: typeof getScreenshots }> { +export function createMockScreenshots(): jest.Mocked { return { - getScreenshots: jest.fn((driverFactory, logger, options) => + getScreenshots: jest.fn((options) => of({ layout: createMockLayout(), metrics$: NEVER, @@ -27,5 +27,5 @@ export function createMockScreenshots(): jest.Mocked<{ getScreenshots: typeof ge })), } as ScreenshotResult) ), - }; + } as unknown as jest.Mocked; } diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts new file mode 100644 index 0000000000000..6d6dd21347974 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { TestScheduler } from 'rxjs/testing'; +import { Semaphore } from './semaphore'; + +describe('Semaphore', () => { + let testScheduler: TestScheduler; + + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => { + return expect(actual).toStrictEqual(expected); + }); + }); + + describe('acquire', () => { + it('should limit the number of concurrently subscribed observables', () => { + testScheduler.run(({ cold, expectObservable }) => { + const semaphore = new Semaphore(2); + const observable1 = cold('500ms a|').pipe(semaphore.acquire()); + const observable2 = cold('500ms b|').pipe(semaphore.acquire()); + const observable3 = cold('500ms c|').pipe(semaphore.acquire()); + + expectObservable(observable1).toBe('500ms a|'); + expectObservable(observable2).toBe('500ms b|'); + expectObservable(observable3).toBe('1001ms c|'); + }); + }); + + it('should release semaphore on unsubscription', () => { + testScheduler.run(({ cold, expectObservable }) => { + const semaphore = new Semaphore(2); + const observable1 = cold('500ms a|').pipe(semaphore.acquire()); + const observable2 = cold('500ms b|').pipe(semaphore.acquire()); + const observable3 = cold('500ms c|').pipe(semaphore.acquire()); + + expectObservable(observable1).toBe('500ms a|'); + expectObservable(observable2, '^ 100ms !').toBe(''); + expectObservable(observable3).toBe('601ms c|'); + }); + }); + + it('should release semaphore on error', () => { + testScheduler.run(({ cold, expectObservable }) => { + const semaphore = new Semaphore(2); + const observable1 = cold('500ms a|').pipe(semaphore.acquire()); + const observable2 = cold('100ms #').pipe(semaphore.acquire()); + const observable3 = cold('500ms c|').pipe(semaphore.acquire()); + + expectObservable(observable1).toBe('500ms a|'); + expectObservable(observable2).toBe('100ms #'); + expectObservable(observable3).toBe('600ms c|'); + }); + }); + + it('should remove from the queue on unsubscription', () => { + testScheduler.run(({ cold, expectObservable }) => { + const semaphore = new Semaphore(1); + const observable1 = cold('500ms a|').pipe(semaphore.acquire()); + const observable2 = cold('500ms b').pipe(semaphore.acquire()); + const observable3 = cold('500ms c|').pipe(semaphore.acquire()); + + expectObservable(observable1).toBe('500ms a|'); + expectObservable(observable2, '^ 100ms !').toBe(''); + expectObservable(observable3).toBe('1001ms c|'); + }); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore.ts new file mode 100644 index 0000000000000..cdf021da0f63e --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/semaphore.ts @@ -0,0 +1,70 @@ +/* + * 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 { Observable } from 'rxjs'; +import type { OperatorFunction } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +type Task = () => void; + +export class Semaphore { + private queue: Task[] = []; + + constructor(private capacity: number) { + this.release = this.release.bind(this); + } + + acquire(): OperatorFunction { + return (inner) => + new Observable((outer) => { + const task = () => { + /** + * outer.remove(cancel); + * + * @todo Uncomment the line above when RxJS is bumped to at least 6.6.3. + * @see https://github.com/ReactiveX/rxjs/pull/5659 + */ + + outer.add(inner.pipe(finalize(this.release)).subscribe(outer)); + }; + const cancel = this.cancel.bind(this, task); + + outer.add(cancel); + this.schedule(task); + }); + } + + protected release(): void { + this.capacity++; + this.next(); + } + + private next() { + if (this.capacity <= 0 || !this.queue.length) { + return; + } + + const task = this.queue.shift()!; + this.capacity--; + + task(); + } + + private schedule(task: Task) { + this.queue.push(task); + this.next(); + } + + private cancel(task: Task) { + const index = this.queue.indexOf(task); + if (index < 0) { + return; + } + + this.queue.splice(index, 1); + } +} From 9790a7e0087838a6c757192039e1f604ee64b765 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 9 Feb 2022 13:49:20 +0000 Subject: [PATCH 080/161] [ML] Adding category definition and category examples api tests (#124710) * [ML] Adding category definition api tests * adding test * adding category examples tests * updating test checks * changes based on review * removing test * correcting mistake where wrong test was removed * correctling text text * fixing test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/results/get_category_definition.ts | 174 +++++++++++++++++ .../apis/ml/results/get_category_examples.ts | 178 ++++++++++++++++++ .../api_integration/apis/ml/results/index.ts | 2 + x-pack/test/functional/services/ml/api.ts | 10 +- 4 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/results/get_category_definition.ts create mode 100644 x-pack/test/api_integration/apis/ml/results/get_category_examples.ts diff --git a/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts b/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts new file mode 100644 index 0000000000000..36f0f3946ac66 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/results/get_category_definition.ts @@ -0,0 +1,174 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + + const jobIdSpace1 = `sample_logs_${Date.now()}`; + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + + const PARTITION_FIELD_NAME = 'event.dataset'; + const testJobConfig = { + job_id: jobIdSpace1, + groups: ['sample_logs', 'bootstrap', 'categorization'], + description: "count by mlcategory (message) on 'sample logs' dataset with 15m bucket span", + analysis_config: { + bucket_span: '15m', + categorization_field_name: 'message', + per_partition_categorization: { enabled: true, stop_on_warn: true }, + detectors: [ + { + function: 'count', + by_field_name: 'mlcategory', + partition_field_name: PARTITION_FIELD_NAME, + }, + ], + influencers: ['mlcategory'], + }, + analysis_limits: { model_memory_limit: '26MB' }, + data_description: { time_field: '@timestamp', time_format: 'epoch_ms' }, + model_plot_config: { enabled: false, annotations_enabled: true }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + allow_lazy_open: false, + }; + // @ts-expect-error not full interface + const testDatafeedConfig: Datafeed = { + datafeed_id: `datafeed-${jobIdSpace1}`, + indices: ['ft_module_sample_logs'], + job_id: jobIdSpace1, + query: { bool: { must: [{ match_all: {} }] } }, + }; + + const expectedCategoryDefinition = { + categoryId: '1', + examplesLength: 4, + }; + + async function getCategoryDefinition( + jobId: string, + categoryId: string, + user: USER, + expectedStatusCode: number, + space?: string + ) { + const { body } = await supertest + .post(`${space ? `/s/${space}` : ''}/api/ml/results/category_definition`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send({ jobId, categoryId }) + .expect(expectedStatusCode); + + return body; + } + + describe('get category_definition', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + // @ts-expect-error not full interface + testJobConfig, + testDatafeedConfig, + idSpace1 + ); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + await ml.api.cleanMlIndices(); + }); + + it('should produce the correct category for the job', async () => { + const resp = await getCategoryDefinition( + jobIdSpace1, + expectedCategoryDefinition.categoryId, + USER.ML_POWERUSER, + 200, + idSpace1 + ); + + expect(resp.categoryId).to.eql( + expectedCategoryDefinition.categoryId, + `categoryId should be ${expectedCategoryDefinition.categoryId} (got ${resp.categoryId})` + ); + expect(resp.examples.length).to.eql( + expectedCategoryDefinition.examplesLength, + `examples list length should be ${expectedCategoryDefinition.examplesLength} (got ${resp.examples.length})` + ); + expect(resp.terms.length).to.be.greaterThan( + 0, + `terms string length should be greater than 0 (got ${resp.terms.length})` + ); + expect(resp.regex.length).to.be.greaterThan( + 0, + `regex string length should be greater than 0 (got ${resp.regex.length})` + ); + }); + + it('should not produce the correct category for the job in the wrong space', async () => { + await getCategoryDefinition( + jobIdSpace1, + expectedCategoryDefinition.categoryId, + USER.ML_POWERUSER, + 404, + idSpace2 + ); + }); + + it('should produce the correct category for ml viewer user', async () => { + const resp = await getCategoryDefinition( + jobIdSpace1, + expectedCategoryDefinition.categoryId, + USER.ML_VIEWER, + 200, + idSpace1 + ); + + expect(resp.categoryId).to.eql( + expectedCategoryDefinition.categoryId, + `categoryId should be ${expectedCategoryDefinition.categoryId} (got ${resp.categoryId})` + ); + expect(resp.examples.length).to.eql( + expectedCategoryDefinition.examplesLength, + `examples list length should be ${expectedCategoryDefinition.examplesLength} (got ${resp.examples.length})` + ); + expect(resp.terms.length).to.be.greaterThan( + 0, + `terms string length should be greater than 0 (got ${resp.terms.length})` + ); + expect(resp.regex.length).to.be.greaterThan( + 0, + `regex string length should be greater than 0 (got ${resp.regex.length})` + ); + }); + + it('should not produce the correct category for ml unauthorized user', async () => { + await getCategoryDefinition( + jobIdSpace1, + expectedCategoryDefinition.categoryId, + USER.ML_UNAUTHORIZED, + 403, + idSpace1 + ); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts b/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts new file mode 100644 index 0000000000000..79586daaa4aac --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/results/get_category_examples.ts @@ -0,0 +1,178 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + + const jobIdSpace1 = `sample_logs_${Date.now()}`; + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + + const PARTITION_FIELD_NAME = 'event.dataset'; + const testJobConfig = { + job_id: jobIdSpace1, + groups: ['sample_logs', 'bootstrap', 'categorization'], + description: "count by mlcategory (message) on 'sample logs' dataset with 15m bucket span", + analysis_config: { + bucket_span: '15m', + categorization_field_name: 'message', + per_partition_categorization: { enabled: true, stop_on_warn: true }, + detectors: [ + { + function: 'count', + by_field_name: 'mlcategory', + partition_field_name: PARTITION_FIELD_NAME, + }, + ], + influencers: ['mlcategory'], + }, + analysis_limits: { model_memory_limit: '26MB' }, + data_description: { time_field: '@timestamp', time_format: 'epoch_ms' }, + model_plot_config: { enabled: false, annotations_enabled: true }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + allow_lazy_open: false, + }; + // @ts-expect-error not full interface + const testDatafeedConfig: Datafeed = { + datafeed_id: `datafeed-${jobIdSpace1}`, + indices: ['ft_module_sample_logs'], + job_id: jobIdSpace1, + query: { bool: { must: [{ match_all: {} }] } }, + }; + + const expectedCategoryExamples = { + categoryId: '1', + examplesLength: 3, + }; + + async function getCategoryExamples( + jobId: string, + categoryIds: string[], + maxExamples: number, + user: USER, + expectedStatusCode: number, + space?: string + ) { + const { body } = await supertest + .post(`${space ? `/s/${space}` : ''}/api/ml/results/category_examples`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send({ jobId, categoryIds, maxExamples }) + .expect(expectedStatusCode); + + return body; + } + + describe('get category_examples', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + // @ts-expect-error not full interface + testJobConfig, + testDatafeedConfig, + idSpace1 + ); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + await ml.api.cleanMlIndices(); + }); + + it('should produce the correct 1 example for the job', async () => { + const maxExamples = 1; + const resp = await getCategoryExamples( + jobIdSpace1, + [expectedCategoryExamples.categoryId], + maxExamples, + USER.ML_POWERUSER, + 200, + idSpace1 + ); + + expect(resp[expectedCategoryExamples.categoryId].length).to.eql( + maxExamples, + `response examples length should be ${maxExamples} (got ${ + resp[expectedCategoryExamples.categoryId].length + })` + ); + }); + + it('should produce the correct 3 examples for the job', async () => { + const resp = await getCategoryExamples( + jobIdSpace1, + [expectedCategoryExamples.categoryId], + expectedCategoryExamples.examplesLength, + USER.ML_POWERUSER, + 200, + idSpace1 + ); + + expect(resp[expectedCategoryExamples.categoryId].length).to.eql( + expectedCategoryExamples.examplesLength, + `response examples length should be ${expectedCategoryExamples.examplesLength} (got ${ + resp[expectedCategoryExamples.categoryId].length + })` + ); + }); + + it('should not produce the correct examples for the job in the wrong space', async () => { + await getCategoryExamples( + jobIdSpace1, + [expectedCategoryExamples.categoryId], + expectedCategoryExamples.examplesLength, + USER.ML_POWERUSER, + 404, + idSpace2 + ); + }); + + it('should produce the correct example for the job for the ml viewer user', async () => { + const resp = await getCategoryExamples( + jobIdSpace1, + [expectedCategoryExamples.categoryId], + expectedCategoryExamples.examplesLength, + USER.ML_VIEWER, + 200, + idSpace1 + ); + + expect(resp[expectedCategoryExamples.categoryId].length).to.eql( + expectedCategoryExamples.examplesLength, + `response examples length should be ${expectedCategoryExamples.examplesLength} (got ${ + resp[expectedCategoryExamples.categoryId].length + })` + ); + }); + + it('should not produce the correct example for the job for the ml unauthorized user', async () => { + await getCategoryExamples( + jobIdSpace1, + [expectedCategoryExamples.categoryId], + expectedCategoryExamples.examplesLength, + USER.ML_UNAUTHORIZED, + 403, + idSpace1 + ); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/results/index.ts b/x-pack/test/api_integration/apis/ml/results/index.ts index 83338dcab57cd..575435fa3a720 100644 --- a/x-pack/test/api_integration/apis/ml/results/index.ts +++ b/x-pack/test/api_integration/apis/ml/results/index.ts @@ -12,5 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get_anomalies_table_data')); loadTestFile(require.resolve('./get_categorizer_stats')); loadTestFile(require.resolve('./get_stopped_partitions')); + loadTestFile(require.resolve('./get_category_definition')); + loadTestFile(require.resolve('./get_category_examples')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index ebe7f7e84d158..58f0b6d678cc2 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -681,9 +681,13 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> Datafeed stopped.'); }, - async createAndRunAnomalyDetectionLookbackJob(jobConfig: Job, datafeedConfig: Datafeed) { - await this.createAnomalyDetectionJob(jobConfig); - await this.createDatafeed(datafeedConfig); + async createAndRunAnomalyDetectionLookbackJob( + jobConfig: Job, + datafeedConfig: Datafeed, + space?: string + ) { + await this.createAnomalyDetectionJob(jobConfig, space); + await this.createDatafeed(datafeedConfig, space); await this.openAnomalyDetectionJob(jobConfig.job_id); await this.startDatafeed(datafeedConfig.datafeed_id, { start: '0', end: `${Date.now()}` }); await this.waitForDatafeedState(datafeedConfig.datafeed_id, DATAFEED_STATE.STOPPED); From 7dfc395423d100144ed9ca235ca0dfb973f92f93 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 9 Feb 2022 06:11:21 -0800 Subject: [PATCH 081/161] Adjusting uptime readonly feature (#125031) * Adjusting uptime readonly feature * Missed another occurrence, good call Xavier --- x-pack/plugins/uptime/server/kibana.index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index f5008e3f39da7..461c2ba16366a 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -60,7 +60,7 @@ export const initServerWithKibana = ( catalogue: ['uptime'], api: ['uptime-read', 'uptime-write', 'lists-all'], savedObject: { - all: [umDynamicSettings.name, 'alert', syntheticsMonitorType, syntheticsApiKeyObjectType], + all: [umDynamicSettings.name, syntheticsMonitorType, syntheticsApiKeyObjectType], read: [], }, alerting: { @@ -91,7 +91,7 @@ export const initServerWithKibana = ( catalogue: ['uptime'], api: ['uptime-read', 'lists-read'], savedObject: { - all: ['alert'], + all: [], read: [umDynamicSettings.name, syntheticsMonitorType, syntheticsApiKeyObjectType], }, alerting: { From 691315a27450ccf19b1f67ede5e1ac32451300c5 Mon Sep 17 00:00:00 2001 From: sphilipse <94373878+sphilipse@users.noreply.github.com> Date: Wed, 9 Feb 2022 15:44:40 +0100 Subject: [PATCH 082/161] Fix minor textual errors in Workplace Search (#121853) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../content_sources/components/add_source/save_custom.tsx | 2 +- .../workplace_search/views/content_sources/constants.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index 9dbbcc537fa31..c136f22d91d3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -174,7 +174,7 @@ export const SaveCustom: React.FC = ({

Date: Wed, 9 Feb 2022 08:54:30 -0600 Subject: [PATCH 083/161] [App Search] Add `EuiThemeProvider` to fix crashing bug (#124993) --- .../shared/log_stream/log_stream.test.tsx | 35 ++++++++++--------- .../shared/log_stream/log_stream.tsx | 15 ++++---- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.test.tsx index a934afb3b0d29..d2dd41e82b2e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.test.tsx @@ -9,18 +9,17 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { LogStream } from '../../../../../infra/public'; - import { EntSearchLogStream } from './'; describe('EntSearchLogStream', () => { const mockDateNow = jest.spyOn(global.Date, 'now').mockReturnValue(160000000); describe('renders with default props', () => { - const wrapper = shallow(); + /** As a result of the theme provider being added, we have to extract the child component to correctly assert */ + const wrapper = shallow(shallow().prop('children')); - it('renders a LogStream component', () => { - expect(wrapper.type()).toEqual(LogStream); + it('renders a LogStream (wrapped in React.Suspense) component', () => { + expect(wrapper.type()).toEqual(React.Suspense); }); it('renders with the enterprise search log source ID', () => { @@ -36,7 +35,9 @@ describe('EntSearchLogStream', () => { describe('renders custom props', () => { it('overrides the default props', () => { const wrapper = shallow( - + shallow().prop( + 'children' + ) ); expect(wrapper.prop('sourceId')).toEqual('test'); @@ -45,7 +46,7 @@ describe('EntSearchLogStream', () => { }); it('allows passing a custom hoursAgo that modifies the default start timestamp', () => { - const wrapper = shallow(); + const wrapper = shallow(shallow().prop('children')); expect(wrapper.prop('startTimestamp')).toEqual(156400000); expect(wrapper.prop('endTimestamp')).toEqual(160000000); @@ -53,15 +54,17 @@ describe('EntSearchLogStream', () => { it('allows passing any prop that the LogStream component takes', () => { const wrapper = shallow( - + shallow( + + ).prop('children') ); expect(wrapper.prop('height')).toEqual(500); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.tsx index c1f4262881bd2..e826a559451f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/log_stream/log_stream.tsx @@ -7,6 +7,7 @@ import React from 'react'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { LogStream, LogStreamProps } from '../../../../../infra/public'; import { LOGS_SOURCE_ID } from '../../../../common/constants'; @@ -37,11 +38,13 @@ export const EntSearchLogStream: React.FC = ({ if (!startTimestamp) startTimestamp = endTimestamp - hoursAgo * 60 * 60 * 1000; return ( - + + + ); }; From 440f5f0d51373b7fa79735879fb853fbda310ed9 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Wed, 9 Feb 2022 18:32:46 +0300 Subject: [PATCH 084/161] [RAC][Logs]Use symbols comparator in logs reason msg (#124727) * Use symbols comparator in logs reason msg * Update i18n keys * Codereview fixes - Use symbols without i18n Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../alerting/logs/log_threshold/types.ts | 31 +++++++++++++++++++ .../log_threshold/reason_formatters.ts | 18 +++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index dba94d2c8fd93..845f5d928df26 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -92,6 +92,37 @@ export const ComparatorToi18nMap = { ), }; +export const ComparatorToi18nSymbolsMap = { + [Comparator.GT]: '>', + [Comparator.GT_OR_EQ]: '≥', + [Comparator.LT]: '<', + [Comparator.LT_OR_EQ]: '≤', + [Comparator.EQ]: '=', + [Comparator.NOT_EQ]: '≠', + [`${Comparator.EQ}:number`]: '=', + [`${Comparator.NOT_EQ}:number`]: '≠', + + // TODO: We could need to update the next messages to use symbols. + [Comparator.MATCH]: i18n.translate('xpack.infra.logs.alerting.comparator.symbol.match', { + defaultMessage: 'matches', + }), + [Comparator.NOT_MATCH]: i18n.translate('xpack.infra.logs.alerting.comparator.symbol.notMatch', { + defaultMessage: 'does not match', + }), + [Comparator.MATCH_PHRASE]: i18n.translate( + 'xpack.infra.logs.alerting.comparator.symbol.matchPhrase', + { + defaultMessage: 'matches phrase', + } + ), + [Comparator.NOT_MATCH_PHRASE]: i18n.translate( + 'xpack.infra.logs.alerting.comparator.symbol.notMatchPhrase', + { + defaultMessage: 'does not match phrase', + } + ), +}; + // Alert parameters // export enum AlertStates { OK, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts index 25f8ca50e995d..5f43ac01485ed 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Comparator, - ComparatorToi18nMap, + ComparatorToi18nSymbolsMap, TimeUnit, } from '../../../../common/alerting/logs/log_threshold/types'; @@ -23,11 +23,11 @@ export const getReasonMessageForUngroupedCountAlert = ( ) => i18n.translate('xpack.infra.logs.alerting.threshold.ungroupedCountAlertReasonDescription', { defaultMessage: - '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries}} in the last {duration}. Alert when ({translatedComparator} {expectedCount}).', + '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries}} in the last {duration}. Alert when {comparator} {expectedCount}.', values: { actualCount, expectedCount, - translatedComparator: ComparatorToi18nMap[comparator], + comparator: ComparatorToi18nSymbolsMap[comparator], duration: formatDurationFromTimeUnitChar(timeSize, timeUnit as TimeUnitChar), }, }); @@ -42,12 +42,12 @@ export const getReasonMessageForGroupedCountAlert = ( ) => i18n.translate('xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription', { defaultMessage: - '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries}} in the last {duration} for {groupName}. Alert when ({translatedComparator} {expectedCount}).', + '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries}} in the last {duration} for {groupName}. Alert when {comparator} {expectedCount}.', values: { actualCount, expectedCount, groupName, - translatedComparator: ComparatorToi18nMap[comparator], + comparator: ComparatorToi18nSymbolsMap[comparator], duration: formatDurationFromTimeUnitChar(timeSize, timeUnit as TimeUnitChar), }, }); @@ -61,11 +61,11 @@ export const getReasonMessageForUngroupedRatioAlert = ( ) => i18n.translate('xpack.infra.logs.alerting.threshold.ungroupedRatioAlertReasonDescription', { defaultMessage: - 'The ratio of selected logs is {actualRatio} in the last {duration}. Alert when ({translatedComparator} {expectedRatio}).', + 'The ratio of selected logs is {actualRatio} in the last {duration}. Alert when {comparator} {expectedRatio}.', values: { actualRatio, expectedRatio, - translatedComparator: ComparatorToi18nMap[comparator], + comparator: ComparatorToi18nSymbolsMap[comparator], duration: formatDurationFromTimeUnitChar(timeSize, timeUnit as TimeUnitChar), }, }); @@ -80,12 +80,12 @@ export const getReasonMessageForGroupedRatioAlert = ( ) => i18n.translate('xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription', { defaultMessage: - 'The ratio of selected logs is {actualRatio} in the last {duration} for {groupName}. Alert when ({translatedComparator} {expectedRatio}).', + 'The ratio of selected logs is {actualRatio} in the last {duration} for {groupName}. Alert when {comparator} {expectedRatio}.', values: { actualRatio, expectedRatio, groupName, - translatedComparator: ComparatorToi18nMap[comparator], + comparator: ComparatorToi18nSymbolsMap[comparator], duration: formatDurationFromTimeUnitChar(timeSize, timeUnit as TimeUnitChar), }, }); From 79c861639929e8618590dbacbe4ce1f56fdc6aa2 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 9 Feb 2022 10:51:01 -0500 Subject: [PATCH 085/161] [Fleet] Get package info should not store the whole package (#123509) --- .../fleet/server/routes/epm/handlers.ts | 4 +- .../routes/package_policy/handlers.test.ts | 2 +- .../fleet/server/services/epm/packages/get.ts | 39 + .../server/services/epm/packages/index.ts | 1 + .../epm/__snapshots__/install_by_upload.snap | 712 ------------------ .../fleet_api_integration/apis/epm/get.ts | 8 +- .../apis/epm/install_by_upload.ts | 14 - 7 files changed, 47 insertions(+), 733 deletions(-) delete mode 100644 x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 4953cecbf211d..16f2d2e13e18c 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -43,7 +43,7 @@ import { getCategories, getPackages, getFile, - getPackageInfo, + getPackageInfoFromRegistry, isBulkInstallError, installPackage, removeInstallation, @@ -199,7 +199,7 @@ export const getInfoHandler: FleetRequestHandler< if (pkgVersion && !semverValid(pkgVersion)) { throw new IngestManagerError('Package version is not a valid semver'); } - const res = await getPackageInfo({ + const res = await getPackageInfoFromRegistry({ savedObjectsClient, pkgName, pkgVersion: pkgVersion || '', diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index c4cef2a4d8d3b..a178422e20ee5 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -90,7 +90,7 @@ jest.mock( jest.mock('../../services/epm/packages', () => { return { ensureInstalledPackage: jest.fn(() => Promise.resolve()), - getPackageInfo: jest.fn(() => Promise.resolve()), + getPackageInfoFromRegistry: jest.fn(() => Promise.resolve()), }; }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 755fafd8fbc7a..a7cbea4d6462a 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -98,6 +98,45 @@ export async function getPackageSavedObjects( export const getInstallations = getPackageSavedObjects; +export async function getPackageInfoFromRegistry(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + pkgVersion: string; +}): Promise { + const { savedObjectsClient, pkgName, pkgVersion } = options; + const [savedObject, latestPackage] = await Promise.all([ + getInstallationObject({ savedObjectsClient, pkgName }), + Registry.fetchFindLatestPackage(pkgName), + ]); + + // If no package version is provided, use the installed version in the response + let responsePkgVersion = pkgVersion || savedObject?.attributes.install_version; + // If no installed version of the given package exists, default to the latest version of the package + if (!responsePkgVersion) { + responsePkgVersion = latestPackage.version; + } + const packageInfo = await Registry.fetchInfo(pkgName, responsePkgVersion); + + // Fix the paths + const paths = + packageInfo?.assets?.map((path) => + path.replace(`/package/${pkgName}/${pkgVersion}`, `${pkgName}-${pkgVersion}`) + ) ?? []; + + // add properties that aren't (or aren't yet) on the package + const additions: EpmPackageAdditions = { + latestVersion: latestPackage.version, + title: packageInfo.title || nameAsTitle(packageInfo.name), + assets: Registry.groupPathsByService(paths || []), + removable: true, + notice: Registry.getNoticePath(paths || []), + keepPoliciesUpToDate: savedObject?.attributes.keep_policies_up_to_date ?? false, + }; + const updated = { ...packageInfo, ...additions }; + + return createInstallableFrom(updated, savedObject); +} + export async function getPackageInfo(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts index 2c79045c626d3..fa2e5781a209e 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts @@ -20,6 +20,7 @@ export { getInstallation, getInstallations, getPackageInfo, + getPackageInfoFromRegistry, getPackages, getLimitedPackages, } from './get'; diff --git a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap deleted file mode 100644 index 2e0014b998bdc..0000000000000 --- a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap +++ /dev/null @@ -1,712 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Fleet Endpoints EPM Endpoints installs packages from direct upload should install a zip archive correctly and package info should return correctly after validation 1`] = ` -Object { - "assets": Object { - "elasticsearch": Object { - "ingest_pipeline": Array [ - Object { - "dataset": "access", - "file": "default.yml", - "path": "apache-0.1.4/data_stream/access/elasticsearch/ingest_pipeline/default.yml", - "pkgkey": "apache-0.1.4", - "service": "elasticsearch", - "type": "ingest_pipeline", - }, - Object { - "dataset": "error", - "file": "default.yml", - "path": "apache-0.1.4/data_stream/error/elasticsearch/ingest_pipeline/default.yml", - "pkgkey": "apache-0.1.4", - "service": "elasticsearch", - "type": "ingest_pipeline", - }, - ], - }, - "kibana": Object { - "dashboard": Array [ - Object { - "file": "apache-Logs-Apache-Dashboard-ecs-new.json", - "path": "apache-0.1.4/kibana/dashboard/apache-Logs-Apache-Dashboard-ecs-new.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "dashboard", - }, - Object { - "file": "apache-Metrics-Apache-HTTPD-server-status-ecs.json", - "path": "apache-0.1.4/kibana/dashboard/apache-Metrics-Apache-HTTPD-server-status-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "dashboard", - }, - ], - "search": Array [ - Object { - "file": "Apache-access-logs-ecs.json", - "path": "apache-0.1.4/kibana/search/Apache-access-logs-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "search", - }, - Object { - "file": "Apache-errors-log-ecs.json", - "path": "apache-0.1.4/kibana/search/Apache-errors-log-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "search", - }, - Object { - "file": "Apache-HTTPD-ecs.json", - "path": "apache-0.1.4/kibana/search/Apache-HTTPD-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "search", - }, - ], - "visualization": Array [ - Object { - "file": "Apache-access-unique-IPs-map-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-access-unique-IPs-map-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-CPU-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-CPU-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-Load1-slash-5-slash-15-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-Load1-slash-5-slash-15-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-response-codes-over-time-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-response-codes-over-time-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-Workers-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-Workers-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-Hostname-list-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-Hostname-list-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-error-logs-over-time-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-error-logs-over-time-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-Scoreboard-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-Scoreboard-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-Uptime-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-Uptime-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-operating-systems-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-operating-systems-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-HTTPD-Total-accesses-and-kbytes-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-HTTPD-Total-accesses-and-kbytes-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-browsers-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-browsers-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - Object { - "file": "Apache-response-codes-of-top-URLs-ecs.json", - "path": "apache-0.1.4/kibana/visualization/Apache-response-codes-of-top-URLs-ecs.json", - "pkgkey": "apache-0.1.4", - "service": "kibana", - "type": "visualization", - }, - ], - }, - }, - "categories": Array [ - "web", - ], - "conditions": Object { - "kibana.version": "^7.9.0", - }, - "data_streams": Array [ - Object { - "dataset": "apache.access", - "package": "apache", - "path": "access", - "release": "experimental", - "streams": Array [ - Object { - "description": "Collect Apache access logs", - "input": "logfile", - "template_path": "log.yml.hbs", - "title": "Apache access logs", - "vars": Array [ - Object { - "default": Array [ - "/var/log/apache2/access.log*", - "/var/log/apache2/other_vhosts_access.log*", - "/var/log/httpd/access_log*", - ], - "multi": true, - "name": "paths", - "required": true, - "show_user": true, - "title": "Paths", - "type": "text", - }, - ], - }, - ], - "title": "Apache access logs", - "type": "logs", - }, - Object { - "dataset": "apache.status", - "package": "apache", - "path": "status", - "release": "experimental", - "streams": Array [ - Object { - "description": "Collect Apache status metrics", - "input": "apache/metrics", - "template_path": "stream.yml.hbs", - "title": "Apache status metrics", - "vars": Array [ - Object { - "default": "10s", - "multi": false, - "name": "period", - "required": true, - "show_user": true, - "title": "Period", - "type": "text", - }, - Object { - "default": "/server-status", - "multi": false, - "name": "server_status_path", - "required": true, - "show_user": false, - "title": "Server Status Path", - "type": "text", - }, - ], - }, - ], - "title": "Apache status metrics", - "type": "metrics", - }, - Object { - "dataset": "apache.error", - "package": "apache", - "path": "error", - "release": "experimental", - "streams": Array [ - Object { - "description": "Collect Apache error logs", - "input": "logfile", - "template_path": "log.yml.hbs", - "title": "Apache error logs", - "vars": Array [ - Object { - "default": Array [ - "/var/log/apache2/error.log*", - "/var/log/httpd/error_log*", - ], - "multi": true, - "name": "paths", - "required": true, - "show_user": true, - "title": "Paths", - "type": "text", - }, - ], - }, - ], - "title": "Apache error logs", - "type": "logs", - }, - ], - "description": "Apache Uploaded Test Integration", - "format_version": "1.0.0", - "icons": Array [ - Object { - "size": "32x32", - "src": "/img/logo_apache_test.svg", - "title": "Apache Logo", - "type": "image/svg+xml", - }, - ], - "keepPoliciesUpToDate": false, - "license": "basic", - "name": "apache", - "owner": Object { - "github": "elastic/integrations-services", - }, - "policy_templates": Array [ - Object { - "description": "Collect logs and metrics from Apache instances", - "inputs": Array [ - Object { - "description": "Collecting Apache access and error logs", - "title": "Collect logs from Apache instances", - "type": "logfile", - "vars": Array [], - }, - Object { - "description": "Collecting Apache status metrics", - "title": "Collect metrics from Apache instances", - "type": "apache/metrics", - "vars": Array [ - Object { - "default": Array [ - "http://127.0.0.1", - ], - "multi": true, - "name": "hosts", - "required": true, - "show_user": true, - "title": "Hosts", - "type": "text", - }, - ], - }, - ], - "multiple": true, - "name": "apache", - "title": "Apache logs and metrics", - }, - ], - "readme": "/package/apache/0.1.4/docs/README.md", - "release": "experimental", - "removable": true, - "savedObject": Object { - "attributes": Object { - "es_index_patterns": Object { - "access": "logs-apache.access-*", - "error": "logs-apache.error-*", - "status": "metrics-apache.status-*", - }, - "install_source": "upload", - "install_status": "installed", - "install_version": "0.1.4", - "installed_es": Array [ - Object { - "id": "logs-apache.access-0.1.4-default", - "type": "ingest_pipeline", - }, - Object { - "id": "logs-apache.error-0.1.4-default", - "type": "ingest_pipeline", - }, - Object { - "id": "logs-apache.access", - "type": "index_template", - }, - Object { - "id": "logs-apache.access@settings", - "type": "component_template", - }, - Object { - "id": "logs-apache.access@custom", - "type": "component_template", - }, - Object { - "id": "metrics-apache.status", - "type": "index_template", - }, - Object { - "id": "metrics-apache.status@settings", - "type": "component_template", - }, - Object { - "id": "metrics-apache.status@custom", - "type": "component_template", - }, - Object { - "id": "logs-apache.error", - "type": "index_template", - }, - Object { - "id": "logs-apache.error@settings", - "type": "component_template", - }, - Object { - "id": "logs-apache.error@custom", - "type": "component_template", - }, - ], - "installed_kibana": Array [ - Object { - "id": "apache-Logs-Apache-Dashboard-ecs", - "type": "dashboard", - }, - Object { - "id": "apache-Metrics-Apache-HTTPD-server-status-ecs", - "type": "dashboard", - }, - Object { - "id": "Apache-access-unique-IPs-map-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-CPU-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-Load1-slash-5-slash-15-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-response-codes-over-time-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-Workers-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-Hostname-list-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-error-logs-over-time-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-Scoreboard-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-Uptime-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-operating-systems-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-HTTPD-Total-accesses-and-kbytes-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-browsers-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-response-codes-of-top-URLs-ecs", - "type": "visualization", - }, - Object { - "id": "Apache-access-logs-ecs", - "type": "search", - }, - Object { - "id": "Apache-errors-log-ecs", - "type": "search", - }, - Object { - "id": "Apache-HTTPD-ecs", - "type": "search", - }, - ], - "installed_kibana_space_id": "default", - "name": "apache", - "package_assets": Array [ - Object { - "id": "2f1ab9c0-8cf6-5e83-afcd-0d12851c8108", - "type": "epm-packages-assets", - }, - Object { - "id": "841166f1-6db0-5f7a-a8d9-768e88ddf984", - "type": "epm-packages-assets", - }, - Object { - "id": "b12ae5e1-daf2-51a7-99d8-0888d1f13b5b", - "type": "epm-packages-assets", - }, - Object { - "id": "2f263b24-c36a-5ea8-a707-76d1f274c888", - "type": "epm-packages-assets", - }, - Object { - "id": "bd5ff9ad-ba4a-5215-b5af-cef58a3aa886", - "type": "epm-packages-assets", - }, - Object { - "id": "5fc59aa9-1d7e-50ae-8ce5-b875ab44cfc5", - "type": "epm-packages-assets", - }, - Object { - "id": "7c850453-346b-5010-a946-28b83fc69e48", - "type": "epm-packages-assets", - }, - Object { - "id": "f02f8adb-3e0c-5f2f-b4f2-a04dc645b713", - "type": "epm-packages-assets", - }, - Object { - "id": "889d88db-6214-5836-aeff-1a87f8513b27", - "type": "epm-packages-assets", - }, - Object { - "id": "06a6b940-a745-563c-abf4-83eb3335926b", - "type": "epm-packages-assets", - }, - Object { - "id": "e68fd7ac-302e-5b75-bbbb-d69b441c8848", - "type": "epm-packages-assets", - }, - Object { - "id": "2c57fe0f-3b1a-57da-a63b-28f9b9e82bce", - "type": "epm-packages-assets", - }, - Object { - "id": "13db43e8-f8f9-57f0-b131-a171c2f2070f", - "type": "epm-packages-assets", - }, - Object { - "id": "e8750081-1c0b-5c55-bcab-fa6d47f01a85", - "type": "epm-packages-assets", - }, - Object { - "id": "71af57fe-25c4-5935-9879-ca4a2fba730e", - "type": "epm-packages-assets", - }, - Object { - "id": "cc287718-9573-5c56-a9ed-6dfef6589506", - "type": "epm-packages-assets", - }, - Object { - "id": "8badd8ba-289a-5e60-a1c0-f3d39e15cda3", - "type": "epm-packages-assets", - }, - Object { - "id": "20300efc-10eb-5fac-ba90-f6aa9b467e84", - "type": "epm-packages-assets", - }, - Object { - "id": "047c89df-33c2-5d74-b0a4-8b441879761c", - "type": "epm-packages-assets", - }, - Object { - "id": "9838a13f-1b89-5c54-844e-978620d66a1d", - "type": "epm-packages-assets", - }, - Object { - "id": "e105414b-221d-5433-8b24-452625f59b7c", - "type": "epm-packages-assets", - }, - Object { - "id": "eb166c25-843b-5271-8d43-6fb005d2df5a", - "type": "epm-packages-assets", - }, - Object { - "id": "342dbf4d-d88d-53e8-b365-d3639ebbbb14", - "type": "epm-packages-assets", - }, - Object { - "id": "f98c44a3-eaea-505f-8598-3b7f1097ef59", - "type": "epm-packages-assets", - }, - Object { - "id": "12da8c6c-d0e3-589c-9244-88d857ea76b6", - "type": "epm-packages-assets", - }, - Object { - "id": "e2d151ed-709c-542d-b797-cb95f353b9b3", - "type": "epm-packages-assets", - }, - Object { - "id": "f434cffe-0b00-59de-a17f-c1e71bd4ab0f", - "type": "epm-packages-assets", - }, - Object { - "id": "5bd0c25f-04a5-5fd0-8298-ba9aa2f6fe5e", - "type": "epm-packages-assets", - }, - Object { - "id": "279da3a3-8e9b-589b-86e0-bd7364821bab", - "type": "epm-packages-assets", - }, - Object { - "id": "b8758fcb-08bf-50fa-89bd-24398955298a", - "type": "epm-packages-assets", - }, - Object { - "id": "96e4eb36-03c3-5856-af44-559fd5133f2b", - "type": "epm-packages-assets", - }, - Object { - "id": "a59a79c3-66bd-5cfc-91f5-ee84f7227855", - "type": "epm-packages-assets", - }, - Object { - "id": "395143f9-54bf-5b46-b1be-a7b2a6142ad9", - "type": "epm-packages-assets", - }, - Object { - "id": "3449b8d2-ffd5-5aec-bb32-4245f2fbcde4", - "type": "epm-packages-assets", - }, - Object { - "id": "ab44094e-6c9d-50b8-b5c4-2e518d89912e", - "type": "epm-packages-assets", - }, - Object { - "id": "b093bfc0-6e98-5a1b-a502-e838a36f6568", - "type": "epm-packages-assets", - }, - Object { - "id": "03d86823-b756-5b91-850d-7ad231d33546", - "type": "epm-packages-assets", - }, - Object { - "id": "a76af2f0-049b-5be1-8d20-e87c9d1c2709", - "type": "epm-packages-assets", - }, - Object { - "id": "bc2f0c1e-992e-5407-9435-fedb39ff74ea", - "type": "epm-packages-assets", - }, - Object { - "id": "84668ac1-d5ef-545b-88f3-1e49f8f1c8ad", - "type": "epm-packages-assets", - }, - Object { - "id": "69b41271-91a0-5a2e-a62c-60364d5a9c8f", - "type": "epm-packages-assets", - }, - Object { - "id": "8e4ec555-5fbf-55d3-bea3-3af12c9aca3f", - "type": "epm-packages-assets", - }, - Object { - "id": "aa18f3f9-f62a-5ab8-9b34-75696efa5c48", - "type": "epm-packages-assets", - }, - Object { - "id": "71c8c6b1-2116-5817-b65f-7a87ef5ef2b7", - "type": "epm-packages-assets", - }, - Object { - "id": "8f6d7a1f-1e7f-5a60-8fe7-ce19115ed460", - "type": "epm-packages-assets", - }, - Object { - "id": "c115dbbf-edad-59f2-b046-c65a0373a81c", - "type": "epm-packages-assets", - }, - Object { - "id": "b7d696c3-8106-585c-9ecc-94a75cf1e3da", - "type": "epm-packages-assets", - }, - Object { - "id": "639e6a78-59d8-5ce8-9687-64e8f9af7e71", - "type": "epm-packages-assets", - }, - Object { - "id": "ae60c853-7a90-58d2-ab6c-04d3be5f1847", - "type": "epm-packages-assets", - }, - Object { - "id": "0cd33163-2ae4-57eb-96f6-c50af6685cab", - "type": "epm-packages-assets", - }, - Object { - "id": "39e0f78f-1172-5e61-9446-65ef3c0cb46c", - "type": "epm-packages-assets", - }, - Object { - "id": "b08f10ee-6afd-5e89-b9b4-569064fbdd9f", - "type": "epm-packages-assets", - }, - Object { - "id": "efcbe1c6-b2d5-521c-b27a-2146f08a604d", - "type": "epm-packages-assets", - }, - Object { - "id": "f9422c02-d43f-5ebb-b7c5-9e32f9b77c21", - "type": "epm-packages-assets", - }, - Object { - "id": "c276e880-3ba8-58e7-a5d5-c07707dba6b7", - "type": "epm-packages-assets", - }, - Object { - "id": "561a3711-c386-541c-9a77-2d0fa256caf6", - "type": "epm-packages-assets", - }, - Object { - "id": "1378350d-2e2b-52dd-ab3a-d8b9a09df92f", - "type": "epm-packages-assets", - }, - Object { - "id": "94e40729-4aea-59c8-86ba-075137c000dc", - "type": "epm-packages-assets", - }, - ], - "removable": true, - "version": "0.1.4", - }, - "id": "apache", - "namespaces": Array [], - "references": Array [], - "type": "epm-packages", - }, - "screenshots": Array [ - Object { - "size": "1215x1199", - "src": "/img/kibana-apache-test.png", - "title": "Apache Integration", - "type": "image/png", - }, - Object { - "size": "1919x1079", - "src": "/img/apache_httpd_server_status.png", - "title": "Apache HTTPD Server Status", - "type": "image/png", - }, - ], - "status": "installed", - "title": "Apache", - "type": "integration", - "version": "0.1.4", -} -`; diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index df6cbf3c4fecb..c605adc44233a 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -66,10 +66,10 @@ export default function (providerContext: FtrProviderContext) { .get(`/api/fleet/epm/packages/${testPkgName}/${testPkgVersion}`) .expect(200); const packageInfo = res.body.item; - // the uploaded version will have this description - expect(packageInfo.description).to.equal('Apache Uploaded Test Integration'); - // download property should not exist on uploaded packages - expect(packageInfo.download).to.equal(undefined); + // Get package info always return data from the registry + expect(packageInfo.description).to.not.equal('Apache Uploaded Test Integration'); + // download property exist on uploaded packages + expect(packageInfo.download).to.not.equal(undefined); await uninstallPackage(testPkgName, testPkgVersion); }); it('returns correct package info from registry if a different version is installed by upload', async function () { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index 1ac5f2750fc98..83ff7611ded8e 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -87,20 +87,6 @@ export default function (providerContext: FtrProviderContext) { .send(buf) .expect(200); expect(res.body.items.length).to.be(27); - - const packageInfoRes = await supertest - .get(`/api/fleet/epm/packages/${testPkgName}/${testPkgVersion}`) - .set('kbn-xsrf', 'xxxx') - .expect(200); - - delete packageInfoRes.body.item.latestVersion; - delete packageInfoRes.body.item.savedObject.attributes.install_started_at; - delete packageInfoRes.body.item.savedObject.version; - delete packageInfoRes.body.item.savedObject.updated_at; - delete packageInfoRes.body.item.savedObject.coreMigrationVersion; - delete packageInfoRes.body.item.savedObject.migrationVersion; - - expectSnapshot(packageInfoRes.body.item).toMatch(); }); it('should throw an error if the archive is zip but content type is gzip', async function () { From 9ccef34fdd1df66b7009a0abf9710af9bfd6eddb Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Wed, 9 Feb 2022 10:59:41 -0500 Subject: [PATCH 086/161] Use sentence case in Index Management list titles (#125037) Updates some description list titles on the Index Management page to use sentence case rather than title case. --- .../home/index_list/detail_panel/summary/summary.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js index 3e78188ebbd24..d01ee49320070 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js @@ -36,16 +36,16 @@ const getHeaders = () => { defaultMessage: 'Replicas', }), documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', { - defaultMessage: 'Docs Count', + defaultMessage: 'Docs count', }), documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', { - defaultMessage: 'Docs Deleted', + defaultMessage: 'Docs deleted', }), size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', { - defaultMessage: 'Storage Size', + defaultMessage: 'Storage size', }), primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', { - defaultMessage: 'Primary Storage Size', + defaultMessage: 'Primary storage size', }), aliases: i18n.translate('xpack.idxMgmt.summary.headers.aliases', { defaultMessage: 'Aliases', From f2e9efe4570e529a5cb6ec2a181770147d3845f8 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 9 Feb 2022 10:15:07 -0600 Subject: [PATCH 087/161] [DOCS] Adds 8.0.0 release notes (#124399) * [DOCS] Adds 8.0.0 release notes * Update docs/CHANGELOG.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/CHANGELOG.asciidoc Co-authored-by: Brandon Morelli Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Brandon Morelli --- docs/CHANGELOG.asciidoc | 114 +++++++++++++++++- .../development-visualize-index.asciidoc | 2 +- docs/index.asciidoc | 2 - docs/setup/upgrade/upgrade-standard.asciidoc | 4 +- docs/user/whats-new.asciidoc | 2 +- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 446c305c03b95..3101b649b09a5 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,7 @@ Review important information about the {kib} 8.0.0 releases. +* <> * <> * <> * <> @@ -17,6 +18,113 @@ Review important information about the {kib} 8.0.0 releases. * <> -- +[[release-notes-8.0.0]] +== {kib} 8.0.0 + +coming::[8.0.0] + +Review the {kib} 8.0.0 changes, then use the {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant] to complete the upgrade. + +[float] +[[breaking-changes-8.0.0]] +=== Breaking change + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 8.0.0, review the breaking change, then mitigate the impact to your application. + +// tag::notable-breaking-changes[] + +[discrete] +[[breaking-123754]] +.Removes the `console.ssl` setting +[%collapsible] +==== +*Details* + +The `console.ssl` setting has been removed. For more information, refer to {kibana-pull}123754[#123754]. + +*Impact* + +Before you upgrade to 8.0.0, remove `console.ssl` from kibana.yml. +==== + +// end::notable-breaking-changes[] + + +To review the breaking changes in previous versions, refer to the following: + +<> | <> | <> | <> | +<> + +[float] +[[deprecations-8.0.0]] +=== Deprecation + +The following functionality is deprecated in 8.0.0, and will be removed in 9.0.0. +Deprecated functionality does not have an immediate impact on your application, but we strongly recommend +you make the necessary updates after you upgrade to 8.0.0. + +[discrete] +[[deprecation-123229]] +.Removes support for `monitoring.cluster_alerts.allowedSpaces` +[%collapsible] +==== +*Details* + +The `monitoring.cluster_alerts.allowedSpaces` setting, which {kib} uses to create Stack Monitoring alerts, has been removed. For more information, refer to {kibana-pull}123229[#123229]. + +*Impact* + +Before you upgrade to 8.0.0, remove `monitoring.cluster_alerts.allowedSpaces` from kibana.yml. +==== + +To review the deprecations in previous versions, refer to the following: + +<> | <> + +[float] +[[features-8.0.0]] +=== Features +For information about the features introduced in 8.0.0, refer to <>. + +Elastic Security:: +For the Elastic Security 8.0.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. + +To review the features in previous versions, refer to the following: + +<> | <> | <> | <> + +[[enhancements-and-bug-fixes-v8.0.0]] +=== Enhancements and bug fixes + +For detailed information about the 8.0.0 release, review the enhancements and bug fixes. + +[float] +[[enhancement-v8.0.0]] +==== Enhancements +Dashboard:: +Clone ReferenceOrValueEmbeddables by value {kibana-pull}122199[#122199] + +Elastic Security:: +For the Elastic Security 8.0.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. + +[float] +[[fixes-v8.0.0]] +==== Bug Fixes +APM:: +Restrict aggregated transaction metrics search to date range {kibana-pull}123445[#123445] + +Elastic Security:: +For the Elastic Security 8.0.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. + +Fleet:: +Allow empty strings for required text fields in package policies {kibana-pull}123610[#123610] + +Maps:: +Fixes Label border color is not removed from legend when disabled {kibana-pull}122705[#122705] + +Monitoring:: +Ensure logstash getNodes always contains a uuid {kibana-pull}124201[#124201] + +Security:: +Long-running requests no longer cause sporadic logouts in certain cases, even when user sessions are active {kibana-pull}122155[#122155] + [[release-notes-8.0.0-rc2]] == {kib} 8.0.0-rc2 @@ -29,8 +137,6 @@ For information about the {kib} 8.0.0-rc2 release, review the following informat Breaking changes can prevent your application from optimal operation and performance. Before you upgrade, review the breaking change, then mitigate the impact to your application. -// tag::notable-breaking-changes[] - [discrete] [[breaking-122722]] .Removes the ability to use `elasticsearch.username: elastic` in production @@ -42,8 +148,6 @@ In production, you are no longer able to use the `elastic` superuser to authenti *Impact* + When you configure `elasticsearch.username: elastic`, {kib} fails. ==== - -// end::notable-breaking-changes[] To review the breaking changes in previous versions, refer to the following: @@ -1304,7 +1408,7 @@ Use the `xpack.monitoring.clusterAlertsEmail` in kibana.yml. ==== [float] -[[enhancements-and-bug-fixes-v8.0.0]] +[[enhancements-and-bug-fixes-v8.0.0-alpha1]] === Bug fix The 8.0.0-alpha1 release includes the following bug fix. diff --git a/docs/developer/architecture/development-visualize-index.asciidoc b/docs/developer/architecture/development-visualize-index.asciidoc index d41ee32c1fb27..b941cdedf9df9 100644 --- a/docs/developer/architecture/development-visualize-index.asciidoc +++ b/docs/developer/architecture/development-visualize-index.asciidoc @@ -19,7 +19,7 @@ We would recommend waiting until later in `7.x` to upgrade your plugins if possi If you would like to keep up with progress on the visualizations plugin in the meantime, here are a few resources: -* The <> documentation, where we try to capture any changes to the APIs as they occur across minors. +* The <> documentation, where we try to capture any changes to the APIs as they occur across minors. * link:https://github.com/elastic/kibana/issues/44121[Meta issue] which is tracking the move of the plugin to the new {kib} platform * Our link:https://www.elastic.co/blog/join-our-elastic-stack-workspace-on-slack[Elastic Stack workspace on Slack]. * The {kib-repo}blob/{branch}/src/plugins/visualizations[source code], which will continue to be diff --git a/docs/index.asciidoc b/docs/index.asciidoc index ec1a99fa5bffc..668a6edcad3db 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -24,8 +24,6 @@ include::user/index.asciidoc[] include::accessibility.asciidoc[] -include::migration.asciidoc[] - include::CHANGELOG.asciidoc[] include::developer/index.asciidoc[] diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index b43da6aef9765..b01759b4f3511 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -37,7 +37,7 @@ from 4.x, you will need to copy the configurations from your old config (`/etc/kibana/kibana.yml`). Make sure you remove or update any configurations -that are indicated in the <> documentation +that are indicated in the <> documentation otherwise {kib} will fail to start. -- . Upgrade any plugins by removing the existing plugin and reinstalling the @@ -58,7 +58,7 @@ and becomes a new instance in the monitoring data. -- . Copy the files from the `config` directory from your old installation to your new installation. Make sure you remove or update any configurations that are - indicated in the <> documentation + indicated in the <> documentation otherwise {kib} will fail to start. . Copy the files from the `data` directory from your old installation to your new installation. diff --git a/docs/user/whats-new.asciidoc b/docs/user/whats-new.asciidoc index 587f4588bb442..aa16a98a27fdb 100644 --- a/docs/user/whats-new.asciidoc +++ b/docs/user/whats-new.asciidoc @@ -2,7 +2,7 @@ == What's new in 8.0 This section summarizes the most important changes in each release. For the -full list, see <> and <>. +full list, see <> and <>. coming[8.0.0] From 67cd496daa93537a37ab1ddafbcc163d4c64c44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 9 Feb 2022 17:17:30 +0100 Subject: [PATCH 088/161] [IM] Removed undefined data stream link (#124847) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../home/indices_tab.helpers.ts | 8 ++++++ .../home/indices_tab.test.ts | 25 +++++++++++++++++-- .../index_list/index_table/index_table.js | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index 719758e18525a..7daa3cc9e2221 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -33,6 +33,7 @@ export interface IndicesTestBed extends TestBed { getIncludeHiddenIndicesToggleStatus: () => boolean; clickIncludeHiddenIndicesToggle: () => void; clickDataStreamAt: (index: number) => void; + dataStreamLinkExistsAt: (index: number) => boolean; clickManageContextMenuButton: () => void; clickContextMenuOption: (optionDataTestSubject: string) => void; clickModalConfirm: () => void; @@ -103,6 +104,12 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { table } = testBed; + const { rows } = table.getMetaData('indexTable'); + return findTestSubject(rows[index].reactWrapper, 'dataStreamLink').exists(); + }; + const clickModalConfirm = async () => { const { find, component } = testBed; @@ -129,6 +136,7 @@ export const setup = async (overridingDependencies: any = {}): Promise', () => { name: 'data-stream-index', data_stream: 'dataStream1', }, + { + health: '', + status: '', + primary: '', + replica: '', + documents: '', + documents_deleted: '', + size: '', + primary_size: '', + name: 'no-data-stream-index', + data_stream: null, + }, ]); // The detail panel should still appear even if there are no data streams. @@ -125,14 +137,23 @@ describe('', () => { const { findDataStreamDetailPanel, findDataStreamDetailPanelTitle, - actions: { clickDataStreamAt }, + actions: { clickDataStreamAt, dataStreamLinkExistsAt }, } = testBed; + expect(dataStreamLinkExistsAt(0)).toBeTruthy(); await clickDataStreamAt(0); expect(findDataStreamDetailPanel().length).toBe(1); expect(findDataStreamDetailPanelTitle()).toBe('dataStream1'); }); + + test(`doesn't show data stream link if the index doesn't have a data stream`, () => { + const { + actions: { dataStreamLinkExistsAt }, + } = testBed; + + expect(dataStreamLinkExistsAt(1)).toBeFalsy(); + }); }); describe('index detail panel with % character in index name', () => { diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 63e4503180f23..70a7f48178192 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -285,7 +285,7 @@ export class IndexTable extends Component { {renderBadges(index, filterChanged, appServices.extensionsService)} ); - } else if (fieldName === 'data_stream') { + } else if (fieldName === 'data_stream' && value) { return ( Date: Wed, 9 Feb 2022 10:32:20 -0600 Subject: [PATCH 089/161] [data view management] Fix set default data view permissions check (#124897) * fix set default data view permissions * fix fields table * fix scripted field add button * fix jest tests * lint fixes * fix test * updte snapshot --- .../create_edit_field/create_edit_field.tsx | 6 +++++- .../edit_index_pattern/edit_index_pattern.tsx | 7 ++++--- .../index_header/index_header.tsx | 6 ++++-- .../indexed_fields_table.test.tsx.snap | 10 +++++++++ .../indexed_fields_table.test.tsx | 21 ++++++++++++++----- .../indexed_fields_table.tsx | 18 ++++------------ .../components/header/header.tsx | 4 ++-- .../scripted_field_table.test.tsx | 18 ++++++++++------ .../scripted_fields_table.tsx | 18 ++++------------ .../edit_index_pattern/tabs/tabs.tsx | 9 +++++--- .../mount_management_section.tsx | 3 +-- .../data_view_management/public/mocks.ts | 6 +++--- .../data_view_management/public/types.ts | 2 -- src/plugins/data_views/public/mocks.ts | 1 + 14 files changed, 72 insertions(+), 57 deletions(-) diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 0f41c08fbc6fe..2d8469975430b 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -70,7 +70,11 @@ export const CreateEditField = withRouter( if (spec) { return ( <> - + { - const { application, uiSettings, overlays, chrome, dataViews } = + const { uiSettings, overlays, chrome, dataViews } = useKibana().services; const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); const [conflictedFields, setConflictedFields] = useState( @@ -143,15 +143,16 @@ export const EditIndexPattern = withRouter( const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); const kibana = useKibana(); const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping; - const userEditPermission = !!application?.capabilities?.indexPatterns?.save; + const userEditPermission = dataViews.getCanSaveSync(); return (

{showTagsSection && ( diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx index b64aed5c0811c..e40ef6a7ddf2f 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -16,6 +16,7 @@ interface IndexHeaderProps { defaultIndex?: string; setDefault?: () => void; deleteIndexPatternClick?: () => void; + canSave: boolean; } const setDefaultAriaLabel = i18n.translate('indexPatternManagement.editDataView.setDefaultAria', { @@ -40,12 +41,13 @@ export const IndexHeader: React.FC = ({ setDefault, deleteIndexPatternClick, children, + canSave, }) => { return ( {indexPattern.title}} rightSideItems={[ - defaultIndex !== indexPattern.id && setDefault && ( + defaultIndex !== indexPattern.id && setDefault && canSave && ( = ({ /> ), - deleteIndexPatternClick && ( + canSave && (
`; @@ -196,6 +198,8 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` }, ] } + openModal={[Function]} + theme={Object {}} /> `; @@ -233,6 +237,8 @@ exports[`IndexedFieldsTable should filter based on the schema filter 1`] = ` }, ] } + openModal={[Function]} + theme={Object {}} /> `; @@ -267,6 +273,8 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` }, ] } + openModal={[Function]} + theme={Object {}} /> `; @@ -366,6 +374,8 @@ exports[`IndexedFieldsTable should render normally 1`] = ` }, ] } + openModal={[Function]} + theme={Object {}} /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index e179050ca7fe2..c7b92c227a5d9 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -97,6 +97,12 @@ const fields = [ }, ].map(mockFieldToIndexPatternField); +const mockedServices = { + userEditPermission: false, + openModal: () => ({ onClose: new Promise(() => {}), close: async () => {} }), + theme: {} as any, +}; + describe('IndexedFieldsTable', () => { test('should render normally', async () => { const component: ShallowWrapper, React.Component<{}, {}, any>> = shallow( @@ -110,8 +116,9 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + {...mockedServices} /> - ).dive(); + ); await new Promise((resolve) => process.nextTick(resolve)); component.update(); @@ -131,8 +138,9 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + {...mockedServices} /> - ).dive(); + ); await new Promise((resolve) => process.nextTick(resolve)); component.setProps({ fieldFilter: 'Elast' }); @@ -153,8 +161,9 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + {...mockedServices} /> - ).dive(); + ); await new Promise((resolve) => process.nextTick(resolve)); component.setProps({ indexedFieldTypeFilter: ['date'] }); @@ -175,8 +184,9 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + {...mockedServices} /> - ).dive(); + ); await new Promise((resolve) => process.nextTick(resolve)); component.setProps({ schemaFieldTypeFilter: ['runtime'] }); @@ -198,8 +208,9 @@ describe('IndexedFieldsTable', () => { indexedFieldTypeFilter={[]} schemaFieldTypeFilter={[]} fieldFilter="" + {...mockedServices} /> - ).dive(); + ); await new Promise((resolve) => process.nextTick(resolve)); component.update(); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 07a3bf50aab53..ad85499009db0 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -10,10 +10,8 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; import { OverlayStart, ThemeServiceStart } from 'src/core/public'; import { DataViewField, DataView } from '../../../../../../plugins/data_views/public'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { Table } from './components/table'; import { IndexedFieldItem } from './types'; -import { IndexPatternManagmentContext } from '../../../types'; interface IndexedFieldsTableProps { fields: DataViewField[]; @@ -36,16 +34,10 @@ interface IndexedFieldsTableState { fields: IndexedFieldItem[]; } -const withHooks = (Comp: typeof Component) => { - return (props: any) => { - const { application } = useKibana().services; - const userEditPermission = !!application?.capabilities?.indexPatterns?.save; - - return ; - }; -}; - -class IndexedFields extends Component { +export class IndexedFieldsTable extends Component< + IndexedFieldsTableProps, + IndexedFieldsTableState +> { constructor(props: IndexedFieldsTableProps) { super(props); @@ -158,5 +150,3 @@ class IndexedFields extends Component { - const { application, docLinks } = useKibana().services; + const { dataViews, docLinks } = useKibana().services; const links = docLinks?.links; - const userEditPermission = !!application?.capabilities?.indexPatterns?.save; + const userEditPermission = dataViews.getCanSaveSync(); return ( diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index 169b3673001a1..4febfdf0e1219 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -68,9 +68,10 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + userEditPermission={false} scriptedFieldLanguageFilter={[]} /> - ).dive(); + ); // Allow the componentWillMount code to execute // https://github.com/airbnb/enzyme/issues/450 @@ -87,9 +88,10 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + userEditPermission={false} scriptedFieldLanguageFilter={[]} /> - ).dive(); + ); // Allow the componentWillMount code to execute // https://github.com/airbnb/enzyme/issues/450 @@ -119,9 +121,10 @@ describe('ScriptedFieldsTable', () => { painlessDocLink={'painlessDoc'} helpers={helpers} saveIndexPattern={async () => {}} + userEditPermission={false} scriptedFieldLanguageFilter={[]} /> - ).dive(); + ); // Allow the componentWillMount code to execute // https://github.com/airbnb/enzyme/issues/450 @@ -145,9 +148,10 @@ describe('ScriptedFieldsTable', () => { painlessDocLink={'painlessDoc'} helpers={helpers} saveIndexPattern={async () => {}} + userEditPermission={false} scriptedFieldLanguageFilter={[]} /> - ).dive(); + ); // Allow the componentWillMount code to execute // https://github.com/airbnb/enzyme/issues/450 @@ -166,9 +170,10 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + userEditPermission={false} scriptedFieldLanguageFilter={[]} /> - ).dive(); + ); await component.update(); // Fire `componentWillMount()` // @ts-expect-error lang is not valid @@ -194,9 +199,10 @@ describe('ScriptedFieldsTable', () => { helpers={helpers} painlessDocLink={'painlessDoc'} saveIndexPattern={async () => {}} + userEditPermission={false} scriptedFieldLanguageFilter={[]} /> - ).dive(); + ); await component.update(); // Fire `componentWillMount()` // @ts-expect-error diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx index 1b9d63e2d2c6a..540131c50b236 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -15,10 +15,8 @@ import { import { Table, Header, CallOuts, DeleteScritpedFieldConfirmationModal } from './components'; import { ScriptedFieldItem } from './types'; -import { IndexPatternManagmentContext } from '../../../types'; import { DataView, DataViewsPublicPluginStart } from '../../../../../../plugins/data_views/public'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; interface ScriptedFieldsTableProps { indexPattern: DataView; @@ -41,16 +39,10 @@ interface ScriptedFieldsTableState { fields: ScriptedFieldItem[]; } -const withHooks = (Comp: typeof Component) => { - return (props: any) => { - const { application } = useKibana().services; - const userEditPermission = !!application?.capabilities?.indexPatterns?.save; - - return ; - }; -}; - -class ScriptedFields extends Component { +export class ScriptedFieldsTable extends Component< + ScriptedFieldsTableProps, + ScriptedFieldsTableState +> { constructor(props: ScriptedFieldsTableProps) { super(props); @@ -168,5 +160,3 @@ class ScriptedFields extends Component().services; const [fieldFilter, setFieldFilter] = useState(''); const [syncingStateFunc, setSyncingStateFunc] = useState({ @@ -241,7 +241,7 @@ export function Tabs({ [uiSettings] ); - const userEditPermission = !!application?.capabilities?.indexPatterns?.save; + const userEditPermission = dataViews.getCanSaveSync(); const getFilterSection = useCallback( (type: string) => { return ( @@ -448,7 +448,8 @@ export function Tabs({ getFieldInfo, }} openModal={overlays.openModal} - theme={theme} + theme={theme!} + userEditPermission={dataViews.getCanSaveSync()} /> )} @@ -472,6 +473,7 @@ export function Tabs({ }} onRemoveField={refreshFilters} painlessDocLink={docLinks.links.scriptedFields.painless} + userEditPermission={dataViews.getCanSaveSync()} /> ); @@ -510,6 +512,7 @@ export function Tabs({ refreshFields, overlays, theme, + dataViews, ] ); diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index 1b876e34a42fb..e4978acbc9d17 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -39,7 +39,7 @@ export async function mountManagementSection( params: ManagementAppMountParams ) { const [ - { chrome, application, uiSettings, notifications, overlays, http, docLinks, theme }, + { chrome, uiSettings, notifications, overlays, http, docLinks, theme }, { data, dataViewFieldEditor, dataViewEditor, dataViews, fieldFormats }, indexPatternManagementStart, ] = await getStartServices(); @@ -51,7 +51,6 @@ export async function mountManagementSection( const deps: IndexPatternManagmentContext = { chrome, - application, uiSettings, notifications, overlays, diff --git a/src/plugins/data_view_management/public/mocks.ts b/src/plugins/data_view_management/public/mocks.ts index 3404ca4912c88..54c1900d37f4c 100644 --- a/src/plugins/data_view_management/public/mocks.ts +++ b/src/plugins/data_view_management/public/mocks.ts @@ -13,6 +13,7 @@ import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; import { indexPatternFieldEditorPluginMock } from '../../data_view_field_editor/public/mocks'; import { indexPatternEditorPluginMock } from '../../data_view_editor/public/mocks'; +import { dataViewPluginMocks } from '../../data_views/public/mocks'; import { IndexPatternManagementSetup, IndexPatternManagementStart, @@ -54,15 +55,14 @@ const docLinks = { const createIndexPatternManagmentContext = (): { [key in keyof IndexPatternManagmentContext]: any; } => { - const { chrome, application, uiSettings, notifications, overlays } = coreMock.createStart(); + const { chrome, uiSettings, notifications, overlays } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); const dataViewFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); - const dataViews = data.indexPatterns; + const dataViews = dataViewPluginMocks.createStartContract(); return { chrome, - application, uiSettings, notifications, overlays, diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index dc5e0198a64f1..f0a79416892ef 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -8,7 +8,6 @@ import { ChromeStart, - ApplicationStart, IUiSettingsClient, OverlayStart, NotificationsStart, @@ -26,7 +25,6 @@ import { FieldFormatsStart } from '../../field_formats/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; - application: ApplicationStart; uiSettings: IUiSettingsClient; notifications: NotificationsStart; overlays: OverlayStart; diff --git a/src/plugins/data_views/public/mocks.ts b/src/plugins/data_views/public/mocks.ts index c9aece61c4e02..61713c9406c23 100644 --- a/src/plugins/data_views/public/mocks.ts +++ b/src/plugins/data_views/public/mocks.ts @@ -27,6 +27,7 @@ const createStartContract = (): Start => { }), get: jest.fn().mockReturnValue(Promise.resolve({})), clearCache: jest.fn(), + getCanSaveSync: jest.fn(), } as unknown as jest.Mocked; }; From 7d57d6be11de7e70d52ead26769d392cf2fb213e Mon Sep 17 00:00:00 2001 From: Melissa Burpo Date: Wed, 9 Feb 2022 10:54:59 -0600 Subject: [PATCH 090/161] Update x-pack readme to fix broken link (#124874) Functional testing info is now available at https://www.elastic.co/guide/en/kibana/current/development-tests.html in the Kibana Developer Guide. --- x-pack/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/README.md b/x-pack/README.md index 852b713e78465..d104dffff3d28 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -16,7 +16,7 @@ By default, this will also set the password for native realm accounts to the pas # Testing -For information on testing, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html). +For information on testing, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html). #### Running functional tests From 5fde0a07e3755c83fab0dfd5b2c22eb9f250c542 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 9 Feb 2022 09:02:48 -0800 Subject: [PATCH 091/161] [DOCS] Edits to upgrade docs (#125019) * [DOCS] Edits to upgrade docs * Apply suggestions from code review Co-authored-by: Kaarina Tungseth Co-authored-by: Kaarina Tungseth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../setup/upgrade/upgrade-migrations.asciidoc | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 7136011a4f8f8..53b89b38cc88d 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -23,7 +23,10 @@ Saved objects are stored in two indices: The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date saved object indices. -The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. +When you start a new {kib} installation, an upgrade migration is performed before starting plugins or serving HTTP traffic. +Before you upgrade, shut down old nodes to prevent losing acknowledged writes. +To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later +adds a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. .Saved object indices and aliases per {kib} version [options="header"] @@ -40,11 +43,15 @@ The first time a newer {kib} starts, it will first perform an upgrade migration |======================= ==== Upgrading multiple {kib} instances -When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. +When upgrading several {kib} instances connected to the same {es} cluster, +ensure that all outdated instances are shut down before starting the upgrade. -Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. +{kib} does not support rolling upgrades. However, once outdated instances are shut down, +all upgraded instances can be started in parallel, in which case all instances will participate in the upgrade migration in parallel. -For large deployments with more than 10 {kib} instances and more than 10 000 saved objects, the upgrade downtime can be reduced by bringing up a single {kib} instance and waiting for it to complete the upgrade migration before bringing up the remaining instances. +For large deployments with more than 10 {kib} instances, and more than 10,000 saved objects, +you can reduce the upgrade downtime by bringing up a single {kib} instance and waiting for it to +complete the upgrade migration before bringing up the remaining instances. [float] [[preventing-migration-failures]] @@ -53,9 +60,9 @@ This section highlights common causes of {kib} upgrade failures and how to preve [float] ===== timeout_exception or receive_timeout_transport_exception -There is a known issue in v7.12.0 for users who tried the fleet beta. Upgrade migrations fail because of a large number of documents in the `.kibana` index. +There is a known issue in 7.12.0 for users who tried the {fleet} beta. +Upgrade migrations fail because of a large number of documents in the `.kibana` index, which causes {kib} to log errors such as: -This can cause Kibana to log errors like: [source,sh] -------------------------------------------- @@ -68,11 +75,12 @@ Instructions to work around this issue are in https://github.com/elastic/kibana/ [float] ===== Corrupt saved objects -We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. +We highly recommend testing your {kib} upgrade in a development cluster to find and remedy problems +caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Unable to migrate the corrupt Saved Object document ...`. -Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. +For a successful upgrade migration, you must fix or delete corrupt documents. For example, given the following error message: @@ -81,7 +89,7 @@ For example, given the following error message: Unable to migrate the corrupt saved object document with _id: 'marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275'. To allow migrations to proceed, please delete this document from the [.kibana_7.12.0_001] index. -------------------------------------------- -The following steps must be followed to delete the document that is causing the migration to fail: +To delete the documents that cause migrations to fail, take the following steps: . Remove the write block which the migration system has placed on the previous index: + @@ -104,15 +112,15 @@ DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab . Restart {kib}. + -In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**. +In this example, the dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **is no longer available**. Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. [float] ===== User defined index templates that causes new `.kibana*` indices to have incompatible settings or mappings -Matching index templates which specify `settings.refresh_interval` or `mappings` are known to interfere with {kib} upgrades. +Matching index templates that specify `settings.refresh_interval` or `mappings` are known to interfere with {kib} upgrades. -Prevention: narrow down the index patterns of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. +Prevention: Narrow down the {data-sources} of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. NOTE: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` and uses an index pattern of `.kibana`. This index template will not interfere and does not need to be changed or removed. @@ -127,19 +135,21 @@ Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensu [float] ===== Different versions of {kib} connected to the same {es} index -When different versions of {kib} are attempting an upgrade migration in parallel this can lead to migration failures. Ensure that all {kib} instances are running the same version, configuration and plugins. +When you perform an upgrade migration of different {kib} versions, the migration can fail. +Ensure that all {kib} instances are running the same version, configuration, and plugins. [float] ===== Incompatible `xpack.tasks.index` configuration setting -For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. +For {kib} 7.5.0 and earlier, when the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, +upgrade migrations fail. In {kib} 7.5.1 and later, the incompatible configuration setting prevents upgrade migrations from starting. [float] [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to -perform the migration again once the process has restarted. Do not delete any saved objects indices to -attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and +If {kib} unexpectedly terminates while migrating a saved object index, {kib} automatically attempts to +perform the migration again when the process restarts. Do not delete any saved objects indices to +attempt to fix a failed migration. Unlike previous versions, {kib} 7.12.0 and later does not require deleting any indices to release a failed migration lock. If upgrade migrations fail repeatedly, follow the advice in @@ -154,41 +164,48 @@ If you're unable to resolve a failed migration following these steps, please con If you've followed the advice in <> and <> and -{kib} is still not able to upgrade successfully, you might choose to rollback {kib} until +If {kib} is still unable to upgrade successfully, rollback {kib} until you're able to identify and fix the root cause. -WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with -your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. -Any changes made after an upgrade will be lost when rolling back to a previous version. +WARNING: Before rolling back {kib}, ensure that the version you want to rollback to is compatible with +your {es} cluster. If the version you're rolling back to is not compatible, you must also rollback {es}. +Any changes made after an upgrade are lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices have to be +To rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kib} version. [float] -===== Rollback by restoring a backup snapshot: +===== Rollback by restoring a backup snapshot -1. Before proceeding, {ref}/snapshots-take-snapshot.html[take a snapshot] that contains the `kibana` feature state. +. Before proceeding, {ref}/snapshots-take-snapshot.html[take a snapshot] that contains the `kibana` feature state. Snapshots include this feature state by default. -2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -3. Delete all saved object indices with `DELETE /.kibana*` -4. {ref}/snapshots-restore-snapshot.html[Restore] the `kibana` feature state from the snapshot. -5. Start up all {kib} instances on the older version you wish to rollback to. +. To make sure no {kib} instances are performing an upgrade migration, shut down all {kib} instances. +. Delete all saved object indices with `DELETE /.kibana*`. +. {ref}/snapshots-restore-snapshot.html[Restore] the `kibana` feature state from the snapshot. +. Start all {kib} instances on the older version you want to rollback to. [float] -===== (Not recommended) Rollback without a backup snapshot: +===== (Not recommended) Rollback without a backup snapshot -1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. -2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. -3. Delete the version specific indices created by the failed upgrade migration. For example, if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` -4. Inspect the output of `GET /_cat/aliases`. -If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. +. To make sure no {kib} instances are performing an upgrade migration, shut down all {kib} instances. +. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. By default, snapshots include the feature state. +. Delete the version-specific indices created by the failed upgrade migration. ++ +For example, to rollback from a failed upgrade +to v7.12.0: `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +. Inspect the output of `GET /_cat/aliases`. ++ +If the `.kibana` or `.kibana_task_manager` aliases are missing, you must create them manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. -For example. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. -5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` -6. Start up {kib} on the older version you wish to rollback to. +For example, if the `.kibana` alias is missing, and the latest index is `.kibana_3`, create a new alias with `POST /.kibana_3/_aliases/.kibana`. +. To remove the write block from the rollback indices: +`PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` +. Start {kib} on the older version you want to rollback to. [float] [[upgrade-migrations-old-indices]] ==== Handling old `.kibana_N` indices -After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. +After the migrations complete, multiple {kib} indices are created in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). +{kib} only uses the index that the `.kibana` and `.kibana_task_manager` aliases point to. +The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. From 8e8c4b8d96a6bc9a23b9d58996882fedef8147be Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Wed, 9 Feb 2022 10:11:10 -0700 Subject: [PATCH 092/161] [Canvas] Migrate by value embeddables (#123515) * Add migrations for by value embeddables Check for id in embeddable input Removed unused import Fixed tests Fix variable name Move migration into embeddable function definition Remove unused code * Cleanup * Fix embeddable test * Remove check for by-value embeddables in embeddable function migration * Removed unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../functions/external/embeddable.test.ts | 11 +++++-- .../functions/external/embeddable.ts | 32 ++++++++++++++++++- .../functions/external/index.ts | 1 + .../canvas/canvas_plugin_src/plugin.ts | 1 + x-pack/plugins/canvas/server/plugin.ts | 1 + 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts index 001fb0e3f62e3..ad25b8a3081a1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts @@ -9,7 +9,6 @@ import { embeddableFunctionFactory } from './embeddable'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; import { encode } from '../../../common/lib/embeddable_dataurl'; -import { InitializeArguments } from '.'; const filterContext: ExpressionValueFilter = { type: 'filter', @@ -32,8 +31,16 @@ const filterContext: ExpressionValueFilter = { ], }; +const embeddablePersistableStateServiceMock = { + extract: jest.fn(), + inject: jest.fn(), + getAllMigrations: jest.fn(), +}; + describe('embeddable', () => { - const fn = embeddableFunctionFactory({} as InitializeArguments)().fn; + const fn = embeddableFunctionFactory({ + embeddablePersistableStateService: embeddablePersistableStateServiceMock, + })().fn; const config = { id: 'some-id', timerange: { from: '15m', to: 'now' }, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts index 7ef8f0a09eb90..8ec299c60dae5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts @@ -5,7 +5,16 @@ * 2.0. */ -import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { mapValues } from 'lodash'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; +import { + ExpressionFunctionDefinition, + ExpressionAstFunction, +} from 'src/plugins/expressions/common'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; import { ExpressionValueFilter, EmbeddableInput } from '../../../types'; import { EmbeddableExpressionType, EmbeddableExpression } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; @@ -45,6 +54,22 @@ export function embeddableFunctionFactory({ return function embeddable(): EmbeddableFunction { const { help, args: argHelp } = getFunctionHelp().embeddable; + const migrateByValueEmbeddable = + ( + migrateFn: MigrateFunction + ): MigrateFunction => + (state: ExpressionAstFunction): ExpressionAstFunction => { + const embeddableInput = decode(state.arguments.config[0] as string); + + const embeddableType = state.arguments.type[0]; + const migratedInput = migrateFn({ ...embeddableInput, type: embeddableType }); + + state.arguments.config[0] = encode(migratedInput); + state.arguments.type[0] = migratedInput.type as string; + + return state; + }; + return { name: 'embeddable', help, @@ -140,6 +165,11 @@ export function embeddableFunctionFactory({ } return state; }, + + migrations: mapValues< + MigrateFunctionsObject, + MigrateFunction + >(embeddablePersistableStateService.getAllMigrations(), migrateByValueEmbeddable), }; }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts index 1d69e181b5fd9..29200d938b53a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts @@ -16,6 +16,7 @@ export interface InitializeArguments { embeddablePersistableStateService: { extract: EmbeddableStart['extract']; inject: EmbeddableStart['inject']; + getAllMigrations: EmbeddableStart['getAllMigrations']; }; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 591795637aebe..6153d20b657f5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -46,6 +46,7 @@ export class CanvasSrcPlugin implements Plugin embeddablePersistableStateService: { extract: depsStart.embeddable.extract, inject: depsStart.embeddable.inject, + getAllMigrations: depsStart.embeddable.getAllMigrations, }, }); plugins.canvas.addFunctions(externalFunctions); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 27b6186216b69..172d1e8dd8bf1 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -58,6 +58,7 @@ export class CanvasPlugin implements Plugin { embeddablePersistableStateService: { extract: plugins.embeddable.extract, inject: plugins.embeddable.inject, + getAllMigrations: plugins.embeddable.getAllMigrations, }, }); From 153b7e135c6a8a24a6b29efece8cd7ca70251fcd Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 9 Feb 2022 12:22:08 -0500 Subject: [PATCH 093/161] [Fleet] Fix fleet server hosts client validation (#125085) --- .../use_fleet_server_host_form.test.tsx | 83 +++++++++++++++++++ .../use_fleet_server_host_form.tsx | 7 +- .../plugins/fleet/public/hooks/use_input.ts | 26 +++--- 3 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.test.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.test.tsx new file mode 100644 index 0000000000000..151a3d5354c17 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 { act } from 'react-test-renderer'; + +import { createFleetTestRendererMock } from '../../../../../../mock'; + +import { useFleetServerHostsForm } from './use_fleet_server_host_form'; + +jest.mock('../../services/agent_and_policies_count', () => ({ + ...jest.requireActual('../../services/agent_and_policies_count'), + getAgentAndPolicyCount: () => ({ agentCount: 0, agentPolicyCount: 0 }), +})); +jest.mock('../../hooks/use_confirm_modal', () => ({ + ...jest.requireActual('../../hooks/use_confirm_modal'), + useConfirmModal: () => ({ confirm: () => true }), +})); + +describe('useFleetServerHostsForm', () => { + it('should not allow to submit an invalid form', async () => { + const testRenderer = createFleetTestRendererMock(); + const onSucess = jest.fn(); + const { result } = testRenderer.renderHook(() => useFleetServerHostsForm([], onSucess)); + + act(() => + result.current.fleetServerHostsInput.props.onChange(['http://test.fr', 'http://test.fr']) + ); + + await act(() => result.current.submit()); + + expect(result.current.fleetServerHostsInput.props.errors).toMatchInlineSnapshot(` + Array [ + Object { + "index": 0, + "message": "Duplicate URL", + }, + Object { + "index": 1, + "message": "Duplicate URL", + }, + ] + `); + expect(onSucess).not.toBeCalled(); + expect(result.current.isDisabled).toBeTruthy(); + }); + + it('should submit a valid form', async () => { + const testRenderer = createFleetTestRendererMock(); + const onSucess = jest.fn(); + testRenderer.startServices.http.post.mockResolvedValue({}); + const { result } = testRenderer.renderHook(() => useFleetServerHostsForm([], onSucess)); + + act(() => result.current.fleetServerHostsInput.props.onChange(['http://test.fr'])); + + await act(() => result.current.submit()); + expect(onSucess).toBeCalled(); + }); + + it('should allow the user to correct and submit a invalid form', async () => { + const testRenderer = createFleetTestRendererMock(); + const onSucess = jest.fn(); + testRenderer.startServices.http.post.mockResolvedValue({}); + const { result } = testRenderer.renderHook(() => useFleetServerHostsForm([], onSucess)); + + act(() => + result.current.fleetServerHostsInput.props.onChange(['http://test.fr', 'http://test.fr']) + ); + + await act(() => result.current.submit()); + expect(onSucess).not.toBeCalled(); + expect(result.current.isDisabled).toBeTruthy(); + + act(() => result.current.fleetServerHostsInput.props.onChange(['http://test.fr'])); + expect(result.current.isDisabled).toBeFalsy(); + + await act(() => result.current.submit()); + expect(onSucess).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx index 230985352da58..afe96713f065d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx @@ -142,7 +142,7 @@ export function useFleetServerHostsForm( const submit = useCallback(async () => { try { - if (!validate) { + if (!validate()) { return; } const { agentCount, agentPolicyCount } = await getAgentAndPolicyCount(); @@ -178,9 +178,12 @@ export function useFleetServerHostsForm( } }, [fleetServerHostsInput.value, validate, notifications, confirm, onSuccess]); + const isDisabled = + isLoading || !fleetServerHostsInput.hasChanged || fleetServerHostsInput.props.isInvalid; + return { isLoading, - isDisabled: isLoading || !fleetServerHostsInput.hasChanged, + isDisabled, submit, fleetServerHostsInput, }; diff --git a/x-pack/plugins/fleet/public/hooks/use_input.ts b/x-pack/plugins/fleet/public/hooks/use_input.ts index 1c89fb232a66e..435cfec95b028 100644 --- a/x-pack/plugins/fleet/public/hooks/use_input.ts +++ b/x-pack/plugins/fleet/public/hooks/use_input.ts @@ -125,11 +125,22 @@ export function useComboInput( const isInvalid = errors !== undefined; + const validateCallback = useCallback(() => { + if (validate) { + const newErrors = validate(value); + setErrors(newErrors); + + return newErrors === undefined; + } + + return true; + }, [validate, value]); + const onChange = useCallback( (newValues: string[]) => { setValue(newValues); - if (errors && validate && validate(newValues) === undefined) { - setErrors(undefined); + if (errors && validate) { + setErrors(validate(newValues)); } }, [validate, errors] @@ -149,16 +160,7 @@ export function useComboInput( setValue([]); }, setValue, - validate: () => { - if (validate) { - const newErrors = validate(value); - setErrors(newErrors); - - return newErrors === undefined; - } - - return true; - }, + validate: validateCallback, hasChanged, }; } From 809246721d966a084b1c748c2839528b8636303b Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Wed, 9 Feb 2022 10:28:39 -0700 Subject: [PATCH 094/161] [ResponseOps] Change the duration/percentile display format to mm:ss (#124647) * Change the duration/percentile display format to mm:ss * Addressed comments * Add time format to tooltip * Addressed comments, percentiles can show N/A * Fix flaky test * remove only * address comments, now tests for N/A Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/lib/monitoring_utils.test.ts | 23 +++++++--- .../application/lib/monitoring_utils.ts | 17 ++++++-- .../components/alerts_list.test.tsx | 42 ++++++++++++------- .../alerts_list/components/alerts_list.tsx | 23 ++++------ .../components/rule_duration_format.tsx | 39 +++++++++++++++++ .../alert_create_flyout.ts | 2 +- .../apps/triggers_actions_ui/alerts_list.ts | 27 ++++++++---- 7 files changed, 126 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_duration_format.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.test.ts index 151fd001f9fa1..2ac04ffd6eaaf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.test.ts @@ -4,7 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { getFormattedSuccessRatio, getFormattedRuleExecutionPercentile } from './monitoring_utils'; +import { + getFormattedSuccessRatio, + getFormattedDuration, + getFormattedMilliseconds, +} from './monitoring_utils'; describe('monitoring_utils', () => { it('should return a decimal as a percent', () => { @@ -12,9 +16,18 @@ describe('monitoring_utils', () => { expect(getFormattedSuccessRatio(0.75345345345345)).toEqual('75%'); }); - it('should return percentiles as an integer', () => { - expect(getFormattedRuleExecutionPercentile(0)).toEqual('0ms'); - expect(getFormattedRuleExecutionPercentile(100.5555)).toEqual('101ms'); - expect(getFormattedRuleExecutionPercentile(99.1111)).toEqual('99ms'); + it('should return a formatted duration', () => { + expect(getFormattedDuration(0)).toEqual('00:00'); + expect(getFormattedDuration(100.111)).toEqual('00:00'); + expect(getFormattedDuration(50000)).toEqual('00:50'); + expect(getFormattedDuration(500000)).toEqual('08:20'); + expect(getFormattedDuration(5000000)).toEqual('83:20'); + expect(getFormattedDuration(50000000)).toEqual('833:20'); + }); + + it('should format a duration as an integer', () => { + expect(getFormattedMilliseconds(0)).toEqual('0 ms'); + expect(getFormattedMilliseconds(100.5555)).toEqual('101 ms'); + expect(getFormattedMilliseconds(99.1111)).toEqual('99 ms'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.ts index 29c03f118436f..f5bec63056103 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/monitoring_utils.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import moment from 'moment'; import numeral from '@elastic/numeral'; export function getFormattedSuccessRatio(successRatio: number) { @@ -11,7 +12,17 @@ export function getFormattedSuccessRatio(successRatio: number) { return `${formatted}%`; } -export function getFormattedRuleExecutionPercentile(percentile: number) { - const formatted = numeral(percentile).format('0,0'); - return `${formatted}ms`; +export function getFormattedDuration(value: number) { + if (!value) { + return '00:00'; + } + const duration = moment.duration(value); + const minutes = Math.floor(duration.asMinutes()).toString().padStart(2, '0'); + const seconds = duration.seconds().toString().padStart(2, '0'); + return `${minutes}:${seconds}`; +} + +export function getFormattedMilliseconds(value: number) { + const formatted = numeral(value).format('0,0'); + return `${formatted} ms`; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 2ca32ec04cd70..28aa0b2097aba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -18,6 +18,8 @@ import { ALERTS_FEATURE_ID, parseDuration, } from '../../../../../../alerting/common'; +import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/monitoring_utils'; + import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); @@ -180,22 +182,22 @@ describe('alerts_list component with items', () => { history: [ { success: true, - duration: 100, + duration: 1000000, }, { success: true, - duration: 200, + duration: 200000, }, { success: false, - duration: 300, + duration: 300000, }, ], calculated_metrics: { success_ratio: 0.66, - p50: 200, - p95: 300, - p99: 300, + p50: 200000, + p95: 300000, + p99: 300000, }, }, }, @@ -227,18 +229,18 @@ describe('alerts_list component with items', () => { history: [ { success: true, - duration: 100, + duration: 100000, }, { success: true, - duration: 500, + duration: 500000, }, ], calculated_metrics: { success_ratio: 1, p50: 0, - p95: 100, - p99: 500, + p95: 100000, + p99: 500000, }, }, }, @@ -458,7 +460,7 @@ describe('alerts_list component with items', () => { wrapper.update(); expect(wrapper.find('.euiToolTipPopover').text()).toBe( - 'The length of time it took for the rule to run.' + 'The length of time it took for the rule to run (mm:ss).' ); // Status column @@ -508,14 +510,24 @@ describe('alerts_list component with items', () => { ).toBeTruthy(); let percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="${Percentiles.P50}Percentile"]` + `EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` ); mockedAlertsData.forEach((rule, index) => { if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { + // Ensure the table cells are getting the correct values expect(percentiles.at(index).text()).toEqual( - `${rule.monitoring.execution.calculated_metrics.p50}ms` + getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50) ); + // Ensure the tooltip is showing the correct content + expect( + wrapper + .find( + 'EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' + ) + .at(index) + .props().content + ).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50)); } else { expect(percentiles.at(index).text()).toEqual('N/A'); } @@ -581,13 +593,13 @@ describe('alerts_list component with items', () => { ).toBeTruthy(); percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="${Percentiles.P95}Percentile"]` + `EuiTableRowCell[data-test-subj="alertsTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` ); mockedAlertsData.forEach((rule, index) => { if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { expect(percentiles.at(index).text()).toEqual( - `${rule.monitoring.execution.calculated_metrics.p95}ms` + getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) ); } else { expect(percentiles.at(index).text()).toEqual('N/A'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 40b5e981c181e..72228c285238d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -86,14 +86,9 @@ import { ManageLicenseModal } from './manage_license_modal'; import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; import { RuleEnabledSwitch } from './rule_enabled_switch'; import { PercentileSelectablePopover } from './percentile_selectable_popover'; -import { - formatMillisForDisplay, - shouldShowDurationWarning, -} from '../../../lib/execution_duration_utils'; -import { - getFormattedSuccessRatio, - getFormattedRuleExecutionPercentile, -} from '../../../lib/monitoring_utils'; +import { RuleDurationFormat } from './rule_duration_format'; +import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; +import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; const ENTER_KEY = 13; @@ -396,7 +391,7 @@ export const AlertsList: React.FunctionComponent = () => { content={i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.ruleExecutionPercentileTooltip', { - defaultMessage: `{percentileOrdinal} percentile of this rule's past {sampleLimit} execution durations`, + defaultMessage: `{percentileOrdinal} percentile of this rule's past {sampleLimit} execution durations (mm:ss).`, values: { percentileOrdinal: percentileOrdinals[selectedPercentile!], sampleLimit: MONITORING_HISTORY_LIMIT, @@ -420,7 +415,7 @@ export const AlertsList: React.FunctionComponent = () => { const renderPercentileCellValue = (value: number) => { return ( - {typeof value === 'number' ? getFormattedRuleExecutionPercentile(value) : 'N/A'} + ); }; @@ -630,7 +625,7 @@ export const AlertsList: React.FunctionComponent = () => { content={i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.durationTitle', { - defaultMessage: 'The length of time it took for the rule to run.', + defaultMessage: 'The length of time it took for the rule to run (mm:ss).', } )} > @@ -651,7 +646,7 @@ export const AlertsList: React.FunctionComponent = () => { return ( <> - {`${formatMillisForDisplay(value)}`} + {} {showDurationWarning && ( { ); }, }, + getPercentileColumn(), { field: 'monitoring.execution.calculated_metrics.success_ratio', width: '12%', @@ -680,7 +676,7 @@ export const AlertsList: React.FunctionComponent = () => { content={i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.successRatioTitle', { - defaultMessage: 'How often this rule executes successfully', + defaultMessage: 'How often this rule executes successfully.', } )} > @@ -701,7 +697,6 @@ export const AlertsList: React.FunctionComponent = () => { ); }, }, - getPercentileColumn(), { field: 'executionStatus.status', name: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_duration_format.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_duration_format.tsx new file mode 100644 index 0000000000000..b6512b93d8a94 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_duration_format.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiToolTip } from '@elastic/eui'; +import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/monitoring_utils'; + +interface Props { + duration: number; + allowZero?: boolean; +} + +export const RuleDurationFormat = memo((props: Props) => { + const { duration, allowZero = true } = props; + + const formattedDuration = useMemo(() => { + if (allowZero || typeof duration === 'number') { + return getFormattedDuration(duration); + } + return 'N/A'; + }, [duration, allowZero]); + + const formattedTooltip = useMemo(() => { + if (allowZero || typeof duration === 'number') { + return getFormattedMilliseconds(duration); + } + return 'N/A'; + }, [duration, allowZero]); + + return ( + + {formattedDuration} + + ); +}); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index eaf71c107edd9..95ff24fc8beef 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -168,7 +168,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { tags: '', interval: '1 min', }); - expect(searchResultAfterSave.duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResultAfterSave.duration).to.match(/\d{2,}:\d{2}/); // clean up created alert const alertsToDelete = await getAlertsByName(alertName); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 0edf65b4e3d42..2b45a12790107 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -81,7 +81,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); expect(searchResults[0].interval).to.equal('1 min'); expect(searchResults[0].tags).to.equal('2'); - expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResults[0].duration).to.match(/\d{2,}:\d{2}/); }); it('should update alert list on the search clear button click', async () => { @@ -103,7 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResults[0].name).to.equal('bTest: Noop'); expect(searchResults[0].interval).to.equal('1 min'); expect(searchResults[0].tags).to.equal('2'); - expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResults[0].duration).to.match(/\d{2,}:\d{2}/); const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); await searchClearButton.click(); @@ -115,11 +115,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResultsAfterClear[0].name).to.equal('bTest: Noop'); expect(searchResultsAfterClear[0].interval).to.equal('1 min'); expect(searchResultsAfterClear[0].tags).to.equal('2'); - expect(searchResultsAfterClear[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResultsAfterClear[0].duration).to.match(/\d{2,}:\d{2}/); expect(searchResultsAfterClear[1].name).to.equal('cTest: Noop'); expect(searchResultsAfterClear[1].interval).to.equal('1 min'); expect(searchResultsAfterClear[1].tags).to.equal(''); - expect(searchResultsAfterClear[1].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResultsAfterClear[1].duration).to.match(/\d{2,}:\d{2}/); }); it('should search for tags', async () => { @@ -136,7 +136,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); expect(searchResults[0].interval).to.equal('1 min'); expect(searchResults[0].tags).to.equal('3'); - expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResults[0].duration).to.match(/\d{2,}:\d{2}/); }); it('should display an empty list when search did not return any alerts', async () => { @@ -369,11 +369,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.existOrFail('P50Percentile'); await retry.try(async () => { + const percentileCell = await find.byCssSelector( + '[data-test-subj="P50Percentile"]:nth-of-type(1)' + ); + const percentileCellText = await percentileCell.getVisibleText(); + expect(percentileCellText).to.match(/^N\/A|\d{2,}:\d{2}$/); + await testSubjects.click('percentileSelectablePopover-iconButton'); await testSubjects.existOrFail('percentileSelectablePopover-selectable'); const searchClearButton = await find.byCssSelector( '[data-test-subj="percentileSelectablePopover-selectable"] li:nth-child(2)' ); + const alertResults = await pageObjects.triggersActionsUI.getAlertsList(); + expect(alertResults[0].duration).to.match(/^N\/A|\d{2,}:\d{2}$/); + await searchClearButton.click(); await testSubjects.missingOrFail('percentileSelectablePopover-selectable'); await testSubjects.existOrFail('alertsTable-P95ColumnName'); @@ -427,7 +436,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(filterErrorOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); expect(filterErrorOnlyResults[0].interval).to.equal('30 sec'); expect(filterErrorOnlyResults[0].status).to.equal('Error'); - expect(filterErrorOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(filterErrorOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/); }); }); @@ -440,7 +449,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); expect(refreshResults[0].interval).to.equal('1 min'); expect(refreshResults[0].status).to.equal('Ok'); - expect(refreshResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(refreshResults[0].duration).to.match(/\d{2,}:\d{2}/); }); const alertsErrorBannerWhenNoErrors = await find.allByCssSelector( @@ -484,7 +493,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(filterFailingAlertOnlyResults.length).to.equal(1); expect(filterFailingAlertOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); expect(filterFailingAlertOnlyResults[0].interval).to.equal('30 sec'); - expect(filterFailingAlertOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(filterFailingAlertOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/); }); }); @@ -518,7 +527,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { `${noopAlertWithAction.name}Test: Noop` ); expect(filterWithSlackOnlyResults[0].interval).to.equal('1 min'); - expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2,}:\d{2}/); }); await testSubjects.click('alertTypeFilterButton'); From 3c73b605aa32bbf1d90dcad48253e8cbfe142410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Wed, 9 Feb 2022 19:00:26 +0100 Subject: [PATCH 095/161] [Unified Observability] Overview style updates (#124702) * Big chunk of style updates * New layout and position for news and resources * Alerts updated * Rename headings and links * Removed unncessary prop * More fixes * Remove active status * Fixing tests * fix tests * fix checks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ester Marti --- .../components/app/chart_container/index.tsx | 4 +- .../public/components/app/news_feed/index.tsx | 85 +++++++------- .../public/components/app/resources/index.tsx | 2 +- .../components/app/section/alerts/index.tsx | 108 ++++++++++-------- .../components/app/section/apm/index.test.tsx | 18 +-- .../components/app/section/apm/index.tsx | 4 +- .../public/components/app/section/index.tsx | 19 +-- .../components/app/section/logs/index.tsx | 6 +- .../components/app/section/metrics/index.tsx | 4 +- .../components/app/section/uptime/index.tsx | 6 +- .../components/app/section/ux/index.test.tsx | 6 +- .../components/app/section/ux/index.tsx | 2 +- .../public/pages/overview/data_sections.tsx | 2 +- .../pages/overview/old_overview_page.tsx | 42 ++++--- 14 files changed, 164 insertions(+), 144 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx index 6251afc1013a8..5164684e3490b 100644 --- a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx +++ b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx @@ -18,12 +18,12 @@ interface Props { children: React.ReactNode; } -const CHART_HEIGHT = 170; +const CHART_HEIGHT = 120; export function ChartContainer({ isInitialLoad, children, - iconSize = 'xl', + iconSize = 'l', height = CHART_HEIGHT, }: Props) { if (isInitialLoad) { diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx index 6f1a5f33b9ba7..bdf82213aaa07 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -9,7 +9,7 @@ import { EuiErrorBoundary, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, + EuiPanel, EuiLink, EuiText, EuiTitle, @@ -56,48 +56,49 @@ function NewsItem({ item }: { item: INewsItem }) { const theme = useContext(ThemeContext); return ( - - - -

{item.title.en}

-
-
- - - - - - - {limitString(item.description.en, 128)} - - + + + + +

{item.title.en}

+
+
+ + + + + + + {limitString(item.description.en, 128)} + + + + + + {i18n.translate('xpack.observability.news.readFullStory', { + defaultMessage: 'Read full story', + })} + + + + + + {item.image_url?.en && ( - - - {i18n.translate('xpack.observability.news.readFullStory', { - defaultMessage: 'Read full story', - })} - - + {item.title.en} - - - {item.image_url?.en && ( - - {item.title.en} - - )} -
-
- -
+ )} +
+
+
+ ); } diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx index 763b30860cd78..f2bcca594653e 100644 --- a/x-pack/plugins/observability/public/components/app/resources/index.tsx +++ b/x-pack/plugins/observability/public/components/app/resources/index.tsx @@ -42,7 +42,7 @@ const resources = [ export function Resources() { return ( - +

diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx index 328de71ac7874..77ea5eeea14ba 100644 --- a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx @@ -9,15 +9,14 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, - EuiIconTip, EuiLink, EuiText, EuiSpacer, EuiTitle, - EuiButton, + EuiButtonEmpty, EuiLoadingSpinner, EuiCallOut, + EuiPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -68,7 +67,7 @@ export function AlertsSection() { return ( - + ); @@ -99,7 +98,12 @@ export function AlertsSection() { return (
- +

@@ -110,73 +114,77 @@ export function AlertsSection() { - + {i18n.translate('xpack.observability.overview.alert.appLink', { - defaultMessage: 'Manage alerts', + defaultMessage: 'Show all alerts', })} - + <> - + setFilter(e.target.value)} + prepend="Show" /> - + {alerts .filter((alert) => filter === ALL_TYPES || alert.consumer === filter) .map((alert, index) => { - const isLastElement = index === alerts.length - 1; return ( - - - - {alert.name} - - - - - - {alert.alertTypeId} - - {alert.tags.map((tag, idx) => { - return ( - - {tag} - - ); - })} - - - - - - - Updated {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago - - - {alert.muteAll && ( + + + + - + {alert.name} + + + + + + {alert.alertTypeId} + + {alert.tags.map((tag, idx) => { + return ( + + {tag} + + ); })} - /> + + + + + {alert.muteAll && ( + + + {i18n.translate('xpack.observability.overview.alerts.muted', { + defaultMessage: 'Muted', + })} + + + )} + + + Last updated{' '} + {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago + + + - )} - + + - {!isLastElement && } ); })} diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index 35835cd0bc8e6..5e45eda0d3176 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -84,12 +84,12 @@ describe('APMSection', () => { status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, queryAllByTestId } = render( + const { getByRole, getByText, queryAllByTestId } = render( ); - expect(getByText('APM')).toBeInTheDocument(); - expect(getByText('View in app')).toBeInTheDocument(); + expect(getByRole('heading')).toHaveTextContent('Services'); + expect(getByText('Show service inventory')).toBeInTheDocument(); expect(getByText('Services 11')).toBeInTheDocument(); expect(getByText('Throughput 900.0 tpm')).toBeInTheDocument(); expect(queryAllByTestId('loading')).toEqual([]); @@ -101,12 +101,12 @@ describe('APMSection', () => { status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText, queryAllByTestId } = render( + const { getByRole, getByText, queryAllByTestId } = render( ); - expect(getByText('APM')).toBeInTheDocument(); - expect(getByText('View in app')).toBeInTheDocument(); + expect(getByRole('heading')).toHaveTextContent('Services'); + expect(getByText('Show service inventory')).toBeInTheDocument(); expect(getByText('Services 11')).toBeInTheDocument(); expect(getByText('Throughput 312.00k tpm')).toBeInTheDocument(); expect(queryAllByTestId('loading')).toEqual([]); @@ -117,13 +117,13 @@ describe('APMSection', () => { status: fetcherHook.FETCH_STATUS.LOADING, refetch: jest.fn(), }); - const { getByText, queryAllByText, getByTestId } = render( + const { getByRole, queryAllByText, getByTestId } = render( ); - expect(getByText('APM')).toBeInTheDocument(); + expect(getByRole('heading')).toHaveTextContent('Services'); expect(getByTestId('loading')).toBeInTheDocument(); - expect(queryAllByText('View in app')).toEqual([]); + expect(queryAllByText('Show service inventory')).toEqual([]); expect(queryAllByText('Services 11')).toEqual([]); expect(queryAllByText('Throughput 312.00k tpm')).toEqual([]); }); diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index 11565cfb972e7..6c61ecb3f270e 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -93,12 +93,12 @@ export function APMSection({ bucketSize }: Props) { return ( + +
{title}
} extraAction={ appLink?.href && ( - + {appLink.label} - + ) } > <> - - {hasError ? : <>{children}} - + {hasError ? : <>{children}}
diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index 0ff2c203c7707..78c23638a91bd 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -92,17 +92,17 @@ export function LogsSection({ bucketSize }: Props) { return ( - +

{i18n.translate('xpack.observability.overview.logs.subtitle', { defaultMessage: 'Logs rate per minute', diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx index 8cd49efe4787a..f7f35552fb686 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -202,12 +202,12 @@ export function MetricsSection({ bucketSize }: Props) { return ( { ); expect(getByText('User Experience')).toBeInTheDocument(); - expect(getByText('View in app')).toBeInTheDocument(); + expect(getByText('Show dashboard')).toBeInTheDocument(); expect(getByText('elastic-co-frontend')).toBeInTheDocument(); expect(getByText('Largest contentful paint')).toBeInTheDocument(); expect(getByText('1.94 s')).toBeInTheDocument(); @@ -113,7 +113,7 @@ describe('UXSection', () => { expect(getByText('User Experience')).toBeInTheDocument(); expect(getAllByText('--')).toHaveLength(3); - expect(queryAllByText('View in app')).toEqual([]); + expect(queryAllByText('Show dashboard')).toEqual([]); expect(getByText('elastic-co-frontend')).toBeInTheDocument(); }); it('shows empty state', () => { @@ -128,7 +128,7 @@ describe('UXSection', () => { expect(getByText('User Experience')).toBeInTheDocument(); expect(getAllByText('No data is available.')).toHaveLength(3); - expect(queryAllByText('View in app')).toEqual([]); + expect(queryAllByText('Show dashboard')).toEqual([]); expect(getByText('elastic-co-frontend')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 3092c7bf77f7a..6863916f9bb8c 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -57,7 +57,7 @@ export function UXSection({ bucketSize }: Props) { appLink={{ href: appLink, label: i18n.translate('xpack.observability.overview.ux.appLink', { - defaultMessage: 'View in app', + defaultMessage: 'Show dashboard', }), }} hasError={status === FETCH_STATUS.FAILURE} diff --git a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx index 335f527560c7a..19827cd3eb459 100644 --- a/x-pack/plugins/observability/public/pages/overview/data_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/data_sections.tsx @@ -23,7 +23,7 @@ interface Props { export function DataSections({ bucketSize }: Props) { return ( - + diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx index 7100a0552876d..a6af0d9182215 100644 --- a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '../..'; @@ -100,30 +100,38 @@ export function OverviewPage({ routeParams }: Props) { {hasData && ( <> - - - {/* Data sections */} - {hasAnyData && } - - - - - {/* Resources / What's New sections */} - - - - {!!newsFeed?.items?.length && } - - + + + {hasDataMap?.alert?.hasData && ( - + )} + + {/* Data sections */} + {hasAnyData && } + + + + + + + + {/* Resources / What's New sections */} + + + {!!newsFeed?.items?.length && } + + + + + + )} From a208ae974f4a2784fe7c050f7921da33258732a7 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Wed, 9 Feb 2022 13:26:04 -0500 Subject: [PATCH 096/161] Change index-pattern text to data view (#125108) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/canvas/i18n/functions/dict/escount.ts | 2 +- x-pack/plugins/canvas/i18n/functions/dict/esdocs.ts | 2 +- x-pack/plugins/canvas/i18n/ui.ts | 2 +- .../canvas/server/routes/es_fields/es_fields.test.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/canvas/i18n/functions/dict/escount.ts b/x-pack/plugins/canvas/i18n/functions/dict/escount.ts index af1337360ba6d..c831213e1f923 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/escount.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/escount.ts @@ -26,7 +26,7 @@ export const help: FunctionHelp> = { }, }), index: i18n.translate('xpack.canvas.functions.escount.args.indexHelpText', { - defaultMessage: 'An index or index pattern. For example, {example}.', + defaultMessage: 'An index or data view. For example, {example}.', values: { example: '`"logstash-*"`', }, diff --git a/x-pack/plugins/canvas/i18n/functions/dict/esdocs.ts b/x-pack/plugins/canvas/i18n/functions/dict/esdocs.ts index 6be5acdb8bc90..99979b566f529 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/esdocs.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/esdocs.ts @@ -35,7 +35,7 @@ export const help: FunctionHelp> = { defaultMessage: 'A comma-separated list of fields. For better performance, use fewer fields.', }), index: i18n.translate('xpack.canvas.functions.esdocs.args.indexHelpText', { - defaultMessage: 'An index or index pattern. For example, {example}.', + defaultMessage: 'An index or data view. For example, {example}.', values: { example: '`"logstash-*"`', }, diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index dcbaf92abbe4e..2448db2d99904 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -405,7 +405,7 @@ export const DataSourceStrings = { }), getIndexLabel: () => i18n.translate('xpack.canvas.uis.dataSources.esdocs.indexLabel', { - defaultMessage: 'Enter an index name or select an index pattern', + defaultMessage: 'Enter an index name or select a data view', }), getQueryTitle: () => i18n.translate('xpack.canvas.uis.dataSources.esdocs.queryTitle', { diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts index 977aaacdc2669..21b3357703866 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts @@ -30,7 +30,7 @@ describe('Retrieve ES Fields', () => { routeHandler = routerDeps.router.get.mock.calls[0][1]; }); - it(`returns 200 with fields from existing index/index pattern`, async () => { + it(`returns 200 with fields from existing index/data view`, async () => { const index = 'test'; const mockResults = { body: { @@ -85,7 +85,7 @@ describe('Retrieve ES Fields', () => { `); }); - it(`returns 200 with empty object when index/index pattern has no fields`, async () => { + it(`returns 200 with empty object when index/data view has no fields`, async () => { const index = 'test'; const mockResults = { body: { indices: [index], fields: {} } }; const request = httpServerMock.createKibanaRequest({ @@ -107,7 +107,7 @@ describe('Retrieve ES Fields', () => { expect(response.payload).toMatchInlineSnapshot('Object {}'); }); - it(`returns 200 with empty object when index/index pattern does not have specified field(s)`, async () => { + it(`returns 200 with empty object when index/data view does not have specified field(s)`, async () => { const index = 'test'; const mockResults = { From a1a273238e676947898ea582ef8e3faebb637e5c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 9 Feb 2022 14:39:17 -0600 Subject: [PATCH 097/161] [DOCS] Updates the 8.0.0 upgrade docs (#125128) * [DOCS] Updates the 8.0.0 upgrade docs * Update docs/setup/upgrade/upgrade-migrations.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/upgrade/upgrade-migrations.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/upgrade/upgrade-migrations.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/upgrade/upgrade-migrations.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/upgrade/upgrade-migrations.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/upgrade/upgrade-migrations.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/setup/upgrade.asciidoc | 23 ++--- .../logging-configuration-changes.asciidoc | 2 +- .../setup/upgrade/upgrade-migrations.asciidoc | 93 +++++++++---------- 3 files changed, 57 insertions(+), 61 deletions(-) diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index 4eabfa0c07714..98713e75d24f6 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -1,33 +1,30 @@ [[upgrade]] == Upgrade {kib} -To upgrade from 7.16 or earlier to {version}, -**you must first upgrade to {prev-major-last}**. -This enables you to use the Upgrade Assistant to -{stack-ref}/upgrading-elastic-stack.html#prepare-to-upgrade[prepare to upgrade]. -You must resolve all critical issues identified by the Upgrade Assistant -before proceeding with the upgrade. +To upgrade from 7.16.0 or earlier to {version}, +**you must first upgrade to {prev-major-last}**, which enables you to use the *Upgrade Assistant* to +{stack-ref}/upgrading-elastic-stack.html#prepare-to-upgrade[prepare for the upgrade]. +Before you upgrade, you must resolve all critical issues identified by the *Upgrade Assistant*. -{kib} does not support rolling upgrades. -You must shut down all {kib} instances, install the new software, and restart {kib}. +Rolling upgrades are unsupported in {kib}. To upgrade, +you must shut down all {kib} instances, install the new software, and restart {kib}. Upgrading while older {kib} instances are running can cause data loss or upgrade failures. [WARNING] ==== -{kib} automatically runs <> -when required. +When required, {kib} automatically migrates <>. In case of an upgrade failure, you can roll back to an earlier version of {kib}. To roll back, you **must** have a {ref}/snapshot-restore.html[backup snapshot] that includes the `kibana` feature -state. Snapshots include this feature state by default. +state. By default, snapshots include the `kibana` feature state. ==== For more information about upgrading, refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] IMPORTANT: You can upgrade to pre-release versions for testing, -but upgrading from a pre-release to the General Available version is not supported. -Pre-releases should only be used for testing in a temporary environment. +but upgrading from a pre-release to the General Available version is unsupported. +You should use pre-release versions only for testing in a temporary environment. include::upgrade/upgrade-migrations.asciidoc[leveloffset=-1] diff --git a/docs/setup/upgrade/logging-configuration-changes.asciidoc b/docs/setup/upgrade/logging-configuration-changes.asciidoc index 4d5f5f732536e..4a9d03d3b5312 100644 --- a/docs/setup/upgrade/logging-configuration-changes.asciidoc +++ b/docs/setup/upgrade/logging-configuration-changes.asciidoc @@ -2,7 +2,7 @@ [[logging-config-changes]] === Logging configuration changes -WARNING: {kib} 8.0 and later uses a new logging system. Be sure to read the documentation for your version of {kib} before proceeding. +WARNING: {kib} 8.0.0 and later uses a new logging system. Before you upgrade, read the documentation for your {kib} version. [[logging-pattern-format-old-and-new-example]] [options="header"] diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 53b89b38cc88d..059ae47d2e476 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -2,14 +2,14 @@ [[saved-object-migrations]] === Saved object migrations -Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version. +Each time you upgrade {kib}, an upgrade migration is performed to ensure that all <> are compatible with the new version. -NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant] -to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*. +NOTE: To help you prepare for the upgrade to 7.0.0, 6.7.0 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[*Upgrade Assistant*]. +To access the assistant, go to *Management > 7.0 Upgrade Assistant*. -WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Be sure to read the documentation for your version of {kib} before proceeding. +WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Before you upgrade, read the documentation for your version of {kib}. -WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly. +WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings are different from the default, adapt the instructions accordingly. [float] [[upgrade-migrations-process]] @@ -17,16 +17,16 @@ WARNING: The following instructions assumes {kib} is using the default index nam Saved objects are stored in two indices: -* `.kibana_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. -* `.kibana_task_manager_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. +* `.kibana_{kibana_version}_001`, e.g. for {kib} 7.12.0 `.kibana_7.12.0_001`. +* `.kibana_task_manager_{kibana_version}_001`, e.g. for {kib} 7.12.0 `.kibana_task_manager_7.12.0_001`. -The index aliases `.kibana` and `.kibana_task_manager` will always point to +The index aliases `.kibana` and `.kibana_task_manager` always point to the most up-to-date saved object indices. When you start a new {kib} installation, an upgrade migration is performed before starting plugins or serving HTTP traffic. Before you upgrade, shut down old nodes to prevent losing acknowledged writes. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later -adds a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. +adds a write block to the outdated index. Table 1 lists the saved objects indices used by previous {kib} versions. .Saved object indices and aliases per {kib} version [options="header"] @@ -46,8 +46,8 @@ adds a write block to the outdated index. Table 1 lists the saved objects indice When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shut down before starting the upgrade. -{kib} does not support rolling upgrades. However, once outdated instances are shut down, -all upgraded instances can be started in parallel, in which case all instances will participate in the upgrade migration in parallel. +Rolling upgrades are unsupported in {kib}. However, when outdated instances are shut down, you can start all upgraded instances in parallel, +which allows all instances to participate in the upgrade migration in parallel. For large deployments with more than 10 {kib} instances, and more than 10,000 saved objects, you can reduce the upgrade downtime by bringing up a single {kib} instance and waiting for it to @@ -56,7 +56,7 @@ complete the upgrade migration before bringing up the remaining instances. [float] [[preventing-migration-failures]] ==== Preventing migration failures -This section highlights common causes of {kib} upgrade failures and how to prevent them. +Review the common causes of {kib} upgrade failures and how to prevent them. [float] ===== timeout_exception or receive_timeout_transport_exception @@ -71,18 +71,18 @@ Error: Unable to complete saved object migrations for the [.kibana] index. Pleas Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] -------------------------------------------- -Instructions to work around this issue are in https://github.com/elastic/kibana/issues/95321[this GitHub issue]. +For instructions on how to mitigate the known issue, refer to https://github.com/elastic/kibana/issues/95321[the GitHub issue]. [float] ===== Corrupt saved objects -We highly recommend testing your {kib} upgrade in a development cluster to find and remedy problems -caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. +To find and remedy problems caused by corrupt documents, we highly recommend testing your {kib} upgrade in a development cluster, +especially when there are custom integrations that create saved objects in your environment. -Saved objects that were corrupted through manual editing or integrations will cause migration -failures with a log message like `Unable to migrate the corrupt Saved Object document ...`. +Saved objects that are corrupted through manual editing or integrations cause migration +failures with a log message, such as `Unable to migrate the corrupt Saved Object document ...`. For a successful upgrade migration, you must fix or delete corrupt documents. -For example, given the following error message: +For example, you receive the following error message: [source,sh] -------------------------------------------- @@ -112,18 +112,18 @@ DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab . Restart {kib}. + -In this example, the dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **is no longer available**. +The dashboard with the `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` ID that belongs to the `marketing_space` space **is no longer available**. -Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. +Be sure you have a snapshot before you delete the corrupt document. If you are unable to restore from a snapshot, it is recommended to also delete the `temp` and `target` indices the migration creates before you restart {kib} and retry the snapshot restore. [float] -===== User defined index templates that causes new `.kibana*` indices to have incompatible settings or mappings +===== User defined index templates that cause new `.kibana*` indices to have incompatible settings or mappings Matching index templates that specify `settings.refresh_interval` or `mappings` are known to interfere with {kib} upgrades. -Prevention: Narrow down the {data-sources} of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. +To make sure the index templates won't apply to new `.kibana*` indices, narrow down the {data-sources} of any user-defined index templates. -NOTE: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` -and uses an index pattern of `.kibana`. This index template will not interfere and does not need to be changed or removed. +NOTE: In {kib} 6.5.0 and earlier, {kib} creates a `kibana_index_template:.kibana` index template +and uses a `.kibana` index pattern. You do not need to change or remove the index template. [float] ===== An unhealthy {es} cluster @@ -140,7 +140,7 @@ Ensure that all {kib} instances are running the same version, configuration, and [float] ===== Incompatible `xpack.tasks.index` configuration setting -For {kib} 7.5.0 and earlier, when the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, +In {kib} 7.5.0 and earlier, when the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations fail. In {kib} 7.5.1 and later, the incompatible configuration setting prevents upgrade migrations from starting. [float] @@ -149,56 +149,55 @@ upgrade migrations fail. In {kib} 7.5.1 and later, the incompatible configuratio If {kib} unexpectedly terminates while migrating a saved object index, {kib} automatically attempts to perform the migration again when the process restarts. Do not delete any saved objects indices to -attempt to fix a failed migration. Unlike previous versions, {kib} 7.12.0 and -later does not require deleting any indices to release a failed migration lock. +to fix a failed migration. Unlike previous versions, {kib} 7.12.0 and +later does not require deleting indices to release a failed migration lock. -If upgrade migrations fail repeatedly, follow the advice in +If upgrade migrations fail repeatedly, refer to <>. -Once the root cause for the migration failure has been addressed, -{kib} will automatically retry the migration without any further intervention. -If you're unable to resolve a failed migration following these steps, please contact support. +When you address the root cause for the migration failure, +{kib} automatically retries the migration. +If you're unable to resolve a failed migration, contact Support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in <> -and <> and -If {kib} is still unable to upgrade successfully, rollback {kib} until +If you've followed <> +and <>, and +{kib} is still unable to successfully upgrade, rollback {kib} until you're able to identify and fix the root cause. -WARNING: Before rolling back {kib}, ensure that the version you want to rollback to is compatible with -your {es} cluster. If the version you're rolling back to is not compatible, you must also rollback {es}. -Any changes made after an upgrade are lost when rolling back to a previous version. +WARNING: Before you roll back {kib}, ensure that the version you want to roll back to is compatible with +your {es} cluster. If the version you want to roll back to is not compatible, you must also rollback {es}. +Any changes made after an upgrade are lost when you roll back to a previous version. -To rollback after a failed upgrade migration, the saved object indices have to be -rolled back to be compatible with the previous {kib} version. +To roll back after a failed upgrade migration, you must also rollback the saved object indices to be compatible with the previous {kib} version. [float] -===== Rollback by restoring a backup snapshot +===== Roll back by restoring a backup snapshot . Before proceeding, {ref}/snapshots-take-snapshot.html[take a snapshot] that contains the `kibana` feature state. - Snapshots include this feature state by default. + By default, snapshots include the `kibana` feature state. . To make sure no {kib} instances are performing an upgrade migration, shut down all {kib} instances. -. Delete all saved object indices with `DELETE /.kibana*`. +. To delete all saved object indices, use `DELETE /.kibana*`. . {ref}/snapshots-restore-snapshot.html[Restore] the `kibana` feature state from the snapshot. . Start all {kib} instances on the older version you want to rollback to. [float] -===== (Not recommended) Rollback without a backup snapshot +===== (Not recommended) Roll back without a backup snapshot . To make sure no {kib} instances are performing an upgrade migration, shut down all {kib} instances. -. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. By default, snapshots include the feature state. +. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. By default, snapshots include the `kibana` feature state. . Delete the version-specific indices created by the failed upgrade migration. + For example, to rollback from a failed upgrade -to v7.12.0: `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +to v7.12.0, use `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*`. . Inspect the output of `GET /_cat/aliases`. + If the `.kibana` or `.kibana_task_manager` aliases are missing, you must create them manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. -For example, if the `.kibana` alias is missing, and the latest index is `.kibana_3`, create a new alias with `POST /.kibana_3/_aliases/.kibana`. -. To remove the write block from the rollback indices: +For example, if the `.kibana` alias is missing, and the latest index is `.kibana_3`, create a new alias using `POST /.kibana_3/_aliases/.kibana`. +. To remove the write block from the roll back indices, use `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` . Start {kib} on the older version you want to rollback to. From c78a4f355cc5546753db03ab5ccedb60bbf8b9ed Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 9 Feb 2022 20:43:57 +0000 Subject: [PATCH 098/161] chore(NA): upgrade bazelisk into v1.11.0 (#125070) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .bazeliskversion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazeliskversion b/.bazeliskversion index 4dae2985b58cc..1cac385c6cb86 100644 --- a/.bazeliskversion +++ b/.bazeliskversion @@ -1 +1 @@ -1.10.1 +1.11.0 From 855c0148c7d889b78f929d008c13ca654870989c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 9 Feb 2022 14:52:39 -0600 Subject: [PATCH 099/161] [DOCS] Reformats the Security settings tables into definition lists (#123965) * [DOCS] Reformats the Security settings tables into definition lists * Review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/security-settings.asciidoc | 430 +++++++++-------------- 1 file changed, 157 insertions(+), 273 deletions(-) diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 56d08ee24efe1..787efa64f0775 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -8,10 +8,6 @@ You do not need to configure any additional settings to use the {security-features} in {kib}. They are enabled by default. -[float] -[[general-security-settings]] -==== General security settings - [float] [[authentication-security-settings]] ==== Authentication security settings @@ -46,123 +42,80 @@ xpack.security.authc: <3> Specifies the settings for the SAML authentication provider with a `saml1` name. <4> Specifies the settings for the SAML authentication provider with a `saml2` name. -The valid settings in the `xpack.security.authc.providers` namespace vary depending on the authentication provider type. For more information, refer to <>. - [float] [[authentication-provider-settings]] -===== Valid settings for all authentication providers - -[cols="2*<"] -|=== -| `xpack.security.authc.providers.` -`..enabled` {ess-icon} -| Determines if the authentication provider should be enabled. By default, {kib} enables the provider as soon as you configure any of its properties. - -| `xpack.security.authc.providers.` -`..order` {ess-icon} -| Order of the provider in the authentication chain and on the Login Selector UI. - -| `xpack.security.authc.providers.` -`..description` {ess-icon} -| Custom description of the provider entry displayed on the Login Selector UI. - -| `xpack.security.authc.providers.` -`..hint` {ess-icon} -| Custom hint for the provider entry displayed on the Login Selector UI. - -| `xpack.security.authc.providers.` -`..icon` {ess-icon} -| Custom icon for the provider entry displayed on the Login Selector UI. - -| `xpack.security.authc.providers..` -`.showInSelector` {ess-icon} -| Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn't remove the provider from the authentication chain. - -2+a| -[TIP] -[NOTE] -============ -You are unable to set this setting to `false` for `basic` and `token` authentication providers. -============ - -| `xpack.security.authc.providers..` -`.accessAgreement.message` {ess-icon} -| Access agreement text in Markdown format. For more information, refer to <>. - -| [[xpack-security-provider-session-idleTimeout]] `xpack.security.authc.providers..` -`.session.idleTimeout` {ess-icon} -| Ensures that user sessions will expire after a period of inactivity. Setting this to `0` will prevent sessions from expiring because of inactivity. By default, this setting is equal to <>. - -2+a| -[TIP] -============ -Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). -============ - -| [[xpack-security-provider-session-lifespan]] `xpack.security.authc.providers..` -`.session.lifespan` {ess-icon} -| Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If -this is set to `0`, user sessions could stay active indefinitely. By default, this setting is equal to <>. +==== Valid settings for all authentication providers + +The valid settings in the `xpack.security.authc.providers` namespace vary depending on the authentication provider type. For more information, refer to <>. + +xpack.security.authc.providers...enabled {ess-icon}:: +Determines if the authentication provider should be enabled. By default, {kib} enables the provider as soon as you configure any of its properties. + +xpack.security.authc.providers...order {ess-icon}:: +Order of the provider in the authentication chain and on the Login Selector UI. + +xpack.security.authc.providers...description {ess-icon}:: +Custom description of the provider entry displayed on the Login Selector UI. -2+a| -[TIP] -============ -Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). -============ +xpack.security.authc.providers...hint {ess-icon}:: +Custom hint for the provider entry displayed on the Login Selector UI. -|=== +xpack.security.authc.providers...icon {ess-icon}:: +Custom icon for the provider entry displayed on the Login Selector UI. + +xpack.security.authc.providers...showInSelector {ess-icon}:: +Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn't remove the provider from the authentication chain. ++ +NOTE: You are unable to set this setting to `false` for `basic` and `token` authentication providers. + +xpack.security.authc.providers...accessAgreement.message {ess-icon}:: +Access agreement text in Markdown format. For more information, refer to <>. + +[[xpack-security-provider-session-idleTimeout]] xpack.security.authc.providers...session.idleTimeout {ess-icon}:: +Ensures that user sessions will expire after a period of inactivity. Setting this to `0` will prevent sessions from expiring because of inactivity. By default, this setting is equal to <>. ++ +NOTE: Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). + +[[xpack-security-provider-session-lifespan]] xpack.security.authc.providers...session.lifespan {ess-icon}:: +Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If +this is set to `0`, user sessions could stay active indefinitely. By default, this setting is equal to <>. ++ +NOTE: Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). [float] [[saml-authentication-provider-settings]] -===== SAML authentication provider settings +==== SAML authentication provider settings In addition to <>, you can specify the following settings: -[cols="2*<"] -|=== -| `xpack.security.authc.providers.` -`saml..realm` {ess-icon} -| SAML realm in {es} that provider should use. +xpack.security.authc.providers.saml..realm {ess-icon}:: +SAML realm in {es} that provider should use. -| `xpack.security.authc.providers.` -`saml..useRelayStateDeepLink` {ess-icon} -| Determines if the provider should treat the `RelayState` parameter as a deep link in {kib} during Identity Provider initiated log in. By default, this setting is set to `false`. The link specified in `RelayState` should be a relative, URL-encoded {kib} URL. For example, the `/app/dashboards#/list` link in `RelayState` parameter would look like this: `RelayState=%2Fapp%2Fdashboards%23%2Flist`. - -|=== +xpack.security.authc.providers.saml..useRelayStateDeepLink {ess-icon}:: +Determines if the provider should treat the `RelayState` parameter as a deep link in {kib} during Identity Provider initiated log in. By default, this setting is set to `false`. The link specified in `RelayState` should be a relative, URL-encoded {kib} URL. For example, the `/app/dashboards#/list` link in `RelayState` parameter would look like this: `RelayState=%2Fapp%2Fdashboards%23%2Flist`. [float] [[oidc-authentication-provider-settings]] -===== OpenID Connect authentication provider settings +==== OpenID Connect authentication provider settings In addition to <>, you can specify the following settings: -[cols="2*<"] -|=== -| `xpack.security.authc.providers.` -`oidc..realm` {ess-icon} -| OpenID Connect realm in {es} that the provider should use. - -|=== +xpack.security.authc.providers.oidc..realm {ess-icon}:: +OpenID Connect realm in {es} that the provider should use. [float] [[anonymous-authentication-provider-settings]] -===== Anonymous authentication provider settings +==== Anonymous authentication provider settings In addition to <>, you can specify the following settings: -[NOTE] -============ -You can configure only one anonymous provider per {kib} instance. -============ - -[cols="2*<"] -|=== -| `xpack.security.authc.providers.` -`anonymous..credentials` {ess-icon} -| Credentials that {kib} should use internally to authenticate anonymous requests to {es}. Possible values are: username and password, API key, or the constant `elasticsearch_anonymous_user` if you want to leverage {ref}/anonymous-access.html[{es} anonymous access]. - -2+a| For example: +NOTE: You can configure only one anonymous provider per {kib} instance. +xpack.security.authc.providers.anonymous..credentials {ess-icon}:: +Credentials that {kib} should use internally to authenticate anonymous requests to {es}. Possible values are: username and password, API key, or the constant `elasticsearch_anonymous_user` if you want to leverage {ref}/anonymous-access.html[{es} anonymous access]. ++ +For example: ++ [source,yaml] ---------------------------------------- # Username and password credentials @@ -187,45 +140,35 @@ xpack.security.authc.providers.anonymous.anonymous1: credentials: "elasticsearch_anonymous_user" ---------------------------------------- -|=== - [float] [[http-authentication-settings]] -===== HTTP authentication settings +==== HTTP authentication settings There is a very limited set of cases when you'd want to change these settings. For more information, refer to <>. -[cols="2*<"] -|=== -| `xpack.security.authc.http.enabled` -| Determines if HTTP authentication should be enabled. By default, this setting is set to `true`. - -| `xpack.security.authc.http.autoSchemesEnabled` -| Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. +xpack.security.authc.http.enabled:: +Determines if HTTP authentication should be enabled. By default, this setting is set to `true`. -| `xpack.security.authc.http.schemes[]` -| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey', 'bearer']` to support HTTP authentication with the <> and <> schemes. +xpack.security.authc.http.autoSchemesEnabled:: +Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. -|=== +xpack.security.authc.http.schemes[]:: +List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey', 'bearer']` to support HTTP authentication with the <> and <> schemes. [float] [[login-ui-settings]] -===== Login user interface settings +==== Login user interface settings You can configure the following settings in the `kibana.yml` file. -[cols="2*<"] -|=== -| `xpack.security.loginAssistanceMessage` {ess-icon} -| Adds a message to the login UI. Useful for displaying information about maintenance windows, links to corporate sign up pages, and so on. +xpack.security.loginAssistanceMessage {ess-icon}:: +Adds a message to the login UI. Useful for displaying information about maintenance windows, links to corporate sign up pages, and so on. -| `xpack.security.loginHelp` {ess-icon} -| Adds a message accessible at the login UI with additional help information for the login process. +xpack.security.loginHelp {ess-icon}:: +Adds a message accessible at the login UI with additional help information for the login process. -| `xpack.security.authc.selector.enabled` {ess-icon} -| Determines if the login selector UI should be enabled. By default, this setting is set to `true` if more than one authentication provider is configured. - -|=== +xpack.security.authc.selector.enabled {ess-icon}:: +Determines if the login selector UI should be enabled. By default, this setting is set to `true` if more than one authentication provider is configured. [float] [[security-session-and-cookie-settings]] @@ -233,81 +176,49 @@ You can configure the following settings in the `kibana.yml` file. You can configure the following settings in the `kibana.yml` file. -[cols="2*<"] -|=== -| `xpack.security.cookieName` - | Sets the name of the cookie used for the session. The default value is `"sid"`. - -|[[xpack-security-encryptionKey]] `xpack.security.encryptionKey` - | An arbitrary string of 32 characters or more that is used to encrypt session information. Do **not** expose this key to users of {kib}. By - default, a value is automatically generated in memory. If you use that default - behavior, all sessions are invalidated when {kib} restarts. - In addition, high-availability deployments of {kib} will behave unexpectedly - if this setting isn't the same for all instances of {kib}. - -|[[xpack-security-secureCookies]] `xpack.security.secureCookies` - | Sets the `secure` flag of the session cookie. The default value is `false`. It - is automatically set to `true` if <> is set to `true`. Set - this to `true` if SSL is configured outside of {kib} (for example, you are - routing requests through a load balancer or proxy). - -| [[xpack-security-sameSiteCookies]] `xpack.security.sameSiteCookies` {ess-icon} - | Sets the `SameSite` attribute of the session cookie. This allows you to declare whether your cookie should be restricted to a first-party or same-site context. - Valid values are `Strict`, `Lax`, `None`. - This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting <>: `true`. - -|[[xpack-session-idleTimeout]] `xpack.security.session.idleTimeout` {ess-icon} - | Ensures that user sessions will expire after a period of inactivity. This and <> are both -highly recommended. You can also specify this setting for <>. If this is set to `0`, then sessions will never expire due to inactivity. By default, this value is 8 hours. - -2+a| -[TIP] -============ -Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). -============ - -|[[xpack-session-lifespan]] `xpack.security.session.lifespan` {ess-icon} - | Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If -this is set to `0`, user sessions could stay active indefinitely. This and <> are both highly -recommended. You can also specify this setting for <>. By default, this value is 30 days. +xpack.security.cookieName:: +Sets the name of the cookie used for the session. The default value is `"sid"`. -2+a| -[TIP] -============ -Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). -============ +[[xpack-security-encryptionKey]] xpack.security.encryptionKey:: +An arbitrary string of 32 characters or more that is used to encrypt session information. Do **not** expose this key to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. In addition, high-availability deployments of {kib} will behave unexpectedly if this setting isn't the same for all instances of {kib}. -| `xpack.security.session.cleanupInterval` {ess-icon} -| Sets the interval at which {kib} tries to remove expired and invalid sessions from the session index. By default, this value is 1 hour. The minimum value is 10 seconds. +[[xpack-security-secureCookies]] xpack.security.secureCookies:: +Sets the `secure` flag of the session cookie. The default value is `false`. It +is automatically set to `true` if <> is set to `true`. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -2+a| -[TIP] -============ -Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). -============ +[[xpack-security-sameSiteCookies]] xpack.security.sameSiteCookies {ess-icon}:: +Sets the `SameSite` attribute of the session cookie. This allows you to declare whether your cookie should be restricted to a first-party or same-site context. +Valid values are `Strict`, `Lax`, `None`. +This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting <>: `true`. -|=== +[[xpack-session-idleTimeout]] xpack.security.session.idleTimeout {ess-icon}:: +Ensures that user sessions will expire after a period of inactivity. This and <> are both highly recommended. You can also specify this setting for <>. If this is set to `0`, then sessions will never expire due to inactivity. By default, this value is 8 hours. ++ +NOTE: Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). + +[[xpack-session-lifespan]] xpack.security.session.lifespan {ess-icon}:: +Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If this is set to `0`, user sessions could stay active indefinitely. This and <> are both highly +recommended. You can also specify this setting for <>. By default, this value is 30 days. ++ +TIP: Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). + +xpack.security.session.cleanupInterval {ess-icon}:: +Sets the interval at which {kib} tries to remove expired and invalid sessions from the session index. By default, this value is 1 hour. The minimum value is 10 seconds. ++ +TIP: Use a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). [[security-encrypted-saved-objects-settings]] ==== Encrypted saved objects settings These settings control the encryption of saved objects with sensitive data. For more details, refer to <>. -[IMPORTANT] -============ -In high-availability deployments, make sure you use the same encryption and decryption keys for all instances of {kib}. Although the keys can be specified in clear text in `kibana.yml`, it's recommended to store them securely in the <>. -============ +IMPORTANT: In high-availability deployments, make sure you use the same encryption and decryption keys for all instances of {kib}. Although the keys can be specified in clear text in `kibana.yml`, it's recommended to store them securely in the <>. -[cols="2*<"] -|=== -| [[xpack-encryptedSavedObjects-encryptionKey]] `xpack.encryptedSavedObjects.` -`encryptionKey` -| An arbitrary string of at least 32 characters that is used to encrypt sensitive properties of saved objects before they're stored in {es}. If not set, {kib} will generate a random key on startup, but certain features won't be available until you set the encryption key explicitly. +[[xpack-encryptedSavedObjects-encryptionKey]] xpack.encryptedSavedObjects.encryptionKey:: +An arbitrary string of at least 32 characters that is used to encrypt sensitive properties of saved objects before they're stored in {es}. If not set, {kib} will generate a random key on startup, but certain features won't be available until you set the encryption key explicitly. -| [[xpack-encryptedSavedObjects-keyRotation-decryptionOnlyKeys]] `xpack.encryptedSavedObjects.` -`keyRotation.decryptionOnlyKeys` -| An optional list of previously used encryption keys. Like <>, these must be at least 32 characters in length. {kib} doesn't use these keys for encryption, but may still require them to decrypt some existing saved objects. Use this setting if you wish to change your encryption key, but don't want to lose access to saved objects that were previously encrypted with a different key. -|=== +[[xpack-encryptedSavedObjects-keyRotation-decryptionOnlyKeys]] xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys:: +An optional list of previously used encryption keys. Like <>, these must be at least 32 characters in length. {kib} doesn't use these keys for encryption, but may still require them to decrypt some existing saved objects. Use this setting if you wish to change your encryption key, but don't want to lose access to saved objects that were previously encrypted with a different key. [float] [[audit-logging-settings]] @@ -315,18 +226,17 @@ In high-availability deployments, make sure you use the same encryption and decr You can enable audit logging to support compliance, accountability, and security. When enabled, {kib} will capture: -- Who performed an action -- What action was performed -- When the action occurred +* Who performed an action +* What action was performed +* When the action occurred For more details and a reference of audit events, refer to <>. -[cols="2*<"] -|====== -| `xpack.security.audit.enabled` {ess-icon} -| Set to `true` to enable audit logging`. *Default:* `false` - -2+a| For example: +xpack.security.audit.enabled {ess-icon}:: +Set to `true` to enable audit logging`. *Default:* `false` ++ +For example: ++ [source,yaml] ---------------------------------------- xpack.security.audit.enabled: true @@ -346,128 +256,103 @@ xpack.security.audit.appender: <1> <2> Rotates log files every 24 hours. <3> Keeps maximum of 10 log files before deleting older ones. -| `xpack.security.audit.appender` -| Optional. Specifies where audit logs should be written to and how they should be formatted. If no appender is specified, a default appender will be used (see above). - -| `xpack.security.audit.appender.type` -| Required. Specifies where audit logs should be written to. Allowed values are `console`, `file`, or `rolling-file`. +xpack.security.audit.appender:: +Optional. Specifies where audit logs should be written to and how they should be formatted. If no appender is specified, a default appender will be used (see above). +xpack.security.audit.appender.type:: +Required. Specifies where audit logs should be written to. Allowed values are `console`, `file`, or `rolling-file`. ++ Refer to <> and <> for appender specific settings. -| `xpack.security.audit.appender.layout.type` -| Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`. - +xpack.security.audit.appender.layout.type:: +Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`. ++ Refer to <> for layout specific settings. - -2+a| -[TIP] -============ -We recommend using `json` format to allow ingesting {kib} audit logs into {es} using Filebeat. -============ - -|====== ++ +TIP: We recommend using `json` format to allow ingesting {kib} audit logs into {es} using Filebeat. [float] [[audit-logging-file-appender,file appender]] -===== File appender +==== File appender The `file` appender writes to a file and can be configured using the following settings: -[cols="2*<"] -|====== -| `xpack.security.audit.appender.fileName` -| Required. Full file path the log file should be written to. -|====== +xpack.security.audit.appender.fileName:: +Required. Full file path the log file should be written to. [float] [[audit-logging-rolling-file-appender, rolling file appender]] -===== Rolling file appender +==== Rolling file appender The `rolling-file` appender writes to a file and rotates it using a rolling strategy, when a particular policy is triggered: -[cols="2*<"] -|====== -| `xpack.security.audit.appender.fileName` -| Required. Full file path the log file should be written to. - -| `xpack.security.audit.appender.policy.type` -| Specifies when a rollover should occur. Allowed values are `size-limit` and `time-interval`. *Default:* `time-interval`. +xpack.security.audit.appender.fileName:: +Required. Full file path the log file should be written to. +xpack.security.audit.appender.policy.type:: +Specifies when a rollover should occur. Allowed values are `size-limit` and `time-interval`. *Default:* `time-interval`. ++ Refer to <> and <> for policy specific settings. -| `xpack.security.audit.appender.strategy.type` -| Specifies how the rollover should occur. Only allowed value is currently `numeric`. *Default:* `numeric` +xpack.security.audit.appender.strategy.type:: +Specifies how the rollover should occur. Only allowed value is currently `numeric`. *Default:* `numeric` ++ Refer to <> for strategy specific settings. -|====== [float] [[audit-logging-size-limit-policy, size limit policy]] -===== Size limit triggering policy +==== Size limit triggering policy The `size-limit` triggering policy will rotate the file when it reaches a certain size: -[cols="2*<"] -|====== -| `xpack.security.audit.appender.policy.size` -| Maximum size the log file should reach before a rollover should be performed. *Default:* `100mb` -|====== +xpack.security.audit.appender.policy.size:: +Maximum size the log file should reach before a rollover should be performed. *Default:* `100mb` [float] [[audit-logging-time-interval-policy, time interval policy]] -===== Time interval triggering policy +==== Time interval triggering policy The `time-interval` triggering policy will rotate the file every given interval of time: -[cols="2*<"] -|====== -| `xpack.security.audit.appender.policy.interval` -| How often a rollover should occur. *Default:* `24h` +xpack.security.audit.appender.policy.interval:: +How often a rollover should occur. *Default:* `24h` -| `xpack.security.audit.appender.policy.modulate` -| Whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. *Default:* `true` -|====== +xpack.security.audit.appender.policy.modulate:: +Whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. *Default:* `true` [float] [[audit-logging-numeric-strategy, numeric strategy]] -===== Numeric rolling strategy +==== Numeric rolling strategy The `numeric` rolling strategy will suffix the log file with a given pattern when rolling over, and will retain a fixed number of rolled files: -[cols="2*<"] -|====== -| `xpack.security.audit.appender.strategy.pattern` -| Suffix to append to the file name when rolling over. Must include `%i`. *Default:* `-%i` +xpack.security.audit.appender.strategy.pattern:: +Suffix to append to the file name when rolling over. Must include `%i`. *Default:* `-%i` -| `xpack.security.audit.appender.strategy.max` -| Maximum number of files to keep. Once this number is reached, oldest files will be deleted. *Default:* `7` -|====== +xpack.security.audit.appender.strategy.max:: +Maximum number of files to keep. Once this number is reached, oldest files will be deleted. *Default:* `7` [float] [[audit-logging-pattern-layout, pattern layout]] -===== Pattern layout +==== Pattern layout The `pattern` layout outputs a string, formatted using a pattern with special placeholders, which will be replaced with data from the actual log message: -[cols="2*<"] -|====== -| `xpack.security.audit.appender.layout.pattern` -| Optional. Specifies how the log line should be formatted. *Default:* `[%date][%level][%logger]%meta %message` +xpack.security.audit.appender.layout.pattern:: +Optional. Specifies how the log line should be formatted. *Default:* `[%date][%level][%logger]%meta %message` -| `xpack.security.audit.appender.layout.highlight` -| Optional. Set to `true` to enable highlighting log messages with colors. -|====== +xpack.security.audit.appender.layout.highlight:: +Optional. Set to `true` to enable highlighting log messages with colors. [float] [[audit-logging-ignore-filters]] -===== Ignore filters - -[cols="2*<"] -|====== -| `xpack.security.audit.ignore_filters[]` {ess-icon} -| List of filters that determine which events should be excluded from the audit log. An event will get filtered out if at least one of the provided filters matches. - -2+a| For example: +==== Ignore filters +xpack.security.audit.ignore_filters[] {ess-icon}:: +List of filters that determine which events should be excluded from the audit log. An event will get filtered out if at least one of the provided filters matches. ++ +For example: ++ [source,yaml] ---------------------------------------- xpack.security.audit.ignore_filters: @@ -478,15 +363,14 @@ xpack.security.audit.ignore_filters: <1> Filters out HTTP request events <2> Filters out any data write events -| `xpack.security.audit.ignore_filters[].actions[]` {ess-icon} -| List of values matched against the `event.action` field of an audit event. Refer to <> for a list of available events. +xpack.security.audit.ignore_filters[].actions[] {ess-icon}:: +List of values matched against the `event.action` field of an audit event. Refer to <> for a list of available events. -| `xpack.security.audit.ignore_filters[].categories[]` {ess-icon} -| List of values matched against the `event.category` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-category.html[ECS categorization field] for allowed values. +xpack.security.audit.ignore_filters[].categories[] {ess-icon}:: +List of values matched against the `event.category` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-category.html[ECS categorization field] for allowed values. -| `xpack.security.audit.ignore_filters[].types[]` {ess-icon} -| List of values matched against the `event.type` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-type.html[ECS type field] for allowed values. +xpack.security.audit.ignore_filters[].types[] {ess-icon}:: +List of values matched against the `event.type` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-type.html[ECS type field] for allowed values. -| `xpack.security.audit.ignore_filters[].outcomes[]` {ess-icon} -| List of values matched against the `event.outcome` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-outcome.html[ECS outcome field] for allowed values. -|====== +xpack.security.audit.ignore_filters[].outcomes[] {ess-icon}:: +List of values matched against the `event.outcome` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-outcome.html[ECS outcome field] for allowed values. \ No newline at end of file From fcd564152008bdf8f4bbccab359eb6a48145ce35 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Wed, 9 Feb 2022 22:14:41 +0100 Subject: [PATCH 100/161] Migrate the pre-7.11 tasks that has no schedule field. (#124304) * Migrate the pre-7.11 tasks that has no schedule field. --- .../server/saved_objects/migrations.test.ts | 62 +++++++++++++++++++ .../server/saved_objects/migrations.ts | 53 +++++++++++----- x-pack/plugins/task_manager/server/task.ts | 7 ++- .../es_archives/task_manager_tasks/data.json | 33 +++++++++- .../test_suites/task_manager/migrations.ts | 30 ++++++++- 5 files changed, 167 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts index 73141479d9081..e912eda258090 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts @@ -108,6 +108,68 @@ describe('successful migrations', () => { }); }); }); + + describe('8.2.0', () => { + test('resets attempts and status of a "failed" alerting tasks without schedule interval', () => { + const migration820 = getMigrations()['8.2.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123', + status: 'failed', + schedule: undefined, + }); + + expect(migration820(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + attempts: 0, + status: 'idle', + }, + }); + }); + + test('resets attempts and status of a "running" alerting tasks without schedule interval', () => { + const migration820 = getMigrations()['8.2.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123', + status: 'running', + schedule: undefined, + }); + + expect(migration820(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + attempts: 0, + status: 'idle', + }, + }); + }); + + test('does not update the tasks that are not "failed"', () => { + const migration820 = getMigrations()['8.2.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123', + status: 'idle', + attempts: 3, + schedule: undefined, + }); + + expect(migration820(taskInstance, migrationContext)).toEqual(taskInstance); + }); + + test('does not update the tasks that are not "failed" and has a schedule', () => { + const migration820 = getMigrations()['8.2.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123', + status: 'idle', + attempts: 3, + schedule: { interval: '1000' }, + }); + + expect(migration820(taskInstance, migrationContext)).toEqual(taskInstance); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts index 89bbb3d783881..f50b3d6a927ad 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -13,15 +13,15 @@ import { SavedObjectsUtils, SavedObjectUnsanitizedDoc, } from '../../../../../src/core/server'; -import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task'; +import { ConcreteTaskInstance, TaskStatus } from '../task'; interface TaskInstanceLogMeta extends LogMeta { - migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc }; + migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc }; } type TaskInstanceMigration = ( - doc: SavedObjectUnsanitizedDoc -) => SavedObjectUnsanitizedDoc; + doc: SavedObjectUnsanitizedDoc +) => SavedObjectUnsanitizedDoc; export function getMigrations(): SavedObjectMigrationMap { return { @@ -37,18 +37,19 @@ export function getMigrations(): SavedObjectMigrationMap { pipeMigrations(alertingTaskLegacyIdToSavedObjectIds, actionsTasksLegacyIdToSavedObjectIds), '8.0.0' ), + '8.2.0': executeMigrationWithErrorHandling( + pipeMigrations(resetAttemptsAndStatusForTheTasksWithoutSchedule), + '8.2.0' + ), }; } function executeMigrationWithErrorHandling( - migrationFunc: SavedObjectMigrationFn< - TaskInstanceWithDeprecatedFields, - TaskInstanceWithDeprecatedFields - >, + migrationFunc: SavedObjectMigrationFn, version: string ) { return ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -68,8 +69,8 @@ function executeMigrationWithErrorHandling( } function alertingTaskLegacyIdToSavedObjectIds( - doc: SavedObjectUnsanitizedDoc -): SavedObjectUnsanitizedDoc { + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { if (doc.attributes.taskType.startsWith('alerting:')) { let params: { spaceId?: string; alertId?: string } = {}; params = JSON.parse(doc.attributes.params as unknown as string); @@ -94,8 +95,8 @@ function alertingTaskLegacyIdToSavedObjectIds( } function actionsTasksLegacyIdToSavedObjectIds( - doc: SavedObjectUnsanitizedDoc -): SavedObjectUnsanitizedDoc { + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { if (doc.attributes.taskType.startsWith('actions:')) { let params: { spaceId?: string; actionTaskParamsId?: string } = {}; params = JSON.parse(doc.attributes.params as unknown as string); @@ -126,7 +127,7 @@ function actionsTasksLegacyIdToSavedObjectIds( function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, ...doc -}: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc { +}: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc { return { ...doc, attributes: { @@ -143,6 +144,28 @@ function moveIntervalIntoSchedule({ } function pipeMigrations(...migrations: TaskInstanceMigration[]): TaskInstanceMigration { - return (doc: SavedObjectUnsanitizedDoc) => + return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); } + +function resetAttemptsAndStatusForTheTasksWithoutSchedule( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.taskType.startsWith('alerting:')) { + if ( + !doc.attributes.schedule?.interval && + (doc.attributes.status === TaskStatus.Failed || doc.attributes.status === TaskStatus.Running) + ) { + return { + ...doc, + attributes: { + ...doc.attributes, + attempts: 0, + status: TaskStatus.Idle, + }, + }; + } + } + + return doc; +} diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 2452e3e6f4920..6d12a3f5984ca 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -307,7 +307,12 @@ export interface ConcreteTaskInstance extends TaskInstance { id: string; /** - * The saved object version from the Elaticsearch document. + * @deprecated This field has been moved under schedule (deprecated) with version 7.6.0 + */ + interval?: string; + + /** + * The saved object version from the Elasticsearch document. */ version?: string; diff --git a/x-pack/test/functional/es_archives/task_manager_tasks/data.json b/x-pack/test/functional/es_archives/task_manager_tasks/data.json index b59abd341a7af..3431419dda17e 100644 --- a/x-pack/test/functional/es_archives/task_manager_tasks/data.json +++ b/x-pack/test/functional/es_archives/task_manager_tasks/data.json @@ -58,4 +58,35 @@ "updated_at": "2020-11-30T15:43:08.277Z" } } -} \ No newline at end of file +} + +{ + "type": "doc", + "value": { + "id": "task:d33d7590-8377-11ec-8c11-2dfe94229b95", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "task": { + "taskType": "alerting:xpack.uptime.alerts.monitorStatus", + "retryAt": null, + "runAt": "2222-02-01T15:59:42.908Z", + "scope": [ + "alerting" + ], + "startedAt": null, + "state": "{}", + "params": "{\"alertId\":\"c9a10a61-7dcc-4297-991a-6c52c10eb7d2\",\"spaceId\":\"default\"}", + "ownerId": null, + "scheduledAt": "2022-02-01T15:58:41.000Z", + "attempts": 3, + "status": "failed" + }, + "references": [], + "updated_at": "2022-02-01T15:58:44.109Z", + "type": "task" + } + } +} diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts index 329aee7e74b98..1e6bb11c13583 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts @@ -8,7 +8,11 @@ import expect from '@kbn/expect'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { TransportResult } from '@elastic/elasticsearch'; -import { TaskInstanceWithDeprecatedFields } from '../../../../plugins/task_manager/server/task'; +import { + ConcreteTaskInstance, + TaskInstanceWithDeprecatedFields, + TaskStatus, +} from '../../../../plugins/task_manager/server/task'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { SavedObjectsUtils } from '../../../../../src/core/server/saved_objects'; @@ -76,5 +80,29 @@ export default function createGetTests({ getService }: FtrProviderContext) { )}"}` ); }); + + it('8.2.0 migrates alerting tasks that has no schedule.interval', async () => { + const searchResult: TransportResult< + estypes.SearchResponse<{ task: ConcreteTaskInstance }>, + unknown + > = await es.search( + { + index: '.kibana_task_manager', + body: { + query: { + term: { + _id: 'task:d33d7590-8377-11ec-8c11-2dfe94229b95', + }, + }, + }, + }, + { meta: true } + ); + expect(searchResult.statusCode).to.equal(200); + expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.equal(1); + const hit = searchResult.body.hits.hits[0]; + expect(hit!._source!.task.attempts).to.be(0); + expect(hit!._source!.task.status).to.be(TaskStatus.Idle); + }); }); } From 46f679878b53c7c8d566379b5b38fdaa28d7bf31 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 9 Feb 2022 13:30:49 -0800 Subject: [PATCH 101/161] [DOCS] Removes Homebrew docs (#125155) --- docs/redirects.asciidoc | 5 ++ docs/setup/install.asciidoc | 8 --- docs/setup/install/brew-running.asciidoc | 9 ---- docs/setup/install/brew.asciidoc | 65 ------------------------ docs/setup/start-stop.asciidoc | 8 --- 5 files changed, 5 insertions(+), 90 deletions(-) delete mode 100644 docs/setup/install/brew-running.asciidoc delete mode 100644 docs/setup/install/brew.asciidoc diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 0ca518c3a8788..6a34076f10988 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -400,3 +400,8 @@ This content has moved. Refer to <>. == Upgrade Assistant This content has moved. Refer to {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant]. + +[role="exclude",id="brew"] +== Install {kib} on macOS with Homebrew + +This page has been deleted. Refer to <>. diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index 8b64bdf5fe2a2..ac49946d877bc 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -46,12 +46,6 @@ downloaded from the Elastic Docker Registry. + <> -`brew`:: - -Formulae are available from the Elastic Homebrew tap for installing {kib} on macOS with the Homebrew package manager. -+ -<> - IMPORTANT: If your Elasticsearch installation is protected by {ref}/elasticsearch-security.html[{stack-security-features}] see {kibana-ref}/using-kibana-with-security.html[Configuring security in {kib}] for @@ -66,5 +60,3 @@ include::install/deb.asciidoc[] include::install/rpm.asciidoc[] include::{kib-repo-dir}/setup/docker.asciidoc[] - -include::install/brew.asciidoc[] diff --git a/docs/setup/install/brew-running.asciidoc b/docs/setup/install/brew-running.asciidoc deleted file mode 100644 index d73102b098ec1..0000000000000 --- a/docs/setup/install/brew-running.asciidoc +++ /dev/null @@ -1,9 +0,0 @@ -==== Run {kib} with `brew services` - -With Homebrew, Kibana can be started and stopped as follows: - -[source,sh] --------------------------------------------------- -brew services start elastic/tap/kibana-full -brew services stop elastic/tap/kibana-full --------------------------------------------------- diff --git a/docs/setup/install/brew.asciidoc b/docs/setup/install/brew.asciidoc deleted file mode 100644 index eeba869a259d4..0000000000000 --- a/docs/setup/install/brew.asciidoc +++ /dev/null @@ -1,65 +0,0 @@ -[[brew]] -=== Install {kib} on macOS with Homebrew -++++ -Install on macOS with Homebrew -++++ - -Elastic publishes Homebrew formulae so you can install {kib} with the https://brew.sh/[Homebrew] package manager. - -To install with Homebrew, you first need to tap the Elastic Homebrew repository: - -[source,sh] -------------------------- -brew tap elastic/tap -------------------------- - -Once you've tapped the Elastic Homebrew repo, you can use `brew install` to -install the **latest version** of {kib}: - -[source,sh] -------------------------- -brew install elastic/tap/kibana-full -------------------------- - -[[brew-layout]] -==== Directory layout for Homebrew installs - -When you install {kib} with `brew install`, the config files, logs, -and data directory are stored in the following locations. - -[cols=" Date: Wed, 9 Feb 2022 16:43:04 -0700 Subject: [PATCH 102/161] [Security Solution] Bugfix for inspect index pattern not aligned with data view index pattern (#125007) --- .../common/components/inspect/index.test.tsx | 141 ++++++++++++-- .../common/components/inspect/index.tsx | 41 ++-- .../common/components/inspect/modal.test.tsx | 180 +++++++----------- .../common/components/inspect/modal.tsx | 59 ++++-- .../common/components/inspect/translations.ts | 15 ++ 5 files changed, 283 insertions(+), 153 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx index d73b4cb7d98d6..b3dbbb86ace68 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx @@ -21,6 +21,10 @@ import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers'; import { InspectButton } from '.'; import { cloneDeep } from 'lodash/fp'; +jest.mock('./modal', () => ({ + ModalInspectQuery: jest.fn(() =>
), +})); + describe('Inspect Button', () => { const refetch = jest.fn(); const state: State = mockGlobalState; @@ -103,6 +107,54 @@ describe('Inspect Button', () => { ); expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); }); + + test('Button disabled when inspect == null', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = null; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + test('Button disabled when inspect.dsl.length == 0', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: [], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + test('Button disabled when inspect.response.length == 0', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['my dsl'], + response: [], + }; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); }); describe('Modal Inspect - happy path', () => { @@ -127,46 +179,103 @@ describe('Inspect Button', () => { wrapper.update(); expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - true - ); + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toBe(true); }); - test('Close Inspect Modal', () => { + test('Do not Open Inspect Modal if it is loading', () => { const wrapper = mount( ); + expect(store.getState().inputs.global.queries[0].isInspected).toBe(false); + store.getState().inputs.global.queries[0].loading = true; wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="modal-inspect-close"]').first().simulate('click'); - - wrapper.update(); - - expect(store.getState().inputs.global.queries[0].isInspected).toBe(false); + expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( false ); }); + }); - test('Do not Open Inspect Modal if it is loading', () => { + describe('Modal Inspect - show or hide', () => { + test('shows when request/response are complete and isInspected=true', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['a length'], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = true; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const wrapper = mount( ); - store.getState().inputs.global.queries[0].loading = true; - wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); - wrapper.update(); + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(true); + }); - expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - false + test('hides when request/response are complete and isInspected=false', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['a length'], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = false; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false); + }); + + test('hides when request is empty and isInspected=true', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: [], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = true; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false); + }); + + test('hides when response is empty and isInspected=true', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['my dsl'], + response: [], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = true; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + ); + + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 4f52703620b5f..defb90b9054f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -7,7 +7,7 @@ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { omit } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { inputsSelectors, State } from '../../store'; @@ -52,10 +52,10 @@ const InspectButtonComponent: React.FC = ({ compact = false, inputId = 'global', inspect, + inspectIndex = 0, isDisabled, isInspected, loading, - inspectIndex = 0, multiple = false, // If multiple = true we ignore the inspectIndex and pass all requests and responses to the inspect modal onCloseInspect, queryId = '', @@ -63,7 +63,6 @@ const InspectButtonComponent: React.FC = ({ setIsInspected, title = '', }) => { - const isShowingModal = !loading && selectedInspectIndex === inspectIndex && isInspected; const handleClick = useCallback(() => { setIsInspected({ id: queryId, @@ -105,6 +104,16 @@ const InspectButtonComponent: React.FC = ({ } } + const isShowingModal = useMemo( + () => !loading && selectedInspectIndex === inspectIndex && isInspected, + [inspectIndex, isInspected, loading, selectedInspectIndex] + ); + + const isButtonDisabled = useMemo( + () => loading || isDisabled || request == null || response == null, + [isDisabled, loading, request, response] + ); + return ( <> {inputId === 'timeline' && !compact && ( @@ -115,7 +124,7 @@ const InspectButtonComponent: React.FC = ({ color="text" iconSide="left" iconType="inspect" - isDisabled={loading || isDisabled || false} + isDisabled={isButtonDisabled} isLoading={loading} onClick={handleClick} > @@ -129,21 +138,23 @@ const InspectButtonComponent: React.FC = ({ data-test-subj="inspect-icon-button" iconSize="m" iconType="inspect" - isDisabled={loading || isDisabled || false} + isDisabled={isButtonDisabled} title={i18n.INSPECT} onClick={handleClick} /> )} - + {isShowingModal && request !== null && response !== null && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx index 572513180025f..7a9c36a986afd 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx @@ -7,103 +7,50 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; +import { TestProviders } from '../../mock'; import { NO_ALERT_INDEX } from '../../../../common/constants'; import { ModalInspectQuery, formatIndexPatternRequested } from './modal'; -import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import { InputsModelId } from '../../store/inputs/constants'; +import { EXCLUDE_ELASTIC_CLOUD_INDEX } from '../../containers/sourcerer'; -const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - }, +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useLocation: jest.fn().mockReturnValue([{ pathname: '/overview' }]), + }; }); -const request = - '{"index": ["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}'; +const getRequest = ( + indices: string[] = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'] +) => + `{"index": ${JSON.stringify( + indices + )},"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}`; + +const request = getRequest(); + const response = '{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}'; describe('Modal Inspect', () => { const closeModal = jest.fn(); - - describe('rendering', () => { - test('when isShowing is positive and request and response are not null', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(true); - expect(wrapper.find('.euiModalHeader__title').first().text()).toBe('Inspect My title'); - }); - - test('when isShowing is negative and request and response are not null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - - test('when isShowing is positive and request is null and response is not null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - - test('when isShowing is positive and request is not null and response is null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - }); + const defaultProps = { + closeModal, + inputId: 'timeline' as InputsModelId, + request, + response, + title: 'My title', + }; describe('functionality from tab statistics/request/response', () => { test('Click on statistic Tab', () => { const wrapper = mount( - - - + + + ); wrapper.find('.euiTab').first().simulate('click'); @@ -134,15 +81,9 @@ describe('Modal Inspect', () => { test('Click on request Tab', () => { const wrapper = mount( - - - + + + ); wrapper.find('.euiTab').at(2).simulate('click'); @@ -201,15 +142,9 @@ describe('Modal Inspect', () => { test('Click on response Tab', () => { const wrapper = mount( - - - + + + ); wrapper.find('.euiTab').at(1).simulate('click'); @@ -237,15 +172,9 @@ describe('Modal Inspect', () => { describe('events', () => { test('Make sure that toggle function has been called when you click on the close button', () => { const wrapper = mount( - - - + + + ); wrapper.find('button[data-test-subj="modal-inspect-close"]').simulate('click'); @@ -280,4 +209,37 @@ describe('Modal Inspect', () => { expect(expected).toEqual('Sorry about that, something went wrong.'); }); }); + + describe('index pattern messaging', () => { + test('no messaging when all patterns are in sourcerer selection', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(false); + expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(false); + }); + test('not-sourcerer-msg when not all patterns are in sourcerer selection', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(true); + expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(false); + }); + test('exclude-logs-msg when EXCLUDE_ELASTIC_CLOUD_INDEX is present in patterns', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(false); + expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx index 78a3d744e46bb..45fcf1e746b87 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx @@ -19,11 +19,19 @@ import { EuiTabbedContent, } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { Fragment, ReactNode } from 'react'; +import React, { useMemo, Fragment, ReactNode } from 'react'; import styled from 'styled-components'; +import { useLocation } from 'react-router-dom'; import { NO_ALERT_INDEX } from '../../../../common/constants'; import * as i18n from './translations'; +import { + EXCLUDE_ELASTIC_CLOUD_INDEX, + getScopeFromPath, + useSourcererDataView, +} from '../../containers/sourcerer'; +import { InputsModelId } from '../../store/inputs/constants'; +import { SourcererScopeName } from '../../store/sourcerer/model'; const DescriptionListStyled = styled(EuiDescriptionList)` @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.s}) { @@ -40,12 +48,12 @@ const DescriptionListStyled = styled(EuiDescriptionList)` DescriptionListStyled.displayName = 'DescriptionListStyled'; interface ModalInspectProps { - closeModal: () => void; - isShowing: boolean; - request: string | null; - response: string | null; additionalRequests?: string[] | null; additionalResponses?: string[] | null; + closeModal: () => void; + inputId?: InputsModelId; + request: string; + response: string; title: string | React.ReactElement | React.ReactNode; } @@ -101,18 +109,18 @@ export const formatIndexPatternRequested = (indices: string[] = []) => { }; export const ModalInspectQuery = ({ + additionalRequests, + additionalResponses, closeModal, - isShowing = false, + inputId, request, response, - additionalRequests, - additionalResponses, title, }: ModalInspectProps) => { - if (!isShowing || request == null || response == null) { - return null; - } - + const { pathname } = useLocation(); + const { selectedPatterns } = useSourcererDataView( + inputId === 'timeline' ? SourcererScopeName.timeline : getScopeFromPath(pathname) + ); const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])]; const responses: string[] = [ response, @@ -122,6 +130,16 @@ export const ModalInspectQuery = ({ const inspectRequests: Request[] = parseInspectStrings(requests); const inspectResponses: Response[] = parseInspectStrings(responses); + const isSourcererPattern = useMemo( + () => (inspectRequests[0]?.index ?? []).every((pattern) => selectedPatterns.includes(pattern)), + [inspectRequests, selectedPatterns] + ); + + const isLogsExclude = useMemo( + () => (inspectRequests[0]?.index ?? []).includes(EXCLUDE_ELASTIC_CLOUD_INDEX), + [inspectRequests] + ); + const statistics: Array<{ title: NonNullable; description: NonNullable; @@ -135,7 +153,22 @@ export const ModalInspectQuery = ({ ), description: ( - {formatIndexPatternRequested(inspectRequests[0]?.index ?? [])} +

{formatIndexPatternRequested(inspectRequests[0]?.index ?? [])}

+ + {!isSourcererPattern && ( +

+ + {i18n.INSPECT_PATTERN_DIFFERENT} + +

+ )} + {isLogsExclude && ( +

+ + {i18n.LOGS_EXCLUDE_MESSAGE} + +

+ )}
), }, diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts b/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts index 28561aadf8d7e..732432c659d4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts @@ -36,6 +36,21 @@ export const INDEX_PATTERN_DESC = i18n.translate( } ); +export const INSPECT_PATTERN_DIFFERENT = i18n.translate( + 'xpack.securitySolution.inspectPatternDifferent', + { + defaultMessage: 'This element has a unique index pattern separate from the data view setting.', + } +); + +export const LOGS_EXCLUDE_MESSAGE = i18n.translate( + 'xpack.securitySolution.inspectPatternExcludeLogs', + { + defaultMessage: + 'When the logs-* index pattern is selected, Elastic cloud logs are excluded from the search.', + } +); + export const QUERY_TIME = i18n.translate('xpack.securitySolution.inspect.modal.queryTimeLabel', { defaultMessage: 'Query time', }); From dba7207787654c54e2bdf1abf399378f3f9c94f8 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 9 Feb 2022 16:02:25 -0800 Subject: [PATCH 103/161] [Security Solution][Lists] - Update exception item viewer overflow (#125145) ### Summary Addresses #119012 - updates exception item viewer UI --- .../exception_lists/new/exception_list.json | 4 +- .../exception_item/exception_details.tsx | 16 +- .../exception_item/exception_entries.tsx | 3 +- .../exceptions/viewer/helpers.test.tsx | 267 +++++++++++------- .../components/exceptions/viewer/helpers.tsx | 35 ++- 5 files changed, 209 insertions(+), 116 deletions(-) diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json index 73271514269da..68bbcc6288df2 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list.json @@ -2,6 +2,6 @@ "list_id": "simple_list", "tags": ["user added string for a tag", "malware"], "type": "detection", - "description": "This is a sample endpoint type exception", - "name": "Sample Endpoint Exception List" + "description": "This is a sample detection type exception", + "name": "Sample Detection Exception List" } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx index 3354637b9f745..429f9672aece5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx @@ -11,7 +11,6 @@ import { EuiDescriptionList, EuiButtonEmpty, EuiDescriptionListTitle, - EuiDescriptionListDescription, EuiToolTip, } from '@elastic/eui'; import React, { useMemo, Fragment } from 'react'; @@ -92,7 +91,11 @@ const ExceptionDetailsComponent = ({ - + {descriptionListItems.map((item) => ( @@ -100,14 +103,7 @@ const ExceptionDetailsComponent = ({ {item.title} - - - {item.description} - - + {item.description} ))} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 10210463e9a1e..4db00bea5c932 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -150,8 +150,7 @@ const ExceptionEntriesComponent = ({ }, }, ], - // eslint-disable-next-line react-hooks/exhaustive-deps - [entries] + [] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index d67f526fa9bdc..25e260e67855a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { getFormattedEntries, formatEntry, getDescriptionListContent } from './helpers'; -import { FormattedEntry, DescriptionListItem } from '../types'; +import { FormattedEntry } from '../types'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntriesArrayMock } from '../../../../../../lists/common/schemas/types/entries.mock'; import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; @@ -157,103 +157,171 @@ describe('Exception viewer helpers', () => { const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); payload.description = ''; const result = getDescriptionListContent(payload); - const expected: DescriptionListItem[] = [ - { - description: 'Linux', - title: 'OS', - }, - { - description: 'April 20th 2020 @ 15:25:31', - title: 'Date created', - }, - { - description: 'some user', - title: 'Created by', - }, - ]; + const os = result.find(({ title }) => title === 'OS'); - expect(result).toEqual(expected); + expect(os).toMatchInlineSnapshot(` + Object { + "description": + + Linux + + , + "title": "OS", + } + `); }); test('it returns formatted description list with a description if one specified', () => { const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); payload.description = 'Im a description'; const result = getDescriptionListContent(payload); - const expected: DescriptionListItem[] = [ - { - description: 'Linux', - title: 'OS', - }, - { - description: 'April 20th 2020 @ 15:25:31', - title: 'Date created', - }, - { - description: 'some user', - title: 'Created by', - }, - { - description: 'Im a description', - title: 'Description', - }, - ]; + const description = result.find(({ title }) => title === 'Description'); - expect(result).toEqual(expected); + expect(description).toMatchInlineSnapshot(` + Object { + "description": + + Im a description + + , + "title": "Description", + } + `); }); - test('it returns just user and date created if no other fields specified', () => { + test('it returns scrolling element when description is longer than 75 charachters', () => { const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - payload.description = ''; + payload.description = + 'Puppy kitty ipsum dolor sit good dog foot stick canary. Teeth Mittens grooming vaccine walk swimming nest good boy furry tongue heel furry treats fish. Cage run fast kitten dinnertime ball run foot park fleas throw house train licks stick dinnertime window. Yawn litter fish yawn toy pet gate throw Buddy kitty wag tail ball groom crate ferret heel wet nose Rover toys pet supplies. Bird Food treats tongue lick teeth ferret litter box slobbery litter box crate bird small animals yawn small animals shake slobber gimme five toys polydactyl meow. '; const result = getDescriptionListContent(payload); - const expected: DescriptionListItem[] = [ - { - description: 'Linux', - title: 'OS', - }, - { - description: 'April 20th 2020 @ 15:25:31', - title: 'Date created', - }, - { - description: 'some user', - title: 'Created by', - }, - ]; + const description = result.find(({ title }) => title === 'Description'); - expect(result).toEqual(expected); + expect(description).toMatchInlineSnapshot(` + Object { + "description": + + Puppy kitty ipsum dolor sit good dog foot stick canary. Teeth Mittens grooming vaccine walk swimming nest good boy furry tongue heel furry treats fish. Cage run fast kitten dinnertime ball run foot park fleas throw house train licks stick dinnertime window. Yawn litter fish yawn toy pet gate throw Buddy kitty wag tail ball groom crate ferret heel wet nose Rover toys pet supplies. Bird Food treats tongue lick teeth ferret litter box slobbery litter box crate bird small animals yawn small animals shake slobber gimme five toys polydactyl meow. + + , + "title": "Description", + } + `); }); - test('it returns Modified By/On info. when `includeModified` is true', () => { + test('it returns just user and date created if no other fields specified', () => { + const payload = getExceptionListItemSchemaMock(); + payload.description = ''; + const result = getDescriptionListContent(payload); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "description": + + April 20th 2020 @ 15:25:31 + + , + "title": "Date created", + }, + Object { + "description": + + some user + + , + "title": "Created by", + }, + ] + `); + }); + + test('it returns Modified By/On info when `includeModified` is true', () => { const result = getDescriptionListContent( getExceptionListItemSchemaMock({ os_types: ['linux'] }), true ); - expect(result).toEqual([ - { - description: 'Linux', - title: 'OS', - }, - { - description: 'April 20th 2020 @ 15:25:31', - title: 'Date created', - }, - { - description: 'some user', - title: 'Created by', - }, - { - description: 'April 20th 2020 @ 15:25:31', - title: 'Date modified', - }, - { - description: 'some user', - title: 'Modified by', - }, - { - description: 'some description', - title: 'Description', - }, - ]); + const dateModified = result.find(({ title }) => title === 'Date modified'); + const modifiedBy = result.find(({ title }) => title === 'Modified by'); + expect(modifiedBy).toMatchInlineSnapshot(` + Object { + "description": + + some user + + , + "title": "Modified by", + } + `); + expect(dateModified).toMatchInlineSnapshot(` + Object { + "description": + + April 20th 2020 @ 15:25:31 + + , + "title": "Date modified", + } + `); }); test('it returns Name when `includeName` is true', () => { @@ -262,28 +330,25 @@ describe('Exception viewer helpers', () => { false, true ); - expect(result).toEqual([ - { - description: 'some name', - title: 'Name', - }, - { - description: 'Linux', - title: 'OS', - }, - { - description: 'April 20th 2020 @ 15:25:31', - title: 'Date created', - }, - { - description: 'some user', - title: 'Created by', - }, - { - description: 'some description', - title: 'Description', - }, - ]); + const name = result.find(({ title }) => title === 'Name'); + expect(name).toMatchInlineSnapshot(` + Object { + "description": + + some name + + , + "title": "Name", + } + `); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 597e8a6fed52f..37bfeb6166405 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -14,6 +14,8 @@ import { BuilderEntry, } from '@kbn/securitysolution-list-utils'; +import React from 'react'; +import { EuiDescriptionListDescription, EuiText, EuiToolTip } from '@elastic/eui'; import { formatOperatingSystems } from '../helpers'; import type { FormattedEntry, DescriptionListItem } from '../types'; import * as i18n from '../translations'; @@ -125,7 +127,38 @@ export const getDescriptionListContent = ( return details.reduce((acc, { value, title }) => { if (value != null && value.trim() !== '') { - return [...acc, { title, description: value }]; + const valueElement = ( + + + {value} + + + ); + if (title === i18n.DESCRIPTION) { + return [ + ...acc, + { + title, + description: + value.length > 75 ? ( + + + {value} + + + ) : ( + valueElement + ), + }, + ]; + } + return [...acc, { title, description: valueElement }]; } else { return acc; } From 7407c245ef0a261b9be5a520129a25fbf023f312 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 10 Feb 2022 02:38:35 +0000 Subject: [PATCH 104/161] skip flaky suite (#118432) --- test/functional/apps/discover/_search_on_page_load.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_search_on_page_load.ts b/test/functional/apps/discover/_search_on_page_load.ts index 0198881e981b8..3df6ce1c13c43 100644 --- a/test/functional/apps/discover/_search_on_page_load.ts +++ b/test/functional/apps/discover/_search_on_page_load.ts @@ -58,7 +58,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); - describe(`when it's false`, () => { + // FLAKY: https://github.com/elastic/kibana/issues/118432 + describe.skip(`when it's false`, () => { beforeEach(async () => await initSearchOnPageLoad(false)); it('should not fetch data from ES initially', async function () { From 560ca276b301cf639f9987f5aeca6f6b0e622473 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 10 Feb 2022 02:43:29 +0000 Subject: [PATCH 105/161] skip flaky suite (#124663) --- .../functional/apps/management/_index_pattern_create_delete.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index 62612ad5a9080..a07141a073d64 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('validation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/124663 + describe.skip('validation', function () { it('can display errors', async function () { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.settings.setIndexPatternField('log-fake*'); From 87eaa7525cd7ad3b56b51b1321e6b2e3dbfe5cd0 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 9 Feb 2022 20:25:43 -0700 Subject: [PATCH 106/161] [Reporting] Switch from EventLog integration to ECS logging (#124762) * [Reporting] Remove EventLog Dependency * calculate duration * use LogMeta interface of core logger * fix ts * rename the debug log tag * clean up return types for testing * remove reporting fields from the event log mappings * unwrap code from iife * add class for log adapter * remove useless factory fn * remove eventLog * user field was meant to be ECS field * duration is nanoseconds * fix nanoseconds application and test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/event_log/generated/mappings.json | 13 -- x-pack/plugins/event_log/generated/schemas.ts | 7 -- x-pack/plugins/event_log/scripts/mappings.js | 14 --- x-pack/plugins/reporting/kibana.json | 1 - x-pack/plugins/reporting/server/core.ts | 4 +- .../server/lib/event_logger/adapter.test.ts | 67 ++++++++++ .../server/lib/event_logger/adapter.ts | 57 +++++++++ .../server/lib/event_logger/index.ts | 7 +- .../server/lib/event_logger/logger.test.ts | 51 +++----- .../server/lib/event_logger/logger.ts | 116 +++++++++--------- .../server/lib/event_logger/types.ts | 63 +++------- .../reporting/server/lib/level_logger.ts | 9 +- x-pack/plugins/reporting/server/plugin.ts | 3 - .../create_mock_reportingplugin.ts | 4 - x-pack/plugins/reporting/server/types.ts | 2 - x-pack/plugins/reporting/tsconfig.json | 1 - .../__snapshots__/event_log.snap | 11 -- .../reporting_and_security/event_log.ts | 88 ------------- .../reporting_and_security/index.ts | 1 - 19 files changed, 220 insertions(+), 299 deletions(-) create mode 100644 x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts create mode 100644 x-pack/plugins/reporting/server/lib/event_logger/adapter.ts delete mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/event_log.snap delete mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/event_log.ts diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 5e6a9d660f82a..e9f030ffbc886 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -309,19 +309,6 @@ } } }, - "reporting": { - "properties": { - "id": { - "type": "keyword" - }, - "jobType": { - "type": "keyword" - }, - "byteSize": { - "type": "long" - } - } - }, "saved_objects": { "type": "nested", "properties": { diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 4607495b85c4e..d61689d6238e4 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -140,13 +140,6 @@ export const EventSchema = schema.maybe( ), }) ), - reporting: schema.maybe( - schema.object({ - id: ecsString(), - jobType: ecsString(), - byteSize: ecsNumber(), - }) - ), saved_objects: schema.maybe( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 22d36d7f20d4c..091b50eceea6c 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -91,20 +91,6 @@ exports.EcsCustomPropertyMappings = { }, }, }, - // reporting specific fields - reporting: { - properties: { - id: { - type: 'keyword', - }, - jobType: { - type: 'keyword', - }, - byteSize: { - type: 'long', - }, - }, - }, // array of saved object references, for "linking" via search saved_objects: { type: 'nested', diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index e7162d0974de6..8f75a462ed8f6 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -18,7 +18,6 @@ "licensing", "uiActions", "taskManager", - "eventLog", "embeddable", "screenshotting", "screenshotMode", diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 5c00089afc381..745542c358a69 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -21,7 +21,6 @@ import type { import type { PluginStart as DataPluginStart } from 'src/plugins/data/server'; import type { FieldFormatsStart } from 'src/plugins/field_formats/server'; import { KibanaRequest, ServiceStatusLevels } from '../../../../src/core/server'; -import type { IEventLogService } from '../../event_log/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import type { LicensingPluginStart } from '../../licensing/server'; import type { ScreenshotResult, ScreenshottingStart } from '../../screenshotting/server'; @@ -40,7 +39,6 @@ import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/t import type { ReportingPluginRouter, ScreenshotOptions } from './types'; export interface ReportingInternalSetup { - eventLog: IEventLogService; basePath: Pick; router: ReportingPluginRouter; features: FeaturesPluginSetup; @@ -390,7 +388,7 @@ export class ReportingCore { } public getEventLogger(report: IReport, task?: { id: string }) { - const ReportingEventLogger = reportingEventLoggerFactory(this.pluginSetupDeps!.eventLog); + const ReportingEventLogger = reportingEventLoggerFactory(this.logger); return new ReportingEventLogger(report, task); } } diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts new file mode 100644 index 0000000000000..aef569a49e357 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { LogMeta } from 'kibana/server'; +import { createMockLevelLogger } from '../../test_helpers'; +import { EcsLogAdapter } from './adapter'; + +describe('EcsLogAdapter', () => { + const logger = createMockLevelLogger(); + beforeAll(() => { + jest + .spyOn(global.Date, 'now') + .mockImplementationOnce(() => new Date('2021-04-12T16:00:00.000Z').valueOf()) + .mockImplementationOnce(() => new Date('2021-04-12T16:02:00.000Z').valueOf()); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('captures a log event', () => { + const eventLogger = new EcsLogAdapter(logger, { event: { provider: 'test-adapting' } }); + + const event = { kibana: { reporting: { wins: 5000 } } } as object & LogMeta; // an object that extends LogMeta + eventLogger.logEvent('hello world', event); + + expect(logger.debug).toBeCalledWith('hello world', ['events'], { + event: { + duration: undefined, + end: undefined, + provider: 'test-adapting', + start: undefined, + }, + kibana: { + reporting: { + wins: 5000, + }, + }, + }); + }); + + it('captures timings between start and complete', () => { + const eventLogger = new EcsLogAdapter(logger, { event: { provider: 'test-adapting' } }); + eventLogger.startTiming(); + + const event = { kibana: { reporting: { wins: 9000 } } } as object & LogMeta; // an object that extends LogMeta + eventLogger.logEvent('hello duration', event); + + expect(logger.debug).toBeCalledWith('hello duration', ['events'], { + event: { + duration: 120000000000, + end: '2021-04-12T16:02:00.000Z', + provider: 'test-adapting', + start: '2021-04-12T16:00:00.000Z', + }, + kibana: { + reporting: { + wins: 9000, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts new file mode 100644 index 0000000000000..c9487a79d9e70 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts @@ -0,0 +1,57 @@ +/* + * 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 deepMerge from 'deepmerge'; +import { LogMeta } from 'src/core/server'; +import { LevelLogger } from '../level_logger'; +import { IReportingEventLogger } from './logger'; + +/** @internal */ +export class EcsLogAdapter implements IReportingEventLogger { + start?: Date; + end?: Date; + + /** + * This class provides a logging system to Reporting code, using a shape similar to the EventLog service. + * The logging action causes ECS data with Reporting metrics sent to DEBUG logs. + * + * @param {LevelLogger} logger - Reporting's wrapper of the core logger + * @param {Partial} properties - initial ECS data with template for Reporting metrics + */ + constructor(private logger: LevelLogger, private properties: Partial) {} + + logEvent(message: string, properties: LogMeta) { + if (this.start && !this.end) { + this.end = new Date(Date.now()); + } + + let duration: number | undefined; + if (this.end && this.start) { + duration = (this.end.valueOf() - this.start.valueOf()) * 1000000; // nanoseconds + } + + // add the derived properties for timing between "start" and "complete" logging calls + const newProperties: LogMeta = deepMerge(this.properties, { + event: { + duration, + start: this.start?.toISOString(), + end: this.end?.toISOString(), + }, + }); + + // sends an ECS object with Reporting metrics to the DEBUG logs + this.logger.debug(message, ['events'], deepMerge(newProperties, properties)); + } + + startTiming() { + this.start = new Date(Date.now()); + } + + stopTiming() { + this.end = new Date(Date.now()); + } +} diff --git a/x-pack/plugins/reporting/server/lib/event_logger/index.ts b/x-pack/plugins/reporting/server/lib/event_logger/index.ts index 566f0a21e2b05..f9c4c5574b3e1 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/index.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/index.ts @@ -5,9 +5,6 @@ * 2.0. */ -import { IEventLogService } from '../../../../event_log/server'; -import { PLUGIN_ID } from '../../../common/constants'; - export enum ActionType { SCHEDULE_TASK = 'schedule-task', CLAIM_TASK = 'claim-task', @@ -16,7 +13,5 @@ export enum ActionType { SAVE_REPORT = 'save-report', RETRY = 'retry', FAIL_REPORT = 'fail-report', -} -export function registerEventLogProviderActions(eventLog: IEventLogService) { - eventLog.registerProviderActions(PLUGIN_ID, Object.values(ActionType)); + EXECUTE_ERROR = 'execute-error', } diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts index 21c4ee2d5e4cf..9a1c282a01a59 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts @@ -6,7 +6,7 @@ */ import { ConcreteTaskInstance } from '../../../../task_manager/server'; -import { eventLogServiceMock } from '../../../../event_log/server/mocks'; +import { createMockLevelLogger } from '../../test_helpers'; import { BasePayload } from '../../types'; import { Report } from '../store'; import { ReportingEventLogger, reportingEventLoggerFactory } from './logger'; @@ -21,7 +21,7 @@ describe('Event Logger', () => { let factory: ReportingEventLogger; beforeEach(() => { - factory = reportingEventLoggerFactory(eventLogServiceMock.create()); + factory = reportingEventLoggerFactory(createMockLevelLogger()); }); it(`should construct with an internal seed object`, () => { @@ -29,7 +29,6 @@ describe('Event Logger', () => { expect(logger.eventObj).toMatchInlineSnapshot(` Object { "event": Object { - "provider": "reporting", "timezone": "UTC", }, "kibana": Object { @@ -38,9 +37,6 @@ describe('Event Logger', () => { "jobType": "csv", }, }, - "log": Object { - "logger": "reporting", - }, "user": undefined, } `); @@ -51,7 +47,6 @@ describe('Event Logger', () => { expect(logger.eventObj).toMatchInlineSnapshot(` Object { "event": Object { - "provider": "reporting", "timezone": "UTC", }, "kibana": Object { @@ -60,9 +55,6 @@ describe('Event Logger', () => { "jobType": "csv", }, }, - "log": Object { - "logger": "reporting", - }, "user": Object { "name": "thundercat", }, @@ -77,7 +69,6 @@ describe('Event Logger', () => { expect(logger.eventObj).toMatchInlineSnapshot(` Object { "event": Object { - "provider": "reporting", "timezone": "UTC", }, "kibana": Object { @@ -89,9 +80,6 @@ describe('Event Logger', () => { "id": "some-task-id-123", }, }, - "log": Object { - "logger": "reporting", - }, "user": Object { "name": "thundercat", }, @@ -101,16 +89,16 @@ describe('Event Logger', () => { it(`logExecutionStart`, () => { const logger = new factory(mockReport); + jest.spyOn(logger.completionLogger, 'startTiming'); + jest.spyOn(logger.completionLogger, 'stopTiming'); const result = logger.logExecutionStart(); expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "execute-start", - "kind": "event", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "execute-start", "id": "12348", "jobType": "csv", }, @@ -119,23 +107,23 @@ describe('Event Logger', () => { `); expect(result.message).toMatchInlineSnapshot(`"starting csv execution"`); expect(logger.completionLogger.startTiming).toBeCalled(); + expect(logger.completionLogger.stopTiming).not.toBeCalled(); }); it(`logExecutionComplete`, () => { const logger = new factory(mockReport); + jest.spyOn(logger.completionLogger, 'startTiming'); + jest.spyOn(logger.completionLogger, 'stopTiming'); logger.logExecutionStart(); const result = logger.logExecutionComplete({ byteSize: 444 }); expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "execute-complete", - "kind": "metrics", - "outcome": "success", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "execute-complete", "byteSize": 444, "id": "12348", "jobType": "csv", @@ -154,13 +142,10 @@ describe('Event Logger', () => { expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "execute-complete", - "kind": "error", - "outcome": "failure", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "execute-error", "id": "12348", "jobType": "csv", }, @@ -176,12 +161,10 @@ describe('Event Logger', () => { expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "claim-task", - "kind": "event", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "claim-task", "id": "12348", "jobType": "csv", }, @@ -196,12 +179,10 @@ describe('Event Logger', () => { expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "fail-report", - "kind": "event", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "fail-report", "id": "12348", "jobType": "csv", }, @@ -215,12 +196,10 @@ describe('Event Logger', () => { expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "save-report", - "kind": "event", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "save-report", "id": "12348", "jobType": "csv", }, @@ -234,12 +213,10 @@ describe('Event Logger', () => { expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(` Array [ Object { - "action": "retry", - "kind": "event", - "provider": "reporting", "timezone": "UTC", }, Object { + "actionType": "retry", "id": "12348", "jobType": "csv", }, diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts index 0ec864e36620b..ccdee24d1879e 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts @@ -6,16 +6,19 @@ */ import deepMerge from 'deepmerge'; -import { IEventLogger, IEventLogService } from '../../../../event_log/server'; +import { LogMeta } from 'src/core/server'; +import { LevelLogger } from '../'; import { PLUGIN_ID } from '../../../common/constants'; import { IReport } from '../store'; import { ActionType } from './'; +import { EcsLogAdapter } from './adapter'; import { ClaimedTask, CompletedExecution, ErrorAction, ExecuteError, FailedReport, + ReportingAction, SavedReport, ScheduledRetry, ScheduledTask, @@ -27,171 +30,162 @@ export interface ExecutionCompleteMetrics { byteSize: number; } +export interface IReportingEventLogger { + logEvent(message: string, properties: LogMeta): void; + startTiming(): void; + stopTiming(): void; +} + /** @internal */ -export function reportingEventLoggerFactory(eventLog: IEventLogService) { - const genericLogger = eventLog.getLogger({ event: { provider: PLUGIN_ID } }); +export function reportingEventLoggerFactory(logger: LevelLogger) { + const genericLogger = new EcsLogAdapter(logger, { event: { provider: PLUGIN_ID } }); return class ReportingEventLogger { readonly eventObj: { event: { timezone: string; - provider: 'reporting'; }; - kibana: { reporting: StartedExecution['kibana']['reporting']; task?: { id: string } }; - log: { logger: 'reporting' }; + kibana: { + reporting: ReportingAction['kibana']['reporting']; + task?: { id: string }; + }; user?: { name: string }; }; readonly report: IReport; readonly task?: { id: string }; - completionLogger: IEventLogger; + completionLogger: IReportingEventLogger; constructor(report: IReport, task?: { id: string }) { this.report = report; this.task = task; this.eventObj = { - event: { timezone: report.payload.browserTimezone, provider: 'reporting' }, + event: { timezone: report.payload.browserTimezone }, kibana: { reporting: { id: report._id, jobType: report.jobtype }, ...(task?.id ? { task: { id: task.id } } : undefined), }, - log: { logger: 'reporting' }, user: report.created_by ? { name: report.created_by } : undefined, }; // create a "complete" logger that will use EventLog helpers to calculate timings - this.completionLogger = eventLog.getLogger({ event: { provider: PLUGIN_ID } }); + this.completionLogger = new EcsLogAdapter(logger, { event: { provider: PLUGIN_ID } }); } logScheduleTask(): ScheduledTask { + const message = `queued report ${this.report._id}`; const event = deepMerge( { - message: `queued report ${this.report._id}`, - event: { kind: 'event', action: ActionType.SCHEDULE_TASK }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.SCHEDULE_TASK } }, } as Partial, this.eventObj ); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } logExecutionStart(): StartedExecution { - this.completionLogger.startTiming(this.eventObj); + const message = `starting ${this.report.jobtype} execution`; + this.completionLogger.startTiming(); const event = deepMerge( { - message: `starting ${this.report.jobtype} execution`, - event: { kind: 'event', action: ActionType.EXECUTE_START }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.EXECUTE_START } }, } as Partial, this.eventObj ); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } logExecutionComplete({ byteSize }: ExecutionCompleteMetrics): CompletedExecution { - this.completionLogger.stopTiming(this.eventObj); + const message = `completed ${this.report.jobtype} execution`; + this.completionLogger.stopTiming(); const event = deepMerge( { - message: `completed ${this.report.jobtype} execution`, - event: { - kind: 'metrics', - outcome: 'success', - action: ActionType.EXECUTE_COMPLETE, - }, - kibana: { reporting: { byteSize } }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.EXECUTE_COMPLETE, byteSize } }, } as Partial, this.eventObj ); - this.completionLogger.logEvent(event); + this.completionLogger.logEvent(message, event); return event; } logError(error: ErrorAction): ExecuteError { - interface LoggedErrorMessage { - message: string; - error: ExecuteError['error']; - event: Omit; - log: Omit; - } - const logErrorMessage: LoggedErrorMessage = { - message: error.message, + const message = `an error occurred`; + const logErrorMessage = { + message, + kibana: { reporting: { actionType: ActionType.EXECUTE_ERROR } }, error: { message: error.message, code: error.code, stack_trace: error.stack_trace, type: error.type, }, - event: { - kind: 'error', - outcome: 'failure', - action: ActionType.EXECUTE_COMPLETE, - }, - log: { level: 'error' }, - }; + } as Partial; const event = deepMerge(logErrorMessage, this.eventObj); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } logClaimTask(): ClaimedTask { + const message = `claimed report ${this.report._id}`; const event = deepMerge( { - message: `claimed report ${this.report._id}`, - event: { kind: 'event', action: ActionType.CLAIM_TASK }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.CLAIM_TASK } }, } as Partial, this.eventObj ); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } logReportFailure(): FailedReport { + const message = `report ${this.report._id} has failed`; const event = deepMerge( { - message: `report ${this.report._id} has failed`, - event: { kind: 'event', action: ActionType.FAIL_REPORT }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.FAIL_REPORT } }, } as Partial, this.eventObj ); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } logReportSaved(): SavedReport { + const message = `saved report ${this.report._id}`; const event = deepMerge( { - message: `saved report ${this.report._id}`, - event: { kind: 'event', action: ActionType.SAVE_REPORT }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.SAVE_REPORT } }, } as Partial, this.eventObj ); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } logRetry(): ScheduledRetry { + const message = `scheduled retry for report ${this.report._id}`; const event = deepMerge( { - message: `scheduled retry for report ${this.report._id}`, - event: { kind: 'event', action: ActionType.RETRY }, - log: { level: 'info' }, + message, + kibana: { reporting: { actionType: ActionType.RETRY } }, } as Partial, this.eventObj ); - genericLogger.logEvent(event); + genericLogger.logEvent(message, event); return event; } }; diff --git a/x-pack/plugins/reporting/server/lib/event_logger/types.ts b/x-pack/plugins/reporting/server/lib/event_logger/types.ts index 1c31292d03e44..3ae06dfdb4775 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/types.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/types.ts @@ -5,31 +5,23 @@ * 2.0. */ +import { LogMeta } from 'src/core/server'; import { ActionType } from './'; -type ActionKind = 'event' | 'error' | 'metrics'; -type ActionOutcome = 'success' | 'failure'; - -interface ActionBase< - A extends ActionType, - K extends ActionKind, - O extends ActionOutcome, - EventProvider -> { +interface ActionBase { event: { - action: A; - kind: K; - outcome?: O; - provider: 'reporting'; timezone: string; }; - kibana: EventProvider & { task?: { id?: string } }; - user?: { name: string }; - log: { - logger: 'reporting'; - level: K extends 'error' ? 'error' : 'info'; - }; message: string; + kibana: { + reporting: { + actionType?: A; + id?: string; // "immediate download" exports have no ID + jobType: string; + byteSize?: number; + }; + } & { task?: { id?: string } }; + user?: { name: string }; } export interface ErrorAction { @@ -39,30 +31,15 @@ export interface ErrorAction { type?: string; } -type ReportingAction< - A extends ActionType, - K extends ActionKind, - O extends ActionOutcome = 'success' -> = ActionBase< - A, - K, - O, - { - reporting: { - id?: string; // "immediate download" exports have no ID - jobType: string; - byteSize?: number; - }; - } ->; +export type ReportingAction = ActionBase & LogMeta; -export type ScheduledTask = ReportingAction; -export type StartedExecution = ReportingAction; -export type CompletedExecution = ReportingAction; -export type SavedReport = ReportingAction; -export type ClaimedTask = ReportingAction; -export type ScheduledRetry = ReportingAction; -export type FailedReport = ReportingAction; -export type ExecuteError = ReportingAction & { +export type ScheduledTask = ReportingAction; +export type StartedExecution = ReportingAction; +export type CompletedExecution = ReportingAction; +export type SavedReport = ReportingAction; +export type ClaimedTask = ReportingAction; +export type ScheduledRetry = ReportingAction; +export type FailedReport = ReportingAction; +export type ExecuteError = ReportingAction & { error: ErrorAction; }; diff --git a/x-pack/plugins/reporting/server/lib/level_logger.ts b/x-pack/plugins/reporting/server/lib/level_logger.ts index 4985ae2d681d0..91cf6757dbee2 100644 --- a/x-pack/plugins/reporting/server/lib/level_logger.ts +++ b/x-pack/plugins/reporting/server/lib/level_logger.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { LoggerFactory } from 'src/core/server'; +import { LoggerFactory, LogMeta } from 'src/core/server'; const trimStr = (toTrim: string) => { return typeof toTrim === 'string' ? toTrim.trim() : toTrim; }; export interface GenericLevelLogger { - debug: (msg: string) => void; + debug: (msg: string, tags: string[], meta: T) => void; info: (msg: string) => void; warning: (msg: string) => void; error: (msg: Error) => void; @@ -46,8 +46,9 @@ export class LevelLogger implements GenericLevelLogger { this.getLogger(tags).warn(msg); } - public debug(msg: string, tags: string[] = []) { - this.getLogger(tags).debug(msg); + // only "debug" logging supports the LogMeta for now... + public debug(msg: string, tags: string[] = [], meta?: T) { + this.getLogger(tags).debug(msg, meta); } public trace(msg: string, tags: string[] = []) { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 3b25fedd0d5fb..a0d4bfed7c7e0 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -11,7 +11,6 @@ import { ReportingCore } from './'; import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; import { registerDeprecations } from './deprecations'; import { LevelLogger, ReportingStore } from './lib'; -import { registerEventLogProviderActions } from './lib/event_logger'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import type { @@ -38,7 +37,6 @@ export class ReportingPlugin public setup(core: CoreSetup, plugins: ReportingSetupDeps) { const { http, status } = core; - const reportingCore = new ReportingCore(this.logger, this.initContext); // prevent throwing errors in route handlers about async deps not being initialized @@ -60,7 +58,6 @@ export class ReportingPlugin ...plugins, }); - registerEventLogProviderActions(plugins.eventLog); registerUiSettings(core); registerDeprecations({ core, reportingCore }); registerReportingUsageCollector(reportingCore, plugins.usageCollection); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index aa065e7be52c7..49d92a0fe4448 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -40,10 +40,6 @@ export const createMockPluginSetup = ( taskManager: taskManagerMock.createSetup(), logger: createMockLevelLogger(), status: statusServiceMock.createSetupContract(), - eventLog: { - registerProviderActions: jest.fn(), - getLogger: jest.fn(() => ({ logEvent: jest.fn() })), - }, ...setupMock, }; }; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index c695df6d0a410..cd28972f5941a 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -12,7 +12,6 @@ import { FieldFormatsStart } from 'src/plugins/field_formats/server'; import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { Writable } from 'stream'; -import { IEventLogService } from '../../event_log/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import type { LicensingPluginStart } from '../../licensing/server'; import type { @@ -96,7 +95,6 @@ export interface ExportTypeDefinition< * @internal */ export interface ReportingSetupDeps { - eventLog: IEventLogService; features: FeaturesPluginSetup; screenshotMode: ScreenshotModePluginSetup; security?: SecurityPluginSetup; diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 24db825856627..cb22a7d9e719a 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -20,7 +20,6 @@ { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../../src/plugins/field_formats/tsconfig.json" }, - { "path": "../event_log/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../screenshotting/tsconfig.json" }, diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/event_log.snap b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/event_log.snap deleted file mode 100644 index 603ab78db8c13..0000000000000 --- a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/event_log.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Reporting APIs Report generation event logging creates a completed action for a PDF report 1`] = ` -"\\"_id\\",\\"_index\\",\\"_score\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user -zQMtOW0BH63Xcmy432DJ,ecommerce,1,\\"Men's Clothing\\",\\"Men's Clothing\\",EUR,Eddie,Eddie,\\"Eddie Underwood\\",\\"Eddie Underwood\\",MALE,38,Underwood,Underwood,\\"(empty)\\",Monday,0,\\"eddie@underwood-family.zzz\\",Cairo,Africa,EG,\\"POINT (31.3 30.1)\\",\\"Cairo Governorate\\",\\"Elitelligence, Oceanavigations\\",\\"Elitelligence, Oceanavigations\\",\\"Jul 7, 2019 @ 00:00:00.000\\",584677,\\"sold_product_584677_6283, sold_product_584677_19400\\",\\"sold_product_584677_6283, sold_product_584677_19400\\",\\"11.992, 24.984\\",\\"11.992, 24.984\\",\\"Men's Clothing, Men's Clothing\\",\\"Men's Clothing, Men's Clothing\\",\\"Dec 26, 2016 @ 00:00:00.000, Dec 26, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Elitelligence, Oceanavigations\\",\\"Elitelligence, Oceanavigations\\",\\"6.352, 11.75\\",\\"11.992, 24.984\\",\\"6,283, 19,400\\",\\"Basic T-shirt - dark blue/white, Sweatshirt - grey multicolor\\",\\"Basic T-shirt - dark blue/white, Sweatshirt - grey multicolor\\",\\"1, 1\\",\\"ZO0549605496, ZO0299602996\\",\\"0, 0\\",\\"11.992, 24.984\\",\\"11.992, 24.984\\",\\"0, 0\\",\\"ZO0549605496, ZO0299602996\\",\\"36.969\\",\\"36.969\\",2,2,order,eddie -zgMtOW0BH63Xcmy432DJ,ecommerce,1,\\"Women's Clothing\\",\\"Women's Clothing\\",EUR,Mary,Mary,\\"Mary Bailey\\",\\"Mary Bailey\\",FEMALE,20,Bailey,Bailey,\\"(empty)\\",Sunday,6,\\"mary@bailey-family.zzz\\",Dubai,Asia,AE,\\"POINT (55.3 25.3)\\",Dubai,\\"Champion Arts, Pyramidustries\\",\\"Champion Arts, Pyramidustries\\",\\"Jul 6, 2019 @ 00:00:00.000\\",584021,\\"sold_product_584021_11238, sold_product_584021_20149\\",\\"sold_product_584021_11238, sold_product_584021_20149\\",\\"24.984, 28.984\\",\\"24.984, 28.984\\",\\"Women's Clothing, Women's Clothing\\",\\"Women's Clothing, Women's Clothing\\",\\"Dec 25, 2016 @ 00:00:00.000, Dec 25, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Champion Arts, Pyramidustries\\",\\"Champion Arts, Pyramidustries\\",\\"11.75, 15.648\\",\\"24.984, 28.984\\",\\"11,238, 20,149\\",\\"Denim dress - black denim, Shorts - black\\",\\"Denim dress - black denim, Shorts - black\\",\\"1, 1\\",\\"ZO0489604896, ZO0185501855\\",\\"0, 0\\",\\"24.984, 28.984\\",\\"24.984, 28.984\\",\\"0, 0\\",\\"ZO0489604896, ZO0185501855\\",\\"53.969\\",\\"53.969\\",2,2,order,mary -zwMtOW0BH63Xcmy432DJ,ecommerce,1,\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",EUR,Gwen,Gwen,\\"Gwen Butler\\",\\"Gwen Butler\\",FEMALE,26,Butler,Butler,\\"(empty)\\",Sunday,6,\\"gwen@butler-family.zzz\\",\\"Los Angeles\\",\\"North America\\",US,\\"POINT (-118.2 34.1)\\",California,\\"Low Tide Media, Oceanavigations\\",\\"Low Tide Media, Oceanavigations\\",\\"Jul 6, 2019 @ 00:00:00.000\\",584058,\\"sold_product_584058_22794, sold_product_584058_23386\\",\\"sold_product_584058_22794, sold_product_584058_23386\\",\\"100, 100\\",\\"100, 100\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",\\"Dec 25, 2016 @ 00:00:00.000, Dec 25, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Low Tide Media, Oceanavigations\\",\\"Low Tide Media, Oceanavigations\\",\\"46, 54\\",\\"100, 100\\",\\"22,794, 23,386\\",\\"Boots - Midnight Blue, Short coat - white/black\\",\\"Boots - Midnight Blue, Short coat - white/black\\",\\"1, 1\\",\\"ZO0374603746, ZO0272202722\\",\\"0, 0\\",\\"100, 100\\",\\"100, 100\\",\\"0, 0\\",\\"ZO0374603746, ZO0272202722\\",200,200,2,2,order,gwen -0AMtOW0BH63Xcmy432DJ,ecommerce,1,\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",EUR,Diane,Diane,\\"Diane Chandler\\",\\"Diane Chandler\\",FEMALE,22,Chandler,Chandler,\\"(empty)\\",Sunday,6,\\"diane@chandler-family.zzz\\",\\"-\\",Europe,GB,\\"POINT (-0.1 51.5)\\",\\"-\\",\\"Primemaster, Oceanavigations\\",\\"Primemaster, Oceanavigations\\",\\"Jul 6, 2019 @ 00:00:00.000\\",584093,\\"sold_product_584093_12304, sold_product_584093_19587\\",\\"sold_product_584093_12304, sold_product_584093_19587\\",\\"75, 100\\",\\"75, 100\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",\\"Dec 25, 2016 @ 00:00:00.000, Dec 25, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Primemaster, Oceanavigations\\",\\"Primemaster, Oceanavigations\\",\\"34.5, 47\\",\\"75, 100\\",\\"12,304, 19,587\\",\\"High heeled sandals - argento, Classic coat - black\\",\\"High heeled sandals - argento, Classic coat - black\\",\\"1, 1\\",\\"ZO0360303603, ZO0272002720\\",\\"0, 0\\",\\"75, 100\\",\\"75, 100\\",\\"0, 0\\",\\"ZO0360303603, ZO0272002720\\",175,175,2,2,order,diane -0QMtOW0BH63Xcmy432DJ,ecommerce,1,\\"Men's Clothing, Men's Accessories\\",\\"Men's Clothing, Men's Accessories\\",EUR,Eddie,Eddie,\\"Eddie Weber\\",\\"Eddie Weber\\",MALE,38,Weber,Weber,\\"(empty)\\",Monday,0,\\"eddie@weber-family.zzz\\",Cairo,Africa,EG,\\"POINT (31.3 30.1)\\",\\"Cairo Governorate\\",Elitelligence,Elitelligence,\\"Jun 30, 2019 @ 00:00:00.000\\",574916,\\"sold_product_574916_11262, sold_product_574916_15713\\",\\"sold_product_574916_11262, sold_product_574916_15713\\",\\"60, 20.984\\",\\"60, 20.984\\",\\"Men's Clothing, Men's Accessories\\",\\"Men's Clothing, Men's Accessories\\",\\"Dec 19, 2016 @ 00:00:00.000, Dec 19, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Elitelligence, Elitelligence\\",\\"Elitelligence, Elitelligence\\",\\"28.203, 10.703\\",\\"60, 20.984\\",\\"11,262, 15,713\\",\\"Winter jacket - black, Watch - green\\",\\"Winter jacket - black, Watch - green\\",\\"1, 1\\",\\"ZO0542505425, ZO0601306013\\",\\"0, 0\\",\\"60, 20.984\\",\\"60, 20.984\\",\\"0, 0\\",\\"ZO0542505425, ZO0601306013\\",81,81,2,2,order,eddie -" -`; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/event_log.ts b/x-pack/test/reporting_api_integration/reporting_and_security/event_log.ts deleted file mode 100644 index 63317e7644021..0000000000000 --- a/x-pack/test/reporting_api_integration/reporting_and_security/event_log.ts +++ /dev/null @@ -1,88 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const reportingAPI = getService('reportingAPI'); - const es = getService('es'); - - // FLAKY: https://github.com/elastic/kibana/issues/124649 - describe.skip('Report generation event logging', () => { - before(async () => { - await reportingAPI.initEcommerce(); - }); - - after(async () => { - await reportingAPI.teardownEcommerce(); - }); - - it('creates a completed action for a PDF report', async () => { - const res = await reportingAPI.generateCsv({ - browserTimezone: 'UTC', - title: 'Test-PDF', - objectType: 'search', - searchSource: { - version: true, - fields: [{ field: '*', include_unmapped: 'true' }], - index: '5193f870-d861-11e9-a311-0fa548c5f953', - }, - columns: [], - version: '7.16.0', - }); - expect(res.status).to.eql(200); - expect(res.body.path).to.match(/download/); - - const { path } = res.body; - - // wait for the the pending job to complete - await reportingAPI.waitForJobToFinish(path); - - const csvFile = await reportingAPI.getCompletedJobOutput(path); - expectSnapshot(csvFile).toMatch(); - - // search for the raw event log data - const events = await es.search<{ event: any; kibana: { reporting: any } }>({ - index: '.kibana-event-log*', - filter_path: 'hits.hits._source.event,hits.hits._source.kibana', - query: { - bool: { - filter: [ - { - bool: { - must: [ - { term: { 'event.provider': 'reporting' } }, - { term: { 'event.action': 'execute-complete' } }, - ], - }, - }, - ], - }, - }, - sort: [{ '@timestamp': { order: 'desc' } }] as unknown as string[], - size: 1, - }); - - // validate the log has the expected fields with expected values - const logSource = events.hits.hits[0]._source; - expect(omit(logSource?.kibana.reporting, 'id')).to.eql({ - byteSize: 5943, - jobType: 'csv_searchsource', - }); - expect(omit(logSource?.event, ['duration', 'start', 'end'])).to.eql({ - action: 'execute-complete', - kind: 'metrics', - outcome: 'success', - provider: 'reporting', - timezone: 'UTC', - }); - }); - }); -} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index e2f70eed3a508..02a2915fffd60 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -24,7 +24,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bwc_existing_indexes')); loadTestFile(require.resolve('./security_roles_privileges')); loadTestFile(require.resolve('./download_csv_dashboard')); - loadTestFile(require.resolve('./event_log')); loadTestFile(require.resolve('./generate_csv_discover')); loadTestFile(require.resolve('./network_policy')); loadTestFile(require.resolve('./spaces')); From 109a3be015aee0ba005e9881fb95993dc436ee5f Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Thu, 10 Feb 2022 08:11:54 +0000 Subject: [PATCH 107/161] Remove transaction type filter in errors table (#124933) * delete transaxtion type filter from errors queries * Remove transaction type filter on errors tableat service overview * Fix api tests --- .../app/error_group_overview/index.tsx | 25 +++---------------- .../service_overview_errors_table/index.tsx | 9 ++----- .../errors/__snapshots__/queries.test.ts.snap | 10 -------- .../get_error_group_detailed_statistics.ts | 7 ------ .../get_error_group_main_statistics.ts | 5 ---- .../apm/server/routes/errors/queries.test.ts | 2 -- .../plugins/apm/server/routes/errors/route.ts | 18 ++----------- .../tests/errors/error_group_list.spec.ts | 1 - .../tests/feature_controls.spec.ts | 2 +- .../error_groups_detailed_statistics.spec.ts | 1 - .../error_groups_main_statistics.spec.ts | 1 - .../error_groups/get_error_group_ids.ts | 1 - 12 files changed, 9 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index a6ba3febeb01d..46b963d13e510 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -46,7 +46,7 @@ const INITIAL_STATE_DETAILED_STATISTICS: ErrorGroupDetailedStatistics = { }; export function ErrorGroupOverview() { - const { serviceName, transactionType } = useApmServiceContext(); + const { serviceName } = useApmServiceContext(); const { query: { @@ -82,7 +82,7 @@ export function ErrorGroupOverview() { const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; - if (start && end && transactionType) { + if (start && end) { return callApmApi( 'GET /internal/apm/services/{serviceName}/errors/groups/main_statistics', { @@ -92,7 +92,6 @@ export function ErrorGroupOverview() { }, query: { environment, - transactionType, kuery, start, end, @@ -110,16 +109,7 @@ export function ErrorGroupOverview() { }); } }, - [ - environment, - kuery, - serviceName, - transactionType, - start, - end, - sortField, - sortDirection, - ] + [environment, kuery, serviceName, start, end, sortField, sortDirection] ); const { requestId, errorGroupMainStatistics } = errorGroupListData; @@ -128,13 +118,7 @@ export function ErrorGroupOverview() { data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS, } = useFetcher( (callApmApi) => { - if ( - requestId && - errorGroupMainStatistics.length && - start && - end && - transactionType - ) { + if (requestId && errorGroupMainStatistics.length && start && end) { return callApmApi( 'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics', { @@ -146,7 +130,6 @@ export function ErrorGroupOverview() { start, end, numBuckets: 20, - transactionType, groupIds: JSON.stringify( errorGroupMainStatistics.map(({ groupId }) => groupId).sort() ), diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 84f65d4e5feb6..cffc5563d75cd 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -15,7 +15,6 @@ import { i18n } from '@kbn/i18n'; import { orderBy } from 'lodash'; import React, { useState } from 'react'; import uuid from 'uuid'; -import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; @@ -62,7 +61,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { urlParams: { comparisonType, comparisonEnabled }, } = useLegacyUrlParams(); - const { transactionType } = useApmServiceContext(); const [tableOptions, setTableOptions] = useState<{ pageIndex: number; sort: { @@ -92,7 +90,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { data = INITIAL_STATE_MAIN_STATISTICS, status } = useFetcher( (callApmApi) => { - if (!start || !end || !transactionType) { + if (!start || !end) { return; } return callApmApi( @@ -105,7 +103,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { kuery, start, end, - transactionType, }, }, } @@ -131,7 +128,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { start, end, serviceName, - transactionType, pageIndex, direction, field, @@ -148,7 +144,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS, } = useFetcher( (callApmApi) => { - if (requestId && items.length && start && end && transactionType) { + if (requestId && items.length && start && end) { return callApmApi( 'GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics', { @@ -160,7 +156,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { start, end, numBuckets: 20, - transactionType, groupIds: JSON.stringify( items.map(({ groupId: groupId }) => groupId).sort() ), diff --git a/x-pack/plugins/apm/server/routes/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/errors/__snapshots__/queries.test.ts.snap index 0317009b01f59..dd28f6d1389ee 100644 --- a/x-pack/plugins/apm/server/routes/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/errors/__snapshots__/queries.test.ts.snap @@ -103,11 +103,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "transaction.type": "request", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -176,11 +171,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "transaction.type": "request", - }, - }, Object { "range": Object { "@timestamp": Object { diff --git a/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_detailed_statistics.ts b/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_detailed_statistics.ts index 870c307a3f769..9eda5769e7fb8 100644 --- a/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_detailed_statistics.ts @@ -10,7 +10,6 @@ import { Coordinate } from '../../../../typings/timeseries'; import { ERROR_GROUP_ID, SERVICE_NAME, - TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { rangeQuery, kqlQuery } from '../../../../../observability/server'; @@ -23,7 +22,6 @@ export async function getErrorGroupDetailedStatistics({ serviceName, setup, numBuckets, - transactionType, groupIds, environment, start, @@ -33,7 +31,6 @@ export async function getErrorGroupDetailedStatistics({ serviceName: string; setup: Setup; numBuckets: number; - transactionType: string; groupIds: string[]; environment: string; start: number; @@ -56,7 +53,6 @@ export async function getErrorGroupDetailedStatistics({ filter: [ { terms: { [ERROR_GROUP_ID]: groupIds } }, { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, ...rangeQuery(start, end), ...environmentQuery(environment), ...kqlQuery(kuery), @@ -111,7 +107,6 @@ export async function getErrorGroupPeriods({ serviceName, setup, numBuckets, - transactionType, groupIds, environment, comparisonStart, @@ -123,7 +118,6 @@ export async function getErrorGroupPeriods({ serviceName: string; setup: Setup; numBuckets: number; - transactionType: string; groupIds: string[]; environment: string; comparisonStart?: number; @@ -137,7 +131,6 @@ export async function getErrorGroupPeriods({ serviceName, setup, numBuckets, - transactionType, groupIds, }; diff --git a/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts b/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts index e460991029915..d6b58b197914d 100644 --- a/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts +++ b/x-pack/plugins/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts @@ -15,7 +15,6 @@ import { ERROR_GROUP_ID, ERROR_LOG_MESSAGE, SERVICE_NAME, - TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { environmentQuery } from '../../../../common/utils/environment_query'; @@ -27,7 +26,6 @@ export async function getErrorGroupMainStatistics({ serviceName, setup, environment, - transactionType, sortField, sortDirection = 'desc', start, @@ -37,7 +35,6 @@ export async function getErrorGroupMainStatistics({ serviceName: string; setup: Setup; environment: string; - transactionType: string; sortField?: string; sortDirection?: 'asc' | 'desc'; start: number; @@ -66,7 +63,6 @@ export async function getErrorGroupMainStatistics({ bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, ...rangeQuery(start, end), ...environmentQuery(environment), ...kqlQuery(kuery), @@ -82,7 +78,6 @@ export async function getErrorGroupMainStatistics({ }, aggs: { sample: { - // change to top_metrics top_hits: { size: 1, _source: [ diff --git a/x-pack/plugins/apm/server/routes/errors/queries.test.ts b/x-pack/plugins/apm/server/routes/errors/queries.test.ts index af4a4aef694fc..7cb84db0d7862 100644 --- a/x-pack/plugins/apm/server/routes/errors/queries.test.ts +++ b/x-pack/plugins/apm/server/routes/errors/queries.test.ts @@ -42,7 +42,6 @@ describe('error queries', () => { sortDirection: 'asc', sortField: 'foo', serviceName: 'serviceName', - transactionType: 'request', setup, environment: ENVIRONMENT_ALL.value, kuery: '', @@ -60,7 +59,6 @@ describe('error queries', () => { sortDirection: 'asc', sortField: 'lastSeen', serviceName: 'serviceName', - transactionType: 'request', setup, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/routes/errors/route.ts b/x-pack/plugins/apm/server/routes/errors/route.ts index f1d1c79b24f18..52a72a70b7d67 100644 --- a/x-pack/plugins/apm/server/routes/errors/route.ts +++ b/x-pack/plugins/apm/server/routes/errors/route.ts @@ -35,9 +35,6 @@ const errorsMainStatisticsRoute = createApmServerRoute({ environmentRt, kueryRt, rangeRt, - t.type({ - transactionType: t.string, - }), ]), }), options: { tags: ['access:apm'] }, @@ -57,21 +54,13 @@ const errorsMainStatisticsRoute = createApmServerRoute({ const { params } = resources; const setup = await setupRequest(resources); const { serviceName } = params.path; - const { - environment, - transactionType, - kuery, - sortField, - sortDirection, - start, - end, - } = params.query; + const { environment, kuery, sortField, sortDirection, start, end } = + params.query; const errorGroups = await getErrorGroupMainStatistics({ environment, kuery, serviceName, - transactionType, sortField, sortDirection, setup, @@ -97,7 +86,6 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({ comparisonRangeRt, t.type({ numBuckets: toNumberRt, - transactionType: t.string, groupIds: jsonRt.pipe(t.array(t.string)), }), ]), @@ -127,7 +115,6 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({ environment, kuery, numBuckets, - transactionType, groupIds, comparisonStart, comparisonEnd, @@ -142,7 +129,6 @@ const errorsDetailedStatisticsRoute = createApmServerRoute({ serviceName, setup, numBuckets, - transactionType, groupIds, comparisonStart, comparisonEnd, diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts index 4820eda1870a0..7ec0380429cb9 100644 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts @@ -38,7 +38,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: new Date(start).toISOString(), end: new Date(end).toISOString(), environment: 'ENVIRONMENT_ALL', - transactionType: 'request', kuery: '', ...overrides?.query, }, diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts b/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts index 64e9ce248b455..77b8faf781eb9 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.spec.ts @@ -44,7 +44,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) { // this doubles as a smoke test for the _inspect query parameter req: { - url: `/internal/apm/services/foo/errors/groups/main_statistics?start=${start}&end=${end}&_inspect=true&environment=ENVIRONMENT_ALL&transactionType=bar&kuery=`, + url: `/internal/apm/services/foo/errors/groups/main_statistics?start=${start}&end=${end}&_inspect=true&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts index 0d5d61ee57297..9d09e7046519b 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts @@ -43,7 +43,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: new Date(start).toISOString(), end: new Date(end).toISOString(), numBuckets: 20, - transactionType: 'request', groupIds: JSON.stringify(['foo']), environment: 'ENVIRONMENT_ALL', kuery: '', diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts index d81c3106e4e2e..43df9a5c4ec5e 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts @@ -38,7 +38,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { query: { start: new Date(start).toISOString(), end: new Date(end).toISOString(), - transactionType: 'request', environment: 'ENVIRONMENT_ALL', kuery: '', ...overrides?.query, diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts index ae0f552819920..914f6962d9bce 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts @@ -27,7 +27,6 @@ export async function getErrorGroupIds({ query: { start: new Date(start).toISOString(), end: new Date(end).toISOString(), - transactionType: 'request', environment: 'ENVIRONMENT_ALL', kuery: '', }, From 8b5293bd6a7f79cca36b05506b9d793a5b426c5b Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 10 Feb 2022 03:40:01 -0500 Subject: [PATCH 108/161] [Response Ops][Cases] Fixing Metrics User Actions access (#125024) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cases/server/authorization/index.ts | 5 +- .../tests/common/metrics/get_case_metrics.ts | 118 +++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 8347a456f0811..bf37e3530a710 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -58,6 +58,7 @@ const EVENT_TYPES: Record = { const DELETE_COMMENT_OPERATION = 'deleteComment'; const ACCESS_COMMENT_OPERATION = 'getComment'; const ACCESS_CASE_OPERATION = 'getCase'; +const ACCESS_USER_ACTION_OPERATION = 'getUserActions'; /** * Database constant for ECS category for use for audit logging. @@ -293,7 +294,7 @@ export const Operations: Record { const supertest = getService('supertest'); const es = getService('es'); const kibanaServer = getService('kibanaServer'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('case metrics', () => { describe('closed case from kbn archive', () => { @@ -95,6 +108,109 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + + describe('rbac', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should retrieve the metrics without getting an authorization error', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const metrics = await getCaseMetrics({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + features: [ + 'lifespan', + 'alerts.hosts', + 'alerts.users', + 'alerts.count', + 'connectors', + 'actions.isolateHost', + ], + auth: { user, space: 'space1' }, + }); + + expect(metrics.alerts).to.eql({ + count: 0, + hosts: { total: 0, values: [] }, + users: { total: 0, values: [] }, + }); + expect(metrics.connectors).to.eql({ + total: 0, + }); + expect(metrics.actions).to.eql({ + isolateHost: { isolate: { total: 0 }, unisolate: { total: 0 } }, + }); + expect(metrics.lifespan).to.not.eql(undefined); + } + }); + + it('should receive a 403 when attempting to retrieve the metrics when the user does not have access to the owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getCaseMetrics({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + features: [ + 'lifespan', + 'alerts.hosts', + 'alerts.users', + 'alerts.count', + 'connectors', + 'actions.isolateHost', + ], + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should receive a 403 when attempting to retrieve the metrics when the user does not have permissions in the space', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await getCaseMetrics({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + features: [ + 'lifespan', + 'alerts.hosts', + 'alerts.users', + 'alerts.count', + 'connectors', + 'actions.isolateHost', + ], + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); }); }; From 227e4d1c6390e216c19fbd3d4845a6eecf06b0af Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 10 Feb 2022 09:48:22 +0100 Subject: [PATCH 109/161] make locator async (#125112) --- src/plugins/visualizations/common/locator.ts | 62 ++--------------- .../visualizations/common/locator_location.ts | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+), 57 deletions(-) create mode 100644 src/plugins/visualizations/common/locator_location.ts diff --git a/src/plugins/visualizations/common/locator.ts b/src/plugins/visualizations/common/locator.ts index a1d15ee5188d3..8e562eedb765a 100644 --- a/src/plugins/visualizations/common/locator.ts +++ b/src/plugins/visualizations/common/locator.ts @@ -6,21 +6,12 @@ * Side Public License, v 1. */ -import type { SerializableRecord, Serializable } from '@kbn/utility-types'; -import { omitBy } from 'lodash'; -import type { ParsedQuery } from 'query-string'; -import { stringify } from 'query-string'; -import rison from 'rison-node'; -import { Filter, isFilterPinned } from '@kbn/es-query'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { Filter } from '@kbn/es-query'; import type { Query, RefreshInterval, TimeRange } from 'src/plugins/data/common'; import type { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; -import { url } from '../../kibana_utils/common'; -import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants'; import type { SavedVisState } from './types'; -const removeEmptyKeys = (o: Record): Record => - omitBy(o, (v) => v == null); - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type VisualizeLocatorParams = { /** @@ -83,51 +74,8 @@ export const VISUALIZE_APP_LOCATOR = 'VISUALIZE_APP_LOCATOR'; export class VisualizeLocatorDefinition implements LocatorDefinition { id = VISUALIZE_APP_LOCATOR; - public async getLocation({ - visId, - timeRange, - filters, - refreshInterval, - linked, - uiState, - query, - vis, - savedSearchId, - indexPattern, - }: VisualizeLocatorParams) { - let path = visId - ? `#${VisualizeConstants.EDIT_PATH}/${visId}` - : `#${VisualizeConstants.CREATE_PATH}`; - - const urlState: ParsedQuery = { - [GLOBAL_STATE_STORAGE_KEY]: rison.encode( - removeEmptyKeys({ - time: timeRange, - filters: filters?.filter((f) => isFilterPinned(f)), - refreshInterval, - }) - ), - [STATE_STORAGE_KEY]: rison.encode( - removeEmptyKeys({ - linked, - filters: filters?.filter((f) => !isFilterPinned(f)), - uiState, - query, - vis, - }) - ), - }; - - path += `?${stringify(url.encodeQuery(urlState), { encode: false, sort: false })}`; - - const otherParams = stringify({ type: vis?.type, savedSearchId, indexPattern }); - - if (otherParams) path += `&${otherParams}`; - - return { - app: VisualizeConstants.APP_ID, - path, - state: {}, - }; + public async getLocation(params: VisualizeLocatorParams) { + const { getLocation } = await import('./locator_location'); + return getLocation(params); } } diff --git a/src/plugins/visualizations/common/locator_location.ts b/src/plugins/visualizations/common/locator_location.ts new file mode 100644 index 0000000000000..1e6ee3d362d68 --- /dev/null +++ b/src/plugins/visualizations/common/locator_location.ts @@ -0,0 +1,68 @@ +/* + * 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 { Serializable } from '@kbn/utility-types'; +import { omitBy } from 'lodash'; +import type { ParsedQuery } from 'query-string'; +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import { isFilterPinned } from '@kbn/es-query'; +import { url } from '../../kibana_utils/common'; +import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants'; +import type { VisualizeLocatorParams } from './locator'; + +const removeEmptyKeys = (o: Record): Record => + omitBy(o, (v) => v == null); + +export async function getLocation({ + visId, + timeRange, + filters, + refreshInterval, + linked, + uiState, + query, + vis, + savedSearchId, + indexPattern, +}: VisualizeLocatorParams) { + let path = visId + ? `#${VisualizeConstants.EDIT_PATH}/${visId}` + : `#${VisualizeConstants.CREATE_PATH}`; + + const urlState: ParsedQuery = { + [GLOBAL_STATE_STORAGE_KEY]: rison.encode( + removeEmptyKeys({ + time: timeRange, + filters: filters?.filter((f) => isFilterPinned(f)), + refreshInterval, + }) + ), + [STATE_STORAGE_KEY]: rison.encode( + removeEmptyKeys({ + linked, + filters: filters?.filter((f) => !isFilterPinned(f)), + uiState, + query, + vis, + }) + ), + }; + + path += `?${stringify(url.encodeQuery(urlState), { encode: false, sort: false })}`; + + const otherParams = stringify({ type: vis?.type, savedSearchId, indexPattern }); + + if (otherParams) path += `&${otherParams}`; + + return { + app: VisualizeConstants.APP_ID, + path, + state: {}, + }; +} From 157f8e1bbb833bda906a155d1ba12922d2c12274 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 10 Feb 2022 09:49:37 +0100 Subject: [PATCH 110/161] remove legacy import (#125119) --- .../vis_types/vega/public/vega_view/vega_base_view.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index a87c8318e319c..23856950e3405 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -15,8 +15,8 @@ import { version as vegaLiteVersion } from 'vega-lite'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { buildQueryFilter, compareFilters } from '@kbn/es-query'; import { TooltipHandler } from './vega_tooltip'; -import { esFilters } from '../../../../data/public'; import { getEnableExternalUrls, getData } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; @@ -344,7 +344,7 @@ export class VegaBaseView { */ async addFilterHandler(query, index, alias) { const indexId = await this.findIndex(index); - const filter = esFilters.buildQueryFilter(query, indexId, alias); + const filter = buildQueryFilter(query, indexId, alias); this._fireEvent({ name: 'applyFilter', data: { filters: [filter] } }); } @@ -355,12 +355,10 @@ export class VegaBaseView { */ async removeFilterHandler(query, index) { const indexId = await this.findIndex(index); - const filterToRemove = esFilters.buildQueryFilter(query, indexId); + const filterToRemove = buildQueryFilter(query, indexId); const currentFilters = this._filterManager.getFilters(); - const existingFilter = currentFilters.find((filter) => - esFilters.compareFilters(filter, filterToRemove) - ); + const existingFilter = currentFilters.find((filter) => compareFilters(filter, filterToRemove)); if (!existingFilter) return; From e93a651c1d7f7c0923a01ed41be87d6daf5abd4e Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 10 Feb 2022 09:54:10 +0100 Subject: [PATCH 111/161] [Uptime monitor management] Add full screen/copy button ability in browser inline script editing (#124500) --- .../__snapshots__/code_editor.test.tsx.snap | 76 +++---- .../public/code_editor/code_editor.tsx | 204 +++++++++++++++--- .../public/code_editor/editor.scss | 17 ++ .../components/fleet_package/code_editor.tsx | 2 + 4 files changed, 228 insertions(+), 71 deletions(-) diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index b05abbcece0b9..9a4511f8b03f5 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -126,6 +126,7 @@ exports[` is rendered 1`] = ` >
is rendered 1`] = ` /> - + -
-
-