From b066e9714d323b98b2f107b2dc64ff5df37f3cbc Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 6 Apr 2021 10:31:18 -0500 Subject: [PATCH 01/65] Convert components to use React.lazy for bundling (#96185) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../actions/copy_to_dashboard_modal.tsx | 4 +- .../public/services/presentation_util.ts | 6 +- .../public/components/dashboard_picker.tsx | 4 + .../public/components/index.tsx | 32 ++++++++ .../saved_object_save_modal_dashboard.tsx | 29 ++----- ...d_object_save_modal_dashboard_selector.tsx | 2 +- .../public/components/types.ts | 24 ++++++ src/plugins/presentation_util/public/index.ts | 8 +- .../application/utils/get_top_nav_config.tsx | 82 +++++++++++-------- ...ed_object_save_modal_dashboard_wrapper.tsx | 5 +- .../public/routes/map_page/top_nav_config.tsx | 19 ++++- 11 files changed, 146 insertions(+), 69 deletions(-) create mode 100644 src/plugins/presentation_util/public/components/index.tsx create mode 100644 src/plugins/presentation_util/public/components/types.ts diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx index 49b12d46dc9a2..cda2f76930627 100644 --- a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx @@ -23,7 +23,7 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { DashboardCopyToCapabilities } from './copy_to_dashboard_action'; -import { DashboardPicker } from '../../services/presentation_util'; +import { LazyDashboardPicker, withSuspense } from '../../services/presentation_util'; import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable'; import { createDashboardEditUrl, DashboardConstants } from '../..'; @@ -37,6 +37,8 @@ interface CopyToDashboardModalProps { closeModal: () => void; } +const DashboardPicker = withSuspense(LazyDashboardPicker); + export function CopyToDashboardModal({ PresentationUtilContext, stateTransfer, diff --git a/src/plugins/dashboard/public/services/presentation_util.ts b/src/plugins/dashboard/public/services/presentation_util.ts index 017b455966f13..d3e6c1ebe9eec 100644 --- a/src/plugins/dashboard/public/services/presentation_util.ts +++ b/src/plugins/dashboard/public/services/presentation_util.ts @@ -6,4 +6,8 @@ * Side Public License, v 1. */ -export { PresentationUtilPluginStart, DashboardPicker } from '../../../presentation_util/public'; +export { + PresentationUtilPluginStart, + LazyDashboardPicker, + withSuspense, +} from '../../../presentation_util/public'; diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index d32afca5cedeb..47ba570765028 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -99,3 +99,7 @@ export function DashboardPicker(props: DashboardPickerProps) { /> ); } + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default DashboardPicker; diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx new file mode 100644 index 0000000000000..1aff029bae846 --- /dev/null +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Suspense, ComponentType, ReactElement } from 'react'; +import { EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui'; + +/** + * A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors. + * @param Component A component deferred by `React.lazy` + * @param fallback A fallback component to render while things load; default is `EuiLoadingSpinner` + */ +export const withSuspense =

( + Component: ComponentType

, + fallback: ReactElement | null = +) => (props: P) => ( + + + + + +); + +export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); + +export const LazySavedObjectSaveModalDashboard = React.lazy( + () => import('./saved_object_save_modal_dashboard') +); diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 4491be04b1a42..6c36cf8b8e3a7 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -10,32 +10,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - OnSaveProps, - SaveModalState, - SavedObjectSaveModal, -} from '../../../../plugins/saved_objects/public'; +import { OnSaveProps, SavedObjectSaveModal } from '../../../../plugins/saved_objects/public'; -import './saved_object_save_modal_dashboard.scss'; import { pluginServices } from '../services'; +import { SaveModalDashboardProps } from './types'; import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; -interface SaveModalDocumentInfo { - id?: string; - title: string; - description?: string; -} - -export interface SaveModalDashboardProps { - documentInfo: SaveModalDocumentInfo; - canSaveByReference: boolean; - objectType: string; - onClose: () => void; - onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; - tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); -} +import './saved_object_save_modal_dashboard.scss'; -export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { +function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); @@ -136,3 +119,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { /> ); } + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default SavedObjectSaveModalDashboard; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 78a1569c02ead..53aaecb070c7a 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -22,7 +22,7 @@ import { EuiCheckbox, } from '@elastic/eui'; -import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; +import DashboardPicker, { DashboardPickerProps } from './dashboard_picker'; import './saved_object_save_modal_dashboard.scss'; diff --git a/src/plugins/presentation_util/public/components/types.ts b/src/plugins/presentation_util/public/components/types.ts new file mode 100644 index 0000000000000..7c5c50982f49e --- /dev/null +++ b/src/plugins/presentation_util/public/components/types.ts @@ -0,0 +1,24 @@ +/* + * 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 { OnSaveProps, SaveModalState } from '../../../../plugins/saved_objects/public'; + +interface SaveModalDocumentInfo { + id?: string; + title: string; + description?: string; +} + +export interface SaveModalDashboardProps { + documentInfo: SaveModalDocumentInfo; + canSaveByReference: boolean; + objectType: string; + onClose: () => void; + onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; + tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); +} diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index f13807032db3e..457f167dd8228 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -8,14 +8,12 @@ import { PresentationUtilPlugin } from './plugin'; -export { - SavedObjectSaveModalDashboard, - SaveModalDashboardProps, -} from './components/saved_object_save_modal_dashboard'; +export { LazyDashboardPicker, LazySavedObjectSaveModalDashboard, withSuspense } from './components'; -export { DashboardPicker } from './components/dashboard_picker'; +export { SaveModalDashboardProps } from './components/types'; export function plugin() { return new PresentationUtilPlugin(); } + export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index e696bcb5dbe4d..b7c7d63cef98f 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -18,7 +18,10 @@ import { SavedObjectSaveOpts, OnSaveProps, } from '../../../../saved_objects/public'; -import { SavedObjectSaveModalDashboard } from '../../../../presentation_util/public'; +import { + LazySavedObjectSaveModalDashboard, + withSuspense, +} from '../../../../presentation_util/public'; import { unhashUrl } from '../../../../kibana_utils/public'; import { @@ -52,6 +55,8 @@ interface TopNavConfigParams { embeddableId?: string; } +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); + export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => { if (!anonymousUserCapabilities.visualize) return false; @@ -420,40 +425,47 @@ export const getTopNavConfig = ( const useByRefFlow = !!originatingApp || !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; - const saveModal = useByRefFlow ? ( - {}} - originatingApp={originatingApp} - returnToOriginSwitchLabel={ - originatingApp && embeddableId - ? i18n.translate('visualize.topNavMenu.updatePanel', { - defaultMessage: 'Update panel on {originatingAppName}', - values: { - originatingAppName: stateTransfer.getAppNameFromId(originatingApp), - }, - }) - : undefined - } - /> - ) : ( - {}} - /> - ); + let saveModal; + + if (useByRefFlow) { + saveModal = ( + {}} + originatingApp={originatingApp} + returnToOriginSwitchLabel={ + originatingApp && embeddableId + ? i18n.translate('visualize.topNavMenu.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { + originatingAppName: stateTransfer.getAppNameFromId(originatingApp), + }, + }) + : undefined + } + /> + ); + } else { + saveModal = ( + {}} + /> + ); + } + showSaveModal( saveModal, I18nContext, diff --git a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx index ee5422fcb90a6..03e2b4d15d0cb 100644 --- a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx +++ b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx @@ -9,7 +9,8 @@ import React, { FC, useState, useMemo, useCallback } from 'react'; import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public'; import { SaveModalDashboardProps, - SavedObjectSaveModalDashboard, + LazySavedObjectSaveModalDashboard, + withSuspense, } from '../../../../../src/plugins/presentation_util/public'; import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; @@ -29,6 +30,8 @@ export type TagEnhancedSavedObjectSaveModalDashboardProps = Omit< onSave: (props: DashboardSaveProps) => void; }; +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); + export const TagEnhancedSavedObjectSaveModalDashboard: FC = ({ initialTags, onSave, diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 7e0aa59756876..7ac8c3070eb9d 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -28,7 +28,12 @@ import { import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { SavedMap } from './saved_map'; import { getMapEmbeddableDisplayName } from '../../../common/i18n_getters'; -import { SavedObjectSaveModalDashboard } from '../../../../../../src/plugins/presentation_util/public'; +import { + LazySavedObjectSaveModalDashboard, + withSuspense, +} from '../../../../../../src/plugins/presentation_util/public'; + +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); export function getTopNavConfig({ savedMap, @@ -192,21 +197,27 @@ export function getTopNavConfig({ }), }; const PresentationUtilContext = getPresentationUtilContext(); - const saveModal = - savedMap.getOriginatingApp() || !getIsAllowByValueEmbeddables() ? ( + + let saveModal; + + if (savedMap.getOriginatingApp() || !getIsAllowByValueEmbeddables()) { + saveModal = ( - ) : ( + ); + } else { + saveModal = ( ); + } showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext); }, From 70b3b2bbc160699c3d0b9599fc90312bf8819665 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 6 Apr 2021 10:36:24 -0500 Subject: [PATCH 02/65] Index pattern field editor - copy improvements (#96269) * copy update * remove stray colon from copy --- .../public/components/delete_field_modal.tsx | 4 ++-- .../components/field_editor_flyout_content.tsx | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx index 73a4837d6e0cc..69092b2bc0922 100644 --- a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx @@ -64,14 +64,14 @@ const geti18nTexts = (fieldsToDelete?: string[]) => { typeConfirm: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.typeConfirm', { - defaultMessage: "Type 'REMOVE' to confirm", + defaultMessage: 'Enter REMOVE to confirm', } ), warningRemovingFields: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningRemovingFields', { defaultMessage: - 'Warning: Removing fields may break searches or visualizations that rely on this field.', + 'Removing fields can break searches and visualizations that rely on this field.', } ), }; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index 486df1a7707af..e0ca654c956c6 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -53,20 +53,29 @@ const geti18nTexts = (field?: Field) => { confirmButtonText: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel', { - defaultMessage: 'Save', + defaultMessage: 'Save changes', } ), warningChangingFields: i18n.translate( 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields', { defaultMessage: - 'Warning: Changing name or type may break searches or visualizations that rely on this field.', + 'Changing name or type can break searches and visualizations that rely on this field.', } ), typeConfirm: i18n.translate( 'indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm', { - defaultMessage: "Type 'CHANGE' to continue:", + defaultMessage: 'Enter CHANGE to continue', + } + ), + titleConfirmChanges: i18n.translate( + 'indexPatternFieldEditor.saveRuntimeField.confirmModal.title', + { + defaultMessage: `Save changes to '{name}'`, + values: { + name: field?.name, + }, } ), }; @@ -211,7 +220,7 @@ const FieldEditorFlyoutContentComponent = ({ const modal = isModalVisible ? ( Date: Tue, 6 Apr 2021 08:38:12 -0700 Subject: [PATCH 03/65] [Metrics UI] Observability Overview Host Summary (#90879) * [Metrics UI] Observability Overview Host Summary * Adding UI elements * Adding logos * Changing the size of the request * Change to new ECS fields for network traffic * Adding logos to HostLink component * Round seconds * fixing data handler test * Fixing test for metrics_overview_fetchers * Adding types for SVG to observability * Adding i18n support to table labels * removing unused translations * move back to host.network.(in,out).bytes * Adding changes to source from #95334 * Fixing source type * Removing unintentional change. * Maybe, fixing types * removing svg typings Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../infra/common/http_api/overview_api.ts | 47 + .../metrics_overview_fetchers.test.ts.snap | 16 +- .../common/components/alert_preview.tsx | 2 +- .../public/metrics_overview_fetchers.test.ts | 4 +- .../infra/public/metrics_overview_fetchers.ts | 61 +- .../plugins/infra/public/test_utils/index.ts | 10 +- .../infra/server/routes/overview/index.ts | 85 +- ...nvert_es_response_to_top_nodes_response.ts | 43 + .../overview/lib/create_top_nodes_query.ts | 127 ++ .../lib/get_matadata_from_node_bucket.ts | 26 + .../routes/overview/lib/get_top_nodes.ts | 26 + .../infra/server/routes/overview/lib/types.ts | 48 + .../app/section/metrics/host_link.tsx | 71 + .../components/app/section/metrics/index.tsx | 237 ++- .../metrics/lib/format_duration.test.ts | 27 + .../section/metrics/lib/format_duration.ts | 23 + .../app/section/metrics/logos/aix.svg | 83 + .../app/section/metrics/logos/android.svg | 19 + .../app/section/metrics/logos/darwin.svg | 1 + .../app/section/metrics/logos/dragonfly.svg | 1 + .../app/section/metrics/logos/freebsd.svg | 8 + .../app/section/metrics/logos/illumos.svg | 1 + .../app/section/metrics/logos/linux.svg | 1532 +++++++++++++++++ .../app/section/metrics/logos/netbsd.svg | 57 + .../app/section/metrics/logos/solaris.svg | 54 + .../section/metrics/metric_with_sparkline.tsx | 61 + .../observability/public/data_handler.test.ts | 105 +- .../pages/overview/mock/metrics.mock.ts | 18 +- .../typings/fetch_overview_data/index.ts | 32 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 31 files changed, 2512 insertions(+), 319 deletions(-) create mode 100644 x-pack/plugins/infra/server/routes/overview/lib/convert_es_response_to_top_nodes_response.ts create mode 100644 x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts create mode 100644 x-pack/plugins/infra/server/routes/overview/lib/get_matadata_from_node_bucket.ts create mode 100644 x-pack/plugins/infra/server/routes/overview/lib/get_top_nodes.ts create mode 100644 x-pack/plugins/infra/server/routes/overview/lib/types.ts create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/host_link.tsx create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg create mode 100644 x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx diff --git a/x-pack/plugins/infra/common/http_api/overview_api.ts b/x-pack/plugins/infra/common/http_api/overview_api.ts index e609256ed837a..551681f6fc139 100644 --- a/x-pack/plugins/infra/common/http_api/overview_api.ts +++ b/x-pack/plugins/infra/common/http_api/overview_api.ts @@ -34,6 +34,53 @@ export const OverviewRequestRT = rt.type({ }), }); +export const TopNodesRequestRT = rt.intersection([ + rt.type({ + sourceId: rt.string, + size: rt.number, + bucketSize: rt.string, + timerange: rt.type({ + from: rt.number, + to: rt.number, + }), + }), + rt.partial({ sort: rt.string, sortDirection: rt.string }), +]); + +export type TopNodesRequest = rt.TypeOf; + +const numberOrNullRT = rt.union([rt.number, rt.null]); +const stringOrNullRT = rt.union([rt.string, rt.null]); + +export const TopNodesTimeseriesRowRT = rt.type({ + timestamp: rt.number, + cpu: numberOrNullRT, + iowait: numberOrNullRT, + load: numberOrNullRT, + rx: numberOrNullRT, + tx: numberOrNullRT, +}); + +export const TopNodesSeriesRT = rt.type({ + id: rt.string, + name: stringOrNullRT, + platform: stringOrNullRT, + provider: stringOrNullRT, + cpu: numberOrNullRT, + iowait: numberOrNullRT, + load: numberOrNullRT, + uptime: numberOrNullRT, + rx: numberOrNullRT, + tx: numberOrNullRT, + timeseries: rt.array(TopNodesTimeseriesRowRT), +}); + +export const TopNodesResponseRT = rt.type({ + series: rt.array(TopNodesSeriesRT), +}); + +export type TopNodesResponse = rt.TypeOf; + export type OverviewMetricType = rt.TypeOf; export type OverviewMetric = rt.TypeOf; export type OverviewResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap index db931905b25db..2904f37141325 100644 --- a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap +++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap @@ -3,19 +3,7 @@ exports[`Metrics UI Observability Homepage Functions createMetricsFetchData() should just work 1`] = ` Object { "appLink": "/app/metrics/inventory?waffleTime=(currentTime:1593696311629,isAutoReloading:!f)", - "stats": Object { - "cpu": Object { - "type": "percent", - "value": 0.10691011235955057, - }, - "hosts": Object { - "type": "number", - "value": 1, - }, - "memory": Object { - "type": "percent", - "value": 0.5389775280898876, - }, - }, + "series": Array [], + "sort": [Function], } `; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index 410ad15b356ea..7d7b0004fa068 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -174,7 +174,7 @@ export const AlertPreview: React.FC = (props) => { title={ } > diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts index 00ff863d205c1..e6ffbc30fe24d 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts @@ -69,9 +69,11 @@ describe('Metrics UI Observability Homepage Functions', () => { bucketSize, }); expect(core.http.post).toHaveBeenCalledTimes(1); - expect(core.http.post).toHaveBeenCalledWith('/api/metrics/overview', { + expect(core.http.post).toHaveBeenCalledWith('/api/metrics/overview/top', { body: JSON.stringify({ sourceId: 'default', + bucketSize, + size: 5, timerange: { from: startTime.valueOf(), to: endTime.valueOf(), diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts index bcc2eec504209..4f5b73d685591 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts @@ -5,8 +5,16 @@ * 2.0. */ +/* + * Copyright + * 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 { FetchDataParams, MetricsFetchDataResponse } from '../../observability/public'; -import { OverviewRequest, OverviewResponse } from '../common/http_api/overview_api'; +import { TopNodesRequest, TopNodesResponse } from '../common/http_api/overview_api'; import { InfraClientCoreSetup } from './types'; export const createMetricsHasData = ( @@ -20,38 +28,33 @@ export const createMetricsHasData = ( export const createMetricsFetchData = ( getStartServices: InfraClientCoreSetup['getStartServices'] -) => async ({ absoluteTime }: FetchDataParams): Promise => { +) => async ({ absoluteTime, bucketSize }: FetchDataParams): Promise => { const [coreServices] = await getStartServices(); const { http } = coreServices; - const { start, end } = absoluteTime; - - const overviewRequest: OverviewRequest = { - sourceId: 'default', - timerange: { - from: start, - to: end, - }, - }; + const makeRequest = async (overrides: Partial = {}) => { + const { start, end } = absoluteTime; - const results = await http.post('/api/metrics/overview', { - body: JSON.stringify(overviewRequest), - }); - return { - appLink: `/app/metrics/inventory?waffleTime=(currentTime:${end},isAutoReloading:!f)`, - stats: { - hosts: { - type: 'number', - value: results.stats.hosts.value, + const overviewRequest: TopNodesRequest = { + sourceId: 'default', + bucketSize, + size: 5, + timerange: { + from: start, + to: end, }, - cpu: { - type: 'percent', - value: results.stats.cpu.value, - }, - memory: { - type: 'percent', - value: results.stats.memory.value, - }, - }, + ...overrides, + }; + const results = await http.post('/api/metrics/overview/top', { + body: JSON.stringify(overviewRequest), + }); + return { + appLink: `/app/metrics/inventory?waffleTime=(currentTime:${end},isAutoReloading:!f)`, + series: results.series, + sort: async (by: string, direction: string) => + makeRequest({ sort: by, sortDirection: direction }), + }; }; + + return await makeRequest(); }; diff --git a/x-pack/plugins/infra/public/test_utils/index.ts b/x-pack/plugins/infra/public/test_utils/index.ts index 22e7d647e0965..2c88bfecf0c5d 100644 --- a/x-pack/plugins/infra/public/test_utils/index.ts +++ b/x-pack/plugins/infra/public/test_utils/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { TopNodesResponse } from '../../common/http_api/overview_api'; + export const FAKE_SNAPSHOT_RESPONSE = { nodes: [ { @@ -309,10 +311,6 @@ export const FAKE_SNAPSHOT_RESPONSE = { interval: '300s', }; -export const FAKE_OVERVIEW_RESPONSE = { - stats: { - hosts: { type: 'number', value: 1 }, - cpu: { type: 'percent', value: 0.10691011235955057 }, - memory: { type: 'percent', value: 0.5389775280898876 }, - }, +export const FAKE_OVERVIEW_RESPONSE: TopNodesResponse = { + series: [], }; diff --git a/x-pack/plugins/infra/server/routes/overview/index.ts b/x-pack/plugins/infra/server/routes/overview/index.ts index fe988abcc2883..fa0e0f4d09aff 100644 --- a/x-pack/plugins/infra/server/routes/overview/index.ts +++ b/x-pack/plugins/infra/server/routes/overview/index.ts @@ -4,25 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import Boom from '@hapi/boom'; -import { schema } from '@kbn/config-schema'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { findInventoryFields } from '../../../common/inventory_models'; -import { throwErrors } from '../../../common/runtime_types'; -import { OverviewRequestRT } from '../../../common/http_api/overview_api'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { TopNodesRequestRT } from '../../../common/http_api/overview_api'; import { InfraBackendLibs } from '../../lib/infra_types'; import { createSearchClient } from '../../lib/create_search_client'; - -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - -interface OverviewESAggResponse { - memory: { value: number }; - hosts: { value: number }; - cpu: { value: number }; -} +import { queryTopNodes } from './lib/get_top_nodes'; export const initOverviewRoute = (libs: InfraBackendLibs) => { const { framework } = libs; @@ -30,76 +16,23 @@ export const initOverviewRoute = (libs: InfraBackendLibs) => { framework.registerRoute( { method: 'post', - path: '/api/metrics/overview', + path: '/api/metrics/overview/top', validate: { - body: escapeHatch, + body: createValidationFunction(TopNodesRequestRT), }, }, async (requestContext, request, response) => { - const overviewRequest = pipe( - OverviewRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - + const options = request.body; const client = createSearchClient(requestContext, framework); const source = await libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, - overviewRequest.sourceId + options.sourceId ); - const inventoryModelFields = findInventoryFields('host', source.configuration.fields); - - const params = { - index: source.configuration.metricAlias, - body: { - query: { - range: { - [source.configuration.fields.timestamp]: { - gte: overviewRequest.timerange.from, - lte: overviewRequest.timerange.to, - format: 'epoch_millis', - }, - }, - }, - aggs: { - hosts: { - cardinality: { - field: inventoryModelFields.id, - }, - }, - cpu: { - avg: { - field: 'system.cpu.total.norm.pct', - }, - }, - memory: { - avg: { - field: 'system.memory.actual.used.pct', - }, - }, - }, - }, - }; - - const esResponse = await client<{}, OverviewESAggResponse>(params); + const topNResponse = await queryTopNodes(options, client, source); return response.ok({ - body: { - stats: { - hosts: { - type: 'number', - value: esResponse.aggregations?.hosts.value ?? 0, - }, - cpu: { - type: 'percent', - value: esResponse.aggregations?.cpu.value ?? 0, - }, - memory: { - type: 'percent', - value: esResponse.aggregations?.memory.value ?? 0, - }, - }, - }, + body: topNResponse, }); } ); diff --git a/x-pack/plugins/infra/server/routes/overview/lib/convert_es_response_to_top_nodes_response.ts b/x-pack/plugins/infra/server/routes/overview/lib/convert_es_response_to_top_nodes_response.ts new file mode 100644 index 0000000000000..c3b4693372db3 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/overview/lib/convert_es_response_to_top_nodes_response.ts @@ -0,0 +1,43 @@ +/* + * 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 { TopNodesResponse } from '../../../../common/http_api/overview_api'; +import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework'; +import { getMetadataFromNodeBucket } from './get_matadata_from_node_bucket'; +import { ESResponseForTopNodes } from './types'; + +export const convertESResponseToTopNodesResponse = ( + response: InfraDatabaseSearchResponse<{}, ESResponseForTopNodes> +): TopNodesResponse => { + if (!response.aggregations) { + return { series: [] }; + } + return { + series: response.aggregations.nodes.buckets.map((node) => { + return { + id: node.key, + ...getMetadataFromNodeBucket(node), + timeseries: node.timeseries.buckets.map((bucket) => { + return { + timestamp: bucket.key, + cpu: bucket.cpu.value, + iowait: bucket.iowait.value, + load: bucket.load.value, + rx: bucket.rx.value, + tx: bucket.tx.value, + }; + }), + cpu: node.cpu.value, + iowait: node.iowait.value, + load: node.load.value, + uptime: node.uptime.value, + rx: node.rx.value, + tx: node.tx.value, + }; + }), + }; +}; diff --git a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts new file mode 100644 index 0000000000000..a9245a8c8ce75 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts @@ -0,0 +1,127 @@ +/* + * 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 { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; +import { TopNodesRequest } from '../../../../common/http_api/overview_api'; + +export const createTopNodesQuery = ( + options: TopNodesRequest, + source: MetricsSourceConfiguration +) => { + const sortByHost = options.sort && options.sort === 'name'; + const sortField = sortByHost ? '_key' : options.sort ?? 'uptime'; + const sortDirection = options.sortDirection ?? 'asc'; + return { + size: 0, + query: { + bool: { + filter: [ + { + range: { + [source.configuration.fields.timestamp]: { + gte: options.timerange.from, + lte: options.timerange.to, + }, + }, + }, + { + match_phrase: { 'event.module': 'system' }, + }, + ], + }, + }, + aggs: { + nodes: { + terms: { + field: 'host.name', + size: options.size, + order: { [sortField]: sortDirection }, + }, + aggs: { + metadata: { + top_metrics: { + metrics: [ + { field: 'host.os.platform' }, + { field: 'host.name' }, + { field: 'cloud.provider' }, + ], + sort: { '@timestamp': 'desc' }, + size: 1, + }, + }, + uptime: { + max: { + field: 'system.uptime.duration.ms', + }, + }, + cpu: { + avg: { + field: 'system.cpu.total.pct', + }, + }, + iowait: { + avg: { + field: 'system.core.iowait.pct', + }, + }, + load: { + avg: { + field: 'system.load.15', + }, + }, + rx: { + sum: { + field: 'host.network.in.bytes', + }, + }, + tx: { + sum: { + field: 'host.network.out.bytes', + }, + }, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: '1m', + extended_bounds: { + min: options.timerange.from, + max: options.timerange.to, + }, + }, + aggs: { + cpu: { + avg: { + field: 'host.cpu.pct', + }, + }, + iowait: { + avg: { + field: 'system.core.iowait.pct', + }, + }, + load: { + avg: { + field: 'system.load.15', + }, + }, + rx: { + rate: { + field: 'host.network.ingress.bytes', + }, + }, + tx: { + rate: { + field: 'host.network.egress.bytes', + }, + }, + }, + }, + }, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/routes/overview/lib/get_matadata_from_node_bucket.ts b/x-pack/plugins/infra/server/routes/overview/lib/get_matadata_from_node_bucket.ts new file mode 100644 index 0000000000000..2f1a2a4cded8d --- /dev/null +++ b/x-pack/plugins/infra/server/routes/overview/lib/get_matadata_from_node_bucket.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 { NodeBucket } from './types'; + +interface Metadata { + name: string | null; + provider: string | null; + platform: string | null; +} + +export const getMetadataFromNodeBucket = (node: NodeBucket): Metadata => { + const metadata = node.metadata.top[0]; + if (!metadata) { + return { name: null, provider: null, platform: null }; + } + return { + name: metadata.metrics['host.name'] || null, + provider: metadata.metrics['cloud.provider'] || null, + platform: metadata.metrics['host.os.platform'] || null, + }; +}; diff --git a/x-pack/plugins/infra/server/routes/overview/lib/get_top_nodes.ts b/x-pack/plugins/infra/server/routes/overview/lib/get_top_nodes.ts new file mode 100644 index 0000000000000..4d46479ac7a54 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/overview/lib/get_top_nodes.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 { TopNodesRequest } from '../../../../common/http_api/overview_api'; +import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; +import { ESSearchClient } from '../../../lib/metrics/types'; +import { convertESResponseToTopNodesResponse } from './convert_es_response_to_top_nodes_response'; +import { createTopNodesQuery } from './create_top_nodes_query'; +import { ESResponseForTopNodes } from './types'; + +export const queryTopNodes = async ( + options: TopNodesRequest, + client: ESSearchClient, + source: MetricsSourceConfiguration +) => { + const params = { + index: source.configuration.metricAlias, + body: createTopNodesQuery(options, source), + }; + + const response = await client<{}, ESResponseForTopNodes>(params); + return convertESResponseToTopNodesResponse(response); +}; diff --git a/x-pack/plugins/infra/server/routes/overview/lib/types.ts b/x-pack/plugins/infra/server/routes/overview/lib/types.ts new file mode 100644 index 0000000000000..d5342d0757ad6 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/overview/lib/types.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +type NumberOrNull = number | null; + +interface TopMetric { + sort: string[]; + metrics: Record; +} + +interface NodeMetric { + value: NumberOrNull; +} + +interface NodeMetrics { + doc_count: number; + uptime: NodeMetric; + cpu: NodeMetric; + iowait: NodeMetric; + load: NodeMetric; + rx: NodeMetric; + tx: NodeMetric; +} + +interface TimeSeriesMetric extends NodeMetrics { + key_as_string: string; + key: number; +} + +export interface NodeBucket extends NodeMetrics { + key: string; + metadata: { + top: TopMetric[]; + }; + timeseries: { + buckets: TimeSeriesMetric[]; + }; +} + +export interface ESResponseForTopNodes { + nodes: { + buckets: NodeBucket[]; + }; +} diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/host_link.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/host_link.tsx new file mode 100644 index 0000000000000..921cec4222ea0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/host_link.tsx @@ -0,0 +1,71 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { StringOrNull } from '../../../..'; + +import aixLogo from './logos/aix.svg'; +import androidLogo from './logos/android.svg'; +import darwinLogo from './logos/darwin.svg'; +import dragonflyLogo from './logos/dragonfly.svg'; +import freebsdLogo from './logos/freebsd.svg'; +import illumosLogo from './logos/illumos.svg'; +import linuxLogo from './logos/linux.svg'; +import solarisLogo from './logos/solaris.svg'; +import netbsdLogo from './logos/netbsd.svg'; + +interface Props { + name: StringOrNull; + id: StringOrNull; + provider: StringOrNull; + platform: StringOrNull; + timerange: { from: number; to: number }; +} + +export function HostLink({ name, id, provider, platform, timerange }: Props) { + const providerLogo = + provider === 'aws' + ? 'logoAWS' + : provider === 'gcp' + ? 'logoGCP' + : provider === 'azure' + ? 'logoAzure' + : 'compute'; + + const platformLogo = + platform === 'darwin' + ? darwinLogo + : platform === 'windows' + ? 'logoWindows' + : platform === 'linux' + ? linuxLogo + : platform === 'aix' + ? aixLogo + : platform === 'andriod' + ? androidLogo + : platform === 'dragonfly' + ? dragonflyLogo + : platform === 'illumos' + ? illumosLogo + : platform === 'freebsd' + ? freebsdLogo + : platform === 'solaris' + ? solarisLogo + : platform === 'netbsd' + ? netbsdLogo + : 'empty'; + const link = `../../app/metrics/link-to/host-detail/${id}?from=${timerange.from}&to=${timerange.to}`; + return ( + <> + {platformLogo !== null && } +   + {providerLogo !== null && } +   + {name} + + ); +} 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 dfdede6e7b32f..5a642084733c7 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 @@ -5,49 +5,56 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui'; +import { + Criteria, + Direction, + EuiBasicTable, + EuiBasicTableColumn, + EuiTableSortingType, +} from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; -import styled, { ThemeContext } from 'styled-components'; +import React, { useState, useCallback } from 'react'; +import { + MetricsFetchDataResponse, + MetricsFetchDataSeries, + NumberOrNull, + StringOrNull, +} from '../../../..'; import { SectionContainer } from '../'; import { getDataHandler } from '../../../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { useTimeRange } from '../../../../hooks/use_time_range'; -import { StyledStat } from '../../styled_stat'; +import { HostLink } from './host_link'; +import { formatDuration } from './lib/format_duration'; +import { MetricWithSparkline } from './metric_with_sparkline'; + +const SPARK_LINE_COLUMN_WIDTH = '120px'; +const COLOR_ORANGE = 7; +const COLOR_BLUE = 1; +const COLOR_GREEN = 0; +const COLOR_PURPLE = 3; interface Props { bucketSize?: string; } -/** - * EuiProgress doesn't support custom color, when it does this component can be removed. - */ -const StyledProgress = styled.div<{ color?: string }>` - progress { - &.euiProgress--native { - &::-webkit-progress-value { - background-color: ${(props) => props.color}; - } +const percentFormatter = (value: NumberOrNull) => + value === null ? '' : numeral(value).format('0[.0]%'); - &::-moz-progress-bar { - background-color: ${(props) => props.color}; - } - } +const numberFormatter = (value: NumberOrNull) => + value === null ? '' : numeral(value).format('0[.0]'); - &.euiProgress--indeterminate { - &:before { - background-color: ${(props) => props.color}; - } - } - } -`; +const bytesPerSecondFormatter = (value: NumberOrNull) => + value === null ? '' : numeral(value).format('0b') + '/s'; export function MetricsSection({ bucketSize }: Props) { - const theme = useContext(ThemeContext); const { forceUpdate, hasData } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); + const [sortDirection, setSortDirection] = useState('asc'); + const [sortField, setSortField] = useState('uptime'); + const [sortedData, setSortedData] = useState(null); const { data, status } = useFetcher( () => { @@ -64,16 +71,138 @@ export function MetricsSection({ bucketSize }: Props) { [bucketSize, relativeStart, relativeEnd, forceUpdate] ); + const handleTableChange = useCallback( + ({ sort }: Criteria) => { + if (sort) { + const { field, direction } = sort; + setSortField(field); + setSortDirection(direction); + if (data) { + (async () => { + const response = await data.sort(field, direction); + setSortedData(response || null); + })(); + } + } + }, + [data, setSortField, setSortDirection] + ); + if (!hasData.infra_metrics?.hasData) { return null; } const isLoading = status === FETCH_STATUS.LOADING; + const isPending = status === FETCH_STATUS.LOADING; + if (isLoading || isPending) { + return

Loading
; + } + + if (!data) { + return
No Data
; + } + + const columns: Array> = [ + { + field: 'uptime', + name: i18n.translate('xpack.observability.overview.metrics.colunms.uptime', { + defaultMessage: 'Uptime', + }), + sortable: true, + width: '80px', + render: (value: NumberOrNull) => (value == null ? 'N/A' : formatDuration(value / 1000)), + }, + { + field: 'name', + name: i18n.translate('xpack.observability.overview.metrics.colunms.hostname', { + defaultMessage: 'Hostname', + }), + sortable: true, + truncateText: true, + isExpander: true, + render: (value: StringOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'cpu', + name: i18n.translate('xpack.observability.overview.metrics.colunms.cpu', { + defaultMessage: 'CPU %', + }), + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'load', + name: i18n.translate('xpack.observability.overview.metrics.colunms.load15', { + defaultMessage: 'Load 15', + }), + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'rx', + name: 'RX', + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + { + field: 'tx', + name: 'TX', + sortable: true, + width: SPARK_LINE_COLUMN_WIDTH, + render: (value: NumberOrNull, record: MetricsFetchDataSeries) => ( + + ), + }, + ]; + + const sorting: EuiTableSortingType = { + sort: { field: sortField, direction: sortDirection }, + }; - const { appLink, stats } = data || {}; + const viewData = sortedData || data; - const cpuColor = theme.eui.euiColorVis7; - const memoryColor = theme.eui.euiColorVis0; + const { appLink } = data || {}; return ( - - - - - - - - - - - - - - - - - - - - - + ); } diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts new file mode 100644 index 0000000000000..b4b03b2194ef2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts @@ -0,0 +1,27 @@ +/* + * 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 { formatDuration } from './format_duration'; + +describe('formatDuration(seconds)', () => { + it('should work for less then a minute', () => { + expect(formatDuration(56)).toBe('56s'); + }); + + it('should work for less then a hour', () => { + expect(formatDuration(2000)).toBe('33m 20s'); + }); + + it('should work for less then a day', () => { + expect(formatDuration(74566)).toBe('20h 42m'); + }); + + it('should work for more then a day', () => { + expect(formatDuration(86400 * 3 + 3600 * 4)).toBe('3d 4h'); + expect(formatDuration(86400 * 419 + 3600 * 6)).toBe('419d 6h'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts new file mode 100644 index 0000000000000..29fb1dcbd1b52 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const MINUTE = 60; +const HOUR = 3600; +const DAY = 86400; + +export const formatDuration = (seconds: number) => { + if (seconds < MINUTE) { + return `${Math.floor(seconds)}s`; + } + if (seconds < HOUR) { + return `${Math.floor(seconds / MINUTE)}m ${Math.floor(seconds % MINUTE)}s`; + } + if (seconds < DAY) { + return `${Math.floor(seconds / HOUR)}h ${Math.floor((seconds % HOUR) / MINUTE)}m`; + } + return `${Math.floor(seconds / DAY)}d ${Math.floor((seconds % DAY) / HOUR)}h`; +}; diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg new file mode 100644 index 0000000000000..6d26c99bec674 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/aix.svg @@ -0,0 +1,83 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg new file mode 100644 index 0000000000000..f53491803db44 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/android.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg new file mode 100644 index 0000000000000..73630c9ba2630 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/darwin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg new file mode 100644 index 0000000000000..4f026bb4dbac5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/dragonfly.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg new file mode 100644 index 0000000000000..4516c4e302ba4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/freebsd.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg new file mode 100644 index 0000000000000..c9ab6aed30151 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/illumos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg new file mode 100644 index 0000000000000..c0a92e0c0f404 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/linux.svg @@ -0,0 +1,1532 @@ + + + + Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Tux + 20 June 2012 + + + Garrett LeSage + + + + + + Larry Ewing, the creator of the original Tux graphic + + + + + tux + Linux + penguin + logo + + + + + Larry Ewing, Garrett LeSage + + + https://github.com/garrett/Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg new file mode 100644 index 0000000000000..7cc046187eb60 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/netbsd.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg b/x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg new file mode 100644 index 0000000000000..1a211689f86f3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/logos/solaris.svg @@ -0,0 +1,54 @@ + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Open Icon Library + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx new file mode 100644 index 0000000000000..3cb61f85d57f0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/metric_with_sparkline.tsx @@ -0,0 +1,61 @@ +/* + * 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 { Chart, Settings, AreaSeries } from '@elastic/charts'; +import { EuiIcon, EuiTextColor } from '@elastic/eui'; +import React, { useContext } from 'react'; +import { + EUI_CHARTS_THEME_DARK, + EUI_CHARTS_THEME_LIGHT, + EUI_SPARKLINE_THEME_PARTIAL, +} from '@elastic/eui/dist/eui_charts_theme'; +import { ThemeContext } from 'styled-components'; + +import { NumberOrNull } from '../../../..'; + +interface Props { + id: string; + value: NumberOrNull; + timeseries: any[]; + formatter: (value: NumberOrNull) => string; + color: number; +} +export function MetricWithSparkline({ id, formatter, value, timeseries, color }: Props) { + const themeCTX = useContext(ThemeContext); + const isDarkTheme = (themeCTX && themeCTX.darkMode) || false; + const theme = [ + EUI_SPARKLINE_THEME_PARTIAL, + isDarkTheme ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, + ]; + + const colors = theme[1].colors?.vizColors ?? []; + + if (!value) { + return ( + + +  N/A + + ); + } + return ( + <> + + + + +   + {formatter(value)} + + ); +} diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index 868e5be2b6317..bba2083aceb80 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -321,56 +321,18 @@ describe('registerDataHandler', () => { }); describe('Metrics', () => { + const makeRequestResponse = { + title: 'metrics', + appLink: '/metrics', + sort: () => makeRequest(), + series: [], + }; + const makeRequest = async () => { + return makeRequestResponse; + }; registerDataHandler({ appName: 'infra_metrics', - fetchData: async () => { - return { - title: 'metrics', - appLink: '/metrics', - stats: { - hosts: { - label: 'hosts', - type: 'number', - value: 1, - }, - cpu: { - label: 'cpu', - type: 'number', - value: 1, - }, - memory: { - label: 'memory', - type: 'number', - value: 1, - }, - disk: { - label: 'disk', - type: 'number', - value: 1, - }, - inboundTraffic: { - label: 'inboundTraffic', - type: 'number', - value: 1, - }, - outboundTraffic: { - label: 'outboundTraffic', - type: 'number', - value: 1, - }, - }, - series: { - inboundTraffic: { - label: 'inbound Traffic', - coordinates: [{ x: 1 }], - }, - outboundTraffic: { - label: 'outbound Traffic', - coordinates: [{ x: 1 }], - }, - }, - }; - }, + fetchData: makeRequest, hasData: async () => true, }); @@ -383,52 +345,7 @@ describe('registerDataHandler', () => { it('returns data when fetchData is called', async () => { const dataHandler = getDataHandler('infra_metrics'); const response = await dataHandler?.fetchData(params); - expect(response).toEqual({ - title: 'metrics', - appLink: '/metrics', - stats: { - hosts: { - label: 'hosts', - type: 'number', - value: 1, - }, - cpu: { - label: 'cpu', - type: 'number', - value: 1, - }, - memory: { - label: 'memory', - type: 'number', - value: 1, - }, - disk: { - label: 'disk', - type: 'number', - value: 1, - }, - inboundTraffic: { - label: 'inboundTraffic', - type: 'number', - value: 1, - }, - outboundTraffic: { - label: 'outboundTraffic', - type: 'number', - value: 1, - }, - }, - series: { - inboundTraffic: { - label: 'inbound Traffic', - coordinates: [{ x: 1 }], - }, - outboundTraffic: { - label: 'outbound Traffic', - coordinates: [{ x: 1 }], - }, - }, - }); + expect(response).toEqual(makeRequestResponse); }); it('returns true when hasData is called', async () => { diff --git a/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts index 82f804ba1a938..f88b89e75389e 100644 --- a/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts +++ b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts @@ -12,19 +12,13 @@ export const fetchMetricsData: FetchData = () => { }; const response: MetricsFetchDataResponse = { - appLink: '/app/apm', - stats: { - hosts: { value: 11, type: 'number' }, - cpu: { value: 0.8, type: 'percent' }, - memory: { value: 0.362, type: 'percent' }, - }, + appLink: '/app/metrics', + sort: async () => response, + series: [], }; export const emptyResponse: MetricsFetchDataResponse = { - appLink: '/app/apm', - stats: { - hosts: { value: 0, type: 'number' }, - cpu: { value: 0, type: 'percent' }, - memory: { value: 0, type: 'percent' }, - }, + appLink: '/app/metrics', + sort: async () => emptyResponse, + series: [], }; diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index e9960833a1c4f..726c83d0c2256 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -7,7 +7,6 @@ import { ObservabilityApp } from '../../../typings/common'; import { UXMetrics } from '../../components/shared/core_web_vitals'; - export interface Stat { type: 'number' | 'percent' | 'bytesPerSecond'; value: number; @@ -67,12 +66,33 @@ export interface LogsFetchDataResponse extends FetchDataResponse { series: Record; } +export type StringOrNull = string | null; +export type NumberOrNull = number | null; + +export interface MetricsFetchDataSeries { + id: string; + name: StringOrNull; + platform: StringOrNull; + provider: StringOrNull; + cpu: NumberOrNull; + iowait: NumberOrNull; + load: NumberOrNull; + uptime: NumberOrNull; + rx: NumberOrNull; + tx: NumberOrNull; + timeseries: Array<{ + timestamp: number; + cpu: NumberOrNull; + iowait: NumberOrNull; + load: NumberOrNull; + rx: NumberOrNull; + tx: NumberOrNull; + }>; +} + export interface MetricsFetchDataResponse extends FetchDataResponse { - stats: { - hosts: Stat; - cpu: Stat; - memory: Stat; - }; + sort: (by: string, direction: string) => Promise; + series: MetricsFetchDataSeries[]; } export interface UptimeFetchDataResponse extends FetchDataResponse { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fddc69a17ceb5..568a27be7a76f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16652,9 +16652,6 @@ "xpack.observability.overview.logs.subtitle": "毎分のログレート", "xpack.observability.overview.logs.title": "ログ", "xpack.observability.overview.metrics.appLink": "アプリで表示", - "xpack.observability.overview.metrics.cpuUsage": "CPU 使用状況", - "xpack.observability.overview.metrics.hosts": "ホスト", - "xpack.observability.overview.metrics.memoryUsage": "メモリー使用状況", "xpack.observability.overview.metrics.title": "メトリック", "xpack.observability.overview.uptime.appLink": "アプリで表示", "xpack.observability.overview.uptime.chart.down": "ダウン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 89af1d0f19d98..9000d481f0231 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16878,9 +16878,6 @@ "xpack.observability.overview.logs.subtitle": "每分钟日志速率", "xpack.observability.overview.logs.title": "日志", "xpack.observability.overview.metrics.appLink": "在应用中查看", - "xpack.observability.overview.metrics.cpuUsage": "CPU 使用", - "xpack.observability.overview.metrics.hosts": "主机", - "xpack.observability.overview.metrics.memoryUsage": "内存使用", "xpack.observability.overview.metrics.title": "指标", "xpack.observability.overview.uptime.appLink": "在应用中查看", "xpack.observability.overview.uptime.chart.down": "关闭", From 177322fd9efb4f250060cedcb2640553bd789719 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 6 Apr 2021 12:06:14 -0400 Subject: [PATCH 04/65] [jest/securitySolution] Switch to jest-environment-jsdom (#96255) * remove testEnvironment from jest config * cleanup all cases Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/security_solution/jest.config.js | 3 - .../cases/components/all_cases/index.test.tsx | 97 ++++++++++--------- .../cases/components/case_view/index.test.tsx | 54 +++++------ .../view/trusted_apps_page.test.tsx | 30 +++--- 4 files changed, 87 insertions(+), 97 deletions(-) diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/jest.config.js index b4dcedfcceeee..700eaebf6c202 100644 --- a/x-pack/plugins/security_solution/jest.config.js +++ b/x-pack/plugins/security_solution/jest.config.js @@ -9,7 +9,4 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/security_solution'], - - // TODO: migrate to "jest-environment-jsdom" https://github.com/elastic/kibana/issues/95201 - testEnvironment: 'jest-environment-jsdom-thirteen', }; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 0fafdaf81f095..3ac0084e96fb3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -304,13 +304,15 @@ describe('AllCases', () => { ); + + wrapper + .find( + '[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]' + ) + .last() + .simulate('click'); + await waitFor(() => { - wrapper - .find( - '[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]' - ) - .last() - .simulate('click'); expect(wrapper.find('[data-test-subj="action-open"]').first().props().disabled).toEqual(true); expect( wrapper.find('[data-test-subj="action-in-progress"]').first().props().disabled @@ -347,8 +349,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="tableHeaderSortButton"]').first().simulate('click'); expect(setQueryParams).toBeCalledWith({ page: 1, perPage: 5, @@ -364,9 +366,10 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ caseId: firstCase.id, @@ -398,9 +401,11 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ caseId: firstCase.id, @@ -418,9 +423,11 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); - wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ caseId: firstCase.id, @@ -454,17 +461,20 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); + + wrapper + .find( + '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' + ) + .last() + .simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); expect(handleToggleModal).toBeCalled(); - wrapper - .find( - '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' - ) - .last() - .simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([ ...useGetCasesMockState.data.cases.map(({ id, type, title }) => ({ id, type, title })), { @@ -488,8 +498,10 @@ describe('AllCases', () => { ); + + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false); expect(wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').exists()).toEqual( false @@ -529,8 +541,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false); expect( wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().props().disabled @@ -556,9 +568,10 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click'); + await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().simulate('click'); expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.closed); }); }); @@ -578,9 +591,9 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-open-button"]').first().simulate('click'); expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.open); }); }); @@ -597,9 +610,9 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click'); expect(updateBulkStatus).toBeCalledWith( useGetCasesMockState.data.cases, CaseStatuses['in-progress'] @@ -695,8 +708,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); expect(onRowClick).toHaveBeenCalled(); }); }); @@ -716,8 +729,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-add-case"]').first().simulate('click'); expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); }); }); @@ -728,8 +741,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); expect(onRowClick).toHaveBeenCalledWith({ closedAt: null, closedBy: null, @@ -783,8 +796,8 @@ describe('AllCases', () => { ); + wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="cases-table-row-1"]').first().simulate('click'); expect(onRowClick).not.toHaveBeenCalled(); }); }); @@ -795,10 +808,9 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click'); expect(setQueryParams).toBeCalledWith({ sortField: 'closedAt', }); @@ -811,10 +823,9 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click'); expect(setQueryParams).toBeCalledWith({ sortField: 'updatedAt', }); @@ -827,10 +838,9 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click'); expect(setQueryParams).toBeCalledWith({ sortField: 'createdAt', }); @@ -843,9 +853,8 @@ describe('AllCases', () => { ); - + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); await waitFor(() => { - wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').text()).toBe( 'Open (20)' ); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index f28c7791d0110..0daa62bf735e8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -22,7 +22,7 @@ import { TestProviders } from '../../../common/mock'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; -import { act, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; @@ -139,7 +139,6 @@ describe('CaseView ', () => { }; beforeEach(() => { - jest.clearAllMocks(); jest.resetAllMocks(); useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); @@ -241,17 +240,15 @@ describe('CaseView ', () => { ); + wrapper.find('[data-test-subj="case-view-status-dropdown"] button').first().simulate('click'); + wrapper + .find('button[data-test-subj="case-view-status-dropdown-closed"]') + .first() + .simulate('click'); await waitFor(() => { - wrapper.find('[data-test-subj="case-view-status-dropdown"] button').first().simulate('click'); - wrapper.update(); - wrapper - .find('button[data-test-subj="case-view-status-dropdown-closed"]') - .first() - .simulate('click'); - - wrapper.update(); const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateCaseProperty).toHaveBeenCalledTimes(1); expect(updateObject.updateKey).toEqual('status'); expect(updateObject.updateValue).toEqual('closed'); }); @@ -572,36 +569,29 @@ describe('CaseView ', () => { ); - await waitFor(() => { - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); - }); + wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); await waitFor(() => { - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - wrapper.update(); }); - act(() => { - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - }); + wrapper.find(`button[data-test-subj="edit-connectors-submit"]`).first().simulate('click'); await waitFor(() => { wrapper.update(); - }); - - const updateObject = updateCaseProperty.mock.calls[0][0]; - expect(updateObject.updateKey).toEqual('connector'); - expect(updateObject.updateValue).toEqual({ - id: 'resilient-2', - name: 'My Connector 2', - type: ConnectorTypes.resilient, - fields: { - incidentTypes: null, - severityCode: null, - }, + const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateCaseProperty).toHaveBeenCalledTimes(1); + expect(updateObject.updateKey).toEqual('connector'); + expect(updateObject.updateValue).toEqual({ + id: 'resilient-2', + name: 'My Connector 2', + type: ConnectorTypes.resilient, + fields: { + incidentTypes: null, + severityCode: null, + }, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 8a1b1ccfa5173..f9fc5f32aa63a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -862,32 +862,26 @@ describe('When on the Trusted Apps Page', () => { }); describe('and the search is dispatched', () => { - const renderWithListData = async () => { - const result = render(); + let renderResult: ReturnType; + beforeEach(async () => { + mockListApis(coreStart.http); + reactTestingLibrary.act(() => { + history.push('/trusted_apps?filter=test'); + }); + renderResult = render(); await act(async () => { await waitForAction('trustedAppsListResourceStateChanged'); }); - return result; - }; - - beforeEach(() => mockListApis(coreStart.http)); + }); - it('search bar is filled with query params', async () => { - reactTestingLibrary.act(() => { - history.push('/trusted_apps?filter=test'); - }); - const result = await renderWithListData(); - expect(result.getByDisplayValue('test')).not.toBeNull(); + it('search bar is filled with query params', () => { + expect(renderResult.getByDisplayValue('test')).not.toBeNull(); }); it('search action is dispatched', async () => { - reactTestingLibrary.act(() => { - history.push('/trusted_apps?filter=test'); - }); - const result = await renderWithListData(); await act(async () => { - fireEvent.click(result.getByTestId('trustedAppSearchButton')); - await waitForAction('userChangedUrl'); + fireEvent.click(renderResult.getByTestId('trustedAppSearchButton')); + expect(await waitForAction('userChangedUrl')).not.toBeNull(); }); }); }); From 40cbb1fe792f9d9b93e9992063f86ca9fe7882c9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 6 Apr 2021 12:06:36 -0400 Subject: [PATCH 05/65] [ML] Data Frame Analytics: adds functional tests for runtime fields support (#96262) * add basic runtime mapping functional tests to analyics * confirm runtime mapping added correctly. --- .../runtime_mappings/runtime_mappings.tsx | 1 + .../runtime_mappings_editor.tsx | 3 +- .../classification_creation.ts | 22 +++++++ .../outlier_detection_creation.ts | 22 +++++++ .../regression_creation.ts | 22 +++++++ .../ml/data_frame_analytics_creation.ts | 64 +++++++++++++++++++ 6 files changed, 133 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx index d9f1d78c302fd..d21bf67a1f51c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx @@ -38,6 +38,7 @@ const COPY_TO_CLIPBOARD_RUNTIME_MAPPINGS = i18n.translate( const { useXJsonMode } = XJson; const xJsonMode = new XJsonMode(); +export type XJsonModeType = ReturnType; interface Props { actions: CreateAnalyticsFormProps['actions']; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx index 70544cc14ba08..66a96e7316e8a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx @@ -10,6 +10,7 @@ import React, { memo, FC } from 'react'; import { EuiCodeEditor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isRuntimeMappings } from '../../../../../../../common/util/runtime_field_utils'; +import { XJsonModeType } from './runtime_mappings'; interface Props { convertToJson: (data: string) => string; @@ -17,7 +18,7 @@ interface Props { setIsRuntimeMappingsEditorApplyButtonEnabled: React.Dispatch>; advancedEditorRuntimeMappingsLastApplied: string | undefined; advancedRuntimeMappingsConfig: string; - xJsonMode: any; + xJsonMode: XJsonModeType; } export const RuntimeMappingsEditor: FC = memo( diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 615b55c6ce56b..5e6a08751c932 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -36,6 +36,12 @@ export default function ({ getService }: FtrProviderContext) { get destinationIndex(): string { return `user-${this.jobId}`; }, + runtimeFields: { + uppercase_y: { + type: 'keyword', + script: 'emit(params._source.y.toUpperCase())', + }, + }, dependentVariable: 'y', trainingPercent: 20, modelMemory: '60mb', @@ -95,6 +101,22 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); + await ml.testExecution.logTestStep('displays the runtime mappings editor switch'); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingSwitchExists(); + + await ml.testExecution.logTestStep('enables the runtime mappings editor'); + await ml.dataFrameAnalyticsCreation.toggleRuntimeMappingsEditorSwitch(true); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingsEditorContent(['']); + + await ml.testExecution.logTestStep('sets runtime mappings'); + await ml.dataFrameAnalyticsCreation.setRuntimeMappingsEditorContent( + JSON.stringify(testData.runtimeFields) + ); + await ml.dataFrameAnalyticsCreation.applyRuntimeMappings(); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingsEditorContent([ + '{"uppercase_y":{"type":"keyword","script":"emit(params._source.y.toUpperCase())"}}', + ]); + await ml.testExecution.logTestStep('inputs the dependent variable'); await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists(); await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index d72ee4fa0fd24..e73a477d21b1b 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -35,6 +35,12 @@ export default function ({ getService }: FtrProviderContext) { get destinationIndex(): string { return `user-${this.jobId}`; }, + runtimeFields: { + lowercase_central_air: { + type: 'keyword', + script: 'emit(params._source.CentralAir.toLowerCase())', + }, + }, modelMemory: '5mb', createIndexPattern: true, expected: { @@ -106,6 +112,22 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); + await ml.testExecution.logTestStep('displays the runtime mappings editor switch'); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingSwitchExists(); + + await ml.testExecution.logTestStep('enables the runtime mappings editor'); + await ml.dataFrameAnalyticsCreation.toggleRuntimeMappingsEditorSwitch(true); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingsEditorContent(['']); + + await ml.testExecution.logTestStep('sets runtime mappings'); + await ml.dataFrameAnalyticsCreation.setRuntimeMappingsEditorContent( + JSON.stringify(testData.runtimeFields) + ); + await ml.dataFrameAnalyticsCreation.applyRuntimeMappings(); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingsEditorContent([ + '{"lowercase_central_air":{"type":"keyword","script":"emit(params._source.CentralAir.toLowerCase())"}}', + ]); + await ml.testExecution.logTestStep('does not display the dependent variable input'); await ml.dataFrameAnalyticsCreation.assertDependentVariableInputMissing(); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 8e28a9933cda0..540fbc10fa0fc 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -35,6 +35,12 @@ export default function ({ getService }: FtrProviderContext) { get destinationIndex(): string { return `user-${this.jobId}`; }, + runtimeFields: { + uppercase_stab: { + type: 'keyword', + script: 'emit(params._source.stabf.toUpperCase())', + }, + }, dependentVariable: 'stab', trainingPercent: 20, modelMemory: '20mb', @@ -89,6 +95,22 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertJobTypeSelectExists(); await ml.dataFrameAnalyticsCreation.selectJobType(testData.jobType); + await ml.testExecution.logTestStep('displays the runtime mappings editor switch'); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingSwitchExists(); + + await ml.testExecution.logTestStep('enables the runtime mappings editor'); + await ml.dataFrameAnalyticsCreation.toggleRuntimeMappingsEditorSwitch(true); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingsEditorContent(['']); + + await ml.testExecution.logTestStep('sets runtime mappings'); + await ml.dataFrameAnalyticsCreation.setRuntimeMappingsEditorContent( + JSON.stringify(testData.runtimeFields) + ); + await ml.dataFrameAnalyticsCreation.applyRuntimeMappings(); + await ml.dataFrameAnalyticsCreation.assertRuntimeMappingsEditorContent([ + '{"uppercase_stab":{"type":"keyword","script":"emit(params._source.stabf.toUpperCase())"}}', + ]); + await ml.testExecution.logTestStep('inputs the dependent variable'); await ml.dataFrameAnalyticsCreation.assertDependentVariableInputExists(); await ml.dataFrameAnalyticsCreation.selectDependentVariable(testData.dependentVariable); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 23bdc17919a7b..b22748608589e 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -25,6 +25,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const retry = getService('retry'); + const aceEditor = getService('aceEditor'); return { async assertJobTypeSelectExists() { @@ -237,6 +238,69 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( ); }, + async assertRuntimeMappingSwitchExists() { + await testSubjects.existOrFail('mlDataFrameAnalyticsRuntimeMappingsEditorSwitch'); + }, + + async assertRuntimeMappingEditorExists() { + await testSubjects.existOrFail('mlDataFrameAnalyticsAdvancedRuntimeMappingsEditor'); + }, + + async assertRuntimeMappingsEditorSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = await this.getRuntimeMappingsEditorSwitchCheckedState(); + expect(actualCheckState).to.eql( + expectedCheckState, + `Advanced runtime mappings editor switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` + ); + }, + + async getRuntimeMappingsEditorSwitchCheckedState(): Promise { + const subj = 'mlDataFrameAnalyticsRuntimeMappingsEditorSwitch'; + const isSelected = await testSubjects.getAttribute(subj, 'aria-checked'); + return isSelected === 'true'; + }, + + async toggleRuntimeMappingsEditorSwitch(toggle: boolean) { + const subj = 'mlDataFrameAnalyticsRuntimeMappingsEditorSwitch'; + if ((await this.getRuntimeMappingsEditorSwitchCheckedState()) !== toggle) { + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(subj); + await this.assertRuntimeMappingsEditorSwitchCheckState(toggle); + }); + } + }, + + async setRuntimeMappingsEditorContent(input: string) { + await aceEditor.setValue('mlDataFrameAnalyticsAdvancedRuntimeMappingsEditor', input); + }, + + async assertRuntimeMappingsEditorContent(expectedContent: string[]) { + await this.assertRuntimeMappingEditorExists(); + + const runtimeMappingsEditorString = await aceEditor.getValue( + 'mlDataFrameAnalyticsAdvancedRuntimeMappingsEditor' + ); + // Not all lines may be visible in the editor and thus aceEditor may not return all lines. + // This means we might not get back valid JSON so we only test against the first few lines + // and see if the string matches. + const splicedAdvancedEditorValue = runtimeMappingsEditorString.split('\n').splice(0, 3); + expect(splicedAdvancedEditorValue).to.eql( + expectedContent, + `Expected the first editor lines to be '${expectedContent}' (got '${splicedAdvancedEditorValue}')` + ); + }, + + async applyRuntimeMappings() { + const subj = 'mlDataFrameAnalyticsRuntimeMappingsApplyButton'; + await testSubjects.existOrFail(subj); + await testSubjects.clickWhenNotDisabled(subj); + const isEnabled = await testSubjects.isEnabled(subj); + expect(isEnabled).to.eql( + false, + `Expected runtime mappings 'Apply changes' button to be disabled, got enabled.` + ); + }, + async assertDependentVariableSelection(expectedSelection: string[]) { await this.waitForDependentVariableInputLoaded(); const actualSelection = await comboBox.getComboBoxSelectedOptions( From 8227ece8853777aa80e6c99075f01c61f895b1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Tue, 6 Apr 2021 18:22:59 +0200 Subject: [PATCH 06/65] Bump cypress@6.8.0 (#95041) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 54 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index f3846bd0f71a2..4daa99b6150a0 100644 --- a/package.json +++ b/package.json @@ -444,7 +444,7 @@ "@bazel/ibazel": "^0.14.0", "@bazel/typescript": "^3.2.3", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.5.0", + "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", @@ -682,7 +682,7 @@ "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", "css-loader": "^3.4.2", - "cypress": "^6.2.1", + "cypress": "^6.8.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "cypress-pipe": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 8e2edde567ff2..3cb5ee24c480c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1294,13 +1294,13 @@ snap-shot-compare "2.8.3" snap-shot-store "1.2.3" -"@cypress/webpack-preprocessor@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.5.0.tgz#e7010b2ee7449691cc16a9d5d1956af17ea175fd" - integrity sha512-iqwPygSNZ1u6bM3r5QRVv6qYngkcgI2xCzi9Jmo4mrkcofwX08UaItJq7xlB2/dHbB2aryQYOsfe4xNKtQIm3A== +"@cypress/webpack-preprocessor@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.6.0.tgz#9648ae22d2e52f17a604e2a493af27a9c96568bd" + integrity sha512-kSelTDe6gs3Skp4vPP2vfTvAl+Ua+9rR/AMTir7bgJihDvzFESqnjWtF6N1TrPo+vCFVGx0VUA6JUvDkhvpwhA== dependencies: bluebird "^3.7.1" - debug "^4.1.1" + debug "4.3.2" lodash "^4.17.20" "@cypress/xvfb@^1.2.4": @@ -5390,7 +5390,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@14.14.14", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^12.0.2": +"@types/node@*", "@types/node@12.12.50", "@types/node@14.14.14", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^12.0.2": version "14.14.14" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== @@ -11004,14 +11004,15 @@ cypress-promise@^1.1.0: resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" integrity sha512-DhIf5PJ/a0iY+Yii6n7Rbwq+9TJxU4pupXYzf9mZd8nPG0AzQrj9i+pqINv4xbI2EV1p+PKW3maCkR7oPG4GrA== -cypress@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.2.1.tgz#27d5fbcf008c698c390fdb0c03441804176d06c4" - integrity sha512-OYkSgzA4J4Q7eMjZvNf5qWpBLR4RXrkqjL3UZ1UzGGLAskO0nFTi/RomNTG6TKvL3Zp4tw4zFY1gp5MtmkCZrA== +cypress@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.8.0.tgz#8338f39212a8f71e91ff8c017a1b6e22d823d8c1" + integrity sha512-W2e9Oqi7DmF48QtOD0LfsOLVq6ef2hcXZvJXI/E3PgFNmZXEVwBefhAxVCW9yTPortjYA2XkM20KyC4HRkOm9w== dependencies: "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" "@cypress/xvfb" "^1.2.4" + "@types/node" "12.12.50" "@types/sinonjs__fake-timers" "^6.0.1" "@types/sizzle" "^2.3.2" arch "^2.1.2" @@ -11023,7 +11024,8 @@ cypress@^6.2.1: cli-table3 "~0.6.0" commander "^5.1.0" common-tags "^1.8.0" - debug "^4.1.1" + dayjs "^1.9.3" + debug "4.3.2" eventemitter2 "^6.4.2" execa "^4.0.2" executable "^4.1.1" @@ -11037,10 +11039,10 @@ cypress@^6.2.1: lodash "^4.17.19" log-symbols "^4.0.0" minimist "^1.2.5" - moment "^2.27.0" + moment "^2.29.1" ospath "^1.2.2" pretty-bytes "^5.4.1" - ramda "~0.26.1" + ramda "~0.27.1" request-progress "^3.0.0" supports-color "^7.2.0" tmp "~0.2.1" @@ -11407,6 +11409,11 @@ dateformat@^3.0.2, dateformat@~3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.9.3: + version "1.10.4" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" + integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== + debug-fabulous@1.X: version "1.1.0" resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.1.0.tgz#af8a08632465224ef4174a9f06308c3c2a1ebc8e" @@ -11465,6 +11472,13 @@ debug@4.2.0: dependencies: ms "2.1.2" +debug@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -20254,11 +20268,16 @@ moment-timezone@^0.5.27: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.19.3, moment@^2.24.0, moment@^2.27.0: +"moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.19.3, moment@^2.24.0: version "2.28.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75" integrity sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw== +moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + monaco-editor@*, monaco-editor@^0.22.3: version "0.22.3" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.22.3.tgz#69b42451d3116c6c08d9b8e052007ff891fd85d7" @@ -23229,11 +23248,16 @@ ramda@^0.21.0: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= -ramda@^0.26, ramda@^0.26.1, ramda@~0.26.1: +ramda@^0.26, ramda@^0.26.1: version "0.26.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +ramda@~0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" + integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw== + randexp@0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" From 7f97f8bc59598cba52d8cd8e873a37e6e2ec7e6f Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 6 Apr 2021 18:37:33 +0200 Subject: [PATCH 07/65] notify main dev process when server is ready (#96332) * notify main dev process when server is ready * check for process.send existence --- src/core/server/bootstrap.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 4a07e0c010685..a2267635e86f2 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -83,6 +83,11 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot try { await root.setup(); await root.start(); + + // notify parent process know when we are ready for dev mode. + if (process.send) { + process.send(['SERVER_LISTENING']); + } } catch (err) { await shutdown(err); } From 92b9482875f27fb4100372464807d0c55ee1ab91 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 6 Apr 2021 11:26:15 -0700 Subject: [PATCH 08/65] [Security Solution][Exceptions] - Moves remaining exceptions builder logic into lists plugin (#95266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Moves part of the exceptions UI out of the security solution plugin and into the lists plugin. In order to keep PRs (relatively) small, I am moving single components at a time. This should also then help more easily pinpoint the source of any issues that come up along the way. The next couple PRs will focus on the exception builder. This one in particular is focused on moving over the `ExceptionBuilderComponent` which deals with rendering numerous exception items and their entries. Quick Summary: - `x-pack/plugins/security_solution/public/common/components/exceptions/builder/` → ` x-pack/plugins/lists/public/exceptions/components/builder/` - Corresponding unit test file moved as well - Updated security solution exception builder to pull `ExceptionBuilderComponent` from lists plugin --- packages/kbn-optimizer/limits.yml | 4 +- .../__tests__/enumerate_patterns.test.js | 4 +- .../components/builder/builder.stories.tsx | 358 ++++++++++++ .../components/builder/entry_renderer.tsx | 11 +- .../builder/exception_item_renderer.tsx | 16 +- .../exception_items_renderer.test.tsx} | 153 ++--- .../builder/exception_items_renderer.tsx} | 118 ++-- .../exceptions/components/builder/helpers.ts | 133 ++++- .../exceptions/components/builder/index.tsx | 10 + .../builder/logic_buttons.test.tsx | 0 .../components}/builder/logic_buttons.tsx | 25 +- .../exceptions/components}/builder/reducer.ts | 13 +- .../components/builder/translations.ts | 22 + x-pack/plugins/lists/public/shared_exports.ts | 5 +- .../add_exception_modal/index.test.tsx | 8 +- .../exceptions/add_exception_modal/index.tsx | 18 +- .../exceptions/builder/helpers.test.tsx | 69 --- .../components/exceptions/builder/helpers.tsx | 43 -- .../builder/logic_buttons.stories.tsx | 110 ---- .../exceptions/builder/reducer.test.ts | 521 ------------------ .../exceptions/builder/translations.ts | 72 --- .../edit_exception_modal/index.test.tsx | 17 +- .../exceptions/edit_exception_modal/index.tsx | 18 +- .../components/exceptions/helpers.test.tsx | 56 +- .../common/components/exceptions/helpers.tsx | 14 + .../public/shared_imports.ts | 5 +- .../translations/translations/ja-JP.json | 10 - .../translations/translations/zh-CN.json | 10 - 28 files changed, 819 insertions(+), 1024 deletions(-) create mode 100644 x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx rename x-pack/plugins/{security_solution/public/common/components/exceptions/builder/index.test.tsx => lists/public/exceptions/components/builder/exception_items_renderer.test.tsx} (94%) rename x-pack/plugins/{security_solution/public/common/components/exceptions/builder/index.tsx => lists/public/exceptions/components/builder/exception_items_renderer.tsx} (92%) create mode 100644 x-pack/plugins/lists/public/exceptions/components/builder/index.tsx rename x-pack/plugins/{security_solution/public/common/components/exceptions => lists/public/exceptions/components}/builder/logic_buttons.test.tsx (100%) rename x-pack/plugins/{security_solution/public/common/components/exceptions => lists/public/exceptions/components}/builder/logic_buttons.tsx (92%) rename x-pack/plugins/{security_solution/public/common/components/exceptions => lists/public/exceptions/components}/builder/reducer.ts (95%) delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index a027768ad66a0..249183d4b1e31 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 202261 + lists: 228500 logstash: 53548 management: 46112 maps: 80000 @@ -68,7 +68,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 189428 securityOss: 30806 - securitySolution: 283440 + securitySolution: 235402 share: 99061 snapshotRestore: 79032 spaces: 387915 diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 738b38ee28bde..bb98498e6d601 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -35,13 +35,13 @@ describe(`enumeratePatterns`, () => { 'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app' ); }); - it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts to kibana-security`, () => { + it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts to kibana-security`, () => { const short = 'x-pack/plugins/security_solution'; const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); expect( actual[0].includes( - `${short}/public/common/components/exceptions/builder/translations.ts kibana-security` + `${short}/public/common/components/exceptions/edit_exception_modal/translations.ts kibana-security` ) ).toBe(true); }); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx new file mode 100644 index 0000000000000..5199ead78ca0a --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx @@ -0,0 +1,358 @@ +/* + * 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. + */ +/* + * 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 { Story, addDecorator } from '@storybook/react'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { HttpStart } from 'kibana/public'; + +import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +import { + fields, + getField, +} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; +import { getMockTheme } from '../../../common/test_utils/kibana_react.mock'; +import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock'; +import { getEntryMatchMock } from '../../../../common/schemas/types/entry_match.mock'; +import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock'; +import { getEntryNestedMock } from '../../../../common/schemas/types/entry_nested.mock'; +import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock'; + +import { + ExceptionBuilderComponent, + ExceptionBuilderProps, + OnChangeProps, +} from './exception_items_renderer'; + +const mockTheme = getMockTheme({ + darkMode: false, + eui: euiLightVars, +}); +const mockHttpService: HttpStart = ({ + addLoadingCountSource: (): void => {}, + anonymousPaths: { + isAnonymous: (): void => {}, + register: (): void => {}, + }, + basePath: {}, + delete: (): void => {}, + externalUrl: { + validateUrl: (): void => {}, + }, + fetch: (): void => {}, + get: (): void => {}, + getLoadingCount$: (): void => {}, + head: (): void => {}, + intercept: (): void => {}, + options: (): void => {}, + patch: (): void => {}, + post: (): void => {}, + put: (): void => {}, +} as unknown) as HttpStart; +const mockAutocompleteService = ({ + getValueSuggestions: () => + new Promise((resolve) => { + setTimeout(() => { + resolve([ + 'siem-kibana', + 'win2019-endpoint-mr-pedro', + 'rock01', + 'windows-endpoint', + 'siem-windows', + 'mothra', + ]); + }, 300); + }), +} as unknown) as AutocompleteStart; + +addDecorator((storyFn) => {storyFn()}); + +export default { + argTypes: { + allowLargeValueLists: { + control: { + type: 'boolean', + }, + description: '`boolean` - set to true to allow large value lists.', + table: { + defaultValue: { + summary: true, + }, + }, + type: { + required: true, + }, + }, + autocompleteService: { + control: { + type: 'object', + }, + description: + '`AutocompleteStart` - Kibana data plugin autocomplete service used for field value autocomplete.', + type: { + required: true, + }, + }, + exceptionListItems: { + control: { + type: 'array', + }, + description: + '`ExceptionsBuilderExceptionItem[]` - Any existing exception items - would be populated when editing an exception item.', + type: { + required: true, + }, + }, + httpService: { + control: { + type: 'object', + }, + description: '`HttpStart` - Kibana service.', + type: { + required: true, + }, + }, + indexPatterns: { + description: + '`IIndexPattern` - index patterns used to populate field options and value autocomplete.', + type: { + required: true, + }, + }, + isAndDisabled: { + control: { + type: 'boolean', + }, + description: + '`boolean` - set to true to disallow users from using the `AND` button to add entries.', + table: { + defaultValue: { + summary: false, + }, + }, + type: { + required: true, + }, + }, + isNestedDisabled: { + control: { + type: 'boolean', + }, + description: + '`boolean` - set to true to disallow users from using the `Add nested` button to add nested entries.', + table: { + defaultValue: { + summary: false, + }, + }, + type: { + required: true, + }, + }, + isOrDisabled: { + control: { + type: 'boolean', + }, + description: + '`boolean` - set to true to disallow users from using the `OR` button to add multiple exception items within the builder.', + table: { + defaultValue: { + summary: false, + }, + }, + type: { + required: true, + }, + }, + listId: { + control: { + type: 'string', + }, + description: '`string` - the exception list id.', + type: { + required: true, + }, + }, + listNamespaceType: { + control: { + options: ['agnostic', 'single'], + type: 'select', + }, + description: '`NamespaceType` - Determines whether the list is global or space specific.', + type: { + required: true, + }, + }, + listType: { + control: { + options: ['detection', 'endpoint'], + type: 'select', + }, + description: + '`ExceptionListType` - Depending on the list type, certain validations may apply.', + type: { + required: true, + }, + }, + listTypeSpecificIndexPatternFilter: { + description: + '`(pattern: IIndexPattern, type: ExceptionListType) => IIndexPattern` - callback invoked when index patterns filtered. Optional to be used if you would only like certain fields displayed.', + type: { + required: false, + }, + }, + onChange: { + description: + '`(arg: OnChangeProps) => void` - callback invoked any time builder update to propagate changes up to parent.', + type: { + required: true, + }, + }, + ruleName: { + description: '`string` - name of the rule list is associated with.', + type: { + required: true, + }, + }, + }, + component: ExceptionBuilderComponent, + title: 'ExceptionBuilderComponent', +}; + +const BuilderTemplate: Story = (args) => ( + +); + +export const Default = BuilderTemplate.bind({}); +Default.args = { + allowLargeValueLists: true, + autocompleteService: mockAutocompleteService, + exceptionListItems: [], + httpService: mockHttpService, + indexPatterns: { fields, id: '1234', title: 'logstash-*' }, + isAndDisabled: false, + isNestedDisabled: false, + isOrDisabled: false, + listId: '1234', + listNamespaceType: 'single', + listType: 'detection', + onChange: (): OnChangeProps => ({ + errorExists: false, + exceptionItems: [], + exceptionsToDelete: [], + }), + ruleName: 'My awesome rule', +}; + +const sampleExceptionItem = { + ...getExceptionListItemSchemaMock(), + entries: [ + { ...getEntryMatchAnyMock(), field: getField('ip').name, value: ['some ip'] }, + { ...getEntryMatchMock(), field: getField('ssl').name, value: 'false' }, + { ...getEntryExistsMock(), field: getField('@timestamp').name }, + ], +}; + +const sampleNestedExceptionItem = { + ...getExceptionListItemSchemaMock(), + entries: [ + { ...getEntryMatchAnyMock(), field: getField('ip').name, value: ['some ip'] }, + { + ...getEntryNestedMock(), + entries: [ + { + ...getEntryMatchMock(), + field: 'child', + value: 'some nested value', + }, + ], + field: 'nestedField', + }, + ], +}; + +const BuilderSingleExceptionItem: Story = (args) => ( + +); + +export const SingleExceptionItem = BuilderSingleExceptionItem.bind({}); +SingleExceptionItem.args = { + allowLargeValueLists: true, + autocompleteService: mockAutocompleteService, + exceptionListItems: [sampleExceptionItem], + httpService: mockHttpService, + indexPatterns: { fields, id: '1234', title: 'logstash-*' }, + isAndDisabled: false, + isNestedDisabled: false, + isOrDisabled: false, + listId: '1234', + listNamespaceType: 'single', + listType: 'detection', + onChange: (): OnChangeProps => ({ + errorExists: false, + exceptionItems: [sampleExceptionItem], + exceptionsToDelete: [], + }), + ruleName: 'My awesome rule', +}; + +const BuilderMultiExceptionItems: Story = (args) => ( + +); + +export const MultiExceptionItems = BuilderMultiExceptionItems.bind({}); +MultiExceptionItems.args = { + allowLargeValueLists: true, + autocompleteService: mockAutocompleteService, + exceptionListItems: [sampleExceptionItem, sampleExceptionItem], + httpService: mockHttpService, + indexPatterns: { fields, id: '1234', title: 'logstash-*' }, + isAndDisabled: false, + isNestedDisabled: false, + isOrDisabled: false, + listId: '1234', + listNamespaceType: 'single', + listType: 'detection', + onChange: (): OnChangeProps => ({ + errorExists: false, + exceptionItems: [sampleExceptionItem, sampleExceptionItem], + exceptionsToDelete: [], + }), + ruleName: 'My awesome rule', +}; + +const BuilderWithNested: Story = (args) => ( + +); + +export const WithNestedExceptionItem = BuilderWithNested.bind({}); +WithNestedExceptionItem.args = { + allowLargeValueLists: true, + autocompleteService: mockAutocompleteService, + exceptionListItems: [sampleNestedExceptionItem, sampleExceptionItem], + httpService: mockHttpService, + indexPatterns: { fields, id: '1234', title: 'logstash-*' }, + isAndDisabled: false, + isNestedDisabled: false, + isOrDisabled: false, + listId: '1234', + listNamespaceType: 'single', + listType: 'detection', + onChange: (): OnChangeProps => ({ + errorExists: false, + exceptionItems: [sampleNestedExceptionItem, sampleExceptionItem], + exceptionsToDelete: [], + }), + ruleName: 'My awesome rule', +}; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 7c45f1c35c55e..e13a7ccf90bdd 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -46,7 +46,10 @@ export interface EntryItemProps { httpService: HttpStart; indexPattern: IIndexPattern; listType: ExceptionListType; - listTypeSpecificFilter?: (pattern: IIndexPattern, type: ExceptionListType) => IIndexPattern; + listTypeSpecificIndexPatternFilter?: ( + pattern: IIndexPattern, + type: ExceptionListType + ) => IIndexPattern; onChange: (arg: BuilderEntry, i: number) => void; onlyShowListOperators?: boolean; setErrorsExist: (arg: boolean) => void; @@ -60,7 +63,7 @@ export const BuilderEntryItem: React.FC = ({ httpService, indexPattern, listType, - listTypeSpecificFilter, + listTypeSpecificIndexPatternFilter, onChange, onlyShowListOperators = false, setErrorsExist, @@ -123,7 +126,7 @@ export const BuilderEntryItem: React.FC = ({ indexPattern, entry, listType, - listTypeSpecificFilter + listTypeSpecificIndexPatternFilter ); const comboBox = ( = ({ ); } }, - [indexPattern, entry, listType, listTypeSpecificFilter, handleFieldChange] + [indexPattern, entry, listType, listTypeSpecificIndexPatternFilter, handleFieldChange] ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index d151ec5a81ec3..c9cbd9a84f5e3 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -45,6 +45,10 @@ interface BuilderExceptionListItemProps { andLogicIncluded: boolean; isOnlyItem: boolean; listType: ExceptionListType; + listTypeSpecificIndexPatternFilter?: ( + pattern: IIndexPattern, + type: ExceptionListType + ) => IIndexPattern; onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; setErrorsExist: (arg: boolean) => void; @@ -61,6 +65,7 @@ export const BuilderExceptionListItemComponent = React.memo diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx rename to x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx index 2863b92ca68ab..b8ec8dc354bf8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx @@ -9,20 +9,19 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { ReactWrapper, mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; +import { coreMock } from 'src/core/public/mocks'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { fields, getField, -} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; - -import { getEmptyValue } from '../../empty_value'; +} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; +import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock'; +import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock'; +import { getMockTheme } from '../../../common/test_utils/kibana_react.mock'; +import { getEmptyValue } from '../../../common/empty_value'; -import { ExceptionBuilderComponent } from './'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; -import { coreMock } from 'src/core/public/mocks'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { ExceptionBuilderComponent } from './exception_items_renderer'; const mockTheme = getMockTheme({ eui: { @@ -47,21 +46,22 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( @@ -85,7 +85,7 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( { ], }, ]} - listType="detection" - listId="list_id" - listNamespaceType="single" - ruleName="Test rule" + httpService={mockKibanaHttpService} indexPatterns={{ + fields, id: '1234', title: 'logstash-*', - fields, }} - isOrDisabled={false} isAndDisabled={false} isNestedDisabled={false} + isOrDisabled={false} + listId="list_id" + listNamespaceType="single" + listType="detection" + ruleName="Test rule" onChange={jest.fn()} /> @@ -129,21 +130,23 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( @@ -164,21 +167,22 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( @@ -220,21 +224,22 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( @@ -280,7 +285,7 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( { ], }, ]} - listType="detection" - listId="list_id" - listNamespaceType="single" - ruleName="Test rule" + httpService={mockKibanaHttpService} indexPatterns={{ + fields, id: '1234', title: 'logstash-*', - fields, }} - isOrDisabled={false} isAndDisabled={false} isNestedDisabled={false} + isOrDisabled={false} + listId="list_id" + listType="detection" + listNamespaceType="single" + ruleName="Test rule" onChange={jest.fn()} /> @@ -334,21 +340,22 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( @@ -369,21 +376,22 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( @@ -407,21 +415,22 @@ describe('ExceptionBuilderComponent', () => { wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx rename to x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 812294ae24c71..60e1abb3a254e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -8,33 +8,32 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; - import { HttpStart } from 'kibana/public'; -import { AutocompleteStart } from 'src/plugins/data/public'; -import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils'; -import { addIdToItem } from '../../../../../common'; -import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; + +import { addIdToItem } from '../../../../common/shared_imports'; +import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - BuilderExceptionListItemComponent, + CreateExceptionListItemSchema, ExceptionListItemSchema, + ExceptionListType, NamespaceType, - exceptionListItemSchema, - OperatorTypeEnum, OperatorEnum, - CreateExceptionListItemSchema, - ExceptionListType, + OperatorTypeEnum, entriesNested, -} from '../../../../../public/shared_imports'; -import { AndOrBadge } from '../../and_or_badge'; + exceptionListItemSchema, +} from '../../../../common'; +import { AndOrBadge } from '../and_or_badge'; + +import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types'; +import { BuilderExceptionListItemComponent } from './exception_item_renderer'; import { BuilderLogicButtons } from './logic_buttons'; -import { getNewExceptionItem, filterExceptionItems } from '../helpers'; -import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; import { State, exceptionsBuilderReducer } from './reducer'; import { containsValueListEntry, + filterExceptionItems, getDefaultEmptyEntry, getDefaultNestedEmptyEntry, + getNewExceptionItem, } from './helpers'; const MyInvisibleAndBadge = styled(EuiFlexItem)` @@ -52,77 +51,82 @@ const MyButtonsContainer = styled(EuiFlexItem)` `; const initialState: State = { + addNested: false, + andLogicIncluded: false, disableAnd: false, disableNested: false, disableOr: false, - andLogicIncluded: false, - addNested: false, + errorExists: 0, exceptions: [], exceptionsToDelete: [], - errorExists: 0, }; -interface OnChangeProps { +export interface OnChangeProps { + errorExists: boolean; exceptionItems: Array; exceptionsToDelete: ExceptionListItemSchema[]; - errorExists: boolean; } -interface ExceptionBuilderProps { - httpService: HttpStart; +export interface ExceptionBuilderProps { + allowLargeValueLists: boolean; autocompleteService: AutocompleteStart; exceptionListItems: ExceptionsBuilderExceptionItem[]; - listType: ExceptionListType; - listId: string; - listNamespaceType: NamespaceType; - ruleName: string; + httpService: HttpStart; indexPatterns: IIndexPattern; - isOrDisabled: boolean; isAndDisabled: boolean; isNestedDisabled: boolean; + isOrDisabled: boolean; + listId: string; + listNamespaceType: NamespaceType; + listType: ExceptionListType; + listTypeSpecificIndexPatternFilter?: ( + pattern: IIndexPattern, + type: ExceptionListType + ) => IIndexPattern; onChange: (arg: OnChangeProps) => void; - ruleType?: Type; + ruleName: string; } export const ExceptionBuilderComponent = ({ - httpService, + allowLargeValueLists, autocompleteService, exceptionListItems, - listType, - listId, - listNamespaceType, - ruleName, + httpService, indexPatterns, - isOrDisabled, isAndDisabled, isNestedDisabled, + isOrDisabled, + listId, + listNamespaceType, + listType, + listTypeSpecificIndexPatternFilter, onChange, - ruleType, -}: ExceptionBuilderProps) => { + ruleName, +}: ExceptionBuilderProps): JSX.Element => { const [ { - exceptions, - exceptionsToDelete, + addNested, andLogicIncluded, disableAnd, disableNested, disableOr, - addNested, errorExists, + exceptions, + exceptionsToDelete, }, dispatch, ] = useReducer(exceptionsBuilderReducer(), { ...initialState, disableAnd: isAndDisabled, - disableOr: isOrDisabled, disableNested: isNestedDisabled, + disableOr: isOrDisabled, }); const setErrorsExist = useCallback( (hasErrors: boolean): void => { dispatch({ - type: 'setErrorsExist', errorExists: hasErrors, + type: 'setErrorsExist', }); }, [dispatch] @@ -131,8 +135,8 @@ export const ExceptionBuilderComponent = ({ const setUpdateExceptions = useCallback( (items: ExceptionsBuilderExceptionItem[]): void => { dispatch({ - type: 'setExceptions', exceptions: items, + type: 'setExceptions', }); }, [dispatch] @@ -141,9 +145,9 @@ export const ExceptionBuilderComponent = ({ const setDefaultExceptions = useCallback( (item: ExceptionsBuilderExceptionItem): void => { dispatch({ - type: 'setDefault', initialState, lastException: item, + type: 'setDefault', }); }, [dispatch] @@ -152,8 +156,8 @@ export const ExceptionBuilderComponent = ({ const setUpdateExceptionsToDelete = useCallback( (items: ExceptionListItemSchema[]): void => { dispatch({ - type: 'setExceptionsToDelete', exceptions: items, + type: 'setExceptionsToDelete', }); }, [dispatch] @@ -162,8 +166,8 @@ export const ExceptionBuilderComponent = ({ const setUpdateAndDisabled = useCallback( (shouldDisable: boolean): void => { dispatch({ - type: 'setDisableAnd', shouldDisable, + type: 'setDisableAnd', }); }, [dispatch] @@ -172,8 +176,8 @@ export const ExceptionBuilderComponent = ({ const setUpdateOrDisabled = useCallback( (shouldDisable: boolean): void => { dispatch({ - type: 'setDisableOr', shouldDisable, + type: 'setDisableOr', }); }, [dispatch] @@ -182,8 +186,9 @@ export const ExceptionBuilderComponent = ({ const setUpdateAddNested = useCallback( (shouldAddNested: boolean): void => { dispatch({ - type: 'setAddNested', addNested: shouldAddNested, + + type: 'setAddNested', }); }, [dispatch] @@ -295,8 +300,8 @@ export const ExceptionBuilderComponent = ({ ...lastEntry.entries, addIdToItem({ field: '', - type: OperatorTypeEnum.MATCH, operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, value: '', }), ], @@ -331,9 +336,9 @@ export const ExceptionBuilderComponent = ({ // Bubble up changes to parent useEffect(() => { onChange({ + errorExists: errorExists > 0, exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete, - errorExists: errorExists > 0, }); }, [onChange, exceptionsToDelete, exceptions, errorExists]); @@ -381,18 +386,19 @@ export const ExceptionBuilderComponent = ({ ))} diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index b3ed9d296a218..4cf9f233f3917 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -5,15 +5,27 @@ * 2.0. */ +import uuid from 'uuid'; + import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { addIdToItem } from '../../../../common/shared_imports'; +import { addIdToItem, removeIdFromItem, validate } from '../../../../common/shared_imports'; import { + CreateExceptionListItemSchema, + EntriesArray, Entry, EntryNested, + ExceptionListItemSchema, ExceptionListType, ListSchema, + NamespaceType, + OperatorEnum, OperatorTypeEnum, + createExceptionListItemSchema, entriesList, + entriesNested, + entry, + exceptionListItemSchema, + nestedEntryItem, } from '../../../../common'; import { EXCEPTION_OPERATORS, @@ -28,6 +40,8 @@ import { OperatorOption } from '../autocomplete/types'; import { BuilderEntry, + CreateExceptionListItemBuilderSchema, + EmptyEntry, EmptyNestedEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry, @@ -37,6 +51,105 @@ export const isEntryNested = (item: BuilderEntry): item is EntryNested => { return (item as EntryNested).entries != null; }; +export const filterExceptionItems = ( + exceptions: ExceptionsBuilderExceptionItem[] +): Array => { + return exceptions.reduce>( + (acc, exception) => { + const entries = exception.entries.reduce((nestedAcc, singleEntry) => { + const strippedSingleEntry = removeIdFromItem(singleEntry); + + if (entriesNested.is(strippedSingleEntry)) { + const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => { + const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry); + const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem); + return validatedNestedEntry != null; + }); + const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) => + removeIdFromItem(singleNestedEntry) + ); + + const [validatedNestedEntry] = validate( + { ...strippedSingleEntry, entries: noIdNestedEntries }, + entriesNested + ); + + if (validatedNestedEntry != null) { + return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }]; + } + return nestedAcc; + } else { + const [validatedEntry] = validate(strippedSingleEntry, entry); + + if (validatedEntry != null) { + return [...nestedAcc, singleEntry]; + } + return nestedAcc; + } + }, []); + + const item = { ...exception, entries }; + + if (exceptionListItemSchema.is(item)) { + return [...acc, item]; + } else if (createExceptionListItemSchema.is(item)) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { meta: _, ...rest } = item; + const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; + return [...acc, itemSansMetaId]; + } else { + return acc; + } + }, + [] + ); +}; + +export const addIdToEntries = (entries: EntriesArray): EntriesArray => { + return entries.map((singleEntry) => { + if (singleEntry.type === 'nested') { + return addIdToItem({ + ...singleEntry, + entries: singleEntry.entries.map((nestedEntry) => addIdToItem(nestedEntry)), + }); + } else { + return addIdToItem(singleEntry); + } + }); +}; + +export const getNewExceptionItem = ({ + listId, + namespaceType, + ruleName, +}: { + listId: string; + namespaceType: NamespaceType; + ruleName: string; +}): CreateExceptionListItemBuilderSchema => { + return { + comments: [], + description: `${ruleName} - exception list item`, + entries: addIdToEntries([ + { + field: '', + operator: 'included', + type: 'match', + value: '', + }, + ]), + item_id: undefined, + list_id: listId, + meta: { + temporaryUuid: uuid.v4(), + }, + name: `${ruleName} - exception list item`, + namespace_type: namespaceType, + tags: [], + type: 'simple', + }; +}; + /** * Returns the operator type, may not need this if using io-ts types * @@ -665,3 +778,21 @@ export const getFormattedBuilderEntries = ( } }, []); }; + +export const getDefaultEmptyEntry = (): EmptyEntry => ({ + field: '', + id: uuid.v4(), + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: '', +}); + +export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({ + entries: [], + field: '', + id: uuid.v4(), + type: OperatorTypeEnum.NESTED, +}); + +export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean => + items.some((item) => item.entries.some(({ type }) => type === OperatorTypeEnum.LIST)); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx new file mode 100644 index 0000000000000..5b3e754f7e423 --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx @@ -0,0 +1,10 @@ +/* + * 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 { BuilderEntryItem } from './entry_renderer'; +export { BuilderExceptionListItemComponent } from './exception_item_renderer'; +export { ExceptionBuilderComponent } from './exception_items_renderer'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.test.tsx rename to x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.tsx rename to x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx index 359a3b3b427ba..30fda556f0df8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx @@ -6,38 +6,37 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from './translations'; -import * as i18nShared from '../translations'; const MyEuiButton = styled(EuiButton)` min-width: 95px; `; interface BuilderLogicButtonsProps { - isOrDisabled: boolean; isAndDisabled: boolean; - isNestedDisabled: boolean; isNested: boolean; + isNestedDisabled: boolean; + isOrDisabled: boolean; showNestedButton: boolean; + onAddClickWhenNested: () => void; onAndClicked: () => void; - onOrClicked: () => void; onNestedClicked: () => void; - onAddClickWhenNested: () => void; + onOrClicked: () => void; } export const BuilderLogicButtons: React.FC = ({ - isOrDisabled = false, isAndDisabled = false, - showNestedButton = false, - isNestedDisabled = true, isNested, + isNestedDisabled = true, + isOrDisabled = false, + showNestedButton = false, + onAddClickWhenNested, onAndClicked, - onOrClicked, onNestedClicked, - onAddClickWhenNested, + onOrClicked, }) => ( @@ -49,7 +48,7 @@ export const BuilderLogicButtons: React.FC = ({ data-test-subj="exceptionsAndButton" isDisabled={isAndDisabled} > - {i18nShared.AND} + {i18n.AND} @@ -61,7 +60,7 @@ export const BuilderLogicButtons: React.FC = ({ isDisabled={isOrDisabled} data-test-subj="exceptionsOrButton" > - {i18nShared.OR} + {i18n.OR} {showNestedButton && ( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.ts b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts similarity index 95% rename from x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.ts rename to x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts index f35e8f0b4ed2b..92df2fd3793de 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { ExceptionsBuilderExceptionItem } from '../types'; -import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../../public/lists_plugin_deps'; +import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../common'; + +import { ExceptionsBuilderExceptionItem } from './types'; import { getDefaultEmptyEntry } from './helpers'; export type ViewerModalName = 'addModal' | 'editModal' | null; @@ -58,7 +59,7 @@ export const exceptionsBuilderReducer = () => (state: State, action: Action): St case 'setExceptions': { const isAndLogicIncluded = action.exceptions.filter(({ entries }) => entries.length > 1).length > 0; - const lastExceptionItem = action.exceptions.slice(-1)[0]; + const [lastExceptionItem] = action.exceptions.slice(-1); const isAddNested = lastExceptionItem != null ? lastExceptionItem.entries.slice(-1).filter(({ type }) => type === 'nested').length > 0 @@ -73,12 +74,12 @@ export const exceptionsBuilderReducer = () => (state: State, action: Action): St return { ...state, - andLogicIncluded: isAndLogicIncluded, - exceptions: action.exceptions, addNested: isAddNested, + andLogicIncluded: isAndLogicIncluded, disableAnd: isAndDisabled, - disableOr: isOrDisabled, disableNested: containsValueList, + disableOr: isOrDisabled, + exceptions: action.exceptions, }; } case 'setDefault': { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts b/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts index 9da598c08bd83..291ef7a420f0f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/translations.ts @@ -53,3 +53,25 @@ export const EXCEPTION_OPERATOR_PLACEHOLDER = i18n.translate( defaultMessage: 'Operator', } ); + +export const ADD_NESTED_DESCRIPTION = i18n.translate( + 'xpack.lists.exceptions.builder.addNestedDescription', + { + defaultMessage: 'Add nested condition', + } +); + +export const ADD_NON_NESTED_DESCRIPTION = i18n.translate( + 'xpack.lists.exceptions.builder.addNonNestedDescription', + { + defaultMessage: 'Add non-nested condition', + } +); + +export const AND = i18n.translate('xpack.lists.exceptions.andDescription', { + defaultMessage: 'AND', +}); + +export const OR = i18n.translate('xpack.lists.exceptions.orDescription', { + defaultMessage: 'OR', +}); diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index d35fe5bb06c0c..39825e5feb6ba 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -38,7 +38,4 @@ export { UseExceptionListItemsSuccess, UseExceptionListsSuccess, } from './exceptions/types'; -export { BuilderEntryItem } from './exceptions/components/builder/entry_renderer'; -export { BuilderAndBadgeComponent } from './exceptions/components/builder/and_badge'; -export { BuilderEntryDeleteButtonComponent } from './exceptions/components/builder/entry_delete_button'; -export { BuilderExceptionListItemComponent } from './exceptions/components/builder/exception_item_renderer'; +export * as ExceptionBuilder from './exceptions/components/builder/index'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 456dabec06c24..686acbe4ef321 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -12,14 +12,13 @@ import { waitFor } from '@testing-library/react'; import { AddExceptionModal } from './'; import { useCurrentUser } from '../../../../common/lib/kibana'; -import { useAsync } from '../../../../shared_imports'; +import { useAsync, ExceptionBuilder } from '../../../../shared_imports'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { useFetchIndex } from '../../../containers/source'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import * as builder from '../builder'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; @@ -49,7 +48,6 @@ jest.mock('../../../containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); -jest.mock('../builder'); jest.mock('../../../../shared_imports'); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); @@ -59,12 +57,12 @@ describe('When the add exception modal is opened', () => { ReturnType >; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest - .spyOn(builder, 'ExceptionBuilderComponent') + .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') .mockReturnValue(<>); (useAsync as jest.Mock).mockImplementation(() => ({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 7e9e7c40258da..07dcb2272748f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -23,19 +23,23 @@ import { EuiText, EuiCallOut, } from '@elastic/eui'; -import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils'; +import { + hasEqlSequenceQuery, + isEqlRule, + isThresholdRule, +} from '../../../../../common/detection_engine/utils'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, ExceptionListType, -} from '../../../../../public/lists_plugin_deps'; + ExceptionBuilder, +} from '../../../../../public/shared_imports'; import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { ExceptionBuilderComponent } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; @@ -50,6 +54,7 @@ import { entryHasListType, entryHasNonEcsType, retrieveAlertOsTypes, + filterIndexPatterns, } from '../helpers'; import { ErrorInfo, ErrorCallout } from '../error_callout'; import { AlertData, ExceptionsBuilderExceptionItem } from '../types'; @@ -393,13 +398,17 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} {i18n.EXCEPTION_BUILDER_INFO} - diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx deleted file mode 100644 index 2046ac46b8517..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ /dev/null @@ -1,69 +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 { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; - -import { filterIndexPatterns } from './helpers'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); - -const getMockIndexPattern = (): IIndexPattern => ({ - id: '1234', - title: 'logstash-*', - fields, -}); - -const mockEndpointFields = [ - { - name: 'file.path.caseless', - type: 'string', - esTypes: ['keyword'], - count: 0, - scripted: false, - searchable: true, - aggregatable: false, - readFromDocValues: false, - }, - { - name: 'file.Ext.code_signature.status', - type: 'string', - esTypes: ['text'], - count: 0, - scripted: false, - searchable: true, - aggregatable: false, - readFromDocValues: false, - subType: { nested: { path: 'file.Ext.code_signature' } }, - }, -]; - -export const getEndpointField = (name: string) => - mockEndpointFields.find((field) => field.name === name) as IFieldType; - -describe('Exception builder helpers', () => { - describe('#filterIndexPatterns', () => { - test('it returns index patterns without filtering if list type is "detection"', () => { - const mockIndexPatterns = getMockIndexPattern(); - const output = filterIndexPatterns(mockIndexPatterns, 'detection'); - - expect(output).toEqual(mockIndexPatterns); - }); - - test('it returns filtered index patterns if list type is "endpoint"', () => { - const mockIndexPatterns = { - ...getMockIndexPattern(), - fields: [...fields, ...mockEndpointFields], - }; - const output = filterIndexPatterns(mockIndexPatterns, 'endpoint'); - - expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx deleted file mode 100644 index 0ad9814484a2f..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ /dev/null @@ -1,43 +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 uuid from 'uuid'; - -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { OperatorTypeEnum, ExceptionListType, OperatorEnum } from '../../../../lists_plugin_deps'; -import { ExceptionsBuilderExceptionItem, EmptyEntry, EmptyNestedEntry } from '../types'; -import exceptionableFields from '../exceptionable_fields.json'; - -export const filterIndexPatterns = ( - patterns: IIndexPattern, - type: ExceptionListType -): IIndexPattern => { - return type === 'endpoint' - ? { - ...patterns, - fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - } - : patterns; -}; - -export const getDefaultEmptyEntry = (): EmptyEntry => ({ - id: uuid.v4(), - field: '', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: '', -}); - -export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({ - id: uuid.v4(), - field: '', - type: OperatorTypeEnum.NESTED, - entries: [], -}); - -export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean => - items.some((item) => item.entries.some((entry) => entry.type === OperatorTypeEnum.LIST)); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx deleted file mode 100644 index 64801bd1892ed..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/logic_buttons.stories.tsx +++ /dev/null @@ -1,110 +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 { storiesOf, addDecorator } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { BuilderLogicButtons } from './logic_buttons'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -storiesOf('Exceptions/BuilderLogicButtons', module) - .add('and/or buttons', () => { - return ( - - ); - }) - .add('nested button - isNested false', () => { - return ( - - ); - }) - .add('nested button - isNested true', () => { - return ( - - ); - }) - .add('and disabled', () => { - return ( - - ); - }) - .add('or disabled', () => { - return ( - - ); - }) - .add('nested disabled', () => { - return ( - - ); - }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts deleted file mode 100644 index dbac7d325b63a..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts +++ /dev/null @@ -1,521 +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 { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; -import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock'; -import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock'; - -import { ExceptionsBuilderExceptionItem } from '../types'; -import { Action, State, exceptionsBuilderReducer } from './reducer'; -import { getDefaultEmptyEntry } from './helpers'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); - -const initialState: State = { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: false, - addNested: false, - exceptions: [], - exceptionsToDelete: [], - errorExists: 0, -}; - -describe('exceptionsBuilderReducer', () => { - let reducer: (state: State, action: Action) => State; - - beforeEach(() => { - reducer = exceptionsBuilderReducer(); - }); - - describe('#setExceptions', () => { - test('should return "andLogicIncluded" ', () => { - const update = reducer(initialState, { - type: 'setExceptions', - exceptions: [], - }); - - expect(update).toEqual({ - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: false, - addNested: false, - exceptions: [], - exceptionsToDelete: [], - errorExists: 0, - }); - }); - - test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryMatchMock()], - }, - ]; - const { andLogicIncluded } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(andLogicIncluded).toBeTruthy(); - }); - - test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { andLogicIncluded } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(andLogicIncluded).toBeFalsy(); - }); - - test('should set "addNested" to true if last exception entry is type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - ]; - const { addNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(addNested).toBeTruthy(); - }); - - test('should set "addNested" to false if last exception item entry is not type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { addNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(addNested).toBeFalsy(); - }); - - test('should set "disableOr" to true if last exception entry is type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - ]; - const { disableOr } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableOr).toBeTruthy(); - }); - - test('should set "disableOr" to false if last exception item entry is not type nested', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { disableOr } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableOr).toBeFalsy(); - }); - - test('should set "disableNested" to true if an exception item includes an entry of type list', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryListMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - ]; - const { disableNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableNested).toBeTruthy(); - }); - - test('should set "disableNested" to false if an exception item does not include an entry of type list', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { disableNested } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableNested).toBeFalsy(); - }); - - // What does that even mean?! :) Just checking if a user has selected - // to add a nested entry but has not yet selected the nested field - test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryListMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], - }, - ]; - const { disableAnd } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableAnd).toBeTruthy(); - }); - - test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock(), getEntryNestedMock()], - }, - { - ...getExceptionListItemSchemaMock(), - entries: [getEntryMatchMock()], - }, - ]; - const { disableAnd } = reducer(initialState, { - type: 'setExceptions', - exceptions, - }); - - expect(disableAnd).toBeFalsy(); - }); - }); - - describe('#setDefault', () => { - test('should restore initial state and add default empty entry to item" ', () => { - const update = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDefault', - initialState, - lastException: { - ...getExceptionListItemSchemaMock(), - entries: [], - }, - } - ); - - expect(update).toEqual({ - ...initialState, - exceptions: [ - { - ...getExceptionListItemSchemaMock(), - entries: [getDefaultEmptyEntry()], - }, - ], - }); - }); - }); - - describe('#setExceptionsToDelete', () => { - test('should add passed in exception item to "exceptionsToDelete"', () => { - const exceptions: ExceptionsBuilderExceptionItem[] = [ - { - ...getExceptionListItemSchemaMock(), - id: '1', - entries: [getEntryListMock()], - }, - { - ...getExceptionListItemSchemaMock(), - id: '2', - entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }], - }, - ]; - const { exceptionsToDelete } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions, - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setExceptionsToDelete', - exceptions: [ - { - ...getExceptionListItemSchemaMock(), - id: '1', - entries: [getEntryListMock()], - }, - ], - } - ); - - expect(exceptionsToDelete).toEqual([ - { - ...getExceptionListItemSchemaMock(), - id: '1', - entries: [getEntryListMock()], - }, - ]); - }); - }); - - describe('#setDisableAnd', () => { - test('should set "disableAnd" to false if "action.shouldDisable" is false', () => { - const { disableAnd } = reducer( - { - disableAnd: true, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableAnd', - shouldDisable: false, - } - ); - - expect(disableAnd).toBeFalsy(); - }); - - test('should set "disableAnd" to true if "action.shouldDisable" is true', () => { - const { disableAnd } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableAnd', - shouldDisable: true, - } - ); - - expect(disableAnd).toBeTruthy(); - }); - }); - - describe('#setDisableOr', () => { - test('should set "disableOr" to false if "action.shouldDisable" is false', () => { - const { disableOr } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: true, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableOr', - shouldDisable: false, - } - ); - - expect(disableOr).toBeFalsy(); - }); - - test('should set "disableOr" to true if "action.shouldDisable" is true', () => { - const { disableOr } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setDisableOr', - shouldDisable: true, - } - ); - - expect(disableOr).toBeTruthy(); - }); - }); - - describe('#setAddNested', () => { - test('should set "addNested" to false if "action.addNested" is false', () => { - const { addNested } = reducer( - { - disableAnd: false, - disableNested: true, - disableOr: false, - andLogicIncluded: true, - addNested: true, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setAddNested', - addNested: false, - } - ); - - expect(addNested).toBeFalsy(); - }); - - test('should set "disableOr" to true if "action.addNested" is true', () => { - const { addNested } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setAddNested', - addNested: true, - } - ); - - expect(addNested).toBeTruthy(); - }); - }); - - describe('#setErrorsExist', () => { - test('should increase "errorExists" by one if payload is "true"', () => { - const { errorExists } = reducer( - { - disableAnd: false, - disableNested: true, - disableOr: false, - andLogicIncluded: true, - addNested: true, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setErrorsExist', - errorExists: true, - } - ); - - expect(errorExists).toEqual(1); - }); - - test('should decrease "errorExists" by one if payload is "false"', () => { - const { errorExists } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 1, - }, - { - type: 'setErrorsExist', - errorExists: false, - } - ); - - expect(errorExists).toEqual(0); - }); - - test('should not decrease "errorExists" if decreasing would dip into negative numbers', () => { - const { errorExists } = reducer( - { - disableAnd: false, - disableNested: false, - disableOr: false, - andLogicIncluded: true, - addNested: false, - exceptions: [getExceptionListItemSchemaMock()], - exceptionsToDelete: [], - errorExists: 0, - }, - { - type: 'setErrorsExist', - errorExists: false, - } - ); - - expect(errorExists).toEqual(0); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts deleted file mode 100644 index c05847fb626d2..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts +++ /dev/null @@ -1,72 +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'; - -export const FIELD = i18n.translate('xpack.securitySolution.exceptions.builder.fieldDescription', { - defaultMessage: 'Field', -}); - -export const OPERATOR = i18n.translate( - 'xpack.securitySolution.exceptions.builder.operatorDescription', - { - defaultMessage: 'Operator', - } -); - -export const VALUE = i18n.translate('xpack.securitySolution.exceptions.builder.valueDescription', { - defaultMessage: 'Value', -}); - -export const EXCEPTION_FIELD_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription', - { - defaultMessage: 'Search', - } -); - -export const EXCEPTION_FIELD_NESTED_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription', - { - defaultMessage: 'Search nested field', - } -); - -export const EXCEPTION_OPERATOR_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription', - { - defaultMessage: 'Operator', - } -); - -export const EXCEPTION_FIELD_VALUE_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription', - { - defaultMessage: 'Search field value...', - } -); - -export const EXCEPTION_FIELD_LISTS_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription', - { - defaultMessage: 'Search for list...', - } -); - -export const ADD_NESTED_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.exceptions.builder.addNestedDescription', - { - defaultMessage: 'Add nested condition', - } -); - -export const ADD_NON_NESTED_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.exceptions.builder.addNonNestedDescription', - { - defaultMessage: 'Add non-nested condition', - } -); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index a30e6f769c47e..a97e71de77abd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -21,13 +21,13 @@ import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { EntriesArray } from '../../../../../../lists/common/schemas/types'; -import * as builder from '../builder'; import { getRulesEqlSchemaMock, getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; +import { ExceptionBuilder } from '../../../../shared_imports'; const mockTheme = getMockTheme({ eui: { @@ -46,19 +46,28 @@ jest.mock('../use_add_exception'); jest.mock('../../../containers/source'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); -jest.mock('../builder'); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); +jest.mock('../../../../shared_imports', () => { + const originalModule = jest.requireActual('../../../../shared_imports'); + + return { + ...originalModule, + ExceptionBuilder: { + ExceptionBuilderComponent: () => ({} as JSX.Element), + }, + }; +}); describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { ExceptionBuilderComponent = jest - .spyOn(builder, 'ExceptionBuilderComponent') + .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') .mockReturnValue(<>); (useSignalIndex as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index e33478ad99660..2c996c600261b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -22,7 +22,11 @@ import { EuiCallOut, } from '@elastic/eui'; -import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils'; +import { + hasEqlSequenceQuery, + isEqlRule, + isThresholdRule, +} from '../../../../../common/detection_engine/utils'; import { useFetchIndex } from '../../../containers/source'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; @@ -30,12 +34,12 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema, ExceptionListType, -} from '../../../../../public/lists_plugin_deps'; + ExceptionBuilder, +} from '../../../../../public/shared_imports'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useKibana } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { ExceptionBuilderComponent } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; import { @@ -44,6 +48,7 @@ import { entryHasListType, entryHasNonEcsType, lowercaseHashValues, + filterIndexPatterns, } from '../helpers'; import { Loader } from '../../loader'; import { ErrorInfo, ErrorCallout } from '../error_callout'; @@ -312,13 +317,17 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} {i18n.EXCEPTION_BUILDER_INFO} - diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 3463f521655cb..c4d18ec24faad 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -30,6 +30,7 @@ import { getFileCodeSignature, getProcessCodeSignature, retrieveAlertOsTypes, + filterIndexPatterns, } from './helpers'; import { AlertData, EmptyEntry } from './types'; import { @@ -49,6 +50,7 @@ import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/ import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock'; +import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { ENTRIES, ENTRIES_WITH_IDS, @@ -60,12 +62,45 @@ import { EntriesArray, OsTypeArray, } from '../../../../../lists/common/schemas'; -import { IIndexPattern } from 'src/plugins/data/common'; +import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('123'), })); +const getMockIndexPattern = (): IIndexPattern => ({ + fields, + id: '1234', + title: 'logstash-*', +}); + +const mockEndpointFields = [ + { + name: 'file.path.caseless', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, +]; + +export const getEndpointField = (name: string) => + mockEndpointFields.find((field) => field.name === name) as IFieldType; + describe('Exception helpers', () => { beforeEach(() => { moment.tz.setDefault('UTC'); @@ -75,6 +110,25 @@ describe('Exception helpers', () => { moment.tz.setDefault('Browser'); }); + describe('#filterIndexPatterns', () => { + test('it returns index patterns without filtering if list type is "detection"', () => { + const mockIndexPatterns = getMockIndexPattern(); + const output = filterIndexPatterns(mockIndexPatterns, 'detection'); + + expect(output).toEqual(mockIndexPatterns); + }); + + test('it returns filtered index patterns if list type is "endpoint"', () => { + const mockIndexPatterns = { + ...getMockIndexPattern(), + fields: [...fields, ...mockEndpointFields], + }; + const output = filterIndexPatterns(mockIndexPatterns, 'endpoint'); + + expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] }); + }); + }); + describe('#getOperatorType', () => { test('returns operator type "match" if entry.type is "match"', () => { const payload = getEntryMatchMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 43c3b6c082f1a..69ec3120a064b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -41,6 +41,7 @@ import { OsTypeArray, EntriesArray, osType, + ExceptionListType, } from '../../../shared_imports'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { validate } from '../../../../common/validate'; @@ -48,6 +49,19 @@ import { Ecs } from '../../../../common/ecs'; import { CodeSignature } from '../../../../common/ecs/file'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { addIdToItem, removeIdFromItem } from '../../../../common'; +import exceptionableFields from './exceptionable_fields.json'; + +export const filterIndexPatterns = ( + patterns: IIndexPattern, + type: ExceptionListType +): IIndexPattern => { + return type === 'endpoint' + ? { + ...patterns, + fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)), + } + : patterns; +}; export const addIdToEntries = (entries: EntriesArray): EntriesArray => { return entries.map((singleEntry) => { diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 032b500b45fb3..757191fdb54ec 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -58,8 +58,5 @@ export { UseExceptionListItemsSuccess, addEndpointExceptionList, withOptionalSignal, - BuilderEntryItem, - BuilderAndBadgeComponent, - BuilderEntryDeleteButtonComponent, - BuilderExceptionListItemComponent, + ExceptionBuilder, } from '../../lists/public'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 568a27be7a76f..276c438f97cc1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19716,16 +19716,6 @@ "xpack.securitySolution.exceptions.addException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。作成された例外は、シーケンスのすべてのイベントに適用されます。", "xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました", "xpack.securitySolution.exceptions.andDescription": "AND", - "xpack.securitySolution.exceptions.builder.addNestedDescription": "ネストされた条件を追加", - "xpack.securitySolution.exceptions.builder.addNonNestedDescription": "ネストされていない条件を追加", - "xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "ネストされたフィールドを検索", - "xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "検索", - "xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "検索フィールド値...", - "xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "リストを検索...", - "xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "演算子", - "xpack.securitySolution.exceptions.builder.fieldDescription": "フィールド", - "xpack.securitySolution.exceptions.builder.operatorDescription": "演算子", - "xpack.securitySolution.exceptions.builder.valueDescription": "値", "xpack.securitySolution.exceptions.cancelLabel": "キャンセル", "xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9000d481f0231..75b3937c3b384 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20011,16 +20011,6 @@ "xpack.securitySolution.exceptions.addException.sequenceWarning": "此规则的查询包含 EQL 序列语句。创建的例外将应用于序列中的所有事件。", "xpack.securitySolution.exceptions.addException.success": "已成功添加例外", "xpack.securitySolution.exceptions.andDescription": "AND", - "xpack.securitySolution.exceptions.builder.addNestedDescription": "添加嵌套条件", - "xpack.securitySolution.exceptions.builder.addNonNestedDescription": "添加非嵌套条件", - "xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "搜索嵌套字段", - "xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "搜索", - "xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "搜索字段值......", - "xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "搜索列表......", - "xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "运算符", - "xpack.securitySolution.exceptions.builder.fieldDescription": "字段", - "xpack.securitySolution.exceptions.builder.operatorDescription": "运算符", - "xpack.securitySolution.exceptions.builder.valueDescription": "值", "xpack.securitySolution.exceptions.cancelLabel": "取消", "xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", From 6b9ff48306e5fd4f9f0e3f58d8837abfcd6369e6 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Tue, 6 Apr 2021 14:51:15 -0400 Subject: [PATCH 09/65] [Lens] Fix Chart Switcher Icon Color Contrast (#96146) * add new lns prefixed classes to target icon colors * correct groupPosition prop on visual options * account for icon accent color contrast * switch to ternary operator Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/lens/public/app_plugin/app.scss | 8 +++++--- .../visual_options_popover/visual_options_popover.tsx | 2 +- .../lens/public/xy_visualization/xy_config_panel.tsx | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.scss b/x-pack/plugins/lens/public/app_plugin/app.scss index 8416577a60421..b2b63015deef3 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.scss +++ b/x-pack/plugins/lens/public/app_plugin/app.scss @@ -30,13 +30,15 @@ .lensChartIcon__subdued { fill: $euiTextSubduedColor; - // Not great, but the easiest way to fix the gray fill when stuck in a button with a fill - // Like when selected in a button group - .euiButton--fill & { + .lnsLayerChartSwitch__item-isSelected & { fill: currentColor; } } .lensChartIcon__accent { fill: $euiColorVis0; + + .lnsLayerChartSwitch__item-isSelected & { + fill: makeGraphicContrastColor($euiColorVis0, $euiColorDarkShade); + } } diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx index fcdef86cc5d0e..b8b89f146bdc0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx @@ -83,7 +83,7 @@ export const VisualOptionsPopover: React.FC = ({ defaultMessage: 'Visual options', })} type="visualOptions" - groupPosition="right" + groupPosition="left" buttonDataTestSubj="lnsVisualOptionsButton" isDisabled={isDisabled} > diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index d7868a17bf9db..6f3017b80be1c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -98,10 +98,13 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { defaultMessage: 'Chart type', })} name="chartType" - className="eui-displayInlineBlock" + className="eui-displayInlineBlock lnsLayerChartSwitch" options={visualizationTypes .filter((t) => isHorizontalSeries(t.id as SeriesType) === horizontalOnly) .map((t) => ({ + className: `lnsLayerChartSwitch__item ${ + layer.seriesType === t.id ? 'lnsLayerChartSwitch__item-isSelected' : '' + }`, id: t.id, label: t.label, iconType: t.icon || 'empty', From c18e3c8c3b95f89a139f07a038a2c2f358283038 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 14:17:50 -0500 Subject: [PATCH 10/65] Update dependency @elastic/charts to v28 (master) (#96163) --- package.json | 2 +- .../charts/public/static/components/current_time.tsx | 4 ++-- .../visualizations/views/timeseries/index.js | 4 ++-- .../public/components/xy_threshold_line.tsx | 4 ++-- .../components/alerting/chart_preview/index.tsx | 4 ++-- .../PageLoadDistribution/PercentileAnnotations.tsx | 4 ++-- .../components/shared/charts/timeseries_chart.tsx | 4 ++-- .../transaction_breakdown_chart_contents.tsx | 4 ++-- .../threshold_annotations.tsx | 4 ++-- .../expression_editor/criterion_preview_chart.tsx | 4 ++-- .../feature_importance/decision_path_chart.tsx | 4 ++-- .../pages/components/charts/common/anomalies.tsx | 12 ++++++------ .../charts/event_rate_chart/overlay_range.tsx | 4 ++-- .../public/alert_types/threshold/visualization.tsx | 4 ++-- .../threshold_watch_edit/watch_visualization.tsx | 4 ++-- yarn.lock | 8 ++++---- 16 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 4daa99b6150a0..d79df127a7d31 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "27.0.0", + "@elastic/charts": "28.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx index ea4cf1582c7c4..9cc261bf3ed86 100644 --- a/src/plugins/charts/public/static/components/current_time.tsx +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -9,7 +9,7 @@ import moment, { Moment } from 'moment'; import React, { FC } from 'react'; -import { LineAnnotation, AnnotationDomainTypes, LineAnnotationStyle } from '@elastic/charts'; +import { LineAnnotation, AnnotationDomainType, LineAnnotationStyle } from '@elastic/charts'; import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; @@ -46,7 +46,7 @@ export const CurrentTime: FC = ({ isDarkMode, domainEnd }) => diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index ab59d85d164a3..f9a52a9450dcb 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -16,7 +16,7 @@ import { Chart, Position, Settings, - AnnotationDomainTypes, + AnnotationDomainType, LineAnnotation, TooltipType, StackMode, @@ -163,7 +163,7 @@ export const TimeSeries = ({ } hideLinesTooltips={true} diff --git a/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx b/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx index 1a78e02540fe4..f28dbf726d287 100644 --- a/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_threshold_line.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; -import { AnnotationDomainTypes, LineAnnotation } from '@elastic/charts'; +import { AnnotationDomainType, LineAnnotation } from '@elastic/charts'; import { ThresholdLineConfig } from '../types'; @@ -32,7 +32,7 @@ export const XYThresholdLine: FC = ({ ({ dataValue: annotation['@timestamp'], header: asAbsoluteDateTime(annotation['@timestamp']), diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 23016cc5dd8e9..436eca4781502 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -6,7 +6,7 @@ */ import { - AnnotationDomainTypes, + AnnotationDomainType, AreaSeries, Axis, Chart, @@ -102,7 +102,7 @@ export function TransactionBreakdownChartContents({ {showAnnotations && ( ({ dataValue: annotation['@timestamp'], header: asAbsoluteDateTime(annotation['@timestamp']), diff --git a/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/threshold_annotations.tsx b/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/threshold_annotations.tsx index 5098eb2c23f5c..397d355eaeb5a 100644 --- a/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/threshold_annotations.tsx +++ b/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/threshold_annotations.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; import { first, last } from 'lodash'; -import { RectAnnotation, AnnotationDomainTypes, LineAnnotation } from '@elastic/charts'; +import { RectAnnotation, AnnotationDomainType, LineAnnotation } from '@elastic/charts'; import { Comparator, @@ -44,7 +44,7 @@ export const ThresholdAnnotations = ({ <> ({ dataValue: t, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 6a67a116dd58f..4e84cf0f9127c 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { ScaleType, - AnnotationDomainTypes, + AnnotationDomainType, Position, Axis, BarSeries, @@ -238,7 +238,7 @@ const CriterionPreviewChart: React.FC = ({ {showThreshold && threshold && threshold.value ? ( = ({ anomalyData }) => { = ({ overlayKey, start, end, color, showMar /> = ({ ); diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index 5b60a401e6a66..d3620692eae3e 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -7,7 +7,7 @@ import React, { Fragment, useContext, useEffect, useMemo } from 'react'; import { - AnnotationDomainTypes, + AnnotationDomainType, Axis, Chart, LineAnnotation, @@ -248,7 +248,7 @@ export const WatchVisualization = () => { ); diff --git a/yarn.lock b/yarn.lock index 3cb5ee24c480c..1a555fae61029 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,10 +1359,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@27.0.0": - version "27.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-27.0.0.tgz#cc6ea80dc90d07cfad0a932200cad2f6b217f7b8" - integrity sha512-gnLT+htGgcYzPUpa3NTBQyD8bw7t+0aAxdpVnBL7fZ0TdbX0xQ7u1yPEI9ljMbGguiVJMKoI1KMVLI49E3f1bg== +"@elastic/charts@28.0.0": + version "28.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-28.0.0.tgz#496a5b4041197b9d4750ca1d4ac3f6e3ff5756f6" + integrity sha512-aFO0J9BLUis5vD7g/m/Sb0Twj48yvm4f3bvmqk5d8RI+++VLW5qzyvyjiijMcHYHys6EuAs3vU3GaGlzx6TXig== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 4610764426ba6ea6d4b4be3aea2a9e381a307d6e Mon Sep 17 00:00:00 2001 From: James Rucker Date: Tue, 6 Apr 2021 12:19:23 -0700 Subject: [PATCH 11/65] Fix reauthenticate links for OneDrive and SharePoint (#96271) For both OneDrive and SharePoint we define a service_type that has an underscore in the middle (one_drive and share_point). This fixes the route definitions so sources of these connector types can be reauthenticated. --- .../public/applications/workplace_search/routes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 50f6596a860c5..9e514d7c73493 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -73,11 +73,11 @@ export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`; export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`; export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira_server`; -export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/onedrive`; +export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/one_drive`; export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce_sandbox`; export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`; -export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/sharepoint`; +export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/share_point`; export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; @@ -108,11 +108,11 @@ export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google_drive/edit`; export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_cloud/edit`; export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_server/edit`; -export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/onedrive/edit`; +export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one_drive/edit`; export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce_sandbox/edit`; export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`; -export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/sharepoint/edit`; +export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share_point/edit`; export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; From 0c33b1f90a23297529adf23d08aa5fe9f7245957 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 6 Apr 2021 12:45:18 -0700 Subject: [PATCH 12/65] skip flaky suite (#89477) --- 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 9726b097c8f62..1d65b9a68bd4d 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); - describe('saved queries saved objects', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/89477 + describe.skip('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); From 2ac6fc7ee21225e74e16e03735bbb3e434cf2ff4 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 6 Apr 2021 12:47:39 -0700 Subject: [PATCH 13/65] skip flaky suite (#95376) --- test/functional/apps/management/_runtime_fields.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index e3ff1819aed13..e2227d4240d40 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const testSubjects = getService('testSubjects'); - describe('runtime fields', function () { + // Failing: See https://github.com/elastic/kibana/issues/95376 + describe.skip('runtime fields', function () { this.tags(['skipFirefox']); before(async function () { From 32a691f440d0abcfe1c0b1ef5a41a33c575b050c Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 6 Apr 2021 12:49:30 -0700 Subject: [PATCH 14/65] skip flaky suite (#92522) --- test/functional/apps/dashboard/dashboard_filtering.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/dashboard_filtering.ts index e995bc4e52c49..86c57efec818b 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/dashboard_filtering.ts @@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); - describe('dashboard filtering', function () { + // Failing: See https://github.com/elastic/kibana/issues/92522 + describe.skip('dashboard filtering', function () { this.tags('includeFirefox'); const populateDashboard = async () => { From 8051fa91d8fc8938c9a14bd49423d66a87ce0be7 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 6 Apr 2021 15:47:58 -0500 Subject: [PATCH 15/65] [ML] Fix runtime mappings not copy-able in Transform wizard (#95996) * [ML] Fix transform runtime mappings not copy-able * [ML] Fix histogram not updating after change * [ML] Fix isRuntimeMappings imports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../transform/public/app/hooks/use_index_data.ts | 11 ++++++++--- .../advanced_runtime_mappings_settings.tsx | 5 ++++- .../hooks/use_advanced_runtime_mappings_editor.ts | 10 ++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index bb83de8e12004..36ba07afd69cd 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -23,6 +23,7 @@ import { useApi } from './use_api'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common'; +import { isRuntimeMappings } from '../../../common/shared_imports'; export const useIndexData = ( indexPattern: SearchItems['indexPattern'], @@ -120,8 +121,7 @@ export const useIndexData = ( from: pagination.pageIndex * pagination.pageSize, size: pagination.pageSize, ...(Object.keys(sort).length > 0 ? { sort } : {}), - ...(typeof combinedRuntimeMappings === 'object' && - Object.keys(combinedRuntimeMappings).length > 0 + ...(isRuntimeMappings(combinedRuntimeMappings) ? { runtime_mappings: combinedRuntimeMappings } : {}), }, @@ -189,7 +189,12 @@ export const useIndexData = ( } // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chartsVisible, indexPattern.title, JSON.stringify([query, dataGrid.visibleColumns])]); + }, [ + chartsVisible, + indexPattern.title, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings]), + ]); const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx index 9a026e839c731..277226c81c925 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx @@ -124,7 +124,10 @@ export const AdvancedRuntimeMappingsSettings: FC = (props) = - + {(copy: () => void) => ( Date: Tue, 6 Apr 2021 19:02:56 -0400 Subject: [PATCH 16/65] [APM] exceptions are encapsulated on Kibana logs(#96343) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/routes/create_api/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 13e70a2043cf0..87bc97d346984 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -120,6 +120,7 @@ export function createApi() { return response.ok({ body }); } catch (error) { + logger.error(error); const opts = { statusCode: 500, body: { From 43baacd246953a09103c9267a36807da1d280cf5 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 6 Apr 2021 21:06:49 -0500 Subject: [PATCH 17/65] Creating a Labs service for Presentation Solutions (#95435) --- .../server/collectors/management/schema.ts | 6 +- .../server/collectors/management/types.ts | 1 + src/plugins/presentation_util/common/index.ts | 2 + src/plugins/presentation_util/common/labs.ts | 68 +++++++++ src/plugins/presentation_util/kibana.json | 6 +- .../public/components/index.tsx | 6 + .../components/labs/environment_switch.tsx | 62 ++++++++ .../public/components/labs/labs.stories.tsx | 32 ++++ .../components/labs/labs_beaker_button.tsx | 48 ++++++ .../public/components/labs/labs_flyout.tsx | 138 ++++++++++++++++++ .../public/components/labs/project_list.tsx | 50 +++++++ .../components/labs/project_list_item.scss | 46 ++++++ .../labs/project_list_item.stories.tsx | 79 ++++++++++ .../components/labs/project_list_item.tsx | 102 +++++++++++++ .../presentation_util/public/i18n/index.ts | 9 ++ .../presentation_util/public/i18n/labs.tsx | 94 ++++++++++++ src/plugins/presentation_util/public/index.ts | 14 +- .../presentation_util/public/plugin.ts | 2 +- .../public/services/capabilities.ts | 13 ++ .../public/services/create/index.ts | 28 +++- .../public/services/create/provider.tsx | 6 +- .../public/services/create/registry.tsx | 24 ++- .../public/services/dashboards.ts | 20 +++ .../public/services/index.ts | 23 +-- .../public/services/kibana/capabilities.ts | 2 +- .../public/services/kibana/dashboards.ts | 2 +- .../public/services/kibana/index.ts | 9 +- .../public/services/kibana/labs.ts | 85 +++++++++++ .../presentation_util/public/services/labs.ts | 82 +++++++++++ .../public/services/storybook/capabilities.ts | 2 +- .../public/services/storybook/index.ts | 4 +- .../public/services/storybook/labs.ts | 53 +++++++ .../public/services/stub/capabilities.ts | 2 +- .../public/services/stub/dashboards.ts | 2 +- .../public/services/stub/index.ts | 4 +- .../public/services/stub/labs.ts | 70 +++++++++ src/plugins/presentation_util/public/types.ts | 3 + src/plugins/presentation_util/server/index.ts | 11 ++ .../presentation_util/server/plugin.ts | 23 +++ .../presentation_util/server/ui_settings.ts | 40 +++++ src/plugins/presentation_util/tsconfig.json | 16 +- src/plugins/telemetry/schema/oss_plugins.json | 6 + x-pack/plugins/canvas/kibana.json | 1 + x-pack/plugins/canvas/public/application.tsx | 13 +- x-pack/plugins/canvas/public/plugin.tsx | 2 + .../canvas/public/services/context.tsx | 2 + .../plugins/canvas/public/services/index.ts | 3 + x-pack/plugins/canvas/public/services/labs.ts | 29 ++++ .../canvas/public/services/stubs/index.ts | 2 + .../canvas/public/services/stubs/labs.ts | 15 ++ 50 files changed, 1294 insertions(+), 68 deletions(-) create mode 100644 src/plugins/presentation_util/common/labs.ts create mode 100644 src/plugins/presentation_util/public/components/labs/environment_switch.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/labs.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/labs_flyout.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/project_list.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/project_list_item.scss create mode 100644 src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/project_list_item.tsx create mode 100644 src/plugins/presentation_util/public/i18n/index.ts create mode 100644 src/plugins/presentation_util/public/i18n/labs.tsx create mode 100644 src/plugins/presentation_util/public/services/capabilities.ts create mode 100644 src/plugins/presentation_util/public/services/dashboards.ts create mode 100644 src/plugins/presentation_util/public/services/kibana/labs.ts create mode 100644 src/plugins/presentation_util/public/services/labs.ts create mode 100644 src/plugins/presentation_util/public/services/storybook/labs.ts create mode 100644 src/plugins/presentation_util/public/services/stub/labs.ts create mode 100644 src/plugins/presentation_util/server/index.ts create mode 100644 src/plugins/presentation_util/server/plugin.ts create mode 100644 src/plugins/presentation_util/server/ui_settings.ts create mode 100644 x-pack/plugins/canvas/public/services/labs.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/labs.ts diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index dc91181268be7..fcdd00380755f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -412,6 +412,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInspectEsQueries': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -428,7 +432,7 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInspectEsQueries': { + 'labs:presentation:unifiedToolbar': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 810f13931225f..613ada418c6e7 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -118,4 +118,5 @@ export interface UsageStats { 'banners:placement': string; 'banners:textColor': string; 'banners:backgroundColor': string; + 'labs:presentation:unifiedToolbar': boolean; } diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 8b556af07dd62..bf8819b13a92d 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -8,3 +8,5 @@ export const PLUGIN_ID = 'presentationUtil'; export const PLUGIN_NAME = 'presentationUtil'; + +export * from './labs'; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts new file mode 100644 index 0000000000000..65e42996ae910 --- /dev/null +++ b/src/plugins/presentation_util/common/labs.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 { i18n } from '@kbn/i18n'; + +export const UNIFIED_TOOLBAR = 'labs:presentation:unifiedToolbar'; + +export const projectIDs = [UNIFIED_TOOLBAR] as const; +export const environmentNames = ['kibana', 'browser', 'session'] as const; +export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; + +/** + * This is a list of active Labs Projects for the Presentation Team. It is the "source of truth" for all projects + * provided to users of our solutions in Kibana. + */ +export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { + [UNIFIED_TOOLBAR]: { + id: UNIFIED_TOOLBAR, + isActive: false, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectName', { + defaultMessage: 'Unified Toolbar', + }), + description: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectDescription', { + defaultMessage: 'Enable the new unified toolbar design for Presentation solutions', + }), + solutions: ['dashboard', 'canvas'], + }, +}; + +export type ProjectID = typeof projectIDs[number]; +export type EnvironmentName = typeof environmentNames[number]; +export type SolutionName = typeof solutionNames[number]; + +export type EnvironmentStatus = { + [env in EnvironmentName]?: boolean; +}; + +export type ProjectStatus = { + defaultValue: boolean; + isEnabled: boolean; + isOverride: boolean; +} & EnvironmentStatus; + +export interface ProjectConfig { + id: ProjectID; + name: string; + isActive: boolean; + environments: EnvironmentName[]; + description: string; + solutions: SolutionName[]; +} + +export type Project = ProjectConfig & { status: ProjectStatus }; + +export const getProjectIDs = () => projectIDs; + +export const isProjectEnabledByStatus = (active: boolean, status: EnvironmentStatus): boolean => { + // If the project is enabled by default, then any false flag will flip the switch, and vice-versa. + return active + ? Object.values(status).every((value) => value === true) + : Object.values(status).some((value) => value === true); +}; diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index b1b3d768c3e76..c7d272dcd02a1 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -2,8 +2,10 @@ "id": "presentationUtil", "version": "1.0.0", "kibanaVersion": "kibana", - "server": false, + "server": true, "ui": true, - "requiredPlugins": ["savedObjects"], + "requiredPlugins": [ + "savedObjects" + ], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx index 1aff029bae846..af806e1c22f1a 100644 --- a/src/plugins/presentation_util/public/components/index.tsx +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -25,6 +25,12 @@ export const withSuspense =

( ); +export const LazyLabsBeakerButton = withSuspense( + React.lazy(() => import('./labs/labs_beaker_button')) +); + +export const LazyLabsFlyout = withSuspense(React.lazy(() => import('./labs/labs_flyout'))); + export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); export const LazySavedObjectSaveModalDashboard = React.lazy( diff --git a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx new file mode 100644 index 0000000000000..0acdd433cbac8 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiIconTip, + EuiSpacer, + EuiScreenReaderOnly, +} from '@elastic/eui'; + +import { EnvironmentName } from '../../../common/labs'; +import { LabsStrings } from '../../i18n'; + +const { Switch: strings } = LabsStrings.Components; + +const switchText: { [env in EnvironmentName]: { name: string; help: string } } = { + kibana: strings.getKibanaSwitchText(), + browser: strings.getBrowserSwitchText(), + session: strings.getSessionSwitchText(), +}; + +export interface Props { + env: EnvironmentName; + isChecked: boolean; + onChange: (checked: boolean) => void; + name: string; +} + +export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => ( + + + + + + {name} - + + {switchText[env].name} + + } + onChange={(e) => onChange(e.target.checked)} + compressed + /> + + + + + + + +); diff --git a/src/plugins/presentation_util/public/components/labs/labs.stories.tsx b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx new file mode 100644 index 0000000000000..a9a1a0753d24b --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { LabsBeakerButton } from './labs_beaker_button'; +import { LabsFlyout } from './labs_flyout'; + +export default { + title: 'Labs/Flyout', + description: + 'A set of components used for providing Labs controls and projects in another solution.', + argTypes: {}, +}; + +export function BeakerButton() { + return ; +} + +export function Flyout() { + return ; +} + +export function EmptyFlyout() { + return ; +} diff --git a/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx new file mode 100644 index 0000000000000..6d7fd4afdac68 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiIcon, EuiNotificationBadge, EuiButtonProps } from '@elastic/eui'; + +import { pluginServices } from '../../services'; +import { LabsFlyout, Props as FlyoutProps } from './labs_flyout'; + +export type Props = EuiButtonProps & Pick; + +export const LabsBeakerButton = ({ solutions, ...props }: Props) => { + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects } = labsService.useService(); + const [isOpen, setIsOpen] = useState(false); + + const projects = getProjects(); + + const [overrideCount, onEnabledCountChange] = useState( + Object.values(projects).filter((project) => project.status.isOverride).length + ); + + const onButtonClick = () => setIsOpen((open) => !open); + const onClose = () => setIsOpen(false); + + return ( + <> + + + {overrideCount > 0 ? ( + + {overrideCount} + + ) : null} + + {isOpen ? : null} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsBeakerButton; diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx new file mode 100644 index 0000000000000..562d3b291a4b3 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactNode, useRef, useState, useEffect } from 'react'; +import { + EuiFlyout, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiButton, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiIcon, +} from '@elastic/eui'; + +import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; +import { pluginServices } from '../../services'; +import { LabsStrings } from '../../i18n'; + +import { ProjectList } from './project_list'; + +const { Flyout: strings } = LabsStrings.Components; + +export interface Props { + onClose: () => void; + solutions?: SolutionName[]; + onEnabledCountChange?: (overrideCount: number) => void; +} + +const hasStatusChanged = ( + original: Record, + current: Record +): boolean => { + for (const id of Object.keys(original) as ProjectID[]) { + for (const key of Object.keys(original[id].status) as Array) { + if (original[id].status[key] !== current[id].status[key]) { + return true; + } + } + } + return false; +}; + +export const getOverridenCount = (projects: Record) => + Object.values(projects).filter((project) => project.status.isOverride).length; + +export const LabsFlyout = (props: Props) => { + const { solutions, onEnabledCountChange = () => {}, onClose } = props; + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects, setProjectStatus, reset } = labsService.useService(); + + const [projects, setProjects] = useState(getProjects()); + const [overrideCount, setOverrideCount] = useState(getOverridenCount(projects)); + const initialStatus = useRef(getProjects()); + + const isChanged = hasStatusChanged(initialStatus.current, projects); + + useEffect(() => { + setOverrideCount(getOverridenCount(projects)); + }, [projects]); + + useEffect(() => { + onEnabledCountChange(overrideCount); + }, [onEnabledCountChange, overrideCount]); + + const onStatusChange = (id: ProjectID, env: EnvironmentName, enabled: boolean) => { + setProjectStatus(id, env, enabled); + setProjects(getProjects()); + }; + + let footer: ReactNode = null; + + const resetButton = ( + { + reset(); + setProjects(getProjects()); + }} + isDisabled={!overrideCount} + > + {strings.getResetToDefaultLabel()} + + ); + + const refreshButton = ( + { + window.location.reload(); + }} + isDisabled={!isChanged} + > + {strings.getRefreshLabel()} + + ); + + footer = ( + + + {resetButton} + {refreshButton} + + + ); + + return ( + + + +

+ + + + + {strings.getTitleLabel()} + +

+ + + + + + {footer} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsFlyout; diff --git a/src/plugins/presentation_util/public/components/labs/project_list.tsx b/src/plugins/presentation_util/public/components/labs/project_list.tsx new file mode 100644 index 0000000000000..4ecf45409b02c --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiCallOut } from '@elastic/eui'; + +import { SolutionName, ProjectID, Project } from '../../../common'; +import { ProjectListItem, Props as ProjectListItemProps } from './project_list_item'; + +import { LabsStrings } from '../../i18n'; + +const { List: strings } = LabsStrings.Components; + +export interface Props { + solutions?: SolutionName[]; + projects: Record; + onStatusChange: ProjectListItemProps['onStatusChange']; +} + +const EmptyList = () => ; + +export const ProjectList = (props: Props) => { + const { solutions, projects, onStatusChange } = props; + + const items = Object.values(projects) + .map((project) => { + // Filter out any panels that don't match the solutions filter, (if provided). + if (solutions && !solutions.some((solution) => project.solutions.includes(solution))) { + return null; + } + + return ( +
  • + +
  • + ); + }) + .filter((item) => item !== null); + + return ( + + {items.length > 0 ?
      {items}
    : } +
    + ); +}; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.scss b/src/plugins/presentation_util/public/components/labs/project_list_item.scss new file mode 100644 index 0000000000000..c91a07576b314 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.scss @@ -0,0 +1,46 @@ +.projectListItem { + position: relative; + background: $euiColorEmptyShade; + padding: $euiSizeL; + min-width: 500px; + + &--isOverridden:before { + position: absolute; + top: $euiSizeL; + left: 4px; + bottom: $euiSizeL; + width: 4px; + background: $euiColorPrimary; + content: ''; + } + + .euiSwitch__label { + width: 100%; + } +} + +.projectListItem + .projectListItem:after { + position: absolute; + top: 0; + right: 0; + left: 0; + height: 1px; + background: $euiColorLightShade; + content: ''; +} + +.euiFlyout .projectListItem { + padding: $euiSizeL $euiSizeXS; + + &:first-child { + padding-top: 0; + } + + &--isOverridden:before { + left: -12px; + } + + &--isOverridden:first-child:before { + top: 0; + } +} diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx new file mode 100644 index 0000000000000..ce93abded521e --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { mapValues } from 'lodash'; + +import { EnvironmentStatus, ProjectConfig, ProjectID, ProjectStatus } from '../../../common'; +import { applyProjectStatus } from '../../services/labs'; +import { ProjectListItem, Props } from './project_list_item'; + +import { projects as projectConfigs } from '../../../common'; +import { ProjectList } from './project_list'; + +export default { + title: 'Labs/ProjectList', + description: 'A set of controls for displaying and manipulating projects.', +}; + +const projects = mapValues(projectConfigs, (project) => + applyProjectStatus(project, { kibana: false, session: false, browser: false }) +); + +export function List() { + return ; +} + +export function EmptyList() { + return ; +} + +export const ListItem = ( + props: Pick< + Props['project'], + 'description' | 'isActive' | 'name' | 'solutions' | 'environments' + > & + Omit +) => { + const { kibana, browser, session, ...rest } = props; + const status: EnvironmentStatus = { kibana, browser, session }; + const projectConfig: ProjectConfig = { + ...rest, + id: 'storybook:component' as ProjectID, + }; + + return ( +
    + ({ ...status, [env]: enabled })} + /> +
    + ); +}; + +ListItem.args = { + isActive: false, + name: 'Demo Project', + description: 'This is a demo project, and this is the description of the demo project.', + kibana: false, + browser: false, + session: false, + solutions: ['dashboard', 'canvas'], + environments: ['kibana', 'browser', 'session'], +}; + +ListItem.argTypes = { + environments: { + control: { + type: 'check', + options: ['kibana', 'browser', 'session'], + }, + }, +}; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx new file mode 100644 index 0000000000000..e4aa1abd3693c --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiTitle, + EuiText, + EuiFormFieldset, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import classnames from 'classnames'; + +import { ProjectID, EnvironmentName, Project, environmentNames } from '../../../common/labs'; +import { EnvironmentSwitch } from './environment_switch'; + +import { LabsStrings } from '../../i18n'; +const { ListItem: strings } = LabsStrings.Components; + +import './project_list_item.scss'; + +export interface Props { + project: Project; + onStatusChange: (id: ProjectID, env: EnvironmentName, enabled: boolean) => void; +} + +export const ProjectListItem = ({ project, onStatusChange }: Props) => { + const { id, status, isActive, name, description, solutions } = project; + const { isEnabled, isOverride } = status; + + return ( + + + + + + +

    {name}

    +
    +
    + +
    + {solutions.map((solution) => ( + {solution} + ))} +
    +
    + + {description} + + + + {isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()} + + +
    +
    + + + + {name} + + {strings.getOverrideLegend()} + + ), + }} + > + {environmentNames.map((env) => { + const envStatus = status[env]; + if (envStatus !== undefined) { + return ( + onStatusChange(id, env, checked)} + {...{ env, name }} + /> + ); + } + })} + + +
    +
    + ); +}; diff --git a/src/plugins/presentation_util/public/i18n/index.ts b/src/plugins/presentation_util/public/i18n/index.ts new file mode 100644 index 0000000000000..cf2f2c111ad58 --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/index.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 './labs'; diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx new file mode 100644 index 0000000000000..ddf6346bd68ca --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const LabsStrings = { + Components: { + Switch: { + getKibanaSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.kibanaSwitchName', { + defaultMessage: 'Kibana', + }), + help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { + defaultMessage: 'Sets the corresponding Advanced Setting for this lab project in Kibana', + }), + }), + getBrowserSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.browserSwitchName', { + defaultMessage: 'Browser', + }), + help: i18n.translate('presentationUtil.labs.components.browserSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for the browser; persists between browser instances', + }), + }), + getSessionSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.sessionSwitchName', { + defaultMessage: 'Session', + }), + help: i18n.translate('presentationUtil.labs.components.sessionSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for this tab; resets when the browser tab is closed', + }), + }), + }, + List: { + getNoProjectsMessage: () => + i18n.translate('presentationUtil.labs.components.noProjectsMessage', { + defaultMessage: 'No available lab projects', + }), + }, + ListItem: { + getOverrideLegend: () => + i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { + defaultMessage: 'Override flags', + }), + getEnabledStatusMessage: () => ( + Enabled, + }} + description="Displays the current status of a lab project" + /> + ), + getDisabledStatusMessage: () => ( + Disabled, + }} + description="Displays the current status of a lab project" + /> + ), + }, + Flyout: { + getTitleLabel: () => + i18n.translate('presentationUtil.labs.components.titleLabel', { + defaultMessage: 'Lab projects', + }), + getResetToDefaultLabel: () => + i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { + defaultMessage: 'Reset to defaults', + }), + getLabFlagsLabel: () => + i18n.translate('presentationUtil.labs.components.labFlagsLabel', { + defaultMessage: 'Lab flags', + }), + getRefreshLabel: () => + i18n.translate('presentationUtil.labs.components.calloutHelp', { + defaultMessage: 'Refresh to apply changes', + }), + }, + }, +}; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index 457f167dd8228..1cbf4b5a4f334 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -8,12 +8,18 @@ import { PresentationUtilPlugin } from './plugin'; -export { LazyDashboardPicker, LazySavedObjectSaveModalDashboard, withSuspense } from './components'; - +export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; export { SaveModalDashboardProps } from './components/types'; +export { projectIDs, ProjectID, Project } from '../common/labs'; + +export { + LazyLabsBeakerButton, + LazyLabsFlyout, + LazyDashboardPicker, + LazySavedObjectSaveModalDashboard, + withSuspense, +} from './components'; export function plugin() { return new PresentationUtilPlugin(); } - -export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 6f74198bb56ab..00931c5730fe3 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -36,9 +36,9 @@ export class PresentationUtilPlugin startPlugins: PresentationUtilPluginStartDeps ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - return { ContextProvider: pluginServices.getContextProvider(), + labsService: pluginServices.getServices().labs, }; } diff --git a/src/plugins/presentation_util/public/services/capabilities.ts b/src/plugins/presentation_util/public/services/capabilities.ts new file mode 100644 index 0000000000000..58d56d1a4d81d --- /dev/null +++ b/src/plugins/presentation_util/public/services/capabilities.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 interface PresentationCapabilitiesService { + canAccessDashboards: () => boolean; + canCreateNewDashboards: () => boolean; + canSaveVisualizations: () => boolean; +} diff --git a/src/plugins/presentation_util/public/services/create/index.ts b/src/plugins/presentation_util/public/services/create/index.ts index 66f7185913323..163e25e26babf 100644 --- a/src/plugins/presentation_util/public/services/create/index.ts +++ b/src/plugins/presentation_util/public/services/create/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { mapValues } from 'lodash'; - import { PluginServiceRegistry } from './registry'; export { PluginServiceRegistry } from './registry'; @@ -18,6 +16,8 @@ export { KibanaPluginServiceParams, } from './factory'; +type ServiceHooks = { [K in keyof Services]: { useService: () => Services[K] } }; + /** * `PluginServices` is a top-level class for specifying and accessing services within a plugin. * @@ -70,13 +70,27 @@ export class PluginServices { /** * Return a map of React Hooks that can be used in React components. */ - getHooks(): { [K in keyof Services]: { useService: () => Services[K] } } { + getHooks(): ServiceHooks { const registry = this.getRegistry(); const providers = registry.getServiceProviders(); - // @ts-expect-error Need to fix this; the type isn't fully understood when inferred. - return mapValues(providers, (provider) => ({ - useService: provider.getUseServiceHook(), - })); + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = { useService: providers[providerName].getServiceReactHook() }; + return acc; + }, {} as ServiceHooks); + } + + getServices(): Services { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = providers[providerName].getService(); + return acc; + }, {} as Services); } } diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index fa16e291a656d..06590bcfbb3d0 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -41,9 +41,9 @@ export class PluginServiceProvider { } /** - * Private getter that will enforce proper setup throughout the class. + * Getter that will enforce proper setup throughout the class. */ - private getService() { + public getService() { if (!this.pluginService) { throw new Error('Service not started'); } @@ -62,7 +62,7 @@ export class PluginServiceProvider { /** * Returns a function for providing a Context hook for the service. */ - getUseServiceHook() { + getServiceReactHook() { return () => { const service = useContext(this.context); diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index 61ada16e241a5..e8f85666bcac4 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { values } from 'lodash'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; /** @@ -47,16 +46,17 @@ export class PluginServiceRegistry { * Returns a React Context Provider for use in consuming applications. */ getContextProvider() { + const values = Object.values(this.getServiceProviders()) as Array< + PluginServiceProvider + >; + // Collect and combine Context.Provider elements from each Service Provider into a single // Functional Component. const provider: React.FC = ({ children }) => ( <> - {values>(this.getServiceProviders()).reduceRight( - (acc, serviceProvider) => { - return {acc}; - }, - children - )} + {values.reduceRight((acc, serviceProvider) => { + return {acc}; + }, children)} ); @@ -69,9 +69,8 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - values>(this.providers).map((serviceProvider) => - serviceProvider.start(params) - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].start(params)); this._isStarted = true; return this; } @@ -80,9 +79,8 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - values>(this.providers).map((serviceProvider) => - serviceProvider.stop() - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].stop()); this._isStarted = false; return this; } diff --git a/src/plugins/presentation_util/public/services/dashboards.ts b/src/plugins/presentation_util/public/services/dashboards.ts new file mode 100644 index 0000000000000..cbca79223063b --- /dev/null +++ b/src/plugins/presentation_util/public/services/dashboards.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 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 { SimpleSavedObject } from 'src/core/public'; +import { PartialDashboardAttributes } from './kibana/dashboards'; + +export interface PresentationDashboardsService { + findDashboards: ( + query: string, + fields: string[] + ) => Promise>>; + findDashboardsByTitle: ( + title: string + ) => Promise>>; +} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 39dae92aa2ba9..c01a95f64619c 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -6,29 +6,14 @@ * Side Public License, v 1. */ -import { SimpleSavedObject } from 'src/core/public'; import { PluginServices } from './create'; -import { PartialDashboardAttributes } from './kibana/dashboards'; - -export interface PresentationDashboardsService { - findDashboards: ( - query: string, - fields: string[] - ) => Promise>>; - findDashboardsByTitle: ( - title: string - ) => Promise>>; -} - -export interface PresentationCapabilitiesService { - canAccessDashboards: () => boolean; - canCreateNewDashboards: () => boolean; - canSaveVisualizations: () => boolean; -} - +import { PresentationCapabilitiesService } from './capabilities'; +import { PresentationDashboardsService } from './dashboards'; +import { PresentationLabsService } from './labs'; export interface PresentationUtilServices { dashboards: PresentationDashboardsService; capabilities: PresentationCapabilitiesService; + labs: PresentationLabsService; } export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index 6949fba00c65a..d46af31b30667 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts index 8735fe7fe2668..59e3ada10a869 100644 --- a/src/plugins/presentation_util/public/services/kibana/dashboards.ts +++ b/src/plugins/presentation_util/public/services/kibana/dashboards.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; export type DashboardsServiceFactory = KibanaPluginServiceFactory< PresentationDashboardsService, diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts index 75388a71d14ca..880f0f8b49c76 100644 --- a/src/plugins/presentation_util/public/services/kibana/index.ts +++ b/src/plugins/presentation_util/public/services/kibana/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, KibanaPluginServiceParams, @@ -17,15 +18,17 @@ import { import { PresentationUtilPluginStartDeps } from '../../types'; import { PresentationUtilServices } from '..'; -export { dashboardsServiceFactory } from './dashboards'; export { capabilitiesServiceFactory } from './capabilities'; +export { dashboardsServiceFactory } from './dashboards'; +export { labsServiceFactory } from './labs'; export const providers: PluginServiceProviders< PresentationUtilServices, KibanaPluginServiceParams > = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), }; export const registry = new PluginServiceRegistry< diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts new file mode 100644 index 0000000000000..d2c0735c76eeb --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + environmentNames, + EnvironmentName, + projectIDs, + projects, + ProjectID, + Project, + getProjectIDs, +} from '../../../common'; +import { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { + PresentationLabsService, + isEnabledByStorageValue, + setStorageStatus, + setUISettingsStatus, + applyProjectStatus, +} from '../labs'; + +export type LabsServiceFactory = KibanaPluginServiceFactory< + PresentationLabsService, + PresentationUtilPluginStartDeps +>; + +export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { + const { uiSettings } = coreStart; + const localStorage = window.localStorage; + const sessionStorage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isEnabledByStorageValue(project, 'kibana', uiSettings.get(id, project.isActive)), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, status: boolean) => { + switch (env) { + case 'session': + setStorageStatus(sessionStorage, name, status); + break; + case 'browser': + setStorageStatus(localStorage, name, status); + break; + case 'kibana': + setUISettingsStatus(uiSettings, name, status); + break; + } + }; + + const reset = () => { + localStorage.clear(); + sessionStorage.clear(); + environmentNames.forEach((env) => + projectIDs.forEach((id) => setProjectStatus(id, env, projects[id].isActive)) + ); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts new file mode 100644 index 0000000000000..72e9a232ea976 --- /dev/null +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -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 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 { IUiSettingsClient } from 'kibana/public'; +import { + EnvironmentName, + projectIDs, + Project, + ProjectConfig, + ProjectID, + EnvironmentStatus, + environmentNames, + isProjectEnabledByStatus, +} from '../../common'; + +export interface PresentationLabsService { + getProjectIDs: () => typeof projectIDs; + getProject: (id: ProjectID) => Project; + getProjects: () => Record; + setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; + reset: () => void; +} + +export const isEnabledByStorageValue = ( + project: ProjectConfig, + environment: EnvironmentName, + value: string | boolean | null +): boolean => { + const defaultValue = project.isActive; + + if (!project.environments.includes(environment)) { + return defaultValue; + } + + if (value === true || value === false) { + return value; + } + + if (value === 'enabled') { + return true; + } + + if (value === 'disabled') { + return false; + } + + return defaultValue; +}; + +export const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => + storage.setItem(id, enabled ? 'enabled' : 'disabled'); + +export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { + const { isActive, environments } = project; + + environmentNames.forEach((name) => { + if (!environments.includes(name)) { + delete status[name]; + } + }); + + const isEnabled = isProjectEnabledByStatus(isActive, status); + const isOverride = isEnabled !== isActive; + + return { + ...project, + status: { + ...status, + defaultValue: isActive, + isEnabled, + isOverride, + }, + }; +}; + +export const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => + client.set(id, enabled); diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index 16fbe3baf488f..60285f00993ab 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -8,7 +8,7 @@ import { PluginServiceFactory } from '../create'; import { StorybookParams } from '.'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index dd7de54264062..37669d52c0096 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -8,6 +8,7 @@ import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create'; import { dashboardsServiceFactory } from '../stub/dashboards'; +import { labsServiceFactory } from './labs'; import { capabilitiesServiceFactory } from './capabilities'; import { PresentationUtilServices } from '..'; @@ -22,8 +23,9 @@ export interface StorybookParams { } export const providers: PluginServiceProviders = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts new file mode 100644 index 0000000000000..8878e218f19e8 --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EnvironmentName, projectIDs, Project } from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { projects, ProjectID, getProjectIDs } from '../../../common'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const storage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const { isActive } = project; + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isActive, + }; + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, enabled: boolean) => { + if (env === 'session') { + storage.setItem(name, enabled ? 'enabled' : 'disabled'); + } + }; + + const reset = () => { + storage.clear(); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 4154fa65a0cd7..80b913c4f0856 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory; diff --git a/src/plugins/presentation_util/public/services/stub/dashboards.ts b/src/plugins/presentation_util/public/services/stub/dashboards.ts index 280ae9582b815..047176836896b 100644 --- a/src/plugins/presentation_util/public/services/stub/dashboards.ts +++ b/src/plugins/presentation_util/public/services/stub/dashboards.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; // TODO (clint): Create set of dashboards to stub and return. diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts index d1a8147f8fb8c..6bf32bba00a3e 100644 --- a/src/plugins/presentation_util/public/services/stub/index.ts +++ b/src/plugins/presentation_util/public/services/stub/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { PresentationUtilServices } from '..'; @@ -17,6 +18,7 @@ export { capabilitiesServiceFactory } from './capabilities'; export const providers: PluginServiceProviders = { dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts new file mode 100644 index 0000000000000..c83bb68b5d072 --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/labs.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 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 { + projects, + projectIDs, + ProjectID, + EnvironmentName, + getProjectIDs, + Project, +} from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const reset = () => + projectIDs.reduce((acc, id) => { + const project = getProject(id); + const defaultValue = project.isActive; + + acc[id] = { + defaultValue, + session: null, + browser: null, + kibana: defaultValue, + }; + return acc; + }, {} as { [id in ProjectID]: { defaultValue: boolean; session: boolean | null; browser: boolean | null; kibana: boolean } }); + + let statuses = reset(); + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const value = statuses[id]; + const status = { + session: isEnabledByStorageValue(project, 'session', value.session), + browser: isEnabledByStorageValue(project, 'browser', value.browser), + kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { + statuses[id] = { ...statuses[id], [env]: value }; + }; + + return { + getProjectIDs, + getProject, + getProjects, + setProjectStatus, + reset: () => { + statuses = reset(); + }, + }; +}; diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index f1bd6c1b747eb..05779ffb206c4 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import { PresentationLabsService } from './services/labs'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginStart { ContextProvider: React.FC; + labsService: PresentationLabsService; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/presentation_util/server/index.ts b/src/plugins/presentation_util/server/index.ts new file mode 100644 index 0000000000000..de7e8de405442 --- /dev/null +++ b/src/plugins/presentation_util/server/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { PresentationUtilPlugin } from './plugin'; + +export const plugin = () => new PresentationUtilPlugin(); diff --git a/src/plugins/presentation_util/server/plugin.ts b/src/plugins/presentation_util/server/plugin.ts new file mode 100644 index 0000000000000..eb55373920625 --- /dev/null +++ b/src/plugins/presentation_util/server/plugin.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { CoreSetup, Plugin } from 'kibana/server'; +import { getUISettings } from './ui_settings'; + +export class PresentationUtilPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUISettings()); + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/presentation_util/server/ui_settings.ts b/src/plugins/presentation_util/server/ui_settings.ts new file mode 100644 index 0000000000000..450354832c3ac --- /dev/null +++ b/src/plugins/presentation_util/server/ui_settings.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { projects, projectIDs, ProjectID } from '../common'; + +export const SETTING_CATEGORY = 'Presentation Labs'; + +const labsProjectSettings: Record> = projectIDs.reduce( + (acc, id) => { + const project = projects[id]; + const { name, description, isActive: value } = project; + acc[id] = { + name, + value, + type: 'boolean', + description, + schema: schema.boolean(), + requiresPageReload: true, + category: [SETTING_CATEGORY], + }; + return acc; + }, + {} as { + [id in ProjectID]: UiSettingsParams; + } +); + +/** + * uiSettings definitions for Presentation Util. + */ +export const getUISettings = (): Record> => ({ + ...labsProjectSettings, +}); diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index 37b9380f6f2b9..63d136cf9445a 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -7,9 +7,19 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "storybook/**/*", + "../../../typings/**/*" + ], "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../saved_objects/tsconfig.json" }, + { + "path": "../../core/tsconfig.json" + }, + { + "path": "../saved_objects/tsconfig.json" + }, ] } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 05ac1eb84089d..d8bcf150ac167 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8137,6 +8137,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "labs:presentation:unifiedToolbar": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index c14e8340957ad..cff1a3e7fa8b7 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -13,6 +13,7 @@ "expressions", "features", "inspector", + "presentationUtil", "uiActions" ], "optionalPlugins": [ diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 66b02bdc16408..f910aff9a83fe 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -56,17 +56,20 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const { presentationUtil } = plugins; element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); ReactDOM.render( - - - - - + + + + + + + , element diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 6871c8d98b8f5..486cd03eb9dd6 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -27,6 +27,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -51,6 +52,7 @@ export interface CanvasStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } /** diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 6e74b5ac98621..3865d98caf2b3 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -35,6 +35,7 @@ export const useEmbeddablesService = () => useServices().embeddables; export const useExpressionsService = () => useServices().expressions; export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; +export const useLabsService = () => useServices().labs; export const withServices = (type: ComponentType) => { const EnhancedType: FC = (props) => @@ -53,6 +54,7 @@ export const ServicesProvider: FC<{ notify: specifiedProviders.notify.getService(), platform: specifiedProviders.platform.getService(), navLink: specifiedProviders.navLink.getService(), + labs: specifiedProviders.labs.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 7452352fc0ef4..9bfc41a782edc 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -13,6 +13,7 @@ import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; +import { labsServiceFactory } from './labs'; export { NotifyService } from './notify'; export { PlatformService } from './platform'; @@ -78,6 +79,7 @@ export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), + labs: new CanvasServiceProvider(labsServiceFactory), }; export type CanvasServiceProviders = typeof services; @@ -88,6 +90,7 @@ export interface CanvasServices { notify: ServiceFromProvider; platform: ServiceFromProvider; navLink: ServiceFromProvider; + labs: ServiceFromProvider; } export const startServices = async ( diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/labs.ts new file mode 100644 index 0000000000000..9bc4bea3e35c3 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/labs.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 { + projectIDs, + Project, + ProjectID, +} from '../../../../../src/plugins/presentation_util/public'; + +import { CanvasServiceFactory } from '.'; + +export interface CanvasLabsService { + getProject: (id: ProjectID) => Project; + getProjects: () => Record; +} + +export const labsServiceFactory: CanvasServiceFactory = async ( + _coreSetup, + _coreStart, + _setupPlugins, + startPlugins +) => ({ + projectIDs, + ...startPlugins.presentationUtil.labsService, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 2565445af2db2..91bda2556284e 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -10,6 +10,7 @@ import { embeddablesService } from './embeddables'; import { expressionsService } from './expressions'; import { navLinkService } from './nav_link'; import { notifyService } from './notify'; +import { labsService } from './labs'; import { platformService } from './platform'; export const stubs: CanvasServices = { @@ -18,6 +19,7 @@ export const stubs: CanvasServices = { navLink: navLinkService, notify: notifyService, platform: platformService, + labs: labsService, }; export const startServices = async (providedServices: Partial = {}) => { diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts new file mode 100644 index 0000000000000..52168ebeb6f80 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/labs.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CanvasLabsService } from '../labs'; + +const noop = (..._args: any[]): any => {}; + +export const labsService: CanvasLabsService = { + getProject: noop, + getProjects: noop, +}; From 532f38f09156d14ba61a99596b5d1e8a069d8707 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 7 Apr 2021 03:46:47 +0100 Subject: [PATCH 18/65] skip flaky suite (#96362) --- .../public/cases/components/user_action_tree/index.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index 056add32add82..a5c6b2d50f4a2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -40,7 +40,8 @@ jest.mock('../../containers/use_update_comment'); jest.mock('./user_action_timestamp'); const patchComment = jest.fn(); -describe('UserActionTree ', () => { +// FLAKY: https://github.com/elastic/kibana/issues/96362 +describe.skip('UserActionTree ', () => { const sampleData = { content: 'what a great comment update', }; From 98fdf7fb847bbcb5e3c14237bd0aecfc910a217b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Apr 2021 10:59:54 +0200 Subject: [PATCH 19/65] Update dependency @elastic/charts to v28.0.1 (#96356) 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 d79df127a7d31..bb383e986e721 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "28.0.0", + "@elastic/charts": "28.0.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", diff --git a/yarn.lock b/yarn.lock index 1a555fae61029..4fba8dc85a09e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,10 +1359,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@28.0.0": - version "28.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-28.0.0.tgz#496a5b4041197b9d4750ca1d4ac3f6e3ff5756f6" - integrity sha512-aFO0J9BLUis5vD7g/m/Sb0Twj48yvm4f3bvmqk5d8RI+++VLW5qzyvyjiijMcHYHys6EuAs3vU3GaGlzx6TXig== +"@elastic/charts@28.0.1": + version "28.0.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-28.0.1.tgz#615f393dc620304fb6cdbc3f6eaf2d6c53e39236" + integrity sha512-uuo7mWTYU4/rdg1a7hxRnNJz7Zjt/u18YwNV4D2SPvBqCDsNxtdRpiF+nLWFDIvBGoAFIGmHIv3cn88Y9dKqdg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 011b234d15d07d2a1ce04ac63ead888be6a819fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 7 Apr 2021 11:16:12 +0200 Subject: [PATCH 20/65] Added readonly action in cold phase (#96036) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 1 + .../edit_policy/features/searchable_snapshots.test.ts | 6 +++++- .../serialization/policy_serialization.test.ts | 2 ++ .../index_lifecycle_management/common/types/policies.ts | 1 + .../components/phases/cold_phase/cold_phase.tsx | 4 ++++ .../components/phases/shared_fields/readonly_field.tsx | 2 +- .../searchable_snapshot_field.tsx | 2 +- .../components/phases/warm_phase/warm_phase.tsx | 2 +- .../sections/edit_policy/form/deserializer.ts | 1 + .../edit_policy/form/deserializer_and_serializer.test.ts | 9 +++++++++ .../application/sections/edit_policy/form/schema.ts | 4 ++++ .../sections/edit_policy/form/serializer/serializer.ts | 9 +++++++++ .../public/application/sections/edit_policy/types.ts | 1 + 13 files changed, 40 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 122fb83edab45..12de34b79ee12 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -380,6 +380,7 @@ export const setup = async (arg?: { setReplicas: setReplicas('cold'), setFreeze: createSetFreeze('cold'), freezeExists: createFreezeExists('cold'), + ...createReadonlyActions('cold'), hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts index a570c817cfe1b..e21793e650683 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -36,7 +36,7 @@ describe(' searchable snapshots', () => { component.update(); }); - test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => { + test('enabling searchable snapshot should hide force merge, freeze, readonly and shrink in subsequent phases', async () => { const { actions } = testBed; await actions.warm.enable(true); @@ -44,16 +44,20 @@ describe(' searchable snapshots', () => { expect(actions.warm.forceMergeFieldExists()).toBeTruthy(); expect(actions.warm.shrinkExists()).toBeTruthy(); + expect(actions.warm.readonlyExists()).toBeTruthy(); expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); expect(actions.cold.freezeExists()).toBeTruthy(); + expect(actions.cold.readonlyExists()).toBeTruthy(); await actions.hot.setSearchableSnapshot('my-repo'); expect(actions.warm.forceMergeFieldExists()).toBeFalsy(); expect(actions.warm.shrinkExists()).toBeFalsy(); + expect(actions.warm.readonlyExists()).toBeFalsy(); // searchable snapshot in cold is still visible expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); expect(actions.cold.freezeExists()).toBeFalsy(); + expect(actions.cold.readonlyExists()).toBeFalsy(); }); test('disabling rollover toggle, but enabling default rollover', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index 17dadb1c6b47e..846e20b48ddca 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -426,6 +426,7 @@ describe(' serialization', () => { await actions.cold.setSelectedNodeAttribute('test:123'); await actions.cold.setReplicas('123'); await actions.cold.setFreeze(true); + await actions.cold.toggleReadonly(true); await actions.cold.setIndexPriority('123'); await actions.savePolicy(); @@ -445,6 +446,7 @@ describe(' serialization', () => { }, }, "freeze": Object {}, + "readonly": Object {}, "set_priority": Object { "priority": 123, }, diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index d3fec300d2d5f..f4ff69f9b5c10 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -108,6 +108,7 @@ export interface SerializedWarmPhase extends SerializedPhase { export interface SerializedColdPhase extends SerializedPhase { actions: { freeze?: {}; + readonly?: {}; allocate?: AllocateAction; set_priority?: { priority: number | null; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 72651778f403e..648aebf8118de 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -15,6 +15,7 @@ import { IndexPriorityField, ReplicasField, FreezeField, + ReadonlyField, } from '../shared_fields'; import { Phase } from '../phase'; @@ -38,6 +39,9 @@ export const ColdPhase: FunctionComponent = () => { {/* Freeze section */} {!isUsingSearchableSnapshotInHotPhase && } + {/* Readonly section */} + {!isUsingSearchableSnapshotInHotPhase && } + {/* Data tier allocation section */} = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 4cef7615a2d8d..50663d936617b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -228,7 +228,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody', { defaultMessage: - 'Force merge, shrink and freeze actions are not allowed when searchable snapshots are enabled in this phase.', + 'Force merge, shrink, read only and freeze actions are not allowed when searchable snapshots are enabled in this phase.', } )} data-test-subj="searchableSnapshotFieldsDisabledCallout" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index d082489c4b918..29445ac8e4715 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -40,7 +40,7 @@ export const WarmPhase: FunctionComponent = () => { {!isUsingSearchableSnapshotInHotPhase && } - + {!isUsingSearchableSnapshotInHotPhase && } {/* Data tier allocation section */} ( enabled: Boolean(cold), dataTierAllocationType: determineDataTierAllocationType(cold?.actions), freezeEnabled: Boolean(cold?.actions?.freeze), + readonlyEnabled: Boolean(cold?.actions?.readonly), }, frozen: { enabled: Boolean(frozen), diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index bdb915ba62d44..7cc48b3fcd90e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -85,6 +85,7 @@ const originalPolicy: SerializedPolicy = { exclude: { test: 'my_value' }, }, freeze: {}, + readonly: {}, set_priority: { priority: 12, }, @@ -206,6 +207,14 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.actions.readonly).toBeUndefined(); }); + it('removes the readonly action if it is disabled in cold', () => { + formInternal._meta.cold.readonlyEnabled = false; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.readonly).toBeUndefined(); + }); + it('allows force merge and readonly actions to be configured in hot with default rollover enabled', () => { formInternal._meta.hot.isUsingDefaultRollover = true; formInternal._meta.hot.bestCompression = false; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 2b90d75fa6da0..ce7b36d69a32e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -201,6 +201,10 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ defaultMessage: 'Freeze index', }), }, + readonlyEnabled: { + defaultValue: false, + label: i18nTexts.editPolicy.readonlyEnabledFieldLabel, + }, minAgeUnit: { defaultValue: 'd', }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index b10e3294f75c7..24dafa6cca237 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -229,6 +229,15 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete coldPhase.actions.freeze; } + /** + * COLD PHASE READ ONLY + */ + if (_meta.cold.readonlyEnabled) { + coldPhase.actions.readonly = coldPhase.actions.readonly ?? {}; + } else { + delete coldPhase.actions.readonly; + } + /** * COLD PHASE SET PRIORITY */ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 977554f12da42..5cc631c5d95c0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -50,6 +50,7 @@ interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, For interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { enabled: boolean; freezeEnabled: boolean; + readonlyEnabled: boolean; } interface FrozenPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { From d8154039531d2c0fd0f88cfb7ae7c23d24889bdf Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 7 Apr 2021 11:21:49 +0200 Subject: [PATCH 21/65] [Uptime] Fixed pagination styling (#95940) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitor_list/__snapshots__/monitor_list.test.tsx.snap | 6 +++--- .../components/overview/monitor_list/overview_page_link.tsx | 4 ++-- .../uptime/server/lib/requests/search/query_context.ts | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap index 779b513915ea8..dc125ec4b8466 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap @@ -831,7 +831,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` } .c3 { - padding-top: 12px; + margin-top: 12px; } .c0 { @@ -1652,7 +1652,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` > + + ); + }; + + const customTestbed = registerTestBed(TestComponent, { + memoryRouter: { + wrapComponent: false, + }, + })() as TestBed; + + testBed = { + ...customTestbed, + actions: getCommonActions(customTestbed), + }; + + const { + form, + component, + find, + actions: { changeFieldType }, + } = testBed; + + // We set some dummy painless error + act(() => { + find('setPainlessErrorButton').simulate('click'); + }); + component.update(); + + expect(form.getErrorsMessages()).toEqual(['Awwww! Painless syntax error']); + + // We change the type and expect the form error to not be there anymore + await changeFieldType('long'); + expect(form.getErrorsMessages()).toEqual([]); + }); }); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx index afb87bd1e7334..3785096e20627 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -21,6 +21,7 @@ import type { CoreStart } from 'src/core/public'; import { Form, useForm, + useFormData, FormHook, UseField, TextField, @@ -184,6 +185,9 @@ const FieldEditorComponent = ({ serializer: formSerializer, }); const { submit, isValid: isFormValid, isSubmitted } = form; + const { clear: clearSyntaxError } = syntaxError; + + const [{ type }] = useFormData({ form }); const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); const i18nTexts = geti18nTexts(); @@ -194,6 +198,12 @@ const FieldEditorComponent = ({ } }, [onChange, isFormValid, isSubmitted, submit]); + useEffect(() => { + // Whenever the field "type" changes we clear any possible painless syntax + // error as it is possibly stale. + clearSyntaxError(); + }, [type, clearSyntaxError]); + return (
    diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts index 46414c264c6b7..286931ad0e854 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -140,7 +140,7 @@ describe('', () => { find, component, form, - actions: { toggleFormRow }, + actions: { toggleFormRow, changeFieldType }, } = setup({ ...defaultProps, onSave }); act(() => { @@ -173,14 +173,7 @@ describe('', () => { }); // Change the type and make sure it is forwarded - act(() => { - find('typeField').simulate('change', [ - { - label: 'Other type', - value: 'other_type', - }, - ]); - }); + await changeFieldType('other_type', 'Other type'); await act(async () => { find('fieldSaveButton').simulate('click'); diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts index 295c32cf28e78..b55a59df34545 100644 --- a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts +++ b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { act } from 'react-dom/test-utils'; import { TestBed } from './test_utils'; export const getCommonActions = (testBed: TestBed) => { @@ -21,7 +22,20 @@ export const getCommonActions = (testBed: TestBed) => { testBed.form.toggleEuiSwitch(testSubj); }; + const changeFieldType = async (value: string, label?: string) => { + await act(async () => { + testBed.find('typeField').simulate('change', [ + { + value, + label: label ?? value, + }, + ]); + }); + testBed.component.update(); + }; + return { toggleFormRow, + changeFieldType, }; }; From 2efea063926c5a1939df965eb110d9d30fa0ec2f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 7 Apr 2021 14:52:08 +0300 Subject: [PATCH 25/65] [TSVB] [Regression] Fix Top Hit / Filter Ratio aggregations (#96288) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/aggs/calculation.js | 2 +- .../public/application/components/aggs/cumulative_sum.js | 2 +- .../public/application/components/aggs/derivative.js | 2 +- .../public/application/components/aggs/filter_ratio.js | 3 ++- .../public/application/components/aggs/math.js | 2 +- .../public/application/components/aggs/moving_average.js | 2 +- .../public/application/components/aggs/positive_only.js | 2 +- .../public/application/components/aggs/serial_diff.js | 2 +- .../public/application/components/aggs/std_sibling.js | 2 +- .../public/application/components/aggs/top_hit.js | 5 +++-- .../public/application/components/aggs/vars.js | 2 +- .../public/application/components/splits/terms.js | 2 +- 12 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js index 42321c2728198..7a29db27a514f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/calculation.js @@ -131,7 +131,7 @@ export function CalculationAgg(props) { CalculationAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index 8a597ffa9d5e8..d82bcbcd885cb 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -84,7 +84,7 @@ export function CumulativeSumAgg(props) { CumulativeSumAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index 8d155b378755a..6f7e5680b2a86 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -110,7 +110,7 @@ export const DerivativeAgg = (props) => { DerivativeAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 7f93567980b2d..90353f9af8e35 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -13,6 +13,7 @@ import { FieldSelect } from './field_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; import { htmlIdGenerator, @@ -29,7 +30,7 @@ import { getDataStart } from '../../../services'; import { QueryBarWrapper } from '../query_bar_wrapper'; const isFieldHistogram = (fields, indexPattern, field) => { - const indexFields = fields[indexPattern]; + const indexFields = fields[getIndexPatternKey(indexPattern)]; if (!indexFields) return false; const fieldObject = indexFields.find((f) => f.name === field); if (!fieldObject) return false; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js index 5fa9912ae17e7..e92659e677860 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/math.js @@ -150,7 +150,7 @@ export function MathAgg(props) { MathAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index bf6c95202ed25..3c53e4597136e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -305,7 +305,7 @@ export const MovingAverageAgg = (props) => { MovingAverageAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 55c14e61bed1a..010a88146595b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -88,7 +88,7 @@ export const PositiveOnlyAgg = (props) => { PositiveOnlyAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index 00688992f819b..675a9868e13b3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -115,7 +115,7 @@ export const SerialDiffAgg = (props) => { SerialDiffAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index d3ff4f64b5351..bebc1cf2bce72 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -163,7 +163,7 @@ const StandardSiblingAggUi = (props) => { StandardSiblingAggUi.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), model: PropTypes.object, onAdd: PropTypes.func, onChange: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 92e754c1dcdaf..12f7ad143cb25 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -26,6 +26,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; import { PANEL_TYPES } from '../../../../common/panel_types'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const isFieldTypeEnabled = (fieldRestrictions, fieldType) => fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true; @@ -115,8 +116,8 @@ const TopHitAggUi = (props) => { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); const handleTextChange = createTextHandler(handleChange); - - const field = fields[indexPattern].find((f) => f.name === model.field); + const fieldsSelector = getIndexPatternKey(indexPattern); + const field = fields[fieldsSelector].find((f) => f.name === model.field); const aggWithOptions = getAggWithOptions(field, aggWithOptionsRestrictFields); const orderOptions = getOrderOptions(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js index ca310ab4153d1..b9d554e254bcc 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js @@ -96,7 +96,7 @@ CalculationVars.defaultProps = { CalculationVars.propTypes = { fields: PropTypes.object, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), metrics: PropTypes.array, model: PropTypes.object, name: PropTypes.string, diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index b996abd6373ab..ab5342e925bd7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -237,7 +237,7 @@ SplitByTermsUI.propTypes = { intl: PropTypes.object, model: PropTypes.object, onChange: PropTypes.func, - indexPattern: PropTypes.string, + indexPattern: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), fields: PropTypes.object, uiRestrictions: PropTypes.object, seriesQuantity: PropTypes.object, From 7584b728c647dffbbbdb7c39545369592ddfda38 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 7 Apr 2021 15:36:55 +0300 Subject: [PATCH 26/65] [Search Sessions] Monitoring hardening part 1 (#96196) * Decrease default pageSize to 100 Set default strategy Don't create sessions when disabled Clear monitoring task when disabled Use concatMap to serialize session checkup * ts * ts * ts * Update x-pack/plugins/data_enhanced/server/search/session/session_service.ts Co-authored-by: Lukas Olson * Search sessions are disabled * Clear task on server start Co-authored-by: Lukas Olson --- x-pack/plugins/data_enhanced/config.ts | 2 +- x-pack/plugins/data_enhanced/server/plugin.ts | 29 +- .../search/session/check_running_sessions.ts | 4 +- .../server/search/session/monitoring_task.ts | 29 +- .../search/session/session_service.test.ts | 1685 +++++++++-------- .../server/search/session/session_service.ts | 25 +- x-pack/plugins/data_enhanced/server/type.ts | 19 + 7 files changed, 959 insertions(+), 834 deletions(-) diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index fc1f22d50b09f..8cbf930fe87bd 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -18,7 +18,7 @@ export const configSchema = schema.object({ * pageSize controls how many search session objects we load at once while monitoring * session completion */ - pageSize: schema.number({ defaultValue: 10000 }), + pageSize: schema.number({ defaultValue: 100 }), /** * trackingInterval controls how often we track search session objects progress */ diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 462d1fc337ae2..ae36b881796c4 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,13 +6,7 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { - PluginSetup as DataPluginSetup, - PluginStart as DataPluginStart, - usageProvider, -} from '../../../../src/plugins/data/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { usageProvider } from '../../../../src/plugins/data/server'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; import { registerSessionRoutes } from './routes'; import { searchSessionSavedObjectType } from './saved_objects'; @@ -22,22 +16,13 @@ import { eqlSearchStrategyProvider, } from './search'; import { getUiSettings } from './ui_settings'; -import type { DataEnhancedRequestHandlerContext } from './type'; +import type { + DataEnhancedRequestHandlerContext, + DataEnhancedSetupDependencies as SetupDependencies, + DataEnhancedStartDependencies as StartDependencies, +} from './type'; import { ConfigSchema } from '../config'; import { registerUsageCollector } from './collectors'; -import { SecurityPluginSetup } from '../../security/server'; - -interface SetupDependencies { - data: DataPluginSetup; - usageCollection?: UsageCollectionSetup; - taskManager: TaskManagerSetupContract; - security?: SecurityPluginSetup; -} - -export interface StartDependencies { - data: DataPluginStart; - taskManager: TaskManagerStartContract; -} export class EnhancedDataServerPlugin implements Plugin { @@ -50,7 +35,7 @@ export class EnhancedDataServerPlugin this.config = this.initializerContext.config.get(); } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { const usage = deps.usageCollection ? usageProvider(core) : undefined; core.uiSettings.register(getUiSettings()); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index 6e52b17f36803..60c7283320d0c 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; -import { expand, mergeMap } from 'rxjs/operators'; +import { expand, concatMap } from 'rxjs/operators'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { ENHANCED_ES_SEARCH_STRATEGY, @@ -154,7 +154,7 @@ export async function checkRunningSessions( try { await getAllSavedSearchSessions$(deps, config) .pipe( - mergeMap(async (runningSearchSessionsResponse) => { + concatMap(async (runningSearchSessionsResponse) => { if (!runningSearchSessionsResponse.total) return; logger.debug(`Found ${runningSearchSessionsResponse.total} running sessions`); diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 8aa35def387b7..101ccb14edf67 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -15,6 +15,7 @@ import { checkRunningSessions } from './check_running_sessions'; import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; import { ConfigSchema } from '../../../config'; import { SEARCH_SESSION_TYPE } from '../../../common'; +import { DataEnhancedStartDependencies } from '../../type'; export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; @@ -25,12 +26,19 @@ interface SearchSessionTaskDeps { config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { +function searchSessionRunner( + core: CoreSetup, + { logger, config }: SearchSessionTaskDeps +) { return ({ taskInstance }: RunContext) => { return { async run() { const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); + if (!sessionConfig.enabled) { + logger.debug('Search sessions are disabled. Skipping task.'); + return; + } const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); await checkRunningSessions( @@ -50,7 +58,10 @@ function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionT }; } -export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionTaskDeps) { +export function registerSearchSessionsTask( + core: CoreSetup, + deps: SearchSessionTaskDeps +) { deps.taskManager.registerTaskDefinitions({ [SEARCH_SESSIONS_TASK_TYPE]: { title: 'Search Sessions Monitor', @@ -59,6 +70,18 @@ export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionT }); } +export async function unscheduleSearchSessionsTask( + taskManager: TaskManagerStartContract, + logger: Logger +) { + try { + await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); + logger.debug(`Search sessions cleared`); + } catch (e) { + logger.error(`Error clearing task, received ${e.message}`); + } +} + export async function scheduleSearchSessionsTasks( taskManager: TaskManagerStartContract, logger: Logger, @@ -79,6 +102,6 @@ export async function scheduleSearchSessionsTasks( logger.debug(`Search sessions task, scheduled to run`); } catch (e) { - logger.debug(`Error scheduling task, received ${e.message}`); + logger.error(`Error scheduling task, received ${e.message}`); } } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index f61d89e2301ab..9344ab973c636 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -15,12 +15,12 @@ import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../common'; import { SearchSessionService } from './session_service'; import { createRequestHash } from './utils'; import moment from 'moment'; -import { coreMock } from 'src/core/server/mocks'; +import { coreMock } from '../../../../../../src/core/server/mocks'; import { ConfigSchema } from '../../../config'; -// @ts-ignore import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { AuthenticatedUser } from '../../../../security/common/model'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; +import { TaskManagerStartContract } from '../../../../task_manager/server'; const MAX_UPDATE_RETRIES = 3; @@ -29,6 +29,7 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); describe('SearchSessionService', () => { let savedObjectsClient: jest.Mocked; let service: SearchSessionService; + let mockTaskManager: jest.Mocked; const MOCK_STRATEGY = 'ese'; @@ -62,925 +63,1009 @@ describe('SearchSessionService', () => { references: [], }; - beforeEach(async () => { - savedObjectsClient = savedObjectsClientMock.create(); - const config: ConfigSchema = { - search: { - sessions: { - enabled: true, - pageSize: 10000, - notTouchedInProgressTimeout: moment.duration(1, 'm'), - notTouchedTimeout: moment.duration(2, 'm'), - maxUpdateRetries: MAX_UPDATE_RETRIES, - defaultExpiration: moment.duration(7, 'd'), - trackingInterval: moment.duration(10, 's'), - management: {} as any, + describe('Feature disabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: false, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, + }, }, - }, - }; - const mockLogger: any = { - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - service = new SearchSessionService(mockLogger, config); - const coreStart = coreMock.createStart(); - const mockTaskManager = taskManagerMock.createStart(); - await flushPromises(); - await service.start(coreStart, { - taskManager: mockTaskManager, - }); - }); - - afterEach(() => { - service.stop(); - }); - - describe('save', () => { - it('throws if `name` is not provided', () => { - expect(() => - service.save({ savedObjectsClient }, mockUser1, sessionId, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('throws if `appId` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) - ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + afterEach(() => { + service.stop(); }); - it('throws if `generator id` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - }) - ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + it('task is cleared, if exists', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); }); - it('saving updates an existing saved object and persists it', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', + it('trackId ignores', async () => { + await service.trackId({ savedObjectsClient }, mockUser1, { params: {} }, '123', { + sessionId: '321', + strategy: MOCK_STRATEGY, }); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); expect(savedObjectsClient.create).not.toHaveBeenCalled(); - - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).not.toHaveProperty('idMapping'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); }); - it('saving creates a new persisted saved object, if it did not exist', async () => { - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', - }); - - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options?.id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', {}); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); - expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); - expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); - expect(callAttributes).toHaveProperty('username', mockUser1.username); + it('Save throws', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toBeInstanceOf(Error); }); - it('throws error if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - expect( - service.get({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + it('Update throws', () => { + const attributes = { name: 'new_name' }; + const response = service.update({ savedObjectsClient }, mockUser1, sessionId, attributes); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - - await service.save( - { savedObjectsClient }, - - null, - sessionId, - { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - } - ); - - expect(savedObjectsClient.create).toHaveBeenCalled(); - const [[, attributes]] = savedObjectsClient.create.mock.calls; - expect(attributes).toHaveProperty('realmType', undefined); - expect(attributes).toHaveProperty('realmName', undefined); - expect(attributes).toHaveProperty('username', undefined); + it('Cancel throws', () => { + const response = service.cancel({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); - }); - - describe('get', () => { - it('calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('getId throws', () => { + const response = service.getId({ savedObjectsClient }, mockUser1, {}, {}); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get({ savedObjectsClient }, null, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('Delete throws', () => { + const response = service.delete({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); }); - describe('find', () => { - it('calls saved objects client with user filter', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find({ savedObjectsClient }, mockUser1, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", + describe('Feature enabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: true, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, }, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); - }); - - it('mixes in passed-in filter as string and KQL node', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, + }, }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options1 = { filter: 'foobar' }; - const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); - - const options2 = { filter: nodeBuilder.is('foo', 'bar') }; - const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); - - expect(response1).toBe(mockResponse); - expect(response2).toBe(mockResponse); - - const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; - expect(findOptions1).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": null, - }, - Object { - "type": "literal", - "value": "foobar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); - expect(findOptions2).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "foo", - }, - Object { - "type": "literal", - "value": "bar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('has no filter without security', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find({ savedObjectsClient }, null, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": undefined, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); + afterEach(() => { + service.stop(); }); - }); - - describe('update', () => { - it('update calls saved objects client with added touch time', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - const response = await service.update( - { savedObjectsClient }, - mockUser1, - sessionId, - attributes - ); + it('task is cleared and re-created', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); + expect(mockTaskManager.ensureScheduled).toHaveBeenCalled(); + }); - expect(response).toBe(mockUpdateSavedObject); + describe('save', () => { + it('throws if `name` is not provided', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + it('throws if `appId` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) + ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', attributes.name); - expect(callAttributes).toHaveProperty('touched'); - }); + it('throws if `generator id` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + }) + ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + }); - it('throws if user conflicts', () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('saving updates an existing saved object and persists it', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - expect( - service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - it('works without security', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).not.toHaveProperty('idMapping'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + }); - const attributes = { name: 'new_name' }; - const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - - expect(response).toBe(mockUpdateSavedObject); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', 'new_name'); - expect(callAttributes).toHaveProperty('touched'); - }); - }); + it('saving creates a new persisted saved object, if it did not exist', async () => { + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - describe('cancel', () => { - it('updates object status', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - await service.cancel({ savedObjectsClient }, mockUser1, sessionId); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); - }); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options?.id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', {}); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); + expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); + expect(callAttributes).toHaveProperty('username', mockUser1.username); + }); - it('throws if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('throws error if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - expect( - service.cancel({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + expect( + service.get({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('works without security', async () => { + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); - await service.cancel({ savedObjectsClient }, null, sessionId); + await service.save( + { savedObjectsClient }, - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + null, + sessionId, + { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + } + ); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); + expect(savedObjectsClient.create).toHaveBeenCalled(); + const [[, attributes]] = savedObjectsClient.create.mock.calls; + expect(attributes).toHaveProperty('realmType', undefined); + expect(attributes).toHaveProperty('realmName', undefined); + expect(attributes).toHaveProperty('username', undefined); + }); }); - }); - describe('trackId', () => { - it('updates the saved object if search session already exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('get', () => { + it('calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + const response = await service.get({ savedObjectsClient }, null, sessionId); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(callAttributes).toHaveProperty('touched'); }); - it('retries updating the saved object if there was a ES conflict 409', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - let counter = 0; - - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - } else { - resolve(mockUpdateSavedObject); + describe('find', () => { + it('calls saved objects client with user filter', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find({ savedObjectsClient }, mockUser1, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "page": 0, + "perPage": 5, + "type": "search-session", } - }); + `); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('mixes in passed-in filter as string and KQL node', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options1 = { filter: 'foobar' }; + const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); + + const options2 = { filter: nodeBuilder.is('foo', 'bar') }; + const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); + + expect(response1).toBe(mockResponse); + expect(response2).toBe(mockResponse); + + const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; + expect(findOptions1).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": null, + }, + Object { + "type": "literal", + "value": "foobar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); + expect(findOptions2).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "foo", + }, + Object { + "type": "literal", + "value": "bar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('has no filter without security', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find({ savedObjectsClient }, null, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": undefined, + "page": 0, + "perPage": 5, + "type": "search-session", + } + `); + }); }); - it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('update', () => { + it('update calls saved objects client with added touch time', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update( + { savedObjectsClient }, + mockUser1, + sessionId, + attributes + ); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - }); + expect(response).toBe(mockUpdateSavedObject); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', attributes.name); + expect(callAttributes).toHaveProperty('touched'); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + expect( + service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - // Track ID doesn't throw errors even in cases of failure! - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(response).toBe(mockUpdateSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', 'new_name'); + expect(callAttributes).toHaveProperty('touched'); + }); }); - it('creates the saved object in non persisted state, if search session doesnt exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('cancel', () => { + it('updates object status', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + await service.cancel({ savedObjectsClient }, mockUser1, sessionId); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.cancel({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options).toStrictEqual({ id: sessionId }); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + await service.cancel({ savedObjectsClient }, null, sessionId); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); }); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('sessionId', sessionId); - expect(callAttributes).toHaveProperty('persisted', false); }); - it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('trackId', () => { + it('updates the saved object if search session already exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - let counter = 0; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); - } else { - resolve(mockUpdateSavedObject); - } + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, }); + expect(callAttributes).toHaveProperty('touched'); }); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + it('retries updating the saved object if there was a ES conflict 409', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - }); + it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('retries everything at most MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + }); + }); - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + // Track ID doesn't throw errors even in cases of failure! + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - }); + it('creates the saved object in non persisted state, if search session doesnt exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('batches updates for the same session', async () => { - const sessionId1 = 'sessiondId1'; - const sessionId2 = 'sessiondId2'; + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - const searchRequest1 = { params: { 1: '1' } }; - const requestHash1 = createRequestHash(searchRequest1.params); - const searchId1 = 'searchId1'; + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - const searchRequest2 = { params: { 2: '2' } }; - const requestHash2 = createRequestHash(searchRequest2.params); - const searchId2 = 'searchId1'; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - const searchRequest3 = { params: { 3: '3' } }; - const requestHash3 = createRequestHash(searchRequest3.params); - const searchId3 = 'searchId3'; + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options).toStrictEqual({ id: sessionId }); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('sessionId', sessionId); + expect(callAttributes).toHaveProperty('persisted', false); + }); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await Promise.all([ - service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { - sessionId: sessionId2, + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }), - ]); + }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + }); - const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; - expect(type1).toBe(SEARCH_SESSION_TYPE); - expect(id1).toBe(sessionId1); - expect(callAttributes1).toHaveProperty('idMapping', { - [requestHash1]: { - id: searchId1, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, - [requestHash2]: { - id: searchId2, - status: SearchSessionStatus.IN_PROGRESS, + it('retries everything at most MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); }); - expect(callAttributes1).toHaveProperty('touched'); - const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; - expect(type2).toBe(SEARCH_SESSION_TYPE); - expect(id2).toBe(sessionId2); - expect(callAttributes2).toHaveProperty('idMapping', { - [requestHash3]: { - id: searchId3, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + it('batches updates for the same session', async () => { + const sessionId1 = 'sessiondId1'; + const sessionId2 = 'sessiondId2'; + + const searchRequest1 = { params: { 1: '1' } }; + const requestHash1 = createRequestHash(searchRequest1.params); + const searchId1 = 'searchId1'; + + const searchRequest2 = { params: { 2: '2' } }; + const requestHash2 = createRequestHash(searchRequest2.params); + const searchId2 = 'searchId1'; + + const searchRequest3 = { params: { 3: '3' } }; + const requestHash3 = createRequestHash(searchRequest3.params); + const searchId3 = 'searchId3'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + await Promise.all([ + service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { + sessionId: sessionId2, + strategy: MOCK_STRATEGY, + }), + ]); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; + expect(type1).toBe(SEARCH_SESSION_TYPE); + expect(id1).toBe(sessionId1); + expect(callAttributes1).toHaveProperty('idMapping', { + [requestHash1]: { + id: searchId1, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + [requestHash2]: { + id: searchId2, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes1).toHaveProperty('touched'); + + const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; + expect(type2).toBe(SEARCH_SESSION_TYPE); + expect(id2).toBe(sessionId2); + expect(callAttributes2).toHaveProperty('idMapping', { + [requestHash3]: { + id: searchId3, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes2).toHaveProperty('touched'); }); - expect(callAttributes2).toHaveProperty('touched'); }); - }); - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: false, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Cannot get search ID from a session that is not stored]` - ); - }); + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: true, + isRestore: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + [requestHash]: { + id: searchId, + }, + }, + }, + }; + savedObjectsClient.get.mockResolvedValue(mockSession); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { sessionId, isStored: true, - isRestore: false, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Get search ID is only supported when restoring a session]` - ); - }); - - it('returns the search ID from the saved object ID mapping', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - [requestHash]: { - id: searchId, - }, - }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + isRestore: true, + }); - const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: true, - isRestore: true, + expect(id).toBe(searchId); }); - - expect(id).toBe(searchId); }); - }); - describe('getSearchIdMapping', () => { - it('retrieves the search IDs and strategies from the saved object', async () => { - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - foo: { - id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', - strategy: MOCK_STRATEGY, + describe('getSearchIdMapping', () => { + it('retrieves the search IDs and strategies from the saved object', async () => { + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + foo: { + id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', + strategy: MOCK_STRATEGY, + }, }, }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); - const searchIdMapping = await service.getSearchIdMapping( - { savedObjectsClient }, - mockUser1, - mockSession.id - ); - expect(searchIdMapping).toMatchInlineSnapshot(` - Map { - "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", - } - `); + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + const searchIdMapping = await service.getSearchIdMapping( + { savedObjectsClient }, + mockUser1, + mockSession.id + ); + expect(searchIdMapping).toMatchInlineSnapshot(` + Map { + "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", + } + `); + }); }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index c95c58a8dc06b..b5f7da594d53b 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -29,6 +29,7 @@ import { TaskManagerStartContract, } from '../../../../task_manager/server'; import { + ENHANCED_ES_SEARCH_STRATEGY, SearchSessionRequestInfo, SearchSessionSavedObjectAttributes, SearchSessionStatus, @@ -36,8 +37,13 @@ import { } from '../../../common'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; -import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; +import { + registerSearchSessionsTask, + scheduleSearchSessionsTasks, + unscheduleSearchSessionsTask, +} from './monitoring_task'; import { SearchSessionsConfig, SearchStatus } from './types'; +import { DataEnhancedStartDependencies } from '../../type'; export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -78,7 +84,7 @@ export class SearchSessionService this.sessionConfig = this.config.search.sessions; } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { config: this.config, taskManager: deps.taskManager, @@ -99,6 +105,8 @@ export class SearchSessionService this.logger, this.sessionConfig.trackingInterval ); + } else { + unscheduleSearchSessionsTask(deps.taskManager, this.logger); } }; @@ -217,6 +225,7 @@ export class SearchSessionService restoreState = {}, }: Partial ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); if (!name) throw new Error('Name is required'); if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); @@ -316,6 +325,7 @@ export class SearchSessionService attributes: Partial ) => { this.logger.debug(`update | ${sessionId}`); + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.update( SEARCH_SESSION_TYPE, @@ -353,6 +363,7 @@ export class SearchSessionService user: AuthenticatedUser | null, sessionId: string ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); this.logger.debug(`delete | ${sessionId}`); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); @@ -367,9 +378,9 @@ export class SearchSessionService user: AuthenticatedUser | null, searchRequest: IKibanaSearchRequest, searchId: string, - { sessionId, strategy }: ISearchOptions + { sessionId, strategy = ENHANCED_ES_SEARCH_STRATEGY }: ISearchOptions ) => { - if (!sessionId || !searchId) return; + if (!this.sessionConfig.enabled || !sessionId || !searchId) return; this.logger.debug(`trackId | ${sessionId} | ${searchId}`); let idMapping: Record = {}; @@ -378,7 +389,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); const searchInfo = { id: searchId, - strategy: strategy!, + strategy, status: SearchStatus.IN_PROGRESS, }; idMapping = { [requestHash]: searchInfo }; @@ -411,7 +422,9 @@ export class SearchSessionService searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions ) => { - if (!sessionId) { + if (!this.sessionConfig.enabled) { + throw new Error('Search sessions are disabled'); + } else if (!sessionId) { throw new Error('Session ID is required'); } else if (!isStored) { throw new Error('Cannot get search ID from a session that is not stored'); diff --git a/x-pack/plugins/data_enhanced/server/type.ts b/x-pack/plugins/data_enhanced/server/type.ts index c4a16eab1a3a7..215700c5dcc5c 100644 --- a/x-pack/plugins/data_enhanced/server/type.ts +++ b/x-pack/plugins/data_enhanced/server/type.ts @@ -7,6 +7,13 @@ import type { IRouter } from 'kibana/server'; import type { DataRequestHandlerContext } from '../../../../src/plugins/data/server'; +import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; /** * @internal @@ -17,3 +24,15 @@ export type DataEnhancedRequestHandlerContext = DataRequestHandlerContext; * @internal */ export type DataEnhancedPluginRouter = IRouter; + +export interface DataEnhancedSetupDependencies { + data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; + taskManager: TaskManagerSetupContract; + security?: SecurityPluginSetup; +} + +export interface DataEnhancedStartDependencies { + data: DataPluginStart; + taskManager: TaskManagerStartContract; +} From 4d43a4f31d1824ce6ed7c721e8f6fcee36349f59 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 7 Apr 2021 14:49:44 +0200 Subject: [PATCH 27/65] [Rollup] Migrate to new ES client (#95926) * initial pass at es client migration * fixed potential for not passing in an error message and triggering an unhandled exception * reworked ad hoc fixing of error response * delete legacy client file and remove use of legacyEs service * remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../errors/handle_es_error.ts | 2 +- .../server/client/elasticsearch_rollup.ts | 142 ------------------ x-pack/plugins/rollup/server/plugin.ts | 25 +-- .../routes/api/indices/register_get_route.ts | 17 +-- .../register_validate_index_pattern_route.ts | 50 +++--- .../routes/api/jobs/register_create_route.ts | 12 +- .../routes/api/jobs/register_delete_route.ts | 27 ++-- .../routes/api/jobs/register_get_route.ts | 10 +- .../routes/api/jobs/register_start_route.ts | 12 +- .../routes/api/jobs/register_stop_route.ts | 12 +- .../api/search/register_search_route.ts | 20 +-- .../plugins/rollup/server/services/license.ts | 7 +- .../plugins/rollup/server/shared_imports.ts | 2 +- x-pack/plugins/rollup/server/types.ts | 27 +--- .../apis/management/rollup/lib/es_index.js | 2 +- .../apps/rollup_job/hybrid_index_pattern.js | 14 +- .../functional/apps/rollup_job/rollup_jobs.js | 2 +- .../test/functional/apps/rollup_job/tsvb.js | 7 +- 18 files changed, 88 insertions(+), 302 deletions(-) delete mode 100644 x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index a98a74375638d..42e18b72057ce 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -36,7 +36,7 @@ export const handleEsError = ({ return response.customError({ statusCode, body: { - message: body.error?.reason, + message: body.error?.reason ?? error.message ?? 'Unknown error', attributes: { // The full original ES error object error: body.error, diff --git a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts deleted file mode 100644 index 0296428c49613..0000000000000 --- a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts +++ /dev/null @@ -1,142 +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. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.rollup = components.clientAction.namespaceFactory(); - const rollup = Client.prototype.rollup.prototype; - - rollup.rollupIndexCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_rollup/data', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.search = ca({ - urls: [ - { - fmt: '/<%=index%>/_rollup_search', - req: { - index: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - rollup.fieldCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_field_caps?fields=*', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.jobs = ca({ - urls: [ - { - fmt: '/_rollup/job/_all', - }, - ], - method: 'GET', - }); - - rollup.job = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - rollup.startJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>/_start', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - rollup.stopJob = ca({ - params: { - waitForCompletion: { - type: 'boolean', - name: 'wait_for_completion', - }, - }, - urls: [ - { - fmt: '/_rollup/job/<%=id%>/_stop', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - rollup.deleteJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - rollup.createJob = ca({ - urls: [ - { - fmt: '/_rollup/job/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); -}; diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 1b982ab45205d..ff6adc1c8d24b 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -19,25 +19,16 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { PLUGIN, CONFIG_ROLLUPS } from '../common'; -import { Dependencies, RollupHandlerContext } from './types'; +import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; import { registerRollupUsageCollector } from './collectors'; import { rollupDataEnricher } from './rollup_data_enricher'; import { IndexPatternsFetcher } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; import { getCapabilitiesForRollupIndices } from '../../../../src/plugins/data/server'; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - // Extend the elasticsearchJs client with additional endpoints. - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - - return core.elasticsearch.legacy.createClient('rollup', esClientConfig); -} - export class RollupPlugin implements Plugin { private readonly logger: Logger; private readonly globalConfig$: Observable; @@ -82,21 +73,11 @@ export class RollupPlugin implements Plugin { ], }); - http.registerRouteHandlerContext( - 'rollup', - async (context, request) => { - this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.rollupEsClient.asScoped(request), - }; - } - ); - registerApiRoutes({ router: http.createRouter(), license: this.license, lib: { - isEsError, + handleEsError, formatEsError, getCapabilitiesForRollupIndices, }, diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts index 694ab3c467c1f..1d3be4b8e1fbb 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -14,7 +14,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices }, + lib: { handleEsError, getCapabilitiesForRollupIndices }, }: RouteDependencies) => { router.get( { @@ -23,18 +23,13 @@ export const registerGetRoute = ({ }, license.guardApiRoute(async (context, request, response) => { try { - const data = await context.rollup!.client.callAsCurrentUser( - 'rollup.rollupIndexCapabilities', - { - indexPattern: '_all', - } - ); + const { client: clusterClient } = context.core.elasticsearch; + const { body: data } = await clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ + index: '_all', + }); return response.ok({ body: getCapabilitiesForRollupIndices(data) }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts index 90eabaa88b641..b2431c3838234 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -32,10 +32,6 @@ interface FieldCapability { scaled_float?: any; } -interface FieldCapabilities { - fields: FieldCapability[]; -} - function isNumericField(fieldCapability: FieldCapability) { const numericTypes = [ 'long', @@ -59,7 +55,7 @@ function isNumericField(fieldCapability: FieldCapability) { export const registerValidateIndexPatternRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -71,16 +67,12 @@ export const registerValidateIndexPatternRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { indexPattern } = request.params; - const [fieldCapabilities, rollupIndexCapabilities]: [ - FieldCapabilities, - { [key: string]: any } - ] = await Promise.all([ - context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }), - context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { - indexPattern, - }), + const [{ body: fieldCapabilities }, { body: rollupIndexCapabilities }] = await Promise.all([ + clusterClient.asCurrentUser.fieldCaps({ index: indexPattern, fields: '*' }), + clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ index: indexPattern }), ]); const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; @@ -92,23 +84,21 @@ export const registerValidateIndexPatternRoute = ({ const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); - fieldCapabilitiesEntries.forEach( - ([fieldName, fieldCapability]: [string, FieldCapability]) => { - if (fieldCapability.date) { - dateFields.push(fieldName); - return; - } + fieldCapabilitiesEntries.forEach(([fieldName, fieldCapability]) => { + if (fieldCapability.date) { + dateFields.push(fieldName); + return; + } - if (isNumericField(fieldCapability)) { - numericFields.push(fieldName); - return; - } + if (isNumericField(fieldCapability)) { + numericFields.push(fieldName); + return; + } - if (fieldCapability.keyword) { - keywordFields.push(fieldName); - } + if (fieldCapability.keyword) { + keywordFields.push(fieldName); } - ); + }); const body = { doesMatchIndices, @@ -132,11 +122,7 @@ export const registerValidateIndexPatternRoute = ({ return response.ok({ body: notFoundBody }); } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts index bcb3a337aa725..11cfaf8851d45 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerCreateRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.put( { @@ -29,21 +29,19 @@ export const registerCreateRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { id, ...rest } = request.body.job; // Create job. - await context.rollup!.client.callAsCurrentUser('rollup.createJob', { + await clusterClient.asCurrentUser.rollup.putJob({ id, body: rest, }); // Then request the newly created job. - const results = await context.rollup!.client.callAsCurrentUser('rollup.job', { id }); + const { body: results } = await clusterClient.asCurrentUser.rollup.getJobs({ id }); return response.ok({ body: results.jobs[0] }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts index 4bbe73753e96c..f90a81f73823e 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerDeleteRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -24,28 +24,29 @@ export const registerDeleteRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; const data = await Promise.all( - jobIds.map((id: string) => - context.rollup!.client.callAsCurrentUser('rollup.deleteJob', { id }) - ) + jobIds.map((id: string) => clusterClient.asCurrentUser.rollup.deleteJob({ id })) ).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { // There is an issue opened on ES to handle the following error correctly // https://github.com/elastic/elasticsearch/issues/42908 // Until then we'll modify the response here. - if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { - err.status = 400; - err.statusCode = 400; - err.displayName = 'Bad request'; - err.message = JSON.parse(err.response).task_failures[0].reason.reason; - } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); + if ( + err?.meta && + err.body?.task_failures[0]?.reason?.reason?.includes( + 'Job must be [STOPPED] before deletion' + ) + ) { + err.meta.status = 400; + err.meta.statusCode = 400; + err.meta.displayName = 'Bad request'; + err.message = err.body.task_failures[0].reason.reason; } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts index a9a30c0370c5f..9944df2e55919 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts @@ -11,7 +11,7 @@ import { RouteDependencies } from '../../../types'; export const registerGetRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.get( { @@ -19,14 +19,12 @@ export const registerGetRoute = ({ validate: false, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { - const data = await context.rollup!.client.callAsCurrentUser('rollup.jobs'); + const { body: data } = await clusterClient.asCurrentUser.rollup.getJobs({ id: '_all' }); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts index 2ebfcc437f41e..133c0cb34c9f5 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerStartRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -29,20 +29,16 @@ export const registerStartRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; const data = await Promise.all( - jobIds.map((id: string) => - context.rollup!.client.callAsCurrentUser('rollup.startJob', { id }) - ) + jobIds.map((id: string) => clusterClient.asCurrentUser.rollup.startJob({ id })) ).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts index faaf377a2d833..164273f604b43 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerStopRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -27,23 +27,21 @@ export const registerStopRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const { jobIds } = request.body; // For our API integration tests we need to wait for the jobs to be stopped // in order to be able to delete them sequentially. const { waitForCompletion } = request.query; const stopRollupJob = (id: string) => - context.rollup!.client.callAsCurrentUser('rollup.stopJob', { + clusterClient.asCurrentUser.rollup.stopJob({ id, - waitForCompletion: waitForCompletion === 'true', + wait_for_completion: waitForCompletion === 'true', }); const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts index f77ae7829bb6c..62aec4e01eaa0 100644 --- a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../../types'; export const registerSearchRoute = ({ router, license, - lib: { isEsError, formatEsError }, + lib: { handleEsError }, }: RouteDependencies) => { router.post( { @@ -27,21 +27,21 @@ export const registerSearchRoute = ({ }, }, license.guardApiRoute(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; try { const requests = request.body.map(({ index, query }: { index: string; query?: any }) => - context.rollup.client.callAsCurrentUser('rollup.search', { - index, - rest_total_hits_as_int: true, - body: query, - }) + clusterClient.asCurrentUser.rollup + .rollupSearch({ + index, + rest_total_hits_as_int: true, + body: query, + }) + .then(({ body }) => body) ); const data = await Promise.all(requests); return response.ok({ body: data }); } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - throw err; + return handleEsError({ error: err, response }); } }) ); diff --git a/x-pack/plugins/rollup/server/services/license.ts b/x-pack/plugins/rollup/server/services/license.ts index d2c3ff82eab1c..1b88a4020afa6 100644 --- a/x-pack/plugins/rollup/server/services/license.ts +++ b/x-pack/plugins/rollup/server/services/license.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { Logger } from 'src/core/server'; +import { Logger, RequestHandlerContext } from 'src/core/server'; import { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'src/core/server'; import { LicensingPluginSetup } from '../../../licensing/server'; import { LicenseType } from '../../../licensing/common/types'; -import type { RollupHandlerContext } from '../types'; export interface LicenseStatus { isValid: boolean; @@ -57,11 +56,11 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( - ctx: RollupHandlerContext, + ctx: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory ) { diff --git a/x-pack/plugins/rollup/server/shared_imports.ts b/x-pack/plugins/rollup/server/shared_imports.ts index 2167558c39652..fe157644c6b3d 100644 --- a/x-pack/plugins/rollup/server/shared_imports.ts +++ b/x-pack/plugins/rollup/server/shared_imports.ts @@ -7,4 +7,4 @@ export { IndexPatternsFetcher } from '../../../../src/plugins/data/server'; -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 45dcc976b211f..c774644da46ce 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; +import { IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; @@ -15,7 +15,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; export interface Dependencies { @@ -27,10 +27,10 @@ export interface Dependencies { } export interface RouteDependencies { - router: RollupPluginRouter; + router: IRouter; license: License; lib: { - isEsError: typeof isEsError; + handleEsError: typeof handleEsError; formatEsError: typeof formatEsError; getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; }; @@ -38,22 +38,3 @@ export interface RouteDependencies { IndexPatternsFetcher: typeof IndexPatternsFetcher; }; } - -/** - * @internal - */ -interface RollupApiRequestHandlerContext { - client: ILegacyScopedClusterClient; -} - -/** - * @internal - */ -export interface RollupHandlerContext extends RequestHandlerContext { - rollup: RollupApiRequestHandlerContext; -} - -/** - * @internal - */ -export type RollupPluginRouter = IRouter; diff --git a/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js b/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js index d4c6be0f4a0dc..7aacd9f6a7fc6 100644 --- a/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js +++ b/x-pack/test/api_integration/apis/management/rollup/lib/es_index.js @@ -13,7 +13,7 @@ import { getRandomString } from './random'; * @param {ElasticsearchClient} es The Elasticsearch client instance */ export const initElasticsearchIndicesHelpers = (getService) => { - const es = getService('legacyEs'); + const es = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); let indicesCreated = []; diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index 4fd7c2cc2f067..1eb2862901277 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import mockRolledUpData, { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); const find = getService('find'); const retry = getService('retry'); @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }) { 'waiting for 3 records to be loaded into elasticsearch.', 10000, async () => { - const response = await es.indices.get({ + const { body: response } = await es.indices.get({ index: `${rollupSourceIndexPrefix}*`, allow_no_indices: false, }); @@ -53,9 +53,8 @@ export default function ({ getService, getPageObjects }) { await retry.try(async () => { //Create a rollup for kibana to recognize - await es.transport.request({ - path: `/_rollup/job/${rollupJobName}`, - method: 'PUT', + await es.rollup.putJob({ + id: rollupJobName, body: { index_pattern: `${rollupSourceIndexPrefix}*`, rollup_index: rollupTargetIndexName, @@ -104,10 +103,7 @@ export default function ({ getService, getPageObjects }) { after(async () => { // Delete the rollup job. - await es.transport.request({ - path: `/_rollup/job/${rollupJobName}`, - method: 'DELETE', - }); + await es.rollup.deleteJob({ id: rollupJobName }); await esDeleteAllIndices([ rollupTargetIndexName, diff --git a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js index 60a878f343c5c..a0684a77748b7 100644 --- a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js +++ b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['rollup', 'common', 'security']); const security = getService('security'); diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index aebea93f1e4bf..d0c7c86d6d5c3 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import mockRolledUpData from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); const esDeleteAllIndices = getService('esDeleteAllIndices'); @@ -49,9 +49,8 @@ export default function ({ getService, getPageObjects }) { await retry.try(async () => { //Create a rollup for kibana to recognize - await es.transport.request({ - path: `/_rollup/job/${rollupJobName}`, - method: 'PUT', + await es.rollup.putJob({ + id: rollupJobName, body: { index_pattern: rollupSourceIndexName, rollup_index: rollupTargetIndexName, From 64470829de7f8cbe8d4f577c9f983cce37c33e0a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 7 Apr 2021 15:17:04 +0200 Subject: [PATCH 28/65] [APM] Optimized type checking for API tests (#96388) Closes #95634. --- .../apm/scripts/optimize-tsconfig/optimize.js | 26 ++++++++++++++++++- .../apm/scripts/optimize-tsconfig/paths.js | 3 +++ .../optimize-tsconfig/test-tsconfig.json | 16 ++++++++++++ x-pack/plugins/apm/scripts/precommit.js | 24 ++++++++++------- 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index fed938119c4a6..58fb096ca3a51 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -18,7 +18,12 @@ const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const unlink = promisify(fs.unlink); -const { kibanaRoot, tsconfigTpl, filesToIgnore } = require('./paths'); +const { + kibanaRoot, + tsconfigTpl, + tsconfigTplTest, + filesToIgnore, +} = require('./paths'); const { unoptimizeTsConfig } = require('./unoptimize'); async function prepareBaseTsConfig() { @@ -57,6 +62,23 @@ async function addApmFilesToRootTsConfig() { ); } +async function addApmFilesToTestTsConfig() { + const template = json5.parse(await readFile(tsconfigTplTest, 'utf-8')); + const testTsConfigFilename = path.join( + kibanaRoot, + 'x-pack/test/tsconfig.json' + ); + const testTsConfig = json5.parse( + await readFile(testTsConfigFilename, 'utf-8') + ); + + await writeFile( + testTsConfigFilename, + JSON.stringify({ ...testTsConfig, ...template, references: [] }, null, 2), + { encoding: 'utf-8' } + ); +} + async function setIgnoreChanges() { for (const filename of filesToIgnore) { await execa('git', ['update-index', '--skip-worktree', filename]); @@ -74,6 +96,8 @@ async function optimizeTsConfig() { await addApmFilesToRootTsConfig(); + await addApmFilesToTestTsConfig(); + await deleteApmTsConfig(); await setIgnoreChanges(); diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index dbc207c9e6d26..bde129f434934 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -9,15 +9,18 @@ const path = require('path'); const kibanaRoot = path.resolve(__dirname, '../../../../..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); +const tsconfigTplTest = path.resolve(__dirname, './test-tsconfig.json'); const filesToIgnore = [ path.resolve(kibanaRoot, 'tsconfig.json'), path.resolve(kibanaRoot, 'tsconfig.base.json'), path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json'), + path.resolve(kibanaRoot, 'x-pack/test', 'tsconfig.json'), ]; module.exports = { kibanaRoot, tsconfigTpl, + tsconfigTplTest, filesToIgnore, }; diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json new file mode 100644 index 0000000000000..d6718b7511179 --- /dev/null +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "types": [ + "node" + ], + "noErrorTruncation": true + }, + "include": [ + "./apm_api_integration/**/*", + "../../packages/kbn-test/types/**/*", + "../../typings/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index c7102ce913f01..695a9ba70f5d7 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -23,6 +23,8 @@ const tsconfig = useOptimizedTsConfig ? resolve(root, 'tsconfig.json') : resolve(root, 'x-pack/plugins/apm/tsconfig.json'); +const testTsconfig = resolve(root, 'x-pack/test/tsconfig.json'); + const tasks = new Listr( [ { @@ -55,16 +57,18 @@ const tasks = new Listr( ], execaOpts ).then(() => - execa( - require.resolve('typescript/bin/tsc'), - [ - '--project', - tsconfig, - '--pretty', - ...(useOptimizedTsConfig ? ['--noEmit'] : []), - ], - execaOpts - ) + Promise.all([ + execa( + require.resolve('typescript/bin/tsc'), + ['--project', tsconfig, '--pretty', '--noEmit'], + execaOpts + ), + execa( + require.resolve('typescript/bin/tsc'), + ['--project', testTsconfig, '--pretty', '--noEmit'], + execaOpts + ), + ]) ), }, { From 6fa23eb61958d82e1abe351e6f7056fa1bd1e5cd Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 7 Apr 2021 16:14:26 +0200 Subject: [PATCH 29/65] Add docs for v2 migration timeouts related to fleet-agent-events (#95370) --- docs/setup/upgrade/upgrade-migrations.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 8603ca9935cac..fdcd71791ad3a 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -50,6 +50,16 @@ For large deployments with more than 10 {kib} instances and more than 10 000 sav ==== Preventing migration failures This section highlights common causes of {kib} upgrade failures and how to prevent them. +[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. + +This can cause Kibana to log errors like: +> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms] +> 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. + [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. From ba84a7105e1db9780c74a129199ffd80d4da2be6 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 7 Apr 2021 16:30:34 +0200 Subject: [PATCH 30/65] Deprecate migrations.enableV2 (#96398) * deprecate migrations.enableV2 * provide deprecation test tool from core * use deprecation test tool in tests * add a test for SO depreacations --- .../deprecation/core_deprecations.test.ts | 21 +------ src/core/server/config/test_utils.ts | 52 ++++++++++++++++++ .../elasticsearch_config.test.ts | 26 +++------ src/core/server/kibana_config.test.ts | 26 +++------ .../saved_objects_config.test.ts | 44 +++++++++++++++ .../saved_objects/saved_objects_config.ts | 55 +++++++++++++------ src/core/server/test_utils.ts | 1 + 7 files changed, 151 insertions(+), 74 deletions(-) create mode 100644 src/core/server/config/test_utils.ts create mode 100644 src/core/server/saved_objects/saved_objects_config.test.ts diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index e3c236405a596..a8063c317b3c5 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -6,27 +6,12 @@ * Side Public License, v 1. */ -import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; +import { getDeprecationsForGlobalSettings } from '../test_utils'; import { coreDeprecationProvider } from './core_deprecations'; - const initialEnv = { ...process.env }; -const applyCoreDeprecations = (settings: Record = {}) => { - const deprecations = coreDeprecationProvider(configDeprecationFactory); - const deprecationMessages: string[] = []; - const migrated = applyDeprecations( - settings, - deprecations.map((deprecation) => ({ - deprecation, - path: '', - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyCoreDeprecations = (settings?: Record) => + getDeprecationsForGlobalSettings({ provider: coreDeprecationProvider, settings }); describe('core deprecations', () => { beforeEach(() => { diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts new file mode 100644 index 0000000000000..2eaf462768724 --- /dev/null +++ b/src/core/server/config/test_utils.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { ConfigDeprecationProvider } from '@kbn/config'; +import { configDeprecationFactory, applyDeprecations } from '@kbn/config'; + +function collectDeprecations( + provider: ConfigDeprecationProvider, + settings: Record, + path: string +) { + const deprecations = provider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map((deprecation) => ({ + deprecation, + path, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +} + +export const getDeprecationsFor = ({ + provider, + settings = {}, + path, +}: { + provider: ConfigDeprecationProvider; + settings?: Record; + path: string; +}) => { + return collectDeprecations(provider, { [path]: settings }, path); +}; + +export const getDeprecationsForGlobalSettings = ({ + provider, + settings = {}, +}: { + provider: ConfigDeprecationProvider; + settings?: Record; +}) => { + return collectDeprecations(provider, settings, ''); +}; diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 23b804b535405..f8ef1a7a20a83 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -12,29 +12,17 @@ import { mockReadPkcs12Truststore, } from './elasticsearch_config.test.mocks'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; import { ElasticsearchConfig, config } from './elasticsearch_config'; +import { getDeprecationsFor } from '../config/test_utils'; const CONFIG_PATH = 'elasticsearch'; -const applyElasticsearchDeprecations = (settings: Record = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config: any = {}; - _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyElasticsearchDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: config.deprecations!, + settings, + path: CONFIG_PATH, + }); test('set correct defaults', () => { const configValue = new ElasticsearchConfig(config.schema.validate({})); diff --git a/src/core/server/kibana_config.test.ts b/src/core/server/kibana_config.test.ts index 1acdff9dd78e6..47bb6cf2a064c 100644 --- a/src/core/server/kibana_config.test.ts +++ b/src/core/server/kibana_config.test.ts @@ -7,28 +7,16 @@ */ import { config } from './kibana_config'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; +import { getDeprecationsFor } from './config/test_utils'; const CONFIG_PATH = 'kibana'; -const applyKibanaDeprecations = (settings: Record = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config: any = {}; - _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => ({ message }) => deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; +const applyKibanaDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: config.deprecations!, + settings, + path: CONFIG_PATH, + }); it('set correct defaults ', () => { const configValue = config.schema.validate({}); diff --git a/src/core/server/saved_objects/saved_objects_config.test.ts b/src/core/server/saved_objects/saved_objects_config.test.ts new file mode 100644 index 0000000000000..720b28403edf2 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_config.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { savedObjectsMigrationConfig } from './saved_objects_config'; +import { getDeprecationsFor } from '../config/test_utils'; + +const applyMigrationsDeprecations = (settings: Record = {}) => + getDeprecationsFor({ + provider: savedObjectsMigrationConfig.deprecations!, + settings, + path: 'migrations', + }); + +describe('migrations config', function () { + describe('deprecations', () => { + it('logs a warning if migrations.enableV2 is set: true', () => { + const { messages } = applyMigrationsDeprecations({ enableV2: true }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"migrations.enableV2\\" is deprecated and will be removed in an upcoming release without any further notice.", + ] + `); + }); + + it('logs a warning if migrations.enableV2 is set: false', () => { + const { messages } = applyMigrationsDeprecations({ enableV2: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"migrations.enableV2\\" is deprecated and will be removed in an upcoming release without any further notice.", + ] + `); + }); + }); + + it('does not log a warning if migrations.enableV2 is not set', () => { + const { messages } = applyMigrationsDeprecations({ batchSize: 1_000 }); + expect(messages).toMatchInlineSnapshot(`Array []`); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 96fac85ded076..7182df74c597f 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -7,31 +7,50 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import type { ServiceConfigDescriptor } from '../internal_types'; +import type { ConfigDeprecationProvider } from '../config'; -export type SavedObjectsMigrationConfigType = TypeOf; +const migrationSchema = schema.object({ + batchSize: schema.number({ defaultValue: 1_000 }), + scrollDuration: schema.string({ defaultValue: '15m' }), + pollInterval: schema.number({ defaultValue: 1_500 }), + skip: schema.boolean({ defaultValue: false }), + enableV2: schema.boolean({ defaultValue: true }), + retryAttempts: schema.number({ defaultValue: 15 }), +}); -export const savedObjectsMigrationConfig = { +export type SavedObjectsMigrationConfigType = TypeOf; + +const migrationDeprecations: ConfigDeprecationProvider = () => [ + (settings, fromPath, addDeprecation) => { + const migrationsConfig = settings[fromPath]; + if (migrationsConfig?.enableV2 !== undefined) { + addDeprecation({ + message: + '"migrations.enableV2" is deprecated and will be removed in an upcoming release without any further notice.', + documentationUrl: 'https://ela.st/kbn-so-migration-v2', + }); + } + return settings; + }, +]; + +export const savedObjectsMigrationConfig: ServiceConfigDescriptor = { path: 'migrations', - schema: schema.object({ - batchSize: schema.number({ defaultValue: 1000 }), - scrollDuration: schema.string({ defaultValue: '15m' }), - pollInterval: schema.number({ defaultValue: 1500 }), - skip: schema.boolean({ defaultValue: false }), - // TODO migrationsV2: remove/deprecate once we release migrations v2 - enableV2: schema.boolean({ defaultValue: true }), - /** the number of times v2 migrations will retry temporary failures such as a timeout, 503 status code or snapshot_in_progress_exception */ - retryAttempts: schema.number({ defaultValue: 15 }), - }), + schema: migrationSchema, + deprecations: migrationDeprecations, }; -export type SavedObjectsConfigType = TypeOf; +const soSchema = schema.object({ + maxImportPayloadBytes: schema.byteSize({ defaultValue: 26_214_400 }), + maxImportExportSize: schema.number({ defaultValue: 10_000 }), +}); + +export type SavedObjectsConfigType = TypeOf; -export const savedObjectsConfig = { +export const savedObjectsConfig: ServiceConfigDescriptor = { path: 'savedObjects', - schema: schema.object({ - maxImportPayloadBytes: schema.byteSize({ defaultValue: 26_214_400 }), - maxImportExportSize: schema.number({ defaultValue: 10_000 }), - }), + schema: soSchema, }; export class SavedObjectConfig { diff --git a/src/core/server/test_utils.ts b/src/core/server/test_utils.ts index 656d2bfe60fac..cf18defb0a960 100644 --- a/src/core/server/test_utils.ts +++ b/src/core/server/test_utils.ts @@ -9,3 +9,4 @@ export { createHttpServer } from './http/test_utils'; export { ServiceStatusLevelSnapshotSerializer } from './status/test_utils'; export { setupServer } from './saved_objects/routes/test_utils'; +export { getDeprecationsFor, getDeprecationsForGlobalSettings } from './config/test_utils'; From 7f4ec48ce63aa41c852ad6bcd9796d409a6e9fe9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 7 Apr 2021 10:32:20 -0400 Subject: [PATCH 31/65] [ML] Data Frame Analytics: add accuracy and recall stats to results view (#96270) * add accuracy and recall to classification results * update accuracy tooltip content --- .../data_frame_analytics/common/analytics.ts | 17 +++++++ .../evaluate_panel.tsx | 49 ++++++++++++++++++- .../evaluate_stat.tsx | 44 +++++++++++++++++ .../use_confusion_matrix.ts | 6 ++- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 0f1b50a7b9316..505673f440ef2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -160,11 +160,24 @@ export interface RocCurveItem { tpr: number; } +interface EvalClass { + class_name: string; + value: number; +} + export interface ClassificationEvaluateResponse { classification: { multiclass_confusion_matrix?: { confusion_matrix: ConfusionMatrix[]; }; + recall?: { + classes: EvalClass[]; + avg_recall: number; + }; + accuracy?: { + classes: EvalClass[]; + overall_accuracy: number; + }; auc_roc?: { curve?: RocCurveItem[]; value: number; @@ -434,6 +447,8 @@ export enum REGRESSION_STATS { interface EvaluateMetrics { classification: { + accuracy?: object; + recall?: object; multiclass_confusion_matrix?: object; auc_roc?: { include_curve: boolean; class_name: string }; }; @@ -486,6 +501,8 @@ export const loadEvalData = async ({ const metrics: EvaluateMetrics = { classification: { + accuracy: {}, + recall: {}, ...(includeMulticlassConfusionMatrix ? { multiclass_confusion_matrix: {} } : {}), ...(rocCurveClassName !== undefined ? { auc_roc: { include_curve: true, class_name: rocCurveClassName } } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index e848f209516f4..3795af32f6638 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -34,6 +34,7 @@ import { DataFrameTaskStateType } from '../../../analytics_management/components import { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '../expandable_section'; +import { EvaluateStat } from './evaluate_stat'; import { getRocCurveChartVegaLiteSpec } from './get_roc_curve_chart_vega_lite_spec'; @@ -112,10 +113,12 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se const isTraining = isTrainingFilter(searchQuery, resultsField); const { + avgRecall, confusionMatrixData, docsCount, error: errorConfusionMatrix, isLoading: isLoadingConfusionMatrix, + overallAccuracy, } = useConfusionMatrix(jobConfig, searchQuery); useEffect(() => { @@ -368,8 +371,52 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se )} ) : null} + {/* Accuracy and Recall */} + + + + + + + + + {/* AUC ROC Chart */} - + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx new file mode 100644 index 0000000000000..4bb8415d833f6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_stat.tsx @@ -0,0 +1,44 @@ +/* + * 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, { FC } from 'react'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EMPTY_STAT } from '../../../../common/analytics'; + +interface Props { + isLoading: boolean; + title: number | null; + description: string; + dataTestSubj: string; + tooltipContent: string; +} + +export const EvaluateStat: FC = ({ + isLoading, + title, + description, + dataTestSubj, + tooltipContent, +}) => ( + + + + + + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index be44a8e36ed00..df48d2c5ab44f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -30,6 +30,8 @@ export const useConfusionMatrix = ( searchQuery: ResultsSearchQuery ) => { const [confusionMatrixData, setConfusionMatrixData] = useState([]); + const [overallAccuracy, setOverallAccuracy] = useState(null); + const [avgRecall, setAvgRecall] = useState(null); const [isLoading, setIsLoading] = useState(false); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); @@ -77,6 +79,8 @@ export const useConfusionMatrix = ( evalData.eval?.classification?.multiclass_confusion_matrix?.confusion_matrix; setError(null); setConfusionMatrixData(confusionMatrix || []); + setAvgRecall(evalData.eval?.classification?.recall?.avg_recall || null); + setOverallAccuracy(evalData.eval?.classification?.accuracy?.overall_accuracy || null); setIsLoading(false); } else { setIsLoading(false); @@ -94,5 +98,5 @@ export const useConfusionMatrix = ( loadConfusionMatrixData(); }, [JSON.stringify([jobConfig, searchQuery])]); - return { confusionMatrixData, docsCount, error, isLoading }; + return { avgRecall, confusionMatrixData, docsCount, error, isLoading, overallAccuracy }; }; From f945f3a425120c55c0b8ed4666899966e68059ac Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 7 Apr 2021 16:39:11 +0200 Subject: [PATCH 32/65] [ML] Transforms: Wizard displays warning callout for source preview when used with CCS against clusters below 7.10. (#96297) The transforms UI source preview uses fields to search and retrieve document attributes. The feature was introduced in 7.10. For cross cluster search, as of now, when a search using fields is used using cross cluster search against a cluster earlier than 7.10, the API won't return an error or other information but just silently drop the fields attribute and return empty hits without field attributes. In Kibana, index patterns can be set up to use cross cluster search using the pattern :. If we identify such a pattern and the search hits don't include fields attributes, we display a warning callout from now on. --- .../components/data_grid/data_grid.tsx | 19 +++++++ .../application/components/data_grid/types.ts | 3 ++ .../components/data_grid/use_data_grid.tsx | 3 ++ .../public/app/hooks/__mocks__/use_api.ts | 13 ++++- .../public/app/hooks/use_index_data.test.tsx | 54 +++++++++++++++++-- .../public/app/hooks/use_index_data.ts | 5 ++ .../step_define/step_define_summary.test.tsx | 11 ++-- 7 files changed, 98 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 88f1c0226cb37..2a851eeccdce6 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -80,6 +80,7 @@ export const DataGrid: FC = memo( baseline, chartsVisible, chartsButtonVisible, + ccsWarning, columnsWithCharts, dataTestSubj, errorMessage, @@ -291,6 +292,24 @@ export const DataGrid: FC = memo( )} + {ccsWarning && ( +
    + +

    + {i18n.translate('xpack.ml.dataGrid.CcsWarningCalloutBody', { + defaultMessage: + 'There was an issue retrieving data for the index pattern. Source preview in combination with cross-cluster search is only supported for versions 7.10 and above. You may still configure and create the transform.', + })} +

    +
    + +
    + )}
    void; rowCount: number; rowCountRelation: RowCountRelation; + setCcsWarning: Dispatch>; setColumnCharts: Dispatch>; setErrorMessage: Dispatch>; setNoDataMessage: Dispatch>; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index e62f2eb2f003b..633c3d9aab002 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -36,6 +36,7 @@ export const useDataGrid = ( ): UseDataGridReturnType => { const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + const [ccsWarning, setCcsWarning] = useState(false); const [noDataMessage, setNoDataMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); @@ -152,6 +153,7 @@ export const useDataGrid = ( }, [chartsVisible, rowCount, rowCountRelation]); return { + ccsWarning, chartsVisible, chartsButtonVisible: true, columnsWithCharts, @@ -166,6 +168,7 @@ export const useDataGrid = ( rowCount, rowCountRelation, setColumnCharts, + setCcsWarning, setErrorMessage, setNoDataMessage, setPagination, diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index a9455877be429..3d5e1783f8c62 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -136,9 +136,20 @@ const apiFactory = () => ({ return Promise.resolve([]); }, async esSearch(payload: any): Promise { + const hits = []; + + // simulate a cross cluster search result + // against a cluster that doesn't support fields + if (payload.index.includes(':')) { + hits.push({ + _id: 'the-doc', + _index: 'the-index', + }); + } + return Promise.resolve({ hits: { - hits: [], + hits, total: { value: 0, relation: 'eq', diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index bd361afac2d8d..3e0a247106f2a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -7,7 +7,8 @@ import React, { FC } from 'react'; -import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { render, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { CoreSetup } from 'src/core/public'; @@ -49,6 +50,7 @@ describe('Transform: useIndexData()', () => { const wrapper: FC = ({ children }) => ( {children} ); + const { result, waitForNextUpdate } = renderHook( () => useIndexData( @@ -62,6 +64,7 @@ describe('Transform: useIndexData()', () => { ), { wrapper } ); + const IndexObj: UseIndexDataReturnType = result.current; await waitForNextUpdate(); @@ -73,7 +76,7 @@ describe('Transform: useIndexData()', () => { }); describe('Transform: with useIndexData()', () => { - test('Minimal initialization', async () => { + test('Minimal initialization, no cross cluster search warning.', async () => { // Arrange const indexPattern = { title: 'the-index-pattern-title', @@ -97,7 +100,47 @@ describe('Transform: with useIndexData()', () => { return ; }; - const { getByText } = render( + + const { queryByText } = render( + + + + ); + + // Act + // Assert + await waitFor(() => { + expect(queryByText('the-index-preview-title')).toBeInTheDocument(); + expect(queryByText('Cross-cluster search returned no fields data.')).not.toBeInTheDocument(); + }); + }); + + test('Cross-cluster search warning', async () => { + // Arrange + const indexPattern = { + title: 'remote:the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const mlSharedImports = await getMlSharedImports(); + + const Wrapper = () => { + const { + ml: { DataGrid }, + } = useAppDependencies(); + const props = { + ...useIndexData(indexPattern, { match_all: {} }, runtimeMappings), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return ; + }; + + const { queryByText } = render( @@ -105,6 +148,9 @@ describe('Transform: with useIndexData()', () => { // Act // Assert - expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByText('the-index-preview-title')).toBeInTheDocument(); + expect(queryByText('Cross-cluster search returned no fields data.')).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 36ba07afd69cd..f97693b8c038a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -87,6 +87,7 @@ export const useIndexData = ( pagination, resetPagination, setColumnCharts, + setCcsWarning, setErrorMessage, setRowCount, setRowCountRelation, @@ -134,8 +135,12 @@ export const useIndexData = ( return; } + const isCrossClusterSearch = indexPattern.title.includes(':'); + const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined'); + const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + setCcsWarning(isCrossClusterSearch && isMissingFields); setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); setRowCountRelation( typeof resp.hits.total === 'number' diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 51d3a0bd02d50..1e3fa2026061b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, wait } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -77,7 +77,7 @@ describe('Transform: ', () => { }, }; - const { getByText } = render( + const { queryByText } = render( @@ -85,8 +85,9 @@ describe('Transform: ', () => { // Act // Assert - expect(getByText('Group by')).toBeInTheDocument(); - expect(getByText('Aggregations')).toBeInTheDocument(); - await wait(); + await waitFor(() => { + expect(queryByText('Group by')).toBeInTheDocument(); + expect(queryByText('Aggregations')).toBeInTheDocument(); + }); }); }); From bb109b533c71d4a477a07472806e90d01aa82f3f Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 7 Apr 2021 10:44:12 -0400 Subject: [PATCH 33/65] [Actions] Hiding time field selector if no field with date mapping in index in Index Connector flyout (#96080) * Hiding time field selector if no field with date mapping in index * Fixing types check * Updating tooltip * PR fixes --- .../es_index/es_index_connector.test.tsx | 313 ++++++++++++++---- .../es_index/es_index_connector.tsx | 100 +++--- 2 files changed, 305 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx index 008fc8237c129..e9212bf633a79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { act } from 'react-dom/test-utils'; import { EsIndexActionConnector } from '../types'; import IndexActionConnectorFields from './es_index_connector'; +import { EuiComboBox, EuiSwitch, EuiSwitchEvent, EuiSelect } from '@elastic/eui'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/index_controls', () => ({ @@ -19,83 +20,263 @@ jest.mock('../../../../common/index_controls', () => ({ getIndexPatterns: jest.fn(), })); +const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); +getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, +]); + +const { getFields } = jest.requireMock('../../../../common/index_controls'); + +async function setup(props: any) { + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; +} + +function setupGetFieldsResponse(getFieldsWithDateMapping: boolean) { + getFields.mockResolvedValueOnce([ + { + type: getFieldsWithDateMapping ? 'date' : 'keyword', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); +} describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); - getIndexPatterns.mockResolvedValueOnce([ - { - id: 'indexPattern1', - attributes: { - title: 'indexPattern1', - }, - }, - { - id: 'indexPattern2', - attributes: { - title: 'indexPattern2', - }, - }, - ]); - const { getFields } = jest.requireMock('../../../../common/index_controls'); - getFields.mockResolvedValueOnce([ - { - type: 'date', - name: 'test1', - }, - { - type: 'text', - name: 'test2', - }, - ]); - - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test', - refresh: false, - executionTimeField: 'test1', - }, - } as EsIndexActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - /> - ); + test('renders correctly when creating connector', async () => { + const props = { + action: { + actionTypeId: '.index', + config: {}, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + + // time field switch shouldn't show up initially + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + + // time field switch should show up if index has date type field mapping + setupGetFieldsResponse(true); await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + // time field switch should go away if index does not has date type field mapping + setupGetFieldsResponse(false); + await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + // time field dropdown should show up if index has date type field mapping and time switch is clicked + setupGetFieldsResponse(true); + await act(async () => { + indexComboBox.prop('onChange')!([{ label: 'selection' }]); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + await act(async () => { + timeFieldSwitch.prop('onChange')!(({ + target: { checked: true }, + } as unknown) as EuiSwitchEvent); + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + }); + + test('renders correctly when editing connector - no date type field mapping', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(false); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + + // time related fields shouldn't show up + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); + }); + + test('renders correctly when editing connector - refresh set to true', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: true, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(false); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(true); + }); + + test('renders correctly when editing connector - with date type field mapping but no time field selected', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(true); + const wrapper = await setup(props); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); + + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); + + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + expect(timeFieldSwitch.prop('checked')).toEqual(false); + }); + + test('renders correctly when editing connector - with date type field mapping and selected time field', async () => { + const indexName = 'index-no-date-fields'; + const props = { + action: { + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + executionTimeField: 'test1', + }, + secrets: {}, + } as EsIndexActionConnector, + editActionConfig: () => {}, + editActionSecrets: () => {}, + errors: { index: [] }, + readOnly: false, + }; + setupGetFieldsResponse(true); + const wrapper = await setup(props); - const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); - expect(indexSearchBoxValue.first().props().value).toEqual(''); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); - const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); - indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox.find('input').first().simulate('change', event); + const indexComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="connectorIndexesComboBox"]'); + expect(indexComboBox.prop('selectedOptions')).toEqual([{ label: indexName, value: indexName }]); - const indexSearchBoxValueBeforeEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + const refreshSwitch = wrapper.find(EuiSwitch).filter('[data-test-subj="indexRefreshCheckbox"]'); + expect(refreshSwitch.prop('checked')).toEqual(false); - const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); - indexComboBoxClear.first().simulate('click'); + const timeFieldSwitch = wrapper + .find(EuiSwitch) + .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + expect(timeFieldSwitch.prop('checked')).toEqual(true); - const indexSearchBoxValueAfterEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); + const timeFieldSelect = wrapper + .find(EuiSelect) + .filter('[data-test-subj="executionTimeFieldSelect"]'); + expect(timeFieldSelect.prop('value')).toEqual('test1'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index cd3a03ecce15c..72af41277c29c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -30,29 +30,45 @@ import { } from '../../../../common/index_controls'; import { useKibana } from '../../../../common/lib/kibana'; +interface TimeFieldOptions { + value: string; + text: string; +} + const IndexActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, errors, readOnly }) => { const { http, docLinks } = useKibana().services; const { index, refresh, executionTimeField } = action.config; - const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState( + const [showTimeFieldCheckbox, setShowTimeFieldCheckboxState] = useState( + executionTimeField != null + ); + const [hasTimeFieldCheckbox, setHasTimeFieldCheckboxState] = useState( executionTimeField != null ); const [indexPatterns, setIndexPatterns] = useState([]); const [indexOptions, setIndexOptions] = useState([]); - const [timeFieldOptions, setTimeFieldOptions] = useState>([ - firstFieldOption, - ]); + const [timeFieldOptions, setTimeFieldOptions] = useState([]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); + const setTimeFields = (fields: TimeFieldOptions[]) => { + if (fields.length > 0) { + setShowTimeFieldCheckboxState(true); + setTimeFieldOptions([firstFieldOption, ...fields]); + } else { + setHasTimeFieldCheckboxState(false); + setShowTimeFieldCheckboxState(false); + setTimeFieldOptions([]); + } + }; + useEffect(() => { const indexPatternsFunction = async () => { setIndexPatterns(await getIndexPatterns()); if (index) { const currentEsFields = await getFields(http!, [index]); - const timeFields = getTimeFieldOptions(currentEsFields as any); - setTimeFieldOptions([firstFieldOption, ...timeFields]); + setTimeFields(getTimeFieldOptions(currentEsFields as any)); } }; indexPatternsFunction(); @@ -123,13 +139,11 @@ const IndexActionConnectorFields: React.FunctionComponent< // reset time field and expression fields if indices are deleted if (indices.length === 0) { - setTimeFieldOptions([]); + setTimeFields([]); return; } const currentEsFields = await getFields(http!, indices); - const timeFields = getTimeFieldOptions(currentEsFields as any); - - setTimeFieldOptions([firstFieldOption, ...timeFields]); + setTimeFields(getTimeFieldOptions(currentEsFields as any)); }} onSearchChange={async (search) => { setIsIndiciesLoading(true); @@ -172,38 +186,40 @@ const IndexActionConnectorFields: React.FunctionComponent< } /> - { - setTimeFieldCheckboxState(!hasTimeFieldCheckbox); - // if changing from checked to not checked (hasTimeField === true), - // set time field to null - if (hasTimeFieldCheckbox) { - editActionConfig('executionTimeField', null); + {showTimeFieldCheckbox && ( + { + setHasTimeFieldCheckboxState(!hasTimeFieldCheckbox); + // if changing from checked to not checked (hasTimeField === true), + // set time field to null + if (hasTimeFieldCheckbox) { + editActionConfig('executionTimeField', null); + } + }} + label={ + <> + + + } - }} - label={ - <> - - - - } - /> - {hasTimeFieldCheckbox ? ( + /> + )} + {hasTimeFieldCheckbox && ( <> - ) : null} + )} ); }; From 76ed8dbeabd9235d0c9aa94f3e8abbdbf46862ac Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 7 Apr 2021 07:54:12 -0700 Subject: [PATCH 34/65] [Alerting UI] Changed alerting UIs use new rule APIs. (#96018) * [Alerting UI] Changed alerting UIs use new rule APIs. * added unit tests * fixed types * fixed types * fixed types * fixed due to comments --- .../components/health_check.test.tsx | 36 +- .../application/components/health_check.tsx | 2 +- .../public/application/constants/index.ts | 5 +- .../public/application/lib/alert_api.test.ts | 875 ------------------ .../public/application/lib/alert_api.ts | 296 ------ .../lib/alert_api/aggregate.test.ts | 212 +++++ .../application/lib/alert_api/aggregate.ts | 44 + .../lib/alert_api/alert_summary.test.ts | 58 ++ .../lib/alert_api/alert_summary.ts | 41 + .../lib/alert_api/common_transformations.ts | 61 ++ .../application/lib/alert_api/create.test.ts | 140 +++ .../application/lib/alert_api/create.ts | 44 + .../application/lib/alert_api/delete.test.ts | 32 + .../application/lib/alert_api/delete.ts | 28 + .../application/lib/alert_api/disable.test.ts | 47 + .../application/lib/alert_api/disable.ts | 22 + .../application/lib/alert_api/enable.test.ts | 47 + .../application/lib/alert_api/enable.ts | 22 + .../lib/alert_api/get_rule.test.ts | 99 ++ .../application/lib/alert_api/get_rule.ts | 21 + .../application/lib/alert_api/health.test.ts | 41 + .../application/lib/alert_api/health.ts | 44 + .../public/application/lib/alert_api/index.ts | 24 + .../lib/alert_api/map_filters_to_kql.test.ts | 68 ++ .../lib/alert_api/map_filters_to_kql.ts | 36 + .../application/lib/alert_api/mute.test.ts | 47 + .../public/application/lib/alert_api/mute.ts | 16 + .../lib/alert_api/mute_alert.test.ts | 25 + .../application/lib/alert_api/mute_alert.ts | 20 + .../lib/alert_api/rule_types.test.ts | 45 + .../application/lib/alert_api/rule_types.ts | 39 + .../application/lib/alert_api/rules.test.ts | 242 +++++ .../public/application/lib/alert_api/rules.ts | 59 ++ .../application/lib/alert_api/state.test.ts | 101 ++ .../public/application/lib/alert_api/state.ts | 49 + .../application/lib/alert_api/unmute.test.ts | 47 + .../application/lib/alert_api/unmute.ts | 22 + .../lib/alert_api/unmute_alert.test.ts | 25 + .../application/lib/alert_api/unmute_alert.ts | 20 + .../application/lib/alert_api/update.test.ts | 60 ++ .../application/lib/alert_api/update.ts | 52 ++ 41 files changed, 2033 insertions(+), 1181 deletions(-) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 3baf4e33fb68d..44c950a500040 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -59,8 +59,13 @@ describe('health check', () => { it('renders children if keys are enabled', async () => { useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, + is_sufficiently_secure: true, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, }); const { queryByText } = render( @@ -78,8 +83,13 @@ describe('health check', () => { test('renders warning if TLS is required', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: false, - hasPermanentEncryptionKey: true, + is_sufficiently_secure: false, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); const { queryAllByText } = render( @@ -110,8 +120,13 @@ describe('health check', () => { test('renders warning if encryption key is ephemeral', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: false, + is_sufficiently_secure: true, + has_permanent_encryption_key: false, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); const { queryByText, queryByRole } = render( @@ -139,8 +154,13 @@ describe('health check', () => { test('renders warning if encryption key is ephemeral and keys are disabled', async () => { useKibanaMock().services.http.get = jest.fn().mockImplementation(async () => ({ - isSufficientlySecure: false, - hasPermanentEncryptionKey: false, + is_sufficiently_secure: false, + has_permanent_encryption_key: false, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, isAlertsAvailable: true, })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 208fd5ec66f1d..d75ab102a8e0c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -15,12 +15,12 @@ import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { DocLinksStart } from 'kibana/public'; -import { alertingFrameworkHealth } from '../lib/alert_api'; import './health_check.scss'; import { useHealthContext } from '../context/health_context'; import { useKibana } from '../../common/lib/kibana'; import { CenterJustifiedSpinner } from './center_justified_spinner'; import { triggersActionsUiHealth } from '../../common/lib/health_api'; +import { alertingFrameworkHealth } from '../lib/alert_api'; interface Props { inFlyout?: boolean; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 8ac1fbaec403b..cc04b8e7871cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; -export { LEGACY_BASE_ALERT_API_PATH } from '../../../../alerting/common'; +export { + BASE_ALERTING_API_PATH, + INTERNAL_BASE_ALERTING_API_PATH, +} from '../../../../alerting/common'; export { BASE_ACTION_API_PATH } from '../../../../actions/common'; export type Section = 'connectors' | 'rules'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts deleted file mode 100644 index d112e7ac284ae..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ /dev/null @@ -1,875 +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 { Alert, AlertType, AlertUpdates } from '../../types'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; -import { - createAlert, - deleteAlerts, - disableAlerts, - enableAlerts, - disableAlert, - enableAlert, - loadAlert, - loadAlertAggregations, - loadAlerts, - loadAlertState, - loadAlertTypes, - muteAlerts, - unmuteAlerts, - muteAlert, - unmuteAlert, - updateAlert, - muteAlertInstance, - unmuteAlertInstance, - alertingFrameworkHealth, - mapFiltersToKql, -} from './alert_api'; -import uuid from 'uuid'; -import { AlertNotifyWhenType, ALERTS_FEATURE_ID } from '../../../../alerting/common'; - -const http = httpServiceMock.createStartContract(); - -beforeEach(() => jest.resetAllMocks()); - -describe('loadAlertTypes', () => { - test('should call get alert types API', async () => { - const resolvedValue: AlertType[] = [ - { - id: 'test', - name: 'Test', - actionVariables: { - context: [{ name: 'var1', description: 'val1' }], - state: [{ name: 'var2', description: 'val2' }], - params: [{ name: 'var3', description: 'val3' }], - }, - producer: ALERTS_FEATURE_ID, - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - defaultActionGroupId: 'default', - authorizedConsumers: {}, - minimumLicenseRequired: 'basic', - enabledInLicense: true, - }, - ]; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertTypes({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/list_alert_types", - ] - `); - }); -}); - -describe('loadAlert', () => { - test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - id: alertId, - name: 'name', - tags: [], - enabled: true, - alertTypeId: '.noop', - schedule: { interval: '1s' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); - }); -}); - -describe('loadAlertState', () => { - test('should call get API with base parameters', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: {}, - second_instance: {}, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); - - test('should parse AlertInstances', async () => { - const alertId = uuid.v4(); - const resolvedValue = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: '2020-02-09T23:15:41.941Z', - }, - }, - }, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - expect(await loadAlertState({ http, alertId })).toEqual({ - ...resolvedValue, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: new Date('2020-02-09T23:15:41.941Z'), - }, - }, - }, - }, - }); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); - - test('should handle empty response from api', async () => { - const alertId = uuid.v4(); - http.get.mockResolvedValueOnce(''); - - expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); - }); -}); - -describe('loadAlerts', () => { - test('should call find API with base parameters', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with searchText', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with actionTypesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'foo', - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "page": 1, - "per_page": 10, - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": undefined, - "search_fields": undefined, - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with actionTypesFilter and typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'baz', - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); - - test('should call find API with searchText and tagsFilter and typesFilter', async () => { - const resolvedValue = { - page: 1, - perPage: 10, - total: 0, - data: [], - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlerts({ - http, - searchText: 'apples, foo, baz', - typesFilter: ['foo', 'bar'], - page: { index: 0, size: 10 }, - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_find", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "page": 1, - "per_page": 10, - "search": "apples, foo, baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - "sort_field": "name", - "sort_order": "asc", - }, - }, - ] - `); - }); -}); - -describe('loadAlertAggregations', () => { - test('should call aggregate API with base parameters', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); - }); - - test('should call aggregate API with searchText', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ http, searchText: 'apples' }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": undefined, - "search": "apples", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); - - test('should call aggregate API with actionTypesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - searchText: 'foo', - actionTypesFilter: ['action', 'type'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", - "search": "foo", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); - - test('should call aggregate API with typesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - typesFilter: ['foo', 'bar'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar)", - "search": undefined, - "search_fields": undefined, - }, - }, - ] - `); - }); - - test('should call aggregate API with actionTypesFilter and typesFilter', async () => { - const resolvedValue = { - alertExecutionStatus: { - ok: 4, - active: 2, - error: 1, - pending: 1, - unknown: 0, - }, - }; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadAlertAggregations({ - http, - searchText: 'baz', - actionTypesFilter: ['action', 'type'], - typesFilter: ['foo', 'bar'], - }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/_aggregate", - Object { - "query": Object { - "default_search_operator": "AND", - "filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", - "search": "baz", - "search_fields": "[\\"name\\",\\"tags\\"]", - }, - }, - ] - `); - }); -}); - -describe('deleteAlerts', () => { - test('should call delete API for each alert', async () => { - const ids = ['1', '2', '3']; - const result = await deleteAlerts({ http, ids }); - expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); - expect(http.delete.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1", - ], - Array [ - "/api/alerts/alert/2", - ], - Array [ - "/api/alerts/alert/3", - ], - ] - `); - }); -}); - -describe('createAlert', () => { - test('should call create alert API', async () => { - const alertToCreate: AlertUpdates = { - name: 'test', - consumer: 'alerts', - tags: ['foo'], - enabled: true, - alertTypeId: 'test', - schedule: { - interval: '1m', - }, - actions: [], - params: {}, - throttle: null, - notifyWhen: 'onActionGroupChange' as AlertNotifyWhenType, - createdAt: new Date('1970-01-01T00:00:00.000Z'), - updatedAt: new Date('1970-01-01T00:00:00.000Z'), - apiKeyOwner: null, - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - }; - const resolvedValue = { - ...alertToCreate, - id: '123', - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - }; - http.post.mockResolvedValueOnce(resolvedValue); - - const result = await createAlert({ http, alert: alertToCreate }); - expect(result).toEqual(resolvedValue); - expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/alert", - Object { - "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"notifyWhen\\":\\"onActionGroupChange\\",\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKeyOwner\\":null,\\"createdBy\\":null,\\"updatedBy\\":null,\\"muteAll\\":false,\\"mutedInstanceIds\\":[]}", - }, - ] - `); - }); -}); - -describe('updateAlert', () => { - test('should call alert update API', async () => { - const alertToUpdate = { - throttle: '1m', - consumer: 'alerts', - name: 'test', - tags: ['foo'], - schedule: { - interval: '1m', - }, - params: {}, - actions: [], - createdAt: new Date('1970-01-01T00:00:00.000Z'), - updatedAt: new Date('1970-01-01T00:00:00.000Z'), - apiKey: null, - apiKeyOwner: null, - notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, - }; - const resolvedValue: Alert = { - ...alertToUpdate, - id: '123', - enabled: true, - alertTypeId: 'test', - createdBy: null, - updatedBy: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - }; - http.put.mockResolvedValueOnce(resolvedValue); - - const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); - expect(result).toEqual(resolvedValue); - expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/alerts/alert/123", - Object { - "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[],\\"notifyWhen\\":\\"onThrottleInterval\\"}", - }, - ] - `); - }); -}); - -describe('enableAlert', () => { - test('should call enable alert API', async () => { - const result = await enableAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_enable", - ], - ] - `); - }); -}); - -describe('disableAlert', () => { - test('should call disable alert API', async () => { - const result = await disableAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_disable", - ], - ] - `); - }); -}); - -describe('muteAlertInstance', () => { - test('should call mute instance alert API', async () => { - const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/alert_instance/123/_mute", - ], - ] - `); - }); -}); - -describe('unmuteAlertInstance', () => { - test('should call mute instance alert API', async () => { - const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/alert_instance/123/_unmute", - ], - ] - `); - }); -}); - -describe('muteAlert', () => { - test('should call mute alert API', async () => { - const result = await muteAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_mute_all", - ], - ] - `); - }); -}); - -describe('unmuteAlert', () => { - test('should call unmute alert API', async () => { - const result = await unmuteAlert({ http, id: '1' }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_unmute_all", - ], - ] - `); - }); -}); - -describe('enableAlerts', () => { - test('should call enable alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await enableAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_enable", - ], - Array [ - "/api/alerts/alert/2/_enable", - ], - Array [ - "/api/alerts/alert/3/_enable", - ], - ] - `); - }); -}); - -describe('disableAlerts', () => { - test('should call disable alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await disableAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_disable", - ], - Array [ - "/api/alerts/alert/2/_disable", - ], - Array [ - "/api/alerts/alert/3/_disable", - ], - ] - `); - }); -}); - -describe('muteAlerts', () => { - test('should call mute alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await muteAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_mute_all", - ], - Array [ - "/api/alerts/alert/2/_mute_all", - ], - Array [ - "/api/alerts/alert/3/_mute_all", - ], - ] - `); - }); -}); - -describe('unmuteAlerts', () => { - test('should call unmute alert API per alert', async () => { - const ids = ['1', '2', '3']; - const result = await unmuteAlerts({ http, ids }); - expect(result).toEqual(undefined); - expect(http.post.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/alert/1/_unmute_all", - ], - Array [ - "/api/alerts/alert/2/_unmute_all", - ], - Array [ - "/api/alerts/alert/3/_unmute_all", - ], - ] - `); - }); -}); - -describe('alertingFrameworkHealth', () => { - test('should call alertingFrameworkHealth API', async () => { - const result = await alertingFrameworkHealth({ http }); - expect(result).toEqual(undefined); - expect(http.get.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerts/_health", - ], - ] - `); - }); -}); - -describe('mapFiltersToKql', () => { - test('should handle no filters', () => { - expect(mapFiltersToKql({})).toEqual([]); - }); - - test('should handle typesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - }) - ).toEqual(['alert.attributes.alertTypeId:(type or filter)']); - }); - - test('should handle actionTypesFilter', () => { - expect( - mapFiltersToKql({ - actionTypesFilter: ['action', 'types', 'filter'], - }) - ).toEqual([ - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - ]); - }); - - test('should handle alertStatusesFilter', () => { - expect( - mapFiltersToKql({ - alertStatusesFilter: ['alert', 'statuses', 'filter'], - }) - ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); - }); - - test('should handle typesFilter and actionTypesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - actionTypesFilter: ['action', 'types', 'filter'], - }) - ).toEqual([ - 'alert.attributes.alertTypeId:(type or filter)', - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - ]); - }); - - test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { - expect( - mapFiltersToKql({ - typesFilter: ['type', 'filter'], - actionTypesFilter: ['action', 'types', 'filter'], - alertStatusesFilter: ['alert', 'statuses', 'filter'], - }) - ).toEqual([ - 'alert.attributes.alertTypeId:(type or filter)', - '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', - 'alert.attributes.executionStatus.status:(alert or statuses or filter)', - ]); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts deleted file mode 100644 index 80ff415582191..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ /dev/null @@ -1,296 +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 { HttpSetup } from 'kibana/public'; -import { Errors, identity } from 'io-ts'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { pick } from 'lodash'; -import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerting/common'; -import { LEGACY_BASE_ALERT_API_PATH } from '../constants'; -import { - Alert, - AlertAggregations, - AlertType, - AlertUpdates, - AlertTaskState, - AlertInstanceSummary, - Pagination, - Sorting, -} from '../../types'; - -export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/list_alert_types`); -} - -export async function loadAlert({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}`); -} - -type EmptyHttpResponse = ''; -export async function loadAlertState({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http - .get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}/state`) - .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) - .then((state: AlertTaskState) => { - return pipe( - alertStateSchema.decode(state), - fold((e: Errors) => { - throw new Error(`Alert "${alertId}" has invalid state`); - }, identity) - ); - }); -} - -export async function loadAlertInstanceSummary({ - http, - alertId, -}: { - http: HttpSetup; - alertId: string; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/alert/${alertId}/_instance_summary`); -} - -export const mapFiltersToKql = ({ - typesFilter, - actionTypesFilter, - alertStatusesFilter, -}: { - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): string[] => { - const filters = []; - if (typesFilter && typesFilter.length) { - filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); - } - if (actionTypesFilter && actionTypesFilter.length) { - filters.push( - [ - '(', - actionTypesFilter - .map((id) => `alert.attributes.actions:{ actionTypeId:${id} }`) - .join(' OR '), - ')', - ].join('') - ); - } - if (alertStatusesFilter && alertStatusesFilter.length) { - filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); - } - return filters; -}; - -export async function loadAlerts({ - http, - page, - searchText, - typesFilter, - actionTypesFilter, - alertStatusesFilter, - sort = { field: 'name', direction: 'asc' }, -}: { - http: HttpSetup; - page: Pagination; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; - sort?: Sorting; -}): Promise<{ - page: number; - perPage: number; - total: number; - data: Alert[]; -}> { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_find`, { - query: { - page: page.index + 1, - per_page: page.size, - search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, - search: searchText, - filter: filters.length ? filters.join(' and ') : undefined, - default_search_operator: 'AND', - sort_field: sort.field, - sort_order: sort.direction, - }, - }); -} - -export async function loadAlertAggregations({ - http, - searchText, - typesFilter, - actionTypesFilter, - alertStatusesFilter, -}: { - http: HttpSetup; - searchText?: string; - typesFilter?: string[]; - actionTypesFilter?: string[]; - alertStatusesFilter?: string[]; -}): Promise { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_aggregate`, { - query: { - search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, - search: searchText, - filter: filters.length ? filters.join(' and ') : undefined, - default_search_operator: 'AND', - }, - }); -} - -export async function deleteAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise<{ successes: string[]; errors: string[] }> { - const successes: string[] = []; - const errors: string[] = []; - await Promise.all(ids.map((id) => http.delete(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`))).then( - function (fulfilled) { - successes.push(...fulfilled); - }, - function (rejected) { - errors.push(...rejected); - } - ); - return { successes, errors }; -} - -export async function createAlert({ - http, - alert, -}: { - http: HttpSetup; - alert: Omit< - AlertUpdates, - 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' - >; -}): Promise { - return await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert`, { - body: JSON.stringify(alert), - }); -} - -export async function updateAlert({ - http, - alert, - id, -}: { - http: HttpSetup; - alert: Pick< - AlertUpdates, - 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' - >; - id: string; -}): Promise { - return await http.put(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}`, { - body: JSON.stringify( - pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) - ), - }); -} - -export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_enable`); -} - -export async function enableAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => enableAlert({ id, http }))); -} - -export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_disable`); -} - -export async function disableAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => disableAlert({ id, http }))); -} - -export async function muteAlertInstance({ - id, - instanceId, - http, -}: { - id: string; - instanceId: string; - http: HttpSetup; -}): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_mute`); -} - -export async function unmuteAlertInstance({ - id, - instanceId, - http, -}: { - id: string; - instanceId: string; - http: HttpSetup; -}): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_unmute`); -} - -export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_mute_all`); -} - -export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map((id) => muteAlert({ http, id }))); -} - -export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${LEGACY_BASE_ALERT_API_PATH}/alert/${id}/_unmute_all`); -} - -export async function unmuteAlerts({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise { - await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); -} - -export async function alertingFrameworkHealth({ - http, -}: { - http: HttpSetup; -}): Promise { - return await http.get(`${LEGACY_BASE_ALERT_API_PATH}/_health`); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts new file mode 100644 index 0000000000000..57feb1e7abae9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.test.ts @@ -0,0 +1,212 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertAggregations } from './aggregate'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertAggregations', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call aggregate API with base parameters', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ http }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); + + test('should call aggregate API with searchText', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ http, searchText: 'apples' }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('should call aggregate API with actionTypesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + searchText: 'foo', + actionTypesFilter: ['action', 'type'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('should call aggregate API with typesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + typesFilter: ['foo', 'bar'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); + + test('should call aggregate API with actionTypesFilter and typesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertAggregations({ + http, + searchText: 'baz', + actionTypesFilter: ['action', 'type'], + typesFilter: ['foo', 'bar'], + }); + expect(result).toEqual({ + alertExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })", + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts new file mode 100644 index 0000000000000..589677ec2322d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/aggregate.ts @@ -0,0 +1,44 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { AlertAggregations } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { mapFiltersToKql } from './map_filters_to_kql'; +import { RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + rule_execution_status: alertExecutionStatus, + ...rest +}: any) => ({ + ...rest, + alertExecutionStatus, +}); + +export async function loadAlertAggregations({ + http, + searchText, + typesFilter, + actionTypesFilter, + alertStatusesFilter, +}: { + http: HttpSetup; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; +}): Promise { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { + query: { + search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, + search: searchText, + filter: filters.length ? filters.join(' and ') : undefined, + default_search_operator: 'AND', + }, + }); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts new file mode 100644 index 0000000000000..e94da81d0f5d5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AlertInstanceSummary } from '../../../../../alerting/common'; +import { loadAlertInstanceSummary } from './alert_summary'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertInstanceSummary', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertInstanceSummary = { + instances: {}, + consumer: 'alerts', + enabled: true, + errorMessages: [], + id: 'test', + lastRun: '2021-04-01T22:18:27.609Z', + muteAll: false, + name: 'test', + alertTypeId: '.index-threshold', + status: 'OK', + statusEndDate: '2021-04-01T22:19:25.174Z', + statusStartDate: '2021-04-01T21:19:25.174Z', + tags: [], + throttle: null, + }; + + http.get.mockResolvedValueOnce({ + alerts: {}, + consumer: 'alerts', + enabled: true, + error_messages: [], + id: 'test', + last_run: '2021-04-01T22:18:27.609Z', + mute_all: false, + name: 'test', + rule_type_id: '.index-threshold', + status: 'OK', + status_end_date: '2021-04-01T22:19:25.174Z', + status_start_date: '2021-04-01T21:19:25.174Z', + tags: [], + throttle: null, + }); + + const result = await loadAlertInstanceSummary({ http, alertId: 'test' }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rule/test/_alert_summary", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts new file mode 100644 index 0000000000000..e37c0640ec1c8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.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 { HttpSetup } from 'kibana/public'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { AlertInstanceSummary } from '../../../types'; +import { RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + alerts, + rule_type_id: alertTypeId, + mute_all: muteAll, + status_start_date: statusStartDate, + status_end_date: statusEndDate, + error_messages: errorMessages, + last_run: lastRun, + ...rest +}: any) => ({ + ...rest, + alertTypeId, + muteAll, + statusStartDate, + statusEndDate, + errorMessages, + lastRun, + instances: alerts, +}); + +export async function loadAlertInstanceSummary({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/_alert_summary`); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts new file mode 100644 index 0000000000000..749cf53cf740b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts @@ -0,0 +1,61 @@ +/* + * 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 { AlertExecutionStatus } from '../../../../../alerting/common'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { Alert, AlertAction } from '../../../types'; + +const transformAction: RewriteRequestCase = ({ + group, + id, + connector_type_id: actionTypeId, + params, +}) => ({ + group, + id, + params, + actionTypeId, +}); + +const transformExecutionStatus: RewriteRequestCase = ({ + last_execution_date: lastExecutionDate, + ...rest +}) => ({ + lastExecutionDate, + ...rest, +}); + +export const transformAlert: RewriteRequestCase = ({ + rule_type_id: alertTypeId, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, + api_key_owner: apiKeyOwner, + notify_when: notifyWhen, + mute_all: muteAll, + muted_alert_ids: mutedInstanceIds, + scheduled_task_id: scheduledTaskId, + execution_status: executionStatus, + actions: actions, + ...rest +}: any) => ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined, + actions: actions + ? actions.map((action: AsApiContract) => transformAction(action)) + : [], + scheduledTaskId, + ...rest, +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts new file mode 100644 index 0000000000000..8d1ec57a4e63e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AlertUpdates } from '../../../types'; +import { createAlert } from './create'; + +const http = httpServiceMock.createStartContract(); + +describe('createAlert', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call create alert API', async () => { + const resolvedValue = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + rule_type_id: '.index-threshold', + notify_when: 'onActionGroupChange', + actions: [ + { + group: 'threshold met', + id: '1', + params: { + level: 'info', + message: 'alert ', + }, + connector_type_id: '.server-log', + }, + ], + scheduled_task_id: '1', + execution_status: { status: 'pending', last_execution_date: '2021-04-01T21:33:13.250Z' }, + create_at: '2021-04-01T21:33:13.247Z', + updated_at: '2021-04-01T21:33:13.247Z', + }; + const alertToCreate: Omit< + AlertUpdates, + 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' + > = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + enabled: true, + throttle: null, + alertTypeId: '.index-threshold', + notifyWhen: 'onActionGroupChange', + actions: [ + { + group: 'threshold met', + id: '83d4d860-9316-11eb-a145-93ab369a4461', + params: { + level: 'info', + message: + "alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}", + }, + actionTypeId: '.server-log', + }, + ], + createdAt: new Date('2021-04-01T21:33:13.247Z'), + updatedAt: new Date('2021-04-01T21:33:13.247Z'), + apiKeyOwner: '', + }; + http.post.mockResolvedValueOnce(resolvedValue); + + const result = await createAlert({ http, alert: alertToCreate }); + expect(result).toEqual({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + id: '1', + params: { + level: 'info', + message: 'alert ', + }, + }, + ], + alertTypeId: '.index-threshold', + apiKeyOwner: undefined, + consumer: 'alerts', + create_at: '2021-04-01T21:33:13.247Z', + createdAt: undefined, + createdBy: undefined, + executionStatus: { + lastExecutionDate: '2021-04-01T21:33:13.250Z', + status: 'pending', + }, + muteAll: undefined, + mutedInstanceIds: undefined, + name: 'test', + notifyWhen: 'onActionGroupChange', + params: { + aggType: 'count', + groupBy: 'all', + index: ['.kibana'], + termSize: 5, + threshold: [1000], + thresholdComparator: '>', + timeField: 'alert.executionStatus.lastExecutionDate', + timeWindowSize: 5, + timeWindowUnit: 'm', + }, + schedule: { + interval: '1m', + }, + scheduledTaskId: '1', + tags: [], + updatedAt: '2021-04-01T21:33:13.247Z', + updatedBy: undefined, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts new file mode 100644 index 0000000000000..bd92769b4bbf3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/create.ts @@ -0,0 +1,44 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { RewriteResponseCase } from '../../../../../actions/common'; +import { Alert, AlertUpdates } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { transformAlert } from './common_transformations'; + +type AlertCreateBody = Omit< + AlertUpdates, + 'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus' +>; +const rewriteBodyRequest: RewriteResponseCase = ({ + alertTypeId, + notifyWhen, + actions, + ...res +}): any => ({ + ...res, + rule_type_id: alertTypeId, + notify_when: notifyWhen, + actions: actions.map(({ group, id, params }) => ({ + group, + id, + params, + })), +}); + +export async function createAlert({ + http, + alert, +}: { + http: HttpSetup; + alert: AlertCreateBody; +}): Promise { + const res = await http.post(`${BASE_ALERTING_API_PATH}/rule`, { + body: JSON.stringify(rewriteBodyRequest(alert)), + }); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts new file mode 100644 index 0000000000000..b279e4c0237d9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { deleteAlerts } from './delete'; + +const http = httpServiceMock.createStartContract(); + +describe('deleteAlerts', () => { + test('should call delete API for each alert', async () => { + const ids = ['1', '2', '3']; + const result = await deleteAlerts({ http, ids }); + expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); + expect(http.delete.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1", + ], + Array [ + "/api/alerting/rule/2", + ], + Array [ + "/api/alerting/rule/3", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts new file mode 100644 index 0000000000000..870d5a409c3dd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/delete.ts @@ -0,0 +1,28 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function deleteAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise<{ successes: string[]; errors: string[] }> { + const successes: string[] = []; + const errors: string[] = []; + await Promise.all(ids.map((id) => http.delete(`${BASE_ALERTING_API_PATH}/rule/${id}`))).then( + function (fulfilled) { + successes.push(...fulfilled); + }, + function (rejected) { + errors.push(...rejected); + } + ); + return { successes, errors }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts new file mode 100644 index 0000000000000..90d1cd13096e8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { disableAlert, disableAlerts } from './disable'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_disable", + ], + ] + `); + }); +}); + +describe('disableAlerts', () => { + test('should call disable alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await disableAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_disable", + ], + Array [ + "/api/alerting/rule/2/_disable", + ], + Array [ + "/api/alerting/rule/3/_disable", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts new file mode 100644 index 0000000000000..cc0939fbebfbd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/disable.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_disable`); +} + +export async function disableAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => disableAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts new file mode 100644 index 0000000000000..ef65e8b605cba --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { enableAlert, enableAlerts } from './enable'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_enable", + ], + ] + `); + }); +}); + +describe('enableAlerts', () => { + test('should call enable alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await enableAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_enable", + ], + Array [ + "/api/alerting/rule/2/_enable", + ], + Array [ + "/api/alerting/rule/3/_enable", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts new file mode 100644 index 0000000000000..3c16ffaec6223 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/enable.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_enable`); +} + +export async function enableAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => enableAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts new file mode 100644 index 0000000000000..f2d8337eb4091 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlert } from './get_rule'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: '1', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + rule_type_id: '.index-threshold', + created_by: 'elastic', + updated_by: 'elastic', + created_at: '2021-04-01T20:29:18.652Z', + updated_at: '2021-04-01T20:33:38.260Z', + api_key_owner: 'elastic', + notify_when: 'onThrottleInterval', + mute_all: false, + muted_alert_ids: [], + scheduled_task_id: '1', + execution_status: { status: 'ok', last_execution_date: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + connector_type_id: '.index', + }, + ], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual({ + id: '1', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + alertTypeId: '.index-threshold', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2021-04-01T20:29:18.652Z', + updatedAt: '2021-04-01T20:33:38.260Z', + apiKeyOwner: 'elastic', + notifyWhen: 'onThrottleInterval', + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '1', + executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + actionTypeId: '.index', + }, + ], + }); + expect(http.get).toHaveBeenCalledWith(`/api/alerting/rule/${alertId}`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts new file mode 100644 index 0000000000000..2e4cbc9b50c51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/get_rule.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { Alert } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { transformAlert } from './common_transformations'; + +export async function loadAlert({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/rule/${alertId}`); + return transformAlert(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.ts new file mode 100644 index 0000000000000..e08306bee0f9c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.test.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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { alertingFrameworkHealth } from './health'; + +describe('alertingFrameworkHealth', () => { + const http = httpServiceMock.createStartContract(); + test('should call alertingFrameworkHealth API', async () => { + http.get.mockResolvedValueOnce({ + is_sufficiently_secure: true, + has_permanent_encryption_key: true, + alerting_framework_heath: { + decryption_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + execution_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + read_health: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, + }); + const result = await alertingFrameworkHealth({ http }); + expect(result).toEqual({ + alertingFrameworkHeath: { + decryptionHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + executionHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + readHealth: { status: 'ok', timestamp: '2021-04-01T21:29:22.991Z' }, + }, + hasPermanentEncryptionKey: true, + isSufficientlySecure: true, + }); + expect(http.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/_health", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts new file mode 100644 index 0000000000000..9468f4b3c03e0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/health.ts @@ -0,0 +1,44 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { AlertingFrameworkHealth, AlertsHealth } from '../../../../../alerting/common'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +const rewriteAlertingFrameworkHeath: RewriteRequestCase = ({ + decryption_health: decryptionHealth, + execution_health: executionHealth, + read_health: readHealth, + ...res +}: AsApiContract) => ({ + decryptionHealth, + executionHealth, + readHealth, + ...res, +}); + +const rewriteBodyRes: RewriteRequestCase = ({ + is_sufficiently_secure: isSufficientlySecure, + has_permanent_encryption_key: hasPermanentEncryptionKey, + alerting_framework_heath: alertingFrameworkHeath, + ...res +}: AsApiContract) => ({ + isSufficientlySecure, + hasPermanentEncryptionKey, + alertingFrameworkHeath, + ...res, +}); + +export async function alertingFrameworkHealth({ + http, +}: { + http: HttpSetup; +}): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/_health`); + const alertingFrameworkHeath = rewriteAlertingFrameworkHeath(res.alerting_framework_heath); + return { ...rewriteBodyRes(res), alertingFrameworkHeath }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts new file mode 100644 index 0000000000000..a0b090a474e28 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { alertingFrameworkHealth } from './health'; +export { mapFiltersToKql } from './map_filters_to_kql'; +export { loadAlertAggregations } from './aggregate'; +export { createAlert } from './create'; +export { deleteAlerts } from './delete'; +export { disableAlert, disableAlerts } from './disable'; +export { enableAlert, enableAlerts } from './enable'; +export { loadAlert } from './get_rule'; +export { loadAlertInstanceSummary } from './alert_summary'; +export { muteAlertInstance } from './mute_alert'; +export { muteAlert, muteAlerts } from './mute'; +export { loadAlertTypes } from './rule_types'; +export { loadAlerts } from './rules'; +export { loadAlertState } from './state'; +export { unmuteAlertInstance } from './unmute_alert'; +export { unmuteAlert, unmuteAlerts } from './unmute'; +export { updateAlert } from './update'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.ts new file mode 100644 index 0000000000000..4e5e2a412dad6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapFiltersToKql } from './map_filters_to_kql'; + +describe('mapFiltersToKql', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should handle no filters', () => { + expect(mapFiltersToKql({})).toEqual([]); + }); + + test('should handle typesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + }) + ).toEqual(['alert.attributes.alertTypeId:(type or filter)']); + }); + + test('should handle actionTypesFilter', () => { + expect( + mapFiltersToKql({ + actionTypesFilter: ['action', 'types', 'filter'], + }) + ).toEqual([ + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + ]); + }); + + test('should handle alertStatusesFilter', () => { + expect( + mapFiltersToKql({ + alertStatusesFilter: ['alert', 'statuses', 'filter'], + }) + ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); + }); + + test('should handle typesFilter and actionTypesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + actionTypesFilter: ['action', 'types', 'filter'], + }) + ).toEqual([ + 'alert.attributes.alertTypeId:(type or filter)', + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + ]); + }); + + test('should handle typesFilter, actionTypesFilter and alertStatusesFilter', () => { + expect( + mapFiltersToKql({ + typesFilter: ['type', 'filter'], + actionTypesFilter: ['action', 'types', 'filter'], + alertStatusesFilter: ['alert', 'statuses', 'filter'], + }) + ).toEqual([ + 'alert.attributes.alertTypeId:(type or filter)', + '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', + 'alert.attributes.executionStatus.status:(alert or statuses or filter)', + ]); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts new file mode 100644 index 0000000000000..4c30e960034bf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/map_filters_to_kql.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mapFiltersToKql = ({ + typesFilter, + actionTypesFilter, + alertStatusesFilter, +}: { + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; +}): string[] => { + const filters = []; + if (typesFilter && typesFilter.length) { + filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); + } + if (actionTypesFilter && actionTypesFilter.length) { + filters.push( + [ + '(', + actionTypesFilter + .map((id) => `alert.attributes.actions:{ actionTypeId:${id} }`) + .join(' OR '), + ')', + ].join('') + ); + } + if (alertStatusesFilter && alertStatusesFilter.length) { + filters.push(`alert.attributes.executionStatus.status:(${alertStatusesFilter.join(' or ')})`); + } + return filters; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts new file mode 100644 index 0000000000000..75143dd6b7f85 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { muteAlert, muteAlerts } from './mute'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_mute_all", + ], + ] + `); + }); +}); + +describe('muteAlerts', () => { + test('should call mute alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await muteAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_mute_all", + ], + Array [ + "/api/alerting/rule/2/_mute_all", + ], + Array [ + "/api/alerting/rule/3/_mute_all", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts new file mode 100644 index 0000000000000..22a96d7a11ff3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_mute_all`); +} + +export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { + await Promise.all(ids.map((id) => muteAlert({ http, id }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts new file mode 100644 index 0000000000000..4365cce42c8c3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { muteAlertInstance } from './mute_alert'; + +const http = httpServiceMock.createStartContract(); + +describe('muteAlertInstance', () => { + test('should call mute instance alert API', async () => { + const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/alert/123/_mute", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.ts new file mode 100644 index 0000000000000..0bb05010cfa3c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/mute_alert.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. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function muteAlertInstance({ + id, + instanceId, + http, +}: { + id: string; + instanceId: string; + http: HttpSetup; +}): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_mute`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts new file mode 100644 index 0000000000000..71513ed0c6e61 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertType } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertTypes } from './rule_types'; +import { ALERTS_FEATURE_ID } from '../../../../../alerting/common'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertTypes', () => { + test('should call get alert types API', async () => { + const resolvedValue: AlertType[] = [ + { + id: 'test', + name: 'Test', + actionVariables: { + context: [{ name: 'var1', description: 'val1' }], + state: [{ name: 'var2', description: 'val2' }], + params: [{ name: 'var3', description: 'val3' }], + }, + producer: ALERTS_FEATURE_ID, + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }, + ]; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlertTypes({ http }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rule_types", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts new file mode 100644 index 0000000000000..54369d7959c93 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.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 { HttpSetup } from 'kibana/public'; +import { AlertType } from '../../../types'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteResponseRes = (results: Array>): AlertType[] => { + return results.map((item) => rewriteBodyReq(item)); +}; + +const rewriteBodyReq: RewriteRequestCase = ({ + enabled_in_license: enabledInLicense, + recovery_action_group: recoveryActionGroup, + action_groups: actionGroups, + default_action_group_id: defaultActionGroupId, + minimum_license_required: minimumLicenseRequired, + action_variables: actionVariables, + authorized_consumers: authorizedConsumers, + ...rest +}: AsApiContract) => ({ + enabledInLicense, + recoveryActionGroup, + actionGroups, + defaultActionGroupId, + minimumLicenseRequired, + actionVariables, + authorizedConsumers, + ...rest, +}); + +export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + const res = await http.get(`${BASE_ALERTING_API_PATH}/rule_types`); + return rewriteResponseRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts new file mode 100644 index 0000000000000..602507c08066c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.test.ts @@ -0,0 +1,242 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlerts } from './rules'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlerts', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call find API with base parameters', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ http, page: { index: 0, size: 10 } }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with searchText', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ http, searchText: 'apples', page: { index: 0, size: 10 } }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "apples", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with actionTypesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'foo', + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": undefined, + "page": 1, + "per_page": 10, + "search": "foo", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with actionTypesFilter and typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'baz', + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); + + test('should call find API with searchText and tagsFilter and typesFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadAlerts({ + http, + searchText: 'apples, foo, baz', + typesFilter: ['foo', 'bar'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.alertTypeId:(foo or bar)", + "page": 1, + "per_page": 10, + "search": "apples, foo, baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.ts new file mode 100644 index 0000000000000..f0bbb57180bb4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rules.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 { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { Alert, Pagination, Sorting } from '../../../types'; +import { AsApiContract } from '../../../../../actions/common'; +import { mapFiltersToKql } from './map_filters_to_kql'; +import { transformAlert } from './common_transformations'; + +const rewriteResponseRes = (results: Array>): Alert[] => { + return results.map((item) => transformAlert(item)); +}; + +export async function loadAlerts({ + http, + page, + searchText, + typesFilter, + actionTypesFilter, + alertStatusesFilter, + sort = { field: 'name', direction: 'asc' }, +}: { + http: HttpSetup; + page: Pagination; + searchText?: string; + typesFilter?: string[]; + actionTypesFilter?: string[]; + alertStatusesFilter?: string[]; + sort?: Sorting; +}): Promise<{ + page: number; + perPage: number; + total: number; + data: Alert[]; +}> { + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, alertStatusesFilter }); + const res = await http.get(`${BASE_ALERTING_API_PATH}/rules/_find`, { + query: { + page: page.index + 1, + per_page: page.size, + search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined, + search: searchText, + filter: filters.length ? filters.join(' and ') : undefined, + default_search_operator: 'AND', + sort_field: sort.field, + sort_order: sort.direction, + }, + }); + return { + page: res.page, + perPage: res.per_page, + total: res.total, + data: rewriteResponseRes(res.data), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts new file mode 100644 index 0000000000000..ae27352be0b90 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAlertState } from './state'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('loadAlertState', () => { + beforeEach(() => jest.resetAllMocks()); + + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: {}, + second_instance: {}, + }, + }; + http.get.mockResolvedValueOnce({ + rule_type_state: { + some: 'value', + }, + alerts: { + first_instance: {}, + second_instance: {}, + }, + }); + + expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); + + test('should parse AlertInstances', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }; + http.get.mockResolvedValueOnce({ + rule_type_state: { + some: 'value', + }, + alerts: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: '2020-02-09T23:15:41.941Z', + }, + }, + }, + }, + }); + + expect(await loadAlertState({ http, alertId })).toEqual({ + ...resolvedValue, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date('2020-02-09T23:15:41.941Z'), + }, + }, + }, + }, + }); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); + + test('should handle empty response from api', async () => { + const alertId = uuid.v4(); + http.get.mockResolvedValueOnce(''); + + expect(await loadAlertState({ http, alertId })).toEqual({}); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${alertId}/state`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts new file mode 100644 index 0000000000000..428bc5b99a70b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/state.ts @@ -0,0 +1,49 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { Errors, identity } from 'io-ts'; +import { AlertTaskState } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { alertStateSchema } from '../../../../../alerting/common'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; + +const rewriteBodyRes: RewriteRequestCase = ({ + rule_type_state: alertTypeState, + alerts: alertInstances, + previous_started_at: previousStartedAt, + ...rest +}: any) => ({ + ...rest, + alertTypeState, + alertInstances, + previousStartedAt, +}); + +type EmptyHttpResponse = ''; +export async function loadAlertState({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http + .get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/state`) + .then((state: AsApiContract | EmptyHttpResponse) => + state ? rewriteBodyRes(state) : {} + ) + .then((state: AlertTaskState) => { + return pipe( + alertStateSchema.decode(state), + fold((e: Errors) => { + throw new Error(`Alert "${alertId}" has invalid state`); + }, identity) + ); + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts new file mode 100644 index 0000000000000..68a6feeb65e1e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { unmuteAlert, unmuteAlerts } from './unmute'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('unmuteAlerts', () => { + test('should call unmute alert API per alert', async () => { + const ids = ['1', '2', '3']; + const result = await unmuteAlerts({ http, ids }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_unmute_all", + ], + Array [ + "/api/alerting/rule/2/_unmute_all", + ], + Array [ + "/api/alerting/rule/3/_unmute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/_unmute_all", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts new file mode 100644 index 0000000000000..c65be6a670a89 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_unmute_all`); +} + +export async function unmuteAlerts({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise { + await Promise.all(ids.map((id) => unmuteAlert({ id, http }))); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts new file mode 100644 index 0000000000000..c0131cbab0ebf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { unmuteAlertInstance } from './unmute_alert'; + +const http = httpServiceMock.createStartContract(); + +describe('unmuteAlertInstance', () => { + test('should call mute instance alert API', async () => { + const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alerting/rule/1/alert/123/_unmute", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.ts new file mode 100644 index 0000000000000..60d2cca72b85e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/unmute_alert.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. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ALERTING_API_PATH } from '../../constants'; + +export async function unmuteAlertInstance({ + id, + instanceId, + http, +}: { + id: string; + instanceId: string; + http: HttpSetup; +}): Promise { + await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_unmute`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts new file mode 100644 index 0000000000000..745a94b8d1134 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.test.ts @@ -0,0 +1,60 @@ +/* + * 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 } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { updateAlert } from './update'; +import { AlertNotifyWhenType } from '../../../../../alerting/common'; + +const http = httpServiceMock.createStartContract(); + +describe('updateAlert', () => { + test('should call alert update API', async () => { + const alertToUpdate = { + throttle: '1m', + consumer: 'alerts', + name: 'test', + tags: ['foo'], + schedule: { + interval: '1m', + }, + params: {}, + actions: [], + createdAt: new Date('1970-01-01T00:00:00.000Z'), + updatedAt: new Date('1970-01-01T00:00:00.000Z'), + apiKey: null, + apiKeyOwner: null, + notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType, + }; + const resolvedValue: Alert = { + ...alertToUpdate, + id: '123', + enabled: true, + alertTypeId: 'test', + createdBy: null, + updatedBy: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + }; + http.put.mockResolvedValueOnce(resolvedValue); + + const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); + expect(result).toEqual(resolvedValue); + expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alerting/rule/123", + Object { + "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"notify_when\\":\\"onThrottleInterval\\",\\"actions\\":[]}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts new file mode 100644 index 0000000000000..44b9306949f81 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/update.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { pick } from 'lodash'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { Alert, AlertUpdates } from '../../../types'; +import { RewriteResponseCase } from '../../../../../actions/common'; +import { transformAlert } from './common_transformations'; + +type AlertUpdatesBody = Pick< + AlertUpdates, + 'name' | 'tags' | 'schedule' | 'actions' | 'params' | 'throttle' | 'notifyWhen' +>; +const rewriteBodyRequest: RewriteResponseCase = ({ + notifyWhen, + actions, + ...res +}): any => ({ + ...res, + notify_when: notifyWhen, + actions: actions.map(({ group, id, params }) => ({ + group, + id, + params, + })), +}); + +export async function updateAlert({ + http, + alert, + id, +}: { + http: HttpSetup; + alert: Pick< + AlertUpdates, + 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' + >; + id: string; +}): Promise { + const res = await http.put(`${BASE_ALERTING_API_PATH}/rule/${id}`, { + body: JSON.stringify( + rewriteBodyRequest( + pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) + ) + ), + }); + return transformAlert(res); +} From df46dc19004d5529f04dfd08f07ad389862b0f4b Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 08:05:11 -0700 Subject: [PATCH 35/65] skip flaky suite (#91107) --- .../migrationsv2/integration_tests/migration.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index fd62fd107648e..4d41a147bc0ef 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -19,7 +19,8 @@ import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -describe('migration v2', () => { +// FLAKY: https://github.com/elastic/kibana/issues/91107 +describe.skip('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let coreStart: InternalCoreStart; From c89922a55ca8da6c739f351fda04e34e7b74c16f Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 7 Apr 2021 11:08:12 -0400 Subject: [PATCH 36/65] [project-assigner] remove extra bracket in issue-mappings config (#96428) --- .github/workflows/project-assigner.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 37d04abda7530..4966a0b506317 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,5 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}], {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} - - From 5de5f23fc3160a264e1cedfb8a43d2fc589b275c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 7 Apr 2021 10:16:39 -0500 Subject: [PATCH 37/65] Updated asset code for canvas-expression-lifecycle (#96414) The demo code need the asset to be wrapped in "' and asset- to be appended onto the id. Co-authored-by: Zachary E Baxter --- docs/canvas/canvas-expression-lifecycle.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/canvas/canvas-expression-lifecycle.asciidoc b/docs/canvas/canvas-expression-lifecycle.asciidoc index 7d48c593f9e18..17903408dff0e 100644 --- a/docs/canvas/canvas-expression-lifecycle.asciidoc +++ b/docs/canvas/canvas-expression-lifecycle.asciidoc @@ -177,8 +177,8 @@ Since all of the sub-expressions are now resolved into actual values, the < Date: Wed, 7 Apr 2021 10:19:29 -0500 Subject: [PATCH 38/65] Adds canvas `clog` function (#96418) * Add canvas `clog` function in the doc * Add basic example to the `clog` canvcas function * clog canvas function: switch definition/purpose Co-authored-by: Laurent HUET --- .../canvas/canvas-function-reference.asciidoc | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index eaaf68eb06195..67210c9d77057 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -376,6 +376,37 @@ Clears the _context_, and returns `null`. *Returns:* `null` +[float] +[[clog_fn]] +=== `clog` + +It outputs the _context_ in the console. This function is for debug purpose. + +*Expression syntax* +[source,js] +---- +clog +---- + +*Code example* +[source,text] +---- +filters + | demodata + | clog + | filterrows fn={getCell "age" | gt 70} + | clog + | pointseries x="time" y="mean(price)" + | plot defaultStyle={seriesStyle lines=1 fill=1} + | render +---- +This prints the `datatable` objects in the browser console before and after the `filterrows` function. + +*Accepts:* `any` + +*Returns:* `any` + + [float] [[columns_fn]] === `columns` From 532145b4188f7c93315f9fcd2c4e3c1487757e4f Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 7 Apr 2021 10:26:34 -0500 Subject: [PATCH 39/65] [DOCS] Add s an example for Timelion yaxis function (#96429) --- docs/user/dashboard/timelion.asciidoc | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 80ce77f30c75e..ff71cd7b383bd 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -33,6 +33,40 @@ If the value of your parameter contains spaces or commas you have to put the val .es(q='some query', index=logstash-*) +[float] +[[customize-data-series-y-axis]] +===== .yaxis() function + +{kib} supports many y-axis scales and ranges for your data series. + +The `.yaxis()` function supports the following parameters: + +* *yaxis* — The numbered y-axis to plot the series on. For example, use `.yaxis(2)` to display a second y-axis. +* *min* — The minimum value for the y-axis range. +* *max* — The maximum value for the y-axis range. +* *position* — The location of the units. Values include `left` or `right`. +* *label* — The label for the axis. +* *color* — The color of the axis label. +* *units* — The function to use for formatting the y-axis labels. Values include `bits`, `bits/s`, `bytes`, `bytes/s`, `currency(:ISO 4217 currency code)`, `percent`, and `custom(:prefix:suffix)`. +* *tickDecimals* — The tick decimal precision. + +Example: + +[source,text] +---------------------------------- +.es(index= kibana_sample_data_logs, + timefield='@timestamp', + metric='avg:bytes') + .label('Average Bytes for request') + .title('Memory consumption over time in bytes').yaxis(1,units=bytes,position=left), <1> +.es(index= kibana_sample_data_logs, + timefield='@timestamp', + metric=avg:machine.ram) + .label('Average Machine RAM amount').yaxis(2,units=bytes,position=right) <2> +---------------------------------- + +<1> `.yaxis(1,units=bytes,position=left)` — Specifies the first y-axis for the first data series, and changes the units on the left. +<2> `.yaxis(2,units=bytes,position=left)` — Specifies the second y-axis for the second data series, and changes the units on the right. [float] ==== Tutorial: Create visualizations with Timelion From 818a74003309d1e731cef71e76ab71caffbf89ed Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 7 Apr 2021 11:56:31 -0400 Subject: [PATCH 40/65] [App Search] Added a query performance rating to the Result Settings page (#96230) --- .../query_performance/index.ts | 8 ++ .../query_performance.test.tsx | 59 +++++++++++++ .../query_performance/query_performance.tsx | 87 +++++++++++++++++++ .../result_settings/result_settings.test.tsx | 19 +++- .../result_settings/result_settings.tsx | 7 +- .../result_settings_logic.test.ts | 71 +++++++++++++++ .../result_settings/result_settings_logic.ts | 32 ++++++- .../sample_response/sample_response.tsx | 3 +- 8 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/index.ts new file mode 100644 index 0000000000000..0bd18ea640850 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/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 { QueryPerformance } from './query_performance'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx new file mode 100644 index 0000000000000..0c62b783a47ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.test.tsx @@ -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 { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBadge } from '@elastic/eui'; + +import { QueryPerformance } from './query_performance'; + +describe('QueryPerformance', () => { + const values = { + queryPerformanceScore: 1, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders as green with the text "optimal" for a performance score of less than 6', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#59deb4'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: optimal'); + }); + + it('renders as blue with the text "good" for a performance score of less than 11', () => { + setMockValues({ + queryPerformanceScore: 10, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#40bfff'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: good'); + }); + + it('renders as yellow with the text "standard" for a performance score of less than 21', () => { + setMockValues({ + queryPerformanceScore: 20, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#fed566'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: standard'); + }); + + it('renders as red with the text "delayed" for a performance score of 21 or more', () => { + setMockValues({ + queryPerformanceScore: 100, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiBadge).prop('color')).toEqual('#ff9173'); + expect(wrapper.find(EuiBadge).children().text()).toEqual('Query performance: delayed'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx new file mode 100644 index 0000000000000..e3dfddc35d88c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/query_performance/query_performance.tsx @@ -0,0 +1,87 @@ +/* + * 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 } from 'kea'; + +import { EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +enum QueryPerformanceRating { + Optimal = 'Optimal', + Good = 'Good', + Standard = 'Standard', + Delayed = 'Delayed', +} + +const QUERY_PERFORMANCE_LABEL = (performanceValue: string) => + i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformanceLabel', { + defaultMessage: 'Query performance: {performanceValue}', + values: { + performanceValue, + }, + }); + +const QUERY_PERFORMANCE_OPTIMAL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.optimalValue', + { defaultMessage: 'optimal' } +); + +const QUERY_PERFORMANCE_GOOD = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.goodValue', + { defaultMessage: 'good' } +); + +const QUERY_PERFORMANCE_STANDARD = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.standardValue', + { defaultMessage: 'standard' } +); + +const QUERY_PERFORMANCE_DELAYED = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.queryPerformance.delayedValue', + { defaultMessage: 'delayed' } +); + +const badgeText: Record = { + [QueryPerformanceRating.Optimal]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_OPTIMAL), + [QueryPerformanceRating.Good]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_GOOD), + [QueryPerformanceRating.Standard]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_STANDARD), + [QueryPerformanceRating.Delayed]: QUERY_PERFORMANCE_LABEL(QUERY_PERFORMANCE_DELAYED), +}; + +const badgeColors: Record = { + [QueryPerformanceRating.Optimal]: '#59deb4', + [QueryPerformanceRating.Good]: '#40bfff', + [QueryPerformanceRating.Standard]: '#fed566', + [QueryPerformanceRating.Delayed]: '#ff9173', +}; + +const getPerformanceRating = (score: number) => { + switch (true) { + case score < 6: + return QueryPerformanceRating.Optimal; + case score < 11: + return QueryPerformanceRating.Good; + case score < 21: + return QueryPerformanceRating.Standard; + default: + return QueryPerformanceRating.Delayed; + } +}; + +export const QueryPerformance: React.FC = () => { + const { queryPerformanceScore } = useValues(ResultSettingsLogic); + const performanceRating = getPerformanceRating(queryPerformanceScore); + return ( + + {badgeText[performanceRating]} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index 3388894c230a0..9eda1362e04fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -7,7 +7,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; -import { setMockActions } from '../../../__mocks__'; +import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; @@ -15,12 +15,19 @@ import { shallow } from 'enzyme'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; +import { SampleResponse } from './sample_response'; describe('RelevanceTuning', () => { + const values = { + dataLoading: false, + }; + const actions = { initializeResultSettingsData: jest.fn(), }; + beforeEach(() => { + setMockValues(values); setMockActions(actions); jest.clearAllMocks(); }); @@ -28,10 +35,20 @@ describe('RelevanceTuning', () => { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(ResultSettingsTable).exists()).toBe(true); + expect(wrapper.find(SampleResponse).exists()).toBe(true); }); it('initializes result settings data when mounted', () => { shallow(); expect(actions.initializeResultSettingsData).toHaveBeenCalled(); }); + + it('renders a loading screen if data has not loaded yet', () => { + setMockValues({ + dataLoading: true, + }); + const wrapper = shallow(); + expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); + expect(wrapper.find(SampleResponse).exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 7f4373835f8d5..336f3f663119f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -7,13 +7,15 @@ import React, { useEffect } from 'react'; -import { useActions } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiPageHeader, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; + import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; @@ -26,12 +28,15 @@ interface Props { } export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { + const { dataLoading } = useValues(ResultSettingsLogic); const { initializeResultSettingsData } = useActions(ResultSettingsLogic); useEffect(() => { initializeResultSettingsData(); }, []); + if (dataLoading) return ; + return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index e7bb065b596c3..a9c161b2bb5be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -40,6 +40,7 @@ describe('ResultSettingsLogic', () => { stagedUpdates: false, nonTextResultFields: {}, textResultFields: {}, + queryPerformanceScore: 0, }; // Values without selectors @@ -487,6 +488,76 @@ describe('ResultSettingsLogic', () => { }); }); }); + + describe('queryPerformanceScore', () => { + describe('returns a score for the current query performance based on the result settings', () => { + it('considers a text value with raw set (but no size) as worth 1.5', () => { + mount({ + resultFields: { foo: { raw: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); + }); + + it('considers a text value with raw set and a size over 250 as also worth 1.5', () => { + mount({ + resultFields: { foo: { raw: true, rawSize: 251 } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1.5); + }); + + it('considers a text value with raw set and a size less than or equal to 250 as worth 1', () => { + mount({ + resultFields: { foo: { raw: true, rawSize: 250 } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(1); + }); + + it('considers a text value with a snippet set as worth 2', () => { + mount({ + resultFields: { foo: { snippet: true, snippetSize: 50, snippetFallback: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(2); + }); + + it('will sum raw and snippet values if both are set', () => { + mount({ + resultFields: { foo: { snippet: true, raw: true } }, + schema: { foo: 'text' as SchemaTypes }, + }); + // 1.5 (raw) + 2 (snippet) = 3.5 + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(3.5); + }); + + it('considers a non-text value with raw set as 0.2', () => { + mount({ + resultFields: { foo: { raw: true } }, + schema: { foo: 'number' as SchemaTypes }, + }); + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(0.2); + }); + + it('can sum variations of all the prior', () => { + mount({ + resultFields: { + foo: { raw: true }, + bar: { raw: true, snippet: true }, + baz: { raw: true }, + }, + schema: { + foo: 'text' as SchemaTypes, + bar: 'text' as SchemaTypes, + baz: 'number' as SchemaTypes, + }, + }); + // 1.5 (foo) + 3.5 (bar) + baz (.2) = 5.2 + expect(ResultSettingsLogic.values.queryPerformanceScore).toEqual(5.2); + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts index 22f4c44f8b543..c345ae7e02e8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.ts @@ -71,18 +71,19 @@ interface ResultSettingsValues { dataLoading: boolean; saving: boolean; openModal: OpenModal; - nonTextResultFields: FieldResultSettingObject; - textResultFields: FieldResultSettingObject; resultFields: FieldResultSettingObject; - serverResultFields: ServerFieldResultSettingObject; lastSavedResultFields: FieldResultSettingObject; schema: Schema; schemaConflicts: SchemaConflicts; // Selectors + textResultFields: FieldResultSettingObject; + nonTextResultFields: FieldResultSettingObject; + serverResultFields: ServerFieldResultSettingObject; resultFieldsAtDefaultSettings: boolean; resultFieldsEmpty: boolean; stagedUpdates: true; reducedServerResultFields: ServerFieldResultSettingObject; + queryPerformanceScore: number; } export const ResultSettingsLogic = kea>({ @@ -221,6 +222,31 @@ export const ResultSettingsLogic = kea [selectors.serverResultFields, selectors.schema], + (serverResultFields: ServerFieldResultSettingObject, schema: Schema) => { + return Object.entries(serverResultFields).reduce((acc, [fieldName, resultField]) => { + let newAcc = acc; + if (resultField.raw) { + if (schema[fieldName] !== 'text') { + newAcc += 0.2; + } else if ( + typeof resultField.raw === 'object' && + resultField.raw.size && + resultField.raw.size <= 250 + ) { + newAcc += 1.0; + } else { + newAcc += 1.5; + } + } + if (resultField.snippet) { + newAcc += 2.0; + } + return newAcc; + }, 0); + }, + ], }), listeners: ({ actions, values }) => ({ clearRawSizeForField: ({ fieldName }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx index ae91b9648356c..2d0cced3730ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { QueryPerformance } from '../query_performance'; import { ResultSettingsLogic } from '../result_settings_logic'; import { SampleResponseLogic } from './sample_response_logic'; @@ -48,7 +49,7 @@ export const SampleResponse: React.FC = () => { - {/* TODO */} + From b96f60f72740f46f600b2a557bdebd9b603e08fd Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 18:11:42 +0200 Subject: [PATCH 41/65] Bump postcss-svgo from 4.0.2 to 4.0.3 (#96409) --- yarn.lock | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4fba8dc85a09e..0390c2f7cdaf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15892,11 +15892,6 @@ hsla-regex@^1.0.0: resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= -html-comment-regex@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" - integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== - html-element-map@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" @@ -17133,13 +17128,6 @@ is-subset@^0.1.1: resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= -is-svg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" - integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== - dependencies: - html-comment-regex "^1.1.0" - is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -22670,11 +22658,10 @@ postcss-selector-parser@^6.0.4: util-deprecate "^1.0.2" postcss-svgo@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" - integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" + integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== dependencies: - is-svg "^3.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" svgo "^1.0.0" From ac46802830ceef3f6ce830a5fa68841a95e08102 Mon Sep 17 00:00:00 2001 From: hardikpnsp Date: Wed, 7 Apr 2021 21:46:39 +0530 Subject: [PATCH 42/65] [Telemetry] enforce import export type (#96199) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-analytics/tsconfig.json | 1 + packages/kbn-telemetry-tools/src/tools/tasks/index.ts | 4 +++- packages/kbn-telemetry-tools/tsconfig.json | 3 ++- src/plugins/kibana_usage_collection/tsconfig.json | 3 ++- .../telemetry/common/telemetry_config/index.ts | 6 ++---- src/plugins/telemetry/public/index.ts | 2 +- src/plugins/telemetry/server/index.ts | 9 ++++++--- .../telemetry_collection/get_data_telemetry/index.ts | 9 ++------- .../telemetry/server/telemetry_collection/index.ts | 11 ++++------- .../telemetry/server/telemetry_repository/index.ts | 2 +- src/plugins/telemetry/tsconfig.json | 3 ++- .../telemetry_collection_manager/server/index.ts | 2 +- .../telemetry_collection_manager/tsconfig.json | 3 ++- .../telemetry_management_section/public/index.ts | 2 +- .../telemetry_management_section/tsconfig.json | 3 ++- src/plugins/usage_collection/public/index.ts | 2 +- .../usage_collection/server/collector/index.ts | 10 ++++++---- src/plugins/usage_collection/server/index.ts | 7 +++---- src/plugins/usage_collection/tsconfig.json | 3 ++- .../telemetry_collection_xpack/server/index.ts | 2 +- .../server/telemetry_collection/index.ts | 2 +- .../plugins/telemetry_collection_xpack/tsconfig.json | 3 ++- 22 files changed, 48 insertions(+), 44 deletions(-) diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index c2e579e7fdbea..80a2255d71805 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -7,6 +7,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, + "isolatedModules": true, "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts index 5d946b73d9759..f55a9aa80d40d 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts @@ -7,7 +7,9 @@ */ export { ErrorReporter } from './error_reporter'; -export { TaskContext, createTaskContext } from './task_context'; + +export type { TaskContext } from './task_context'; +export { createTaskContext } from './task_context'; export { parseConfigsTask } from './parse_configs_task'; export { extractCollectorsTask } from './extract_collectors_task'; diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 39946fe9907e5..419af1d02f83b 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -6,7 +6,8 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", + "isolatedModules": true }, "include": [ "src/**/*", diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index d664d936f6667..ee07dfe589e4a 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/*", diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts index 84b6486f35b24..cc4ff102742d7 100644 --- a/src/plugins/telemetry/common/telemetry_config/index.ts +++ b/src/plugins/telemetry/common/telemetry_config/index.ts @@ -9,7 +9,5 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; -export { - getTelemetryFailureDetails, - TelemetryFailureDetails, -} from './get_telemetry_failure_details'; +export { getTelemetryFailureDetails } from './get_telemetry_failure_details'; +export type { TelemetryFailureDetails } from './get_telemetry_failure_details'; diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts index 6cca9bdf881dd..47ba7828eaec2 100644 --- a/src/plugins/telemetry/public/index.ts +++ b/src/plugins/telemetry/public/index.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { TelemetryPlugin, TelemetryPluginConfig } from './plugin'; -export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; +export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryPlugin(initializerContext); diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index debdf7515cd58..1c335426ffd03 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; +export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, @@ -34,9 +34,12 @@ export { constants }; export { getClusterUuids, getLocalStats, - TelemetryLocalStats, DATA_TELEMETRY_ID, + buildDataTelemetryPayload, +} from './telemetry_collection'; + +export type { + TelemetryLocalStats, DataTelemetryIndex, DataTelemetryPayload, - buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index def1131dfb1a3..c93b7e872924b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -7,10 +7,5 @@ */ export { DATA_TELEMETRY_ID } from './constants'; - -export { - getDataTelemetry, - buildDataTelemetryPayload, - DataTelemetryPayload, - DataTelemetryIndex, -} from './get_data_telemetry'; +export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 55f9c7f0e624c..151e89a11a192 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -6,12 +6,9 @@ * Side Public License, v 1. */ -export { - DATA_TELEMETRY_ID, - DataTelemetryIndex, - DataTelemetryPayload, - buildDataTelemetryPayload, -} from './get_data_telemetry'; -export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; +export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry'; +export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry'; +export { getLocalStats } from './get_local_stats'; +export type { TelemetryLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts index 4e3f046f7611f..594b53259a65f 100644 --- a/src/plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/plugins/telemetry/server/telemetry_repository/index.ts @@ -8,7 +8,7 @@ export { getTelemetrySavedObject } from './get_telemetry_saved_object'; export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; -export { +export type { TelemetrySavedObject, TelemetrySavedObjectAttributes, } from '../../common/telemetry_config/types'; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index bdced01d9eb6f..6629e479906c9 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/**/*", diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index 77077b73cf8ad..c0cd124a132c0 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryCollectionManagerPlugin(initializerContext); } -export { +export type { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, StatsCollectionConfig, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 1bba81769f0dd..1329979860603 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "server/**/*", diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index 28b04418f512d..db6ea17556ed3 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export { TelemetryManagementSectionPluginSetup } from './plugin'; +export type { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 48e40814b8570..2daee868ac200 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index b9e0e0a8985b1..9b009b1d9e264 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; +export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 5f48f9fb93813..d5e0d95659e58 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -export { CollectorSet, CollectorSetPublic } from './collector_set'; -export { - Collector, +export { CollectorSet } from './collector_set'; +export type { CollectorSetPublic } from './collector_set'; +export { Collector } from './collector'; +export type { AllowedSchemaTypes, AllowedSchemaNumberTypes, SchemaField, @@ -16,4 +17,5 @@ export { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector, UsageCollectorOptions } from './usage_collector'; +export { UsageCollector } from './usage_collector'; +export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index dfc9d19b69646..dd9e6644a827d 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,17 +9,16 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { +export { Collector } from './collector'; +export type { AllowedSchemaTypes, MakeSchemaFrom, SchemaField, CollectorOptions, UsageCollectorOptions, - Collector, CollectorFetchContext, } from './collector'; - -export { UsageCollectionSetup } from './plugin'; +export type { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 96b2c4d37e17c..68a0853994e80 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "public/**/*", diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index d924882e17fbd..aab1bdb58fe59 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -7,7 +7,7 @@ import { TelemetryCollectionXpackPlugin } from './plugin'; -export { ESLicense } from './telemetry_collection'; +export type { ESLicense } from './telemetry_collection'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index 4599b068b9b38..c1a11caf44f24 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ESLicense } from './get_license'; +export type { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 476f5926f757a..1221200c7548c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true }, "include": [ "common/**/*", From 752308f6d838480a935ed0c55a7467dd5c8145d0 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 09:16:37 -0700 Subject: [PATCH 43/65] skip flaky suite (#96372) --- x-pack/test/accessibility/apps/login_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index 02d817612671c..f46a684194810 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'security']); - describe('Security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/96372 + describe.skip('Security', () => { describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); From e6ef368cfeb0fe3965c5d81db5be4633f27a7f91 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 18:35:36 +0200 Subject: [PATCH 44/65] Correctly specify css-minimizer-webpack-plugin as a dev-dependency (#96417) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb383e986e721..d5d136305bc9c 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,6 @@ "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", "core-js": "^3.6.5", - "css-minimizer-webpack-plugin": "^1.3.0", "custom-event-polyfill": "^0.3.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", @@ -682,6 +681,7 @@ "copy-webpack-plugin": "^6.0.2", "cpy": "^8.1.1", "css-loader": "^3.4.2", + "css-minimizer-webpack-plugin": "^1.3.0", "cypress": "^6.8.0", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", From 9a0c73e515f0e0c7ff88d6b06df46aa34cd8d84f Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 7 Apr 2021 12:36:28 -0400 Subject: [PATCH 45/65] [Security Solution][Endpoint] Endpoint Event Filtering List, Test Data Generator and Loader (#96263) * Added new const to List plugin for new Endpont Event Filter list * Data Generator for event filters ++ script to load event filters (WIP) * refactor `generate_data` to use `BaseDataGenerator` class --- x-pack/plugins/lists/common/constants.ts | 9 ++ .../create_endoint_event_filters_list.ts | 79 +++++++++++++ .../data_generators/base_data_generator.ts | 94 +++++++++++++++ .../data_generators/event_filter_generator.ts | 30 +++++ .../common/endpoint/generate_data.ts | 56 ++------- .../scripts/endpoint/event_filters/index.ts | 111 ++++++++++++++++++ .../scripts/endpoint/load_event_filters.js | 11 ++ 7 files changed, 341 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts create mode 100755 x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index 92d8b6f5f7571..4f897c83cb41d 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -60,3 +60,12 @@ export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps L /** Description of trusted apps agnostic list */ export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List'; + +/** ID of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_ID = 'endpoint_event_filters'; + +/** Name of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters List'; + +/** Description of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts new file mode 100644 index 0000000000000..95e9df03400af --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import uuid from 'uuid'; + +import { + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, +} from '../../../common/constants'; +import { ExceptionListSchema, ExceptionListSoSchema, Version } from '../../../common/schemas'; + +import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; + +interface CreateEndpointEventFiltersListOptions { + savedObjectsClient: SavedObjectsClientContract; + user: string; + tieBreaker?: string; + version: Version; +} + +/** + * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist + * + * @param savedObjectsClient + * @param user + * @param tieBreaker + * @param version + */ +export const createEndpointEventFiltersList = async ({ + savedObjectsClient, + user, + tieBreaker, + version, +}: CreateEndpointEventFiltersListOptions): Promise => { + const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); + const dateNow = new Date().toISOString(); + try { + const savedObject = await savedObjectsClient.create( + savedObjectType, + { + comments: undefined, + created_at: dateNow, + created_by: user, + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + entries: undefined, + immutable: false, + item_id: undefined, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + list_type: 'list', + meta: undefined, + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + os_types: [], + tags: [], + tie_breaker_id: tieBreaker ?? uuid.v4(), + type: 'endpoint', + updated_by: user, + version, + }, + { + // We intentionally hard coding the id so that there can only be one Event Filters list within the space + id: ENDPOINT_EVENT_FILTERS_LIST_ID, + } + ); + + return transformSavedObjectToExceptionList({ savedObject }); + } catch (err) { + if (savedObjectsClient.errors.isConflictError(err)) { + return null; + } else { + throw err; + } + } +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts new file mode 100644 index 0000000000000..c0888a6c2a4bd --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -0,0 +1,94 @@ +/* + * 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 seedrandom from 'seedrandom'; +import uuid from 'uuid'; + +const OS_FAMILY = ['windows', 'macos', 'linux']; + +/** + * A generic base class to assist in creating domain specific data generators. It includes + * several general purpose random data generators for use within the class and exposes one + * public method named `generate()` which should be implemented by sub-classes. + */ +export class BaseDataGenerator { + protected random: seedrandom.prng; + + constructor(seed: string | seedrandom.prng = Math.random().toString()) { + if (typeof seed === 'string') { + this.random = seedrandom(seed); + } else { + this.random = seed; + } + } + + /** + * Generate a new record + */ + public generate(): GeneratedDoc { + throw new Error('method not implemented!'); + } + + /** generate random OS family value */ + protected randomOSFamily(): string { + return this.randomChoice(OS_FAMILY); + } + + /** generate a UUID (v4) */ + protected randomUUID(): string { + return uuid.v4(); + } + + /** Generate a random number up to the max provided */ + protected randomN(max: number): number { + return Math.floor(this.random() * max); + } + + protected *randomNGenerator(max: number, count: number) { + let iCount = count; + while (iCount > 0) { + yield this.randomN(max); + iCount = iCount - 1; + } + } + + /** + * Create an array of a given size and fill it with data provided by a generator + * + * @param lengthLimit + * @param generator + * @protected + */ + protected randomArray(lengthLimit: number, generator: () => T): T[] { + const rand = this.randomN(lengthLimit) + 1; + return [...Array(rand).keys()].map(generator); + } + + protected randomMac(): string { + return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-'); + } + + protected randomIP(): string { + return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.'); + } + + protected randomVersion(): string { + return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); + } + + protected randomChoice(choices: T[]): T { + return choices[this.randomN(choices.length)]; + } + + protected randomString(length: number): string { + return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join(''); + } + + protected randomHostname(): string { + return `Host-${this.randomString(10)}`; + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts new file mode 100644 index 0000000000000..6bdbb9cde2034 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.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 { BaseDataGenerator } from './base_data_generator'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../lists/common/constants'; +import { CreateExceptionListItemSchema } from '../../../../lists/common'; +import { getCreateExceptionListItemSchemaMock } from '../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; + +export class EventFilterGenerator extends BaseDataGenerator { + generate(): CreateExceptionListItemSchema { + const overrides: Partial = { + name: `generator event ${this.randomString(5)}`, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + item_id: `generator_endpoint_event_filter_${this.randomUUID()}`, + os_types: [this.randomOSFamily()] as CreateExceptionListItemSchema['os_types'], + tags: ['policy:all'], + namespace_type: 'agnostic', + meta: undefined, + }; + + return Object.assign>( + getCreateExceptionListItemSchemaMock(), + overrides + ); + } +} diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 8aec9768dd50d..36d0b0cbf3b21 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -35,6 +35,7 @@ import { EsAssetReference, KibanaAssetReference } from '../../../fleet/common/ty import { agentPolicyStatuses } from '../../../fleet/common/constants'; import { firstNonNullValue } from './models/ecs_safety_helpers'; import { EventOptions } from './types/generator'; +import { BaseDataGenerator } from './data_generators/base_data_generator'; export type Event = AlertEvent | SafeEndpointEvent; /** @@ -386,9 +387,8 @@ const alertsDefaultDataStream = { namespace: 'default', }; -export class EndpointDocGenerator { +export class EndpointDocGenerator extends BaseDataGenerator { commonInfo: HostInfo; - random: seedrandom.prng; sequence: number = 0; /** * The EndpointDocGenerator parameters @@ -396,12 +396,7 @@ export class EndpointDocGenerator { * @param seed either a string to seed the random number generator or a random number generator function */ constructor(seed: string | seedrandom.prng = Math.random().toString()) { - if (typeof seed === 'string') { - this.random = seedrandom(seed); - } else { - this.random = seed; - } - + super(seed); this.commonInfo = this.createHostData(); } @@ -1568,47 +1563,6 @@ export class EndpointDocGenerator { }; } - private randomN(n: number): number { - return Math.floor(this.random() * n); - } - - private *randomNGenerator(max: number, count: number) { - let iCount = count; - while (iCount > 0) { - yield this.randomN(max); - iCount = iCount - 1; - } - } - - private randomArray(lengthLimit: number, generator: () => T): T[] { - const rand = this.randomN(lengthLimit) + 1; - return [...Array(rand).keys()].map(generator); - } - - private randomMac(): string { - return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-'); - } - - public randomIP(): string { - return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.'); - } - - private randomVersion(): string { - return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.'); - } - - private randomChoice(choices: T[]): T { - return choices[this.randomN(choices.length)]; - } - - private randomString(length: number): string { - return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join(''); - } - - private randomHostname(): string { - return `Host-${this.randomString(10)}`; - } - private seededUUIDv4(): string { return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); } @@ -1646,6 +1600,10 @@ export class EndpointDocGenerator { private randomProcessName(): string { return this.randomChoice(fakeProcessNames); } + + public randomIP(): string { + return super.randomIP(); + } } const fakeProcessNames = [ diff --git a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts new file mode 100644 index 0000000000000..93af1f406300c --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts @@ -0,0 +1,111 @@ +/* + * 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 { run, RunFn, createFailError } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; +import { AxiosError } from 'axios'; +import bluebird from 'bluebird'; +import { EventFilterGenerator } from '../../../common/endpoint/data_generators/event_filter_generator'; +import { + ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_EVENT_FILTERS_LIST_NAME, + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../lists/common/constants'; +import { CreateExceptionListSchema } from '../../../../lists/common'; + +export const cli = () => { + run( + async (options) => { + try { + await createEventFilters(options); + options.log.success(`${options.flags.count} endpoint event filters created`); + } catch (e) { + options.log.error(e); + throw createFailError(e.message); + } + }, + { + description: 'Load Endpoint Event Filters', + flags: { + string: ['kibana'], + default: { + count: 10, + kibana: 'http://elastic:changeme@localhost:5601', + }, + help: ` + --count Number of event filters to create. Default: 10 + --kibana The URL to kibana including credentials. Default: http://elastic:changeme@localhost:5601 + `, + }, + } + ); +}; + +class EventFilterDataLoaderError extends Error { + constructor(message: string, public readonly meta: unknown) { + super(message); + } +} + +const handleThrowAxiosHttpError = (err: AxiosError): never => { + let message = err.message; + + if (err.response) { + message = `[${err.response.status}] ${err.response.data.message ?? err.message} [ ${String( + err.response.config.method + ).toUpperCase()} ${err.response.config.url} ]`; + } + throw new EventFilterDataLoaderError(message, err.toJSON()); +}; + +const createEventFilters: RunFn = async ({ flags, log }) => { + const eventGenerator = new EventFilterGenerator(); + const kbn = new KbnClient({ log, url: flags.kibana as string }); + + await ensureCreateEndpointEventFiltersList(kbn); + + await bluebird.map( + Array.from({ length: (flags.count as unknown) as number }), + () => + kbn + .request({ + method: 'POST', + path: EXCEPTION_LIST_ITEM_URL, + body: eventGenerator.generate(), + }) + .catch((e) => handleThrowAxiosHttpError(e)), + { concurrency: 10 } + ); +}; + +const ensureCreateEndpointEventFiltersList = async (kbn: KbnClient) => { + const newListDefinition: CreateExceptionListSchema = { + description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + meta: undefined, + name: ENDPOINT_EVENT_FILTERS_LIST_NAME, + os_types: [], + tags: [], + type: 'endpoint', + namespace_type: 'agnostic', + }; + + await kbn + .request({ + method: 'POST', + path: EXCEPTION_LIST_URL, + body: newListDefinition, + }) + .catch((e) => { + // Ignore if list was already created + if (e.response.status !== 409) { + handleThrowAxiosHttpError(e); + } + }); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js b/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js new file mode 100755 index 0000000000000..ca0f4ff9365c5 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +/* + * 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. + */ + +require('../../../../../src/setup_node_env'); +require('./event_filters').cli(); From 6b9ba109587f42e74453a6f50d46fb839374bf7a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 09:50:36 -0700 Subject: [PATCH 46/65] Revert "[Telemetry] enforce import export type (#96199)" This reverts commit ac46802830ceef3f6ce830a5fa68841a95e08102. --- packages/kbn-analytics/tsconfig.json | 1 - packages/kbn-telemetry-tools/src/tools/tasks/index.ts | 4 +--- packages/kbn-telemetry-tools/tsconfig.json | 3 +-- src/plugins/kibana_usage_collection/tsconfig.json | 3 +-- .../telemetry/common/telemetry_config/index.ts | 6 ++++-- src/plugins/telemetry/public/index.ts | 2 +- src/plugins/telemetry/server/index.ts | 9 +++------ .../telemetry_collection/get_data_telemetry/index.ts | 9 +++++++-- .../telemetry/server/telemetry_collection/index.ts | 11 +++++++---- .../telemetry/server/telemetry_repository/index.ts | 2 +- src/plugins/telemetry/tsconfig.json | 3 +-- .../telemetry_collection_manager/server/index.ts | 2 +- .../telemetry_collection_manager/tsconfig.json | 3 +-- .../telemetry_management_section/public/index.ts | 2 +- .../telemetry_management_section/tsconfig.json | 3 +-- src/plugins/usage_collection/public/index.ts | 2 +- .../usage_collection/server/collector/index.ts | 10 ++++------ src/plugins/usage_collection/server/index.ts | 7 ++++--- src/plugins/usage_collection/tsconfig.json | 3 +-- .../telemetry_collection_xpack/server/index.ts | 2 +- .../server/telemetry_collection/index.ts | 2 +- .../plugins/telemetry_collection_xpack/tsconfig.json | 3 +-- 22 files changed, 44 insertions(+), 48 deletions(-) diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index 80a2255d71805..c2e579e7fdbea 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -7,7 +7,6 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, - "isolatedModules": true, "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts index f55a9aa80d40d..5d946b73d9759 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts @@ -7,9 +7,7 @@ */ export { ErrorReporter } from './error_reporter'; - -export type { TaskContext } from './task_context'; -export { createTaskContext } from './task_context'; +export { TaskContext, createTaskContext } from './task_context'; export { parseConfigsTask } from './parse_configs_task'; export { extractCollectorsTask } from './extract_collectors_task'; diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 419af1d02f83b..39946fe9907e5 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -6,8 +6,7 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-telemetry-tools/src", - "isolatedModules": true + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" }, "include": [ "src/**/*", diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index ee07dfe589e4a..d664d936f6667 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "common/*", diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts index cc4ff102742d7..84b6486f35b24 100644 --- a/src/plugins/telemetry/common/telemetry_config/index.ts +++ b/src/plugins/telemetry/common/telemetry_config/index.ts @@ -9,5 +9,7 @@ export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; -export { getTelemetryFailureDetails } from './get_telemetry_failure_details'; -export type { TelemetryFailureDetails } from './get_telemetry_failure_details'; +export { + getTelemetryFailureDetails, + TelemetryFailureDetails, +} from './get_telemetry_failure_details'; diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts index 47ba7828eaec2..6cca9bdf881dd 100644 --- a/src/plugins/telemetry/public/index.ts +++ b/src/plugins/telemetry/public/index.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { TelemetryPlugin, TelemetryPluginConfig } from './plugin'; -export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; +export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryPlugin(initializerContext); diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index 1c335426ffd03..debdf7515cd58 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; +export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, @@ -34,12 +34,9 @@ export { constants }; export { getClusterUuids, getLocalStats, - DATA_TELEMETRY_ID, - buildDataTelemetryPayload, -} from './telemetry_collection'; - -export type { TelemetryLocalStats, + DATA_TELEMETRY_ID, DataTelemetryIndex, DataTelemetryPayload, + buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index c93b7e872924b..def1131dfb1a3 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -7,5 +7,10 @@ */ export { DATA_TELEMETRY_ID } from './constants'; -export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry'; -export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry'; + +export { + getDataTelemetry, + buildDataTelemetryPayload, + DataTelemetryPayload, + DataTelemetryIndex, +} from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 151e89a11a192..55f9c7f0e624c 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ -export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry'; -export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry'; -export { getLocalStats } from './get_local_stats'; -export type { TelemetryLocalStats } from './get_local_stats'; +export { + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, +} from './get_data_telemetry'; +export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts index 594b53259a65f..4e3f046f7611f 100644 --- a/src/plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/plugins/telemetry/server/telemetry_repository/index.ts @@ -8,7 +8,7 @@ export { getTelemetrySavedObject } from './get_telemetry_saved_object'; export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; -export type { +export { TelemetrySavedObject, TelemetrySavedObjectAttributes, } from '../../common/telemetry_config/types'; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 6629e479906c9..bdced01d9eb6f 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "public/**/**/*", diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index c0cd124a132c0..77077b73cf8ad 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TelemetryCollectionManagerPlugin(initializerContext); } -export type { +export { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, StatsCollectionConfig, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 1329979860603..1bba81769f0dd 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "server/**/*", diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index db6ea17556ed3..28b04418f512d 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export type { TelemetryManagementSectionPluginSetup } from './plugin'; +export { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 2daee868ac200..48e40814b8570 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "public/**/*", diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index 9b009b1d9e264..b9e0e0a8985b1 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { UsageCollectionPlugin } from './plugin'; export { METRIC_TYPE } from '@kbn/analytics'; -export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; +export { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index d5e0d95659e58..5f48f9fb93813 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -export { CollectorSet } from './collector_set'; -export type { CollectorSetPublic } from './collector_set'; -export { Collector } from './collector'; -export type { +export { CollectorSet, CollectorSetPublic } from './collector_set'; +export { + Collector, AllowedSchemaTypes, AllowedSchemaNumberTypes, SchemaField, @@ -17,5 +16,4 @@ export type { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector } from './usage_collector'; -export type { UsageCollectorOptions } from './usage_collector'; +export { UsageCollector, UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index dd9e6644a827d..dfc9d19b69646 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,16 +9,17 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { Collector } from './collector'; -export type { +export { AllowedSchemaTypes, MakeSchemaFrom, SchemaField, CollectorOptions, UsageCollectorOptions, + Collector, CollectorFetchContext, } from './collector'; -export type { UsageCollectionSetup } from './plugin'; + +export { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 68a0853994e80..96b2c4d37e17c 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "public/**/*", diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index aab1bdb58fe59..d924882e17fbd 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -7,7 +7,7 @@ import { TelemetryCollectionXpackPlugin } from './plugin'; -export type { ESLicense } from './telemetry_collection'; +export { ESLicense } from './telemetry_collection'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index c1a11caf44f24..4599b068b9b38 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export type { ESLicense } from './get_license'; +export { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index 1221200c7548c..476f5926f757a 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -5,8 +5,7 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true, - "isolatedModules": true + "declarationMap": true }, "include": [ "common/**/*", From 92b659dae04e980b3989ae6b9e1f28ac6ca579f8 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 7 Apr 2021 12:01:26 -0500 Subject: [PATCH 47/65] [Workplace Search] Add AccountHeader to Personal dashboard (#96353) * Revert change to wrap setContext in useEffect A recommendation was made to wrap the setContext call in a previous PR, which lets the app know if the context is org or account, in a useEffect call, for potential performance reasons. Unfortunately, this causes the lifecycle to change so that changing routes from org to personal dashboard does not register the change in time. This commit changes it back to a working state. * Add constants and routes for Account nav * Add AccountHeader component * Add header to layout and fix height The main layout stylesheet, https://github.com/elastic/kibana/blob/master/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss gives a static height that includes the main Kibana navigation. The height with the account nav only is added to the existing privateSourcesLayout css class * Refactor test --- .../account_header/account_header.test.tsx | 53 +++++++++ .../layout/account_header/account_header.tsx | 107 ++++++++++++++++++ .../components/layout/account_header/index.ts | 8 ++ .../components/layout/index.ts | 1 + .../workplace_search/constants.ts | 31 +++++ .../applications/workplace_search/index.tsx | 10 +- .../applications/workplace_search/routes.ts | 2 + .../private_sources_layout.test.tsx | 2 + .../private_sources_layout.tsx | 38 ++++--- .../views/content_sources/sources.scss | 4 + 10 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx new file mode 100644 index 0000000000000..e8035f01a9405 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiHeader, EuiPopover } from '@elastic/eui'; + +import { AccountHeader } from './'; + +describe('AccountHeader', () => { + const mockValues = { + account: { + isAdmin: true, + }, + }; + + beforeEach(() => { + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiHeader)).toHaveLength(1); + }); + + describe('accountSubNav', () => { + it('handles popover trigger click', () => { + const wrapper = shallow(); + const popover = wrapper.find(EuiPopover); + const onClick = popover.dive().find('[data-test-subj="AccountButton"]').prop('onClick'); + onClick!({} as any); + + expect(onClick).toBeDefined(); + }); + + it('handles close popover', () => { + const wrapper = shallow(); + const popover = wrapper.find(EuiPopover); + popover.prop('closePopover')!(); + + expect(popover.prop('isOpen')).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx new file mode 100644 index 0000000000000..a878d87af09e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/account_header.tsx @@ -0,0 +1,107 @@ +/* + * 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 { useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiHeader, + EuiHeaderLogo, + EuiHeaderLinks, + EuiHeaderSection, + EuiHeaderSectionItem, + EuiText, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, +} from '@elastic/eui'; + +import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import { WORKPLACE_SEARCH_TITLE, ACCOUNT_NAV } from '../../../constants'; +import { + ALPHA_PATH, + PERSONAL_SOURCES_PATH, + LOGOUT_ROUTE, + KIBANA_ACCOUNT_ROUTE, +} from '../../../routes'; + +export const AccountHeader: React.FC = () => { + const [isPopoverOpen, setPopover] = useState(false); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + const closePopover = () => { + setPopover(false); + }; + + const { + account: { isAdmin }, + } = useValues(AppLogic); + + const accountNavItems = [ + + {/* TODO: Once auth is completed, we need to have non-admins redirect to the self-hosted form */} + {ACCOUNT_NAV.SETTINGS} + , + + {ACCOUNT_NAV.LOGOUT} + , + ]; + + const accountButton = ( + + {ACCOUNT_NAV.ACCOUNT} + + ); + + return ( + + + + + {WORKPLACE_SEARCH_TITLE} + + + + {ACCOUNT_NAV.SOURCES} + + + + + + {isAdmin && ( + {ACCOUNT_NAV.ORG_DASHBOARD} + )} + + + + + {ACCOUNT_NAV.SEARCH} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/index.ts new file mode 100644 index 0000000000000..e6cd2516fc03a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/account_header/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 { AccountHeader } from './account_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index 2678b5d01b475..b9a49c416f283 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -7,3 +7,4 @@ export { WorkplaceSearchNav } from './nav'; export { WorkplaceSearchHeaderActions } from './kibana_header_actions'; +export { AccountHeader } from './account_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index a6e9ce282bf3d..d771673506761 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -9,6 +9,13 @@ import { i18n } from '@kbn/i18n'; import { UPDATE_BUTTON_LABEL, SAVE_BUTTON_LABEL, CANCEL_BUTTON_LABEL } from '../shared/constants'; +export const WORKPLACE_SEARCH_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.title', + { + defaultMessage: 'Workplace Search', + } +); + export const NAV = { OVERVIEW: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { defaultMessage: 'Overview', @@ -76,6 +83,30 @@ export const NAV = { }), }; +export const ACCOUNT_NAV = { + SOURCES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.sources.link', { + defaultMessage: 'Content sources', + }), + ORG_DASHBOARD: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link', + { + defaultMessage: 'Go to organizational dashboard', + } + ), + SEARCH: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.search.link', { + defaultMessage: 'Search', + }), + ACCOUNT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.account.link', { + defaultMessage: 'My account', + }), + SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.settings.link', { + defaultMessage: 'Account settings', + }), + LOGOUT: i18n.translate('xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link', { + defaultMessage: 'Logout', + }), +}; + export const MAX_TABLE_ROW_ICONS = 3; export const SOURCE_STATUSES = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 7a76de43be41b..a8d6fc54f7924 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - useEffect(() => { - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' - const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - setContext(isOrganization); + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. + + setContext(isOrganization); + + useEffect(() => { setChromeIsVisible(isOrganization); }, [pathname]); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 9e514d7c73493..e08050335671e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -12,6 +12,8 @@ import { docLinks } from '../shared/doc_links'; export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; +export const LOGOUT_ROUTE = '/logout'; +export const KIBANA_ACCOUNT_ROUTE = '/security/account'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 9e3b50ea083eb..7558eb1e4e662 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; +import { AccountHeader } from '../../components/layout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; import { SourceSubNav } from './components/source_sub_nav'; @@ -43,6 +44,7 @@ describe('PrivateSourcesLayout', () => { expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); expect(wrapper.find(SourceSubNav)).toHaveLength(1); + expect(wrapper.find(AccountHeader)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index 2a6281075dc40..c565ee5f39a71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -12,6 +12,7 @@ import { useValues } from 'kea'; import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; +import { AccountHeader } from '../../components/layout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; import { SourceSubNav } from './components/source_sub_nav'; @@ -48,22 +49,25 @@ export const PrivateSourcesLayout: React.FC = ({ : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; return ( - - - - - - - {readOnlyMode && ( - - )} - {children} - - + <> + + + + + + + + {readOnlyMode && ( + + )} + {children} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index abab139e32369..549ca3ae9154e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -20,14 +20,18 @@ .privateSourcesLayout { $sideBarWidth: $euiSize * 30; + $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes + $pageHeight: calc(100vh - #{$consoleHeaderHeight}); left: $sideBarWidth; width: calc(100% - #{$sideBarWidth}); + min-height: $pageHeight; &__sideBar { padding: 32px 40px 40px; width: $sideBarWidth; margin-left: -$sideBarWidth; + height: $pageHeight; } } From 22f7f17fdf021b095ae40d9fb309525d4b208fd8 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 7 Apr 2021 13:07:35 -0400 Subject: [PATCH 48/65] [Fleet] Move fleet server indices creation out of Kibana (#96338) --- .../fleet/server/services/agents/crud.ts | 3 + .../services/api_keys/enrollment_api_key.ts | 2 + .../services/artifacts/artifacts.test.ts | 2 + .../server/services/artifacts/artifacts.ts | 1 + .../fleet_server/elastic_index.test.ts | 141 ----------- .../services/fleet_server/elastic_index.ts | 136 ----------- .../elasticsearch/fleet_actions.json | 33 --- .../elasticsearch/fleet_agents.json | 224 ------------------ .../elasticsearch/fleet_artifacts.json | 48 ---- .../fleet_enrollment_api_keys.json | 32 --- .../elasticsearch/fleet_policies.json | 27 --- .../elasticsearch/fleet_policies_leader.json | 21 -- .../elasticsearch/fleet_servers.json | 47 ---- .../server/services/fleet_server/index.ts | 2 - .../fleet_server/saved_object_migrations.ts | 1 + 15 files changed, 9 insertions(+), 711 deletions(-) delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json delete mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index ecf18430da668..a23efa1e50fc0 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -125,6 +125,7 @@ export async function getAgentsByKuery( size: perPage, sort: `${sortField}:${sortOrder}`, track_total_hits: true, + ignore_unavailable: true, body, }); @@ -180,6 +181,7 @@ export async function countInactiveAgents( index: AGENTS_INDEX, size: 0, track_total_hits: true, + ignore_unavailable: true, body, }); // @ts-expect-error value is number | TotalHits @@ -249,6 +251,7 @@ export async function getAgentByAccessAPIKeyId( ): Promise { const res = await esClient.search({ index: AGENTS_INDEX, + ignore_unavailable: true, q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 643caa8d3bb6f..7059cc96159b9 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -38,6 +38,7 @@ export async function listEnrollmentApiKeys( size: perPage, sort: 'created_at:desc', track_total_hits: true, + ignore_unavailable: true, q: kuery, }); @@ -230,6 +231,7 @@ export async function generateEnrollmentAPIKey( export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { const res = await esClient.search({ index: ENROLLMENT_API_KEYS_INDEX, + ignore_unavailable: true, q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, }); diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts index d4f129a1ae241..5681be3e8793b 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts @@ -152,6 +152,7 @@ describe('When using the artifacts services', () => { expect(esClientMock.search).toHaveBeenCalledWith({ index: FLEET_SERVER_ARTIFACTS_INDEX, sort: 'created:asc', + ignore_unavailable: true, q: '', from: 0, size: 20, @@ -184,6 +185,7 @@ describe('When using the artifacts services', () => { index: FLEET_SERVER_ARTIFACTS_INDEX, sort: 'identifier:desc', q: 'packageName:endpoint', + ignore_unavailable: true, from: 450, size: 50, }); diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts index 6e2c22cc2f045..26032ab94dbc8 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts @@ -105,6 +105,7 @@ export const listArtifacts = async ( sort: `${sortField}:${sortOrder}`, q: kuery, from: (page - 1) * perPage, + ignore_unavailable: true, size: perPage, }); diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts deleted file mode 100644 index 275ea421a508f..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts +++ /dev/null @@ -1,141 +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 { elasticsearchServiceMock } from 'src/core/server/mocks'; -import hash from 'object-hash'; - -import { FLEET_SERVER_INDICES } from '../../../common'; - -import { setupFleetServerIndexes } from './elastic_index'; -import ESFleetAgentIndex from './elasticsearch/fleet_agents.json'; -import ESFleetPoliciesIndex from './elasticsearch/fleet_policies.json'; -import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.json'; -import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; -import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; -import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; -import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; - -const FLEET_INDEXES_MIGRATION_HASH: Record = { - '.fleet-actions': hash(EsFleetActionsIndex), - '.fleet-agents': hash(ESFleetAgentIndex), - '.fleet-artifacts': hash(EsFleetArtifactsIndex), - '.fleet-enrollment-apy-keys': hash(ESFleetEnrollmentApiKeysIndex), - '.fleet-policies': hash(ESFleetPoliciesIndex), - '.fleet-policies-leader': hash(ESFleetPoliciesLeaderIndex), - '.fleet-servers': hash(ESFleetServersIndex), -}; - -const getIndexList = (returnAliases: boolean = false): string[] => { - const response = [...FLEET_SERVER_INDICES]; - - if (returnAliases) { - return response.sort(); - } - - return response.map((index) => `${index}_1`).sort(); -}; - -describe('setupFleetServerIndexes ', () => { - it('should create all the indices and aliases if nothings exists', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - await setupFleetServerIndexes(esMock); - - const indexesCreated = esMock.indices.create.mock.calls.map((call) => call[0].index).sort(); - expect(indexesCreated).toEqual(getIndexList()); - const aliasesCreated = esMock.indices.updateAliases.mock.calls - .map((call) => (call[0].body as any)?.actions[0].add.alias) - .sort(); - - expect(aliasesCreated).toEqual(getIndexList(true)); - }); - - it('should not create any indices and create aliases if indices exists but not the aliases', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - // @ts-expect-error - esMock.indices.exists.mockResolvedValue({ body: true }); - // @ts-expect-error - esMock.indices.getMapping.mockImplementation((params: { index: string }) => { - return { - body: { - [params.index]: { - mappings: { - _meta: { - migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], - }, - }, - }, - }, - }; - }); - - await setupFleetServerIndexes(esMock); - - expect(esMock.indices.create).not.toBeCalled(); - const aliasesCreated = esMock.indices.updateAliases.mock.calls - .map((call) => (call[0].body as any)?.actions[0].add.alias) - .sort(); - - expect(aliasesCreated).toEqual(getIndexList(true)); - }); - - it('should put new indices mapping if the mapping has been updated ', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - // @ts-expect-error - esMock.indices.exists.mockResolvedValue({ body: true }); - // @ts-expect-error - esMock.indices.getMapping.mockImplementation((params: { index: string }) => { - return { - body: { - [params.index]: { - mappings: { - _meta: { - migrationHash: 'NOT_VALID_HASH', - }, - }, - }, - }, - }; - }); - - await setupFleetServerIndexes(esMock); - - expect(esMock.indices.create).not.toBeCalled(); - const indexesMappingUpdated = esMock.indices.putMapping.mock.calls - .map((call) => call[0].index) - .sort(); - - expect(indexesMappingUpdated).toEqual(getIndexList()); - }); - - it('should not create any indices or aliases if indices and aliases already exists', async () => { - const esMock = elasticsearchServiceMock.createInternalClient(); - - // @ts-expect-error - esMock.indices.exists.mockResolvedValue({ body: true }); - // @ts-expect-error - esMock.indices.getMapping.mockImplementation((params: { index: string }) => { - return { - body: { - [params.index]: { - mappings: { - _meta: { - migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], - }, - }, - }, - }, - }; - }); - // @ts-expect-error - esMock.indices.existsAlias.mockResolvedValue({ body: true }); - - await setupFleetServerIndexes(esMock); - - expect(esMock.indices.create).not.toBeCalled(); - expect(esMock.indices.updateAliases).not.toBeCalled(); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts deleted file mode 100644 index b0dce60085529..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts +++ /dev/null @@ -1,136 +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 type { ElasticsearchClient } from 'kibana/server'; -import hash from 'object-hash'; - -import type { FLEET_SERVER_INDICES } from '../../../common'; -import { FLEET_SERVER_INDICES_VERSION } from '../../../common'; -import { appContextService } from '../app_context'; - -import { FleetSetupError } from '../../errors'; - -import ESFleetAgentIndex from './elasticsearch/fleet_agents.json'; -import ESFleetPoliciesIndex from './elasticsearch/fleet_policies.json'; -import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.json'; -import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; -import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; -import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; -import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; - -const FLEET_INDEXES: Array<[typeof FLEET_SERVER_INDICES[number], any]> = [ - ['.fleet-actions', EsFleetActionsIndex], - ['.fleet-agents', ESFleetAgentIndex], - ['.fleet-artifacts', EsFleetArtifactsIndex], - ['.fleet-enrollment-api-keys', ESFleetEnrollmentApiKeysIndex], - ['.fleet-policies', ESFleetPoliciesIndex], - ['.fleet-policies-leader', ESFleetPoliciesLeaderIndex], - ['.fleet-servers', ESFleetServersIndex], -]; - -export async function setupFleetServerIndexes( - esClient = appContextService.getInternalUserESClient() -) { - await Promise.all( - FLEET_INDEXES.map(async ([indexAlias, indexData]) => { - const index = `${indexAlias}_${FLEET_SERVER_INDICES_VERSION}`; - await createOrUpdateIndex(esClient, index, indexData); - await createAliasIfDoNotExists(esClient, indexAlias, index); - }) - ); -} - -export async function createAliasIfDoNotExists( - esClient: ElasticsearchClient, - alias: string, - index: string -) { - try { - const { body: exists } = await esClient.indices.existsAlias({ - name: alias, - }); - - if (exists === true) { - return; - } - await esClient.indices.updateAliases({ - body: { - actions: [ - { - add: { index, alias }, - }, - ], - }, - }); - } catch (e) { - throw new FleetSetupError(`Create of alias [${alias}] for index [${index}] failed`, e); - } -} - -async function createOrUpdateIndex( - esClient: ElasticsearchClient, - indexName: string, - indexData: any -) { - const resExists = await esClient.indices.exists({ - index: indexName, - }); - - // Support non destructive migration only (adding new field) - if (resExists.body === true) { - return updateIndex(esClient, indexName, indexData); - } - - return createIndex(esClient, indexName, indexData); -} - -async function updateIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) { - try { - const res = await esClient.indices.getMapping({ - index: indexName, - }); - - const migrationHash = hash(indexData); - if (res.body[indexName].mappings?._meta?.migrationHash !== migrationHash) { - await esClient.indices.putMapping({ - index: indexName, - body: Object.assign({ - ...indexData.mappings, - _meta: { ...(indexData.mappings._meta || {}), migrationHash }, - }), - }); - } - } catch (e) { - throw new FleetSetupError(`update of index [${indexName}] failed`, e); - } -} - -async function createIndex(esClient: ElasticsearchClient, indexName: string, indexData: any) { - try { - const migrationHash = hash(indexData); - await esClient.indices.create({ - index: indexName, - body: { - ...indexData, - settings: { - ...(indexData.settings || {}), - auto_expand_replicas: '0-1', - }, - - mappings: Object.assign({ - ...indexData.mappings, - _meta: { ...(indexData.mappings._meta || {}), migrationHash }, - }), - }, - }); - } catch (err) { - // Swallow already exists errors as concurent Kibana can try to create that indice - if (err?.body?.error?.type !== 'resource_already_exists_exception') { - throw new FleetSetupError(`create of index [${indexName}] Failed`, err); - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json deleted file mode 100644 index 94ad02c6d5f18..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "action_id": { - "type": "keyword" - }, - "agents": { - "type": "keyword" - }, - "data": { - "enabled": false, - "type": "object" - }, - "expiration": { - "type": "date" - }, - "input_type": { - "type": "keyword" - }, - "@timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - }, - "user_id" : { - "type": "keyword" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json deleted file mode 100644 index 32caa684679d8..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json +++ /dev/null @@ -1,224 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "action_seq_no": { - "type": "integer", - "index": false - }, - "active": { - "type": "boolean" - }, - "agent": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "default_api_key": { - "type": "keyword" - }, - "default_api_key_id": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_checkin_status": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "properties": { - "elastic": { - "properties": { - "agent": { - "properties": { - "build": { - "properties": { - "original": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "id": { - "type": "keyword" - }, - "log_level": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "upgradeable": { - "type": "boolean" - }, - "version": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 16 - } - } - } - } - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "hostname": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "id": { - "type": "keyword" - }, - "ip": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 64 - } - } - }, - "mac": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 17 - } - } - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "os": { - "properties": { - "family": { - "type": "keyword" - }, - "full": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 128 - } - } - }, - "kernel": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 128 - } - } - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 32 - } - } - } - } - } - } - }, - "packages": { - "type": "keyword" - }, - "policy_coordinator_idx": { - "type": "integer" - }, - "policy_id": { - "type": "keyword" - }, - "policy_output_permissions_hash": { - "type": "keyword" - }, - "policy_revision_idx": { - "type": "integer" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "unenrolled_at": { - "type": "date" - }, - "unenrollment_started_at": { - "type": "date" - }, - "updated_at": { - "type": "date" - }, - "upgrade_started_at": { - "type": "date" - }, - "upgraded_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "object", - "enabled": false - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json deleted file mode 100644 index 1f9643fd599d5..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "identifier": { - "type": "keyword" - }, - "compression_algorithm": { - "type": "keyword", - "index": false - }, - "encryption_algorithm": { - "type": "keyword", - "index": false - }, - "encoded_sha256": { - "type": "keyword" - }, - "encoded_size": { - "type": "long", - "index": false - }, - "decoded_sha256": { - "type": "keyword" - }, - "decoded_size": { - "type": "long", - "index": false - }, - "created": { - "type": "date" - }, - "package_name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "relative_url": { - "type": "keyword" - }, - "body": { - "type": "binary" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json deleted file mode 100644 index fc3898aff55c6..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_enrollment_api_keys.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "keyword" - }, - "api_key_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json deleted file mode 100644 index 50078aaa5ea98..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "coordinator_idx": { - "type": "integer" - }, - "data": { - "enabled": false, - "type": "object" - }, - "default_fleet_server": { - "type": "boolean" - }, - "policy_id": { - "type": "keyword" - }, - "revision_idx": { - "type": "integer" - }, - "@timestamp": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json deleted file mode 100644 index ad3dfe64df57c..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_policies_leader.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "server": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "@timestamp": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json deleted file mode 100644 index 9ee68735d5b6f..0000000000000 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_servers.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "settings": {}, - "mappings": { - "dynamic": false, - "properties": { - "agent": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - } - } - }, - "server": { - "properties": { - "id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "@timestamp": { - "type": "date" - } - } - } -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index c2b24ce96c213..94f14fac01d3f 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -10,7 +10,6 @@ import { first } from 'rxjs/operators'; import { appContextService } from '../app_context'; import { licenseService } from '../license'; -import { setupFleetServerIndexes } from './elastic_index'; import { runFleetServerMigration } from './saved_object_migrations'; let _isFleetServerSetup = false; @@ -45,7 +44,6 @@ export async function startFleetServerSetup() { try { // We need licence to be initialized before using the SO service. await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); - await setupFleetServerIndexes(); await runFleetServerMigration(); _isFleetServerSetup = true; } catch (err) { diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 78172e4dae366..df8aa7cb01286 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -177,6 +177,7 @@ async function migrateAgentPolicies() { index: AGENT_POLICY_INDEX, q: `policy_id:${agentPolicy.id}`, track_total_hits: true, + ignore_unavailable: true, }); // @ts-expect-error value is number | TotalHits From b89776db6d770370b29f371fadc22a30102d6416 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 19:25:19 +0200 Subject: [PATCH 49/65] Bump color-string from 1.5.3 to 1.5.5 (#96433) --- yarn.lock | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0390c2f7cdaf8..6b977be1797ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9858,15 +9858,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.4.0, color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-string@^1.5.4: +color-string@^1.4.0, color-string@^1.5.2, color-string@^1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== From f1230849616311f2fb34df3ce4e3b2a615bc4625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 7 Apr 2021 19:30:15 +0200 Subject: [PATCH 50/65] [APM] Remove dynamic index pattern caching (#96346) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/index_pattern/get_dynamic_index_pattern.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 8b81101fd2f39..5d5e6eebb4c9f 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -5,7 +5,6 @@ * 2.0. */ -import LRU from 'lru-cache'; import { IndexPatternsFetcher, FieldDescriptor, @@ -19,11 +18,6 @@ export interface IndexPatternTitleAndFields { fields: FieldDescriptor[]; } -const cache = new LRU({ - max: 100, - maxAge: 1000 * 60, -}); - // TODO: this is currently cached globally. In the future we might want to cache this per user export const getDynamicIndexPattern = ({ context, @@ -33,11 +27,6 @@ export const getDynamicIndexPattern = ({ return withApmSpan('get_dynamic_index_pattern', async () => { const indexPatternTitle = context.config['apm_oss.indexPattern']; - const CACHE_KEY = `apm_dynamic_index_pattern_${indexPatternTitle}`; - if (cache.has(CACHE_KEY)) { - return cache.get(CACHE_KEY); - } - const indexPatternsFetcher = new IndexPatternsFetcher( context.core.elasticsearch.client.asCurrentUser ); @@ -57,11 +46,8 @@ export const getDynamicIndexPattern = ({ title: indexPatternTitle, }; - cache.set(CACHE_KEY, indexPattern); return indexPattern; } catch (e) { - // since `getDynamicIndexPattern` can be called multiple times per request it can be expensive not to cache failed lookups - cache.set(CACHE_KEY, undefined); const notExists = e.output?.statusCode === 404; if (notExists) { context.logger.error( From 8e1bd9ccf35cda70d578633b22eaf21f3c0bf9a4 Mon Sep 17 00:00:00 2001 From: Davey Holler Date: Wed, 7 Apr 2021 10:37:14 -0700 Subject: [PATCH 51/65] App Search Polish (#96345) * Button adjustments to Engine Overview page * Subdued preview panel color * Vertically aligns "manage fields" and "preview" Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app_search/components/engines/components/header.tsx | 2 +- .../app_search/components/engines/engines_overview.tsx | 6 ++++-- .../relevance_tuning_form/relevance_tuning_form.tsx | 1 + .../relevance_tuning/relevance_tuning_preview.tsx | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index fb3b771850a31..df87f2e5230db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -26,7 +26,7 @@ export const EnginesOverviewHeader: React.FC = () => { rightSideItems={[ // eslint-disable-next-line @elastic/eui/href-or-on-click { {canManageEngines && ( @@ -108,6 +109,7 @@ export const EnginesOverview: React.FC = () => { + { return (
    +

    {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx index 5e5ee2ea8d0f0..911e97de5b53f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -48,7 +48,7 @@ export const RelevanceTuningPreview: React.FC = () => { const { engineName, isMetaEngine } = useValues(EngineLogic); return ( - +

    {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.title', { From c8e23ad440d722b31247fc7ad383b194d74972a5 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 7 Apr 2021 13:07:39 -0500 Subject: [PATCH 52/65] [Fleet] Fixes to preconfigure API (#96094) --- .../common/types/models/preconfiguration.ts | 4 ++ .../server/services/epm/packages/install.ts | 5 ++- .../server/services/preconfiguration.test.ts | 30 +++++++++----- .../fleet/server/services/preconfiguration.ts | 39 +++++++++++++------ .../server/types/models/preconfiguration.ts | 3 ++ 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts index b16234d5a5f97..c9fff1c1581bd 100644 --- a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts @@ -27,3 +27,7 @@ export interface PreconfiguredAgentPolicy extends Omit; } + +export interface PreconfiguredPackage extends Omit { + force?: boolean; +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 7095bb1688c73..168ec55b14876 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -115,8 +115,9 @@ export async function ensureInstalledPackage(options: { pkgName: string; esClient: ElasticsearchClient; pkgVersion?: string; + force?: boolean; }): Promise { - const { savedObjectsClient, pkgName, esClient, pkgVersion } = options; + const { savedObjectsClient, pkgName, esClient, pkgVersion, force } = options; const installedPackage = await isPackageVersionInstalled({ savedObjectsClient, pkgName, @@ -136,7 +137,7 @@ export async function ensureInstalledPackage(options: { savedObjectsClient, pkgkey, esClient, - force: true, + force, }); } else { await installLatestPackage({ diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index bcde8ade427e5..8a885f9c5c821 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -66,9 +66,19 @@ function getPutPreconfiguredPackagesMock() { } jest.mock('./epm/packages/install', () => ({ - ensureInstalledPackage({ pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }) { + ensureInstalledPackage({ + pkgName, + pkgVersion, + force, + }: { + pkgName: string; + pkgVersion: string; + force?: boolean; + }) { const installedPackage = mockInstalledPackages.get(pkgName); - if (installedPackage) return installedPackage; + if (installedPackage) { + if (installedPackage.version === pkgVersion) return installedPackage; + } const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName }; mockInstalledPackages.set(pkgName, packageInstallation); @@ -138,12 +148,12 @@ describe('policy preconfiguration', () => { soClient, esClient, [], - [{ name: 'test-package', version: '3.0.0' }], + [{ name: 'test_package', version: '3.0.0' }], mockDefaultOutput ); expect(policies.length).toBe(0); - expect(packages).toEqual(expect.arrayContaining(['test-package:3.0.0'])); + expect(packages).toEqual(expect.arrayContaining(['test_package-3.0.0'])); }); it('should install packages and configure agent policies successfully', async () => { @@ -160,19 +170,19 @@ describe('policy preconfiguration', () => { id: 'test-id', package_policies: [ { - package: { name: 'test-package' }, + package: { name: 'test_package' }, name: 'Test package', }, ], }, ] as PreconfiguredAgentPolicy[], - [{ name: 'test-package', version: '3.0.0' }], + [{ name: 'test_package', version: '3.0.0' }], mockDefaultOutput ); expect(policies.length).toEqual(1); expect(policies[0].id).toBe('mocked-test-id'); - expect(packages).toEqual(expect.arrayContaining(['test-package:3.0.0'])); + expect(packages).toEqual(expect.arrayContaining(['test_package-3.0.0'])); }); it('should throw an error when trying to install duplicate packages', async () => { @@ -185,13 +195,13 @@ describe('policy preconfiguration', () => { esClient, [], [ - { name: 'test-package', version: '3.0.0' }, - { name: 'test-package', version: '2.0.0' }, + { name: 'test_package', version: '3.0.0' }, + { name: 'test_package', version: '2.0.0' }, ], mockDefaultOutput ) ).rejects.toThrow( - 'Duplicate packages specified in configuration: test-package:3.0.0, test-package:2.0.0' + 'Duplicate packages specified in configuration: test_package-3.0.0, test_package-2.0.0' ); }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index bd1c2ca1f23ef..97480fcf6b2a8 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -7,10 +7,9 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { i18n } from '@kbn/i18n'; -import { groupBy } from 'lodash'; +import { groupBy, omit } from 'lodash'; import type { - PackagePolicyPackage, NewPackagePolicy, AgentPolicy, Installation, @@ -18,8 +17,10 @@ import type { NewPackagePolicyInput, NewPackagePolicyInputStream, PreconfiguredAgentPolicy, + PreconfiguredPackage, } from '../../common'; +import { pkgToPkgKey } from './epm/registry'; import { getInstallation } from './epm/packages'; import { ensureInstalledPackage } from './epm/packages/install'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; @@ -32,7 +33,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, policies: PreconfiguredAgentPolicy[] = [], - packages: Array> = [], + packages: PreconfiguredPackage[] = [], defaultOutput: Output ) { // Validate configured packages to ensure there are no version conflicts @@ -45,7 +46,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( // If there are multiple packages with duplicate versions, separate them with semicolons, e.g // package-a:1.0.0, package-a:2.0.0; package-b:1.0.0, package-b:2.0.0 const duplicateList = duplicatePackages - .map(([, versions]) => versions.map((v) => `${v.name}:${v.version}`).join(', ')) + .map(([, versions]) => versions.map((v) => pkgToPkgKey(v)).join(', ')) .join('; '); throw new Error( @@ -60,8 +61,8 @@ export async function ensurePreconfiguredPackagesAndPolicies( // Preinstall packages specified in Kibana config const preconfiguredPackages = await Promise.all( - packages.map(({ name, version }) => - ensureInstalledPreconfiguredPackage(soClient, esClient, name, version) + packages.map(({ name, version, force }) => + ensureInstalledPreconfiguredPackage(soClient, esClient, name, version, force) ) ); @@ -71,7 +72,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( const { created, policy } = await agentPolicyService.ensurePreconfiguredAgentPolicy( soClient, esClient, - preconfiguredAgentPolicy + omit(preconfiguredAgentPolicy, 'is_managed') // Don't add `is_managed` until the policy has been fully configured ); if (!created) return { created, policy }; @@ -101,12 +102,22 @@ export async function ensurePreconfiguredPackagesAndPolicies( }) ); - return { created, policy, installedPackagePolicies }; + return { + created, + policy, + installedPackagePolicies, + shouldAddIsManagedFlag: preconfiguredAgentPolicy.is_managed, + }; }) ); for (const preconfiguredPolicy of preconfiguredPolicies) { - const { created, policy, installedPackagePolicies } = preconfiguredPolicy; + const { + created, + policy, + installedPackagePolicies, + shouldAddIsManagedFlag, + } = preconfiguredPolicy; if (created) { await addPreconfiguredPolicyPackages( soClient, @@ -115,6 +126,10 @@ export async function ensurePreconfiguredPackagesAndPolicies( installedPackagePolicies!, defaultOutput ); + // Add the is_managed flag after configuring package policies to avoid errors + if (shouldAddIsManagedFlag) { + agentPolicyService.update(soClient, esClient, policy.id, { is_managed: true }); + } } } @@ -123,7 +138,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( id: p.policy.id, updated_at: p.policy.updated_at, })), - packages: preconfiguredPackages.map((pkg) => `${pkg.name}:${pkg.version}`), + packages: preconfiguredPackages.map((pkg) => pkgToPkgKey(pkg)), }; } @@ -160,13 +175,15 @@ async function ensureInstalledPreconfiguredPackage( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, pkgName: string, - pkgVersion: string + pkgVersion: string, + force?: boolean ) { return ensureInstalledPackage({ savedObjectsClient: soClient, pkgName, esClient, pkgVersion, + force, }); } diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 77a28defaf1bd..0dc0ae8f1db88 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -33,6 +33,7 @@ export const PreconfiguredPackagesSchema = schema.arrayOf( } }, }), + force: schema.maybe(schema.boolean()), }) ); @@ -41,6 +42,8 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( ...AgentPolicyBaseSchema, namespace: schema.maybe(NamespaceSchema), id: schema.oneOf([schema.string(), schema.number()]), + is_default: schema.maybe(schema.boolean()), + is_default_fleet_server: schema.maybe(schema.boolean()), package_policies: schema.arrayOf( schema.object({ name: schema.string(), From 0aa348d9beb8e46344244a66a66d67269908aa1e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 20:22:42 +0200 Subject: [PATCH 53/65] Bump ssri from 8.0.0 to 8.0.1 (#96452) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6b977be1797ec..bb5d9ff8c23aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26242,9 +26242,9 @@ ssri@^7.0.0: minipass "^3.1.1" ssri@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" - integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== dependencies: minipass "^3.1.1" From 88847b98451034f27506502f491d9fc536bec0eb Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 7 Apr 2021 20:31:10 +0200 Subject: [PATCH 54/65] Bump Node.js from version 14.16.0 to 14.16.1 (#96382) --- .ci/Dockerfile | 2 +- .node-version | 2 +- .nvmrc | 2 +- WORKSPACE.bazel | 12 ++++++------ package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 445cc0e51073f..1c59d6d9aaaf8 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.16.0 +ARG NODE_VERSION=14.16.1 FROM node:${NODE_VERSION} AS base diff --git a/.node-version b/.node-version index 2a0dc9a810cf3..6b17d228d3351 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.16.0 +14.16.1 diff --git a/.nvmrc b/.nvmrc index 2a0dc9a810cf3..6b17d228d3351 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.16.0 +14.16.1 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 4639414b4564e..e74c646eedeaf 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.2.3") # we can update that rule. node_repositories( node_repositories = { - "14.16.0-darwin_amd64": ("node-v14.16.0-darwin-x64.tar.gz", "node-v14.16.0-darwin-x64", "14ec767e376d1e2e668f997065926c5c0086ec46516d1d45918af8ae05bd4583"), - "14.16.0-linux_arm64": ("node-v14.16.0-linux-arm64.tar.xz", "node-v14.16.0-linux-arm64", "440489a08bfd020e814c9e65017f58d692299ac3f150c8e78d01abb1104c878a"), - "14.16.0-linux_s390x": ("node-v14.16.0-linux-s390x.tar.xz", "node-v14.16.0-linux-s390x", "335348e46f45284b6356416ef58f85602d2dee99094588b65900f6c8839df77e"), - "14.16.0-linux_amd64": ("node-v14.16.0-linux-x64.tar.xz", "node-v14.16.0-linux-x64", "2e079cf638766fedd720d30ec8ffef5d6ceada4e8b441fc2a093cb9a865f4087"), - "14.16.0-windows_amd64": ("node-v14.16.0-win-x64.zip", "node-v14.16.0-win-x64", "716045c2f16ea10ca97bd04cf2e5ef865f9c4d6d677a9bc25e2ea522b594af4f"), + "14.16.1-darwin_amd64": ("node-v14.16.1-darwin-x64.tar.gz", "node-v14.16.1-darwin-x64", "b762b72fc149629b7e394ea9b75a093cad709a9f2f71480942945d8da0fc1218"), + "14.16.1-linux_arm64": ("node-v14.16.1-linux-arm64.tar.xz", "node-v14.16.1-linux-arm64", "b4d474e79f7d33b3b4430fad25c3f836b82ce2d5bb30d4a2c9fa20df027e40da"), + "14.16.1-linux_s390x": ("node-v14.16.1-linux-s390x.tar.xz", "node-v14.16.1-linux-s390x", "af9982fef32e4a3e4a5d66741dcf30ac9c27613bd73582fa1dae1fb25003047a"), + "14.16.1-linux_amd64": ("node-v14.16.1-linux-x64.tar.xz", "node-v14.16.1-linux-x64", "85a89d2f68855282c87851c882d4c4bbea4cd7f888f603722f0240a6e53d89df"), + "14.16.1-windows_amd64": ("node-v14.16.1-win-x64.zip", "node-v14.16.1-win-x64", "e469db37b4df74627842d809566c651042d86f0e6006688f0f5fe3532c6dfa41"), }, - node_version = "14.16.0", + node_version = "14.16.1", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/package.json b/package.json index d5d136305bc9c..a1acf73ea26f0 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "**/typescript": "4.1.3" }, "engines": { - "node": "14.16.0", + "node": "14.16.1", "yarn": "^1.21.1" }, "dependencies": { From 324c6c05a44985fcd9ff15e5285b29b1dcc3c596 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 7 Apr 2021 15:00:55 -0400 Subject: [PATCH 55/65] [Maps] Support query-time runtime fields (#95701) --- .../es_search_source/es_search_source.tsx | 44 +-------------- .../get_docvalue_source_fields.test.ts | 42 +++++++++++++++ .../get_docvalue_source_fields.ts | 54 +++++++++++++++++++ .../apps/maps/embeddable/dashboard.js | 8 +-- .../maps/embeddable/tooltip_filter_actions.js | 4 +- x-pack/test/functional/apps/maps/joins.js | 10 ++-- .../es_archives/maps/kibana/data.json | 3 +- .../es_archives/maps/kibana/mappings.json | 3 ++ 8 files changed, 116 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 168448b6f72a0..ac3a15d2ac490 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -61,54 +61,12 @@ import { DataRequest } from '../../util/data_request'; import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common'; import { isValidStringConfig } from '../../util/valid_string_config'; import { TopHitsUpdateSourceEditor } from './top_hits'; +import { getDocValueAndSourceFields, ScriptField } from './get_docvalue_source_fields'; export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); -export interface ScriptField { - source: string; - lang: string; -} - -function getDocValueAndSourceFields( - indexPattern: IndexPattern, - fieldNames: string[], - dateFormat: string -): { - docValueFields: Array; - sourceOnlyFields: string[]; - scriptFields: Record; -} { - const docValueFields: Array = []; - const sourceOnlyFields: string[] = []; - const scriptFields: Record = {}; - fieldNames.forEach((fieldName) => { - const field = getField(indexPattern, fieldName); - if (field.scripted) { - scriptFields[field.name] = { - script: { - source: field.script || '', - lang: field.lang || '', - }, - }; - } else if (field.readFromDocValues) { - const docValueField = - field.type === 'date' - ? { - field: fieldName, - format: dateFormat, - } - : fieldName; - docValueFields.push(docValueField); - } else { - sourceOnlyFields.push(fieldName); - } - }); - - return { docValueFields, sourceOnlyFields, scriptFields }; -} - export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { readonly _descriptor: ESSearchSourceDescriptor; protected readonly _tooltipFields: ESDocField[]; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts new file mode 100644 index 0000000000000..41744c4343f97 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.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 { getDocValueAndSourceFields } from './get_docvalue_source_fields'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { IFieldType } from '../../../../../../../src/plugins/data/common/index_patterns/fields'; + +function createMockIndexPattern(fields: IFieldType[]): IndexPattern { + const indexPattern = { + get fields() { + return { + getByName(fieldname: string) { + return fields.find((f) => f.name === fieldname); + }, + }; + }, + }; + + return (indexPattern as unknown) as IndexPattern; +} + +describe('getDocValueAndSourceFields', () => { + it('should add runtime fields to docvalue fields', () => { + const { docValueFields } = getDocValueAndSourceFields( + createMockIndexPattern([ + { + name: 'foobar', + // @ts-expect-error runtimeField not added yet to IFieldType. API tbd + runtimeField: {}, + }, + ]), + ['foobar'], + 'epoch_millis' + ); + + expect(docValueFields).toEqual(['foobar']); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts new file mode 100644 index 0000000000000..a8d10233b4d54 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { getField } from '../../../../common/elasticsearch_util'; + +export interface ScriptField { + source: string; + lang: string; +} + +export function getDocValueAndSourceFields( + indexPattern: IndexPattern, + fieldNames: string[], + dateFormat: string +): { + docValueFields: Array; + sourceOnlyFields: string[]; + scriptFields: Record; +} { + const docValueFields: Array = []; + const sourceOnlyFields: string[] = []; + const scriptFields: Record = {}; + fieldNames.forEach((fieldName) => { + const field = getField(indexPattern, fieldName); + if (field.scripted) { + scriptFields[field.name] = { + script: { + source: field.script || '', + lang: field.lang || '', + }, + }; + } + // @ts-expect-error runtimeField has not been added to public API yet. exact shape of type TBD. + else if (field.readFromDocValues || field.runtimeField) { + const docValueField = + field.type === 'date' + ? { + field: fieldName, + format: dateFormat, + } + : fieldName; + docValueFields.push(docValueField); + } else { + sourceOnlyFields.push(fieldName); + } + }); + + return { docValueFields, sourceOnlyFields, scriptFields }; +} diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 89c1cbded9a26..e1181119bee09 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -69,7 +69,9 @@ export default function ({ getPageObjects, getService }) { await dashboardPanelActions.openInspectorByTitle('join example'); await retry.try(async () => { const joinExampleRequestNames = await inspector.getRequestNames(); - expect(joinExampleRequestNames).to.equal('geo_shapes*,meta_for_geo_shapes*.shape_name'); + expect(joinExampleRequestNames).to.equal( + 'geo_shapes*,meta_for_geo_shapes*.runtime_shape_name' + ); }); await inspector.close(); @@ -90,7 +92,7 @@ export default function ({ getPageObjects, getService }) { await filterBar.selectIndexPattern('logstash-*'); await filterBar.addFilter('machine.os', 'is', 'win 8'); await filterBar.selectIndexPattern('meta_for_geo_shapes*'); - await filterBar.addFilter('shape_name', 'is', 'alpha'); + await filterBar.addFilter('shape_name', 'is', 'alpha'); // runtime fields do not have autocomplete const gridResponse = await PageObjects.maps.getResponseFromDashboardPanel( 'geo grid vector grid example' @@ -99,7 +101,7 @@ export default function ({ getPageObjects, getService }) { const joinResponse = await PageObjects.maps.getResponseFromDashboardPanel( 'join example', - 'meta_for_geo_shapes*.shape_name' + 'meta_for_geo_shapes*.runtime_shape_name' ); expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index 19d77a10a1979..d583e41e5e280 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }) { // const hasSourceFilter = await filterBar.hasFilter('name', 'charlie'); // expect(hasSourceFilter).to.be(true); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + const hasJoinFilter = await filterBar.hasFilter('runtime_shape_name', 'charlie'); expect(hasJoinFilter).to.be(true); }); }); @@ -78,7 +78,7 @@ export default function ({ getPageObjects, getService }) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.equal(2); - const hasJoinFilter = await filterBar.hasFilter('shape_name', 'charlie'); + const hasJoinFilter = await filterBar.hasFilter('runtime_shape_name', 'charlie'); expect(hasJoinFilter).to.be(true); }); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 8b40651ea5674..181b6928e0ec0 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -39,7 +39,7 @@ export default function ({ getPageObjects, getService }) { it('should re-fetch join with refresh timer', async () => { async function getRequestTimestamp() { - await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.shape_name'); + await PageObjects.maps.openInspectorRequest('meta_for_geo_shapes*.runtime_shape_name'); const requestStats = await inspector.getTableData(); const requestTimestamp = PageObjects.maps.getInspectorStatRowHit( requestStats, @@ -121,7 +121,9 @@ export default function ({ getPageObjects, getService }) { }); it('should not apply query to source and apply query to join', async () => { - const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + const joinResponse = await PageObjects.maps.getResponse( + 'meta_for_geo_shapes*.runtime_shape_name' + ); expect(joinResponse.aggregations.join.buckets.length).to.equal(2); }); }); @@ -136,7 +138,9 @@ export default function ({ getPageObjects, getService }) { }); it('should apply query to join request', async () => { - const joinResponse = await PageObjects.maps.getResponse('meta_for_geo_shapes*.shape_name'); + const joinResponse = await PageObjects.maps.getResponse( + 'meta_for_geo_shapes*.runtime_shape_name' + ); expect(joinResponse.aggregations.join.buckets.length).to.equal(1); }); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 79f869040f74a..631efb58f9c7b 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -51,6 +51,7 @@ "index": ".kibana", "source": { "index-pattern": { + "runtimeFieldMap" : "{\"runtime_shape_name\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['shape_name'].value)\"}}}", "fields" : "[]", "title": "meta_for_geo_shapes*" }, @@ -498,7 +499,7 @@ "type": "envelope" }, "description": "", - "layerListJSON" : "[{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3},\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", + "layerListJSON" : "[{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3},\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.runtime_shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.runtime_shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"runtime_shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", "mapStateJSON": "{\"zoom\":3.02,\"center\":{\"lon\":77.33426,\"lat\":-0.04647},\"timeFilters\":{\"from\":\"now-17m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", "title": "join example", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"n1t6f\"]}" diff --git a/x-pack/test/functional/es_archives/maps/kibana/mappings.json b/x-pack/test/functional/es_archives/maps/kibana/mappings.json index 7f421123bddf8..f370d4d5fe233 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/maps/kibana/mappings.json @@ -136,6 +136,9 @@ "fieldFormatMap": { "type": "text" }, + "runtimeFieldMap": { + "type": "text" + }, "fields": { "type": "text" }, From 21f38afd27a7cf01ee9059cf9bb73ac0a51af85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Wed, 7 Apr 2021 21:01:54 +0200 Subject: [PATCH 56/65] [SECURITY SOLUTION] Add new exception list type and feature flag for event filtering (#96037) * New exception list type for event filtering * New feature flag for event filtering --- x-pack/plugins/lists/common/schemas/common/schemas.ts | 7 ++++++- .../common/detection_engine/schemas/types/lists.test.ts | 6 +++--- .../security_solution/common/experimental_features.ts | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index e553b65a2f610..f261e4e3eefa6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -212,13 +212,18 @@ export type Tags = t.TypeOf; export const tagsOrUndefined = t.union([tags, t.undefined]); export type TagsOrUndefined = t.TypeOf; -export const exceptionListType = t.keyof({ detection: null, endpoint: null }); +export const exceptionListType = t.keyof({ + detection: null, + endpoint: null, + endpoint_events: null, +}); export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]); export type ExceptionListType = t.TypeOf; export type ExceptionListTypeOrUndefined = t.TypeOf; export enum ExceptionListTypeEnum { DETECTION = 'detection', ENDPOINT = 'endpoint', + ENDPOINT_EVENTS = 'endpoint_events', } export const exceptionListItemType = t.keyof({ simple: null }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts index e331eea51eec0..28b70f51742a7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts @@ -94,7 +94,7 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}>"', + 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', ]); expect(message.schema).toEqual({}); }); @@ -125,8 +125,8 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"', - 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 19de81cb95c3f..39551e3ee6f1c 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -14,6 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const allowedExperimentalValues = Object.freeze({ fleetServerEnabled: false, trustedAppsByPolicyEnabled: false, + eventFilteringEnabled: false, }); type ExperimentalConfigKeys = Array; From ad06d16beb8a747f03900455510f6588cf51e82e Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 7 Apr 2021 15:20:47 -0400 Subject: [PATCH 57/65] [actions] adds proxyBypassHosts and proxyOnlyHosts Kibana config keys (#95365) resolves https://github.com/elastic/kibana/issues/92949 This PR adds two new Kibana config keys to further customize when the proxy is used when making HTTP requests. Prior to this PR, if a proxy was set via the `xpack.actions.proxyUrl` config key, all requests would be proxied. Now, there's a further refinement in that hostnames can be added to the `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` config keys. Only one of these config keys can be used at a time. If the target URL hostname of the HTTP request is listed in the `proxyBypassHosts` list, the proxy won't be used. If the target URL hostname of the HTTP request is **NOT** listed in the `proxyOnlyHosts` list, the proxy won't be used. Depending on the customer's environment, it may be easier to list the hosts to bypass, or easier to list the hosts that should only be proxied, so they can choose either method. --- docs/settings/alert-action-settings.asciidoc | 6 + .../resources/base/bin/kibana-docker | 2 + .../actions/server/actions_client.test.ts | 2 + .../actions/server/actions_config.test.ts | 79 +++++++++++ .../plugins/actions/server/actions_config.ts | 17 +-- .../lib/axios_utils.test.ts | 98 ++++++++++++- .../builtin_action_types/lib/axios_utils.ts | 2 +- .../lib/get_custom_agents.test.ts | 70 ++++++++- .../lib/get_custom_agents.ts | 25 +++- .../lib/send_email.test.ts | 134 ++++++++++++++++++ .../builtin_action_types/lib/send_email.ts | 13 +- .../server/builtin_action_types/slack.test.ts | 102 +++++++++++++ .../server/builtin_action_types/slack.ts | 8 +- x-pack/plugins/actions/server/config.test.ts | 60 +++++++- x-pack/plugins/actions/server/config.ts | 31 +++- x-pack/plugins/actions/server/plugin.ts | 4 +- x-pack/plugins/actions/server/types.ts | 2 + .../alerting_api_integration/common/config.ts | 14 +- 18 files changed, 645 insertions(+), 24 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 3645499d5f9ff..08cbee8851b98 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -59,6 +59,12 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.actions.proxyUrl` {ess-icon} | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. +| `xpack.actions.proxyBypassHosts` {ess-icon} + | Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. + +| `xpack.actions.proxyOnlyHosts` {ess-icon} + | Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. + | `xpack.actions.proxyHeaders` {ess-icon} | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 9617a556e2cdd..e0fd649a43df7 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -163,6 +163,8 @@ kibana_vars=( xpack.actions.proxyHeaders xpack.actions.proxyRejectUnauthorizedCertificates xpack.actions.proxyUrl + xpack.actions.proxyBypassHosts + xpack.actions.proxyOnlyHosts xpack.actions.rejectUnauthorized xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index a333d86b27129..92d3b4f29d967 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -406,6 +406,8 @@ describe('create()', () => { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index cae6777a82441..36899f7661ba4 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -253,3 +253,82 @@ describe('ensureActionTypeEnabled', () => { expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined(); }); }); + +describe('getProxySettings', () => { + test('returns undefined when no proxy URL set', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyHeaders: { someHeaderName: 'some header value' }, + proxyBypassHosts: ['avoid-proxy.co'], + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings).toBeUndefined(); + }); + + test('returns proxy url', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + }; + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyUrl).toBe(config.proxyUrl); + }); + + test('returns proxyRejectUnauthorizedCertificates', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyRejectUnauthorizedCertificates: true, + }; + let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); + expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyRejectUnauthorizedCertificates: false, + }; + proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); + expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false); + }); + + test('returns proxy headers', () => { + const proxyHeaders = { + someHeaderName: 'some header value', + someOtherHeader: 'some other header', + }; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyHeaders, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyHeaders).toEqual(config.proxyHeaders); + }); + + test('returns proxy bypass hosts', () => { + const proxyBypassHosts = ['proxy-bypass-1.elastic.co', 'proxy-bypass-2.elastic.co']; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyBypassHosts, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyBypassHosts).toEqual(new Set(proxyBypassHosts)); + }); + + test('returns proxy only hosts', () => { + const proxyOnlyHosts = ['proxy-only-1.elastic.co', 'proxy-only-2.elastic.co']; + const config: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + proxyOnlyHosts, + }; + + const proxySettings = getActionsConfigurationUtilities(config).getProxySettings(); + expect(proxySettings?.proxyOnlyHosts).toEqual(new Set(proxyOnlyHosts)); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index 2787f8f971101..b35a4a0d7b6c5 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -11,17 +11,11 @@ import url from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; -import { ActionsConfig } from './config'; +import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config'; import { ActionTypeDisabledError } from './lib'; import { ProxySettings } from './types'; -export enum AllowedHosts { - Any = '*', -} - -export enum EnabledActionTypes { - Any = '*', -} +export { AllowedHosts, EnabledActionTypes } from './config'; enum AllowListingField { URL = 'url', @@ -93,11 +87,18 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet return { proxyUrl: config.proxyUrl, + proxyBypassHosts: arrayAsSet(config.proxyBypassHosts), + proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts), proxyHeaders: config.proxyHeaders, proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates, }; } +function arrayAsSet(arr: T[] | undefined): Set | undefined { + if (!arr) return; + return new Set(arr); +} + export function getActionsConfigurationUtilities( config: ActionsConfig ): ActionsConfigurationUtilities { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 6a67f4f6752c2..a932b38ede2bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -7,12 +7,16 @@ import axios from 'axios'; import { Agent as HttpsAgent } from 'https'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; import { getCustomAgents } from './get_custom_agents'; +const TestUrl = 'https://elastic.co/foo/bar/baz'; + const logger = loggingSystemMock.create().get() as jest.Mocked; const configurationUtilities = actionsConfigMock.create(); jest.mock('axios'); @@ -66,17 +70,19 @@ describe('request', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyRejectUnauthorizedCertificates: true, proxyUrl: 'https://localhost:1212', + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, TestUrl); const res = await request({ axios, - url: 'http://testProxy', + url: TestUrl, logger, configurationUtilities, }); - expect(axiosMock).toHaveBeenCalledWith('http://testProxy', { + expect(axiosMock).toHaveBeenCalledWith(TestUrl, { method: 'get', data: {}, httpAgent, @@ -94,6 +100,8 @@ describe('request', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope:', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const res = await request({ axios, @@ -116,6 +124,90 @@ describe('request', () => { }); }); + test('it bypasses with proxyBypassHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: new Set(['elastic.co']), + proxyOnlyHosts: undefined, + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(false); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(false); + }); + + test('it does not bypass with proxyBypassHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: new Set(['not-elastic.co']), + proxyOnlyHosts: undefined, + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(true); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(true); + }); + + test('it proxies with proxyOnlyHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['elastic.co']), + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(true); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(true); + }); + + test('it does not proxy with proxyOnlyHosts when expected', async () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyRejectUnauthorizedCertificates: true, + proxyUrl: 'https://elastic.proxy.co', + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-elastic.co']), + }); + + await request({ + axios, + url: TestUrl, + logger, + configurationUtilities, + }); + + expect(axiosMock.mock.calls.length).toBe(1); + const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1]; + expect(httpAgent instanceof HttpProxyAgent).toBe(false); + expect(httpsAgent instanceof HttpsProxyAgent).toBe(false); + }); + test('it fetch correctly', async () => { const res = await request({ axios, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index f86f3b86c506a..edce369096142 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -30,7 +30,7 @@ export const request = async ({ validateStatus?: (status: number) => boolean; auth?: AxiosBasicCredentials; }): Promise => { - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url); return await axios(url, { ...rest, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts index 340ac0f6dda3a..f6d1be9bffc6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts @@ -14,6 +14,10 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; const logger = loggingSystemMock.create().get() as jest.Mocked; +const targetHost = 'elastic.co'; +const targetUrl = `https://${targetHost}/foo/bar/baz`; +const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`; + describe('getCustomAgents', () => { const configurationUtilities = actionsConfigMock.create(); @@ -21,8 +25,10 @@ describe('getCustomAgents', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); }); @@ -31,15 +37,73 @@ describe('getCustomAgents', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope: not a valid URL', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent).toBe(undefined); expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); test('return default agents for undefined proxy options', () => { - const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); expect(httpAgent).toBe(undefined); expect(httpsAgent instanceof HttpsAgent).toBeTruthy(); }); + + test('returns non-proxy agents for matching proxyBypassHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set([targetHost]), + proxyOnlyHosts: undefined, + }); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); + expect(httpAgent instanceof HttpProxyAgent).toBeFalsy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy(); + }); + + test('returns proxy agents for non-matching proxyBypassHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set([targetHost]), + proxyOnlyHosts: undefined, + }); + const { httpAgent, httpsAgent } = getCustomAgents( + configurationUtilities, + logger, + nonMatchingUrl + ); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('returns proxy agents for matching proxyOnlyHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set([targetHost]), + }); + const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl); + expect(httpAgent instanceof HttpProxyAgent).toBeTruthy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('returns non-proxy agents for non-matching proxyOnlyHosts', () => { + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set([targetHost]), + }); + const { httpAgent, httpsAgent } = getCustomAgents( + configurationUtilities, + logger, + nonMatchingUrl + ); + expect(httpAgent instanceof HttpProxyAgent).toBeFalsy(); + expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts index 92ababf830aa7..ff2d005f4d841 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts @@ -19,7 +19,8 @@ interface GetCustomAgentsResponse { export function getCustomAgents( configurationUtilities: ActionsConfigurationUtilities, - logger: Logger + logger: Logger, + url: string ): GetCustomAgentsResponse { const proxySettings = configurationUtilities.getProxySettings(); const defaultAgents = { @@ -33,6 +34,28 @@ export function getCustomAgents( return defaultAgents; } + let targetUrl: URL; + try { + targetUrl = new URL(url); + } catch (err) { + logger.warn(`error determining proxy state for invalid url "${url}", using default agents`); + return defaultAgents; + } + + // filter out hostnames in the proxy bypass or only lists + const { hostname } = targetUrl; + + if (proxySettings.proxyBypassHosts) { + if (proxySettings.proxyBypassHosts.has(hostname)) { + return defaultAgents; + } + } + + if (proxySettings.proxyOnlyHosts) { + if (!proxySettings.proxyOnlyHosts.has(hostname)) { + return defaultAgents; + } + } logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`); let proxyUrl: URL; try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index cc3f03f50c36f..4b45c6d787cd6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -76,6 +76,8 @@ describe('send_email module', () => { { proxyUrl: 'https://example.com', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, } ); @@ -222,6 +224,138 @@ describe('send_email module', () => { await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops'); }); + + test('it bypasses with proxyBypassHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it does not bypass with proxyBypassHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://proxy.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it proxies with proxyOnlyHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://proxy.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); + + test('it does not proxy with proxyOnlyHosts when expected', async () => { + const sendEmailOptions = getSendEmailOptionsNoAuth( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://proxy.com', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), + } + ); + + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + }); }); function getSendEmailOptions( diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index d4905015f7663..c0a254967b4fe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -63,6 +63,17 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom }; } + let useProxy = !!proxySettings; + + if (host) { + if (proxySettings?.proxyBypassHosts && proxySettings?.proxyBypassHosts?.has(host)) { + useProxy = false; + } + if (proxySettings?.proxyOnlyHosts && !proxySettings?.proxyOnlyHosts?.has(host)) { + useProxy = false; + } + } + if (service === JSON_TRANSPORT_SERVICE) { transportConfig.jsonTransport = true; delete transportConfig.auth; @@ -73,7 +84,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.port = port; transportConfig.secure = !!secure; - if (proxySettings) { + if (proxySettings && useProxy) { transportConfig.tls = { // do not fail on invalid certs if value is false rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates, diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 6479e29b5a76f..76612696e8e58 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -195,6 +195,8 @@ describe('execute()', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); const actionTypeProxy = getActionType({ logger: mockedLogger, @@ -212,6 +214,106 @@ describe('execute()', () => { ); }); + test('ensure proxy bypass will bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy bypass will not bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy only will proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + + test('ensure proxy only will not proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configurationUtilities = actionsConfigMock.create(); + configurationUtilities.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxyRejectUnauthorizedCertificates: false, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), + }); + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities, + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); + test('renders parameter templates as expected', async () => { expect(actionType.renderParameterTemplates).toBeTruthy(); const paramsWithTemplates = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index a6173229e3267..d0fb4a8c4b935 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -7,6 +7,8 @@ import { URL } from 'url'; import { curry } from 'lodash'; +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; @@ -131,13 +133,15 @@ async function slackExecutor( const { message } = params; const proxySettings = configurationUtilities.getProxySettings(); - const customAgents = getCustomAgents(configurationUtilities, logger); + const customAgents = getCustomAgents(configurationUtilities, logger, webhookUrl); const agent = webhookUrl.toLowerCase().startsWith('https') ? customAgents.httpsAgent : customAgents.httpAgent; if (proxySettings) { - logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); + if (agent instanceof HttpProxyAgent || agent instanceof HttpsProxyAgent) { + logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`); + } } try { diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index c90a5b2fb9768..0d270512d1dee 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -5,9 +5,17 @@ * 2.0. */ -import { configSchema } from './config'; +import { configSchema, ActionsConfig, getValidatedConfig } from './config'; +import { Logger } from '../../../..//src/core/server'; +import { loggingSystemMock } from '../../../..//src/core/server/mocks'; + +const mockLogger = loggingSystemMock.create().get() as jest.Mocked; describe('config validation', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + test('action defaults', () => { const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` @@ -84,6 +92,56 @@ describe('config validation', () => { `"[preconfigured]: invalid preconfigured action id \\"__proto__\\""` ); }); + + test('validates proxyBypassHosts and proxyOnlyHosts', () => { + const bypassHosts = ['bypass.elastic.co']; + const onlyHosts = ['only.elastic.co']; + let validated: ActionsConfig; + + validated = configSchema.validate({}); + expect(validated.proxyBypassHosts).toEqual(undefined); + expect(validated.proxyOnlyHosts).toEqual(undefined); + + validated = configSchema.validate({ + proxyBypassHosts: bypassHosts, + }); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(undefined); + + validated = configSchema.validate({ + proxyOnlyHosts: onlyHosts, + }); + expect(validated.proxyBypassHosts).toEqual(undefined); + expect(validated.proxyOnlyHosts).toEqual(onlyHosts); + }); + + test('validates proxyBypassHosts and proxyOnlyHosts used at the same time', () => { + const bypassHosts = ['bypass.elastic.co']; + const onlyHosts = ['only.elastic.co']; + const config: Record = { + proxyBypassHosts: bypassHosts, + proxyOnlyHosts: onlyHosts, + }; + + let validated: ActionsConfig; + + // the config schema validation validates with both set + validated = configSchema.validate(config); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(onlyHosts); + + // getValidatedConfig will warn and set onlyHosts to undefined with both set + validated = getValidatedConfig(mockLogger, validated); + expect(validated.proxyBypassHosts).toEqual(bypassHosts); + expect(validated.proxyOnlyHosts).toEqual(undefined); + expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.", + ], + ] + `); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index b4f29b752957f..450f03308ab0b 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -6,7 +6,15 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { AllowedHosts, EnabledActionTypes } from './actions_config'; +import { Logger } from '../../../../src/core/server'; + +export enum AllowedHosts { + Any = '*', +} + +export enum EnabledActionTypes { + Any = '*', +} const preconfiguredActionSchema = schema.object({ name: schema.string({ minLength: 1 }), @@ -36,11 +44,32 @@ export const configSchema = schema.object({ proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), + proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), + proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), rejectUnauthorized: schema.boolean({ defaultValue: true }), }); export type ActionsConfig = TypeOf; +// It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on +// simultaneous usage in the config validator directly, but there's no good way to express +// this relationship in the cloud config constraints, so we're doing it "live". +export function getValidatedConfig(logger: Logger, originalConfig: ActionsConfig): ActionsConfig { + const proxyBypassHosts = originalConfig.proxyBypassHosts; + const proxyOnlyHosts = originalConfig.proxyOnlyHosts; + + if (proxyBypassHosts && proxyOnlyHosts) { + logger.warn( + 'The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.' + ); + const tmp: Record = originalConfig; + delete tmp.proxyOnlyHosts; + return tmp as ActionsConfig; + } + + return originalConfig; +} + const invalidActionIds = new Set(['', '__proto__', 'constructor']); function validatePreconfigured(preconfigured: Record): string | undefined { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5ec9241533b3c..bfe3b0a09ff2e 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -30,7 +30,7 @@ import { SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; -import { ActionsConfig } from './config'; +import { ActionsConfig, getValidatedConfig } from './config'; import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; @@ -141,8 +141,8 @@ export class ActionsPlugin implements Plugin(); this.logger = initContext.logger.get('actions'); + this.actionsConfig = getValidatedConfig(this.logger, initContext.config.get()); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; this.kibanaIndexConfig = initContext.config.legacy.get(); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 4e3916f5d6e23..6830f013ade5f 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -133,6 +133,8 @@ export interface ActionTaskExecutorParams { export interface ProxySettings { proxyUrl: string; + proxyBypassHosts: Set | undefined; + proxyOnlyHosts: Set | undefined; proxyHeaders?: Record; proxyRejectUnauthorizedCertificates: boolean; } diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 560ff6c0b317f..beb639eb46334 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -68,12 +68,24 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const proxyPort = process.env.ALERTING_PROXY_PORT ?? (await getPort({ port: getPort.makeRange(6200, 6300) })); + + // If testing with proxy, also test proxyOnlyHosts for this proxy; + // all the actions are assumed to be acccessing localhost anyway. + // If not testing with proxy, set a bogus proxy up, and set the bypass + // flag for all our localhost actions to bypass it. Currently, + // security_and_spaces uses enableActionsProxy: true, and spaces_only + // uses enableActionsProxy: false. + const proxyHosts = ['localhost', 'some.non.existent.com']; const actionsProxyUrl = options.enableActionsProxy ? [ `--xpack.actions.proxyUrl=http://localhost:${proxyPort}`, + `--xpack.actions.proxyOnlyHosts=${JSON.stringify(proxyHosts)}`, '--xpack.actions.proxyRejectUnauthorizedCertificates=false', ] - : []; + : [ + `--xpack.actions.proxyUrl=http://elastic.co`, + `--xpack.actions.proxyBypassHosts=${JSON.stringify(proxyHosts)}`, + ]; return { testFiles: [require.resolve(`../${name}/tests/`)], From 71c326c8bff4c0b7f9286d18e6239da91f2cbfb6 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 7 Apr 2021 15:43:06 -0400 Subject: [PATCH 58/65] handle runtime fields in validation step (#96340) --- .../ml/common/types/data_frame_analytics.ts | 3 +- .../models/data_frame_analytics/validation.ts | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index d9632f4d4a83b..ff5069e7d59ad 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import type { estypes } from '@elastic/elasticsearch'; import { RuntimeMappings } from './fields'; import { EsErrorBody } from '../util/errors'; @@ -75,7 +76,7 @@ export interface DataFrameAnalyticsConfig { }; source: { index: IndexName | IndexName[]; - query?: any; + query?: estypes.QueryContainer; runtime_mappings?: RuntimeMappings; }; analysis: AnalysisConfig; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 3f0a02f5eaad8..bbfc304958f9a 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -195,12 +195,13 @@ function getTrainingPercentMessage(trainingDocs: number) { async function getValidationCheckMessages( asCurrentUser: IScopedClusterClient['asCurrentUser'], analyzedFields: string[], - index: string | string[], analysisConfig: AnalysisConfig, - query: estypes.QueryContainer = defaultQuery + source: DataFrameAnalyticsConfig['source'] ) { const analysisType = getAnalysisType(analysisConfig); const depVar = getDependentVar(analysisConfig); + const index = source.index; + const query = source.query || defaultQuery; const messages = []; const emptyFields: string[] = []; const percentEmptyLimit = FRACTION_EMPTY_LIMIT * 100; @@ -236,6 +237,7 @@ async function getValidationCheckMessages( size: 0, track_total_hits: true, body: { + ...(source.runtime_mappings ? { runtime_mappings: source.runtime_mappings } : {}), query, aggs, }, @@ -247,21 +249,22 @@ async function getValidationCheckMessages( if (body.aggregations) { // @ts-expect-error Object.entries(body.aggregations).forEach(([aggName, { doc_count: docCount, value }]) => { - const empty = docCount / totalDocs; + if (docCount !== undefined) { + const empty = docCount / totalDocs; + if (docCount > 0 && empty > FRACTION_EMPTY_LIMIT) { + emptyFields.push(aggName); - if (docCount > 0 && empty > FRACTION_EMPTY_LIMIT) { - emptyFields.push(aggName); - - if (aggName === depVar) { - depVarValid = false; - dependentVarWarningMessage.text = i18n.translate( - 'xpack.ml.models.dfaValidation.messages.depVarEmptyWarning', - { - defaultMessage: - 'The dependent variable has at least {percentEmpty}% empty values. It may be unsuitable for analysis.', - values: { percentEmpty: percentEmptyLimit }, - } - ); + if (aggName === depVar) { + depVarValid = false; + dependentVarWarningMessage.text = i18n.translate( + 'xpack.ml.models.dfaValidation.messages.depVarEmptyWarning', + { + defaultMessage: + 'The dependent variable has at least {percentEmpty}% empty values. It may be unsuitable for analysis.', + values: { percentEmpty: percentEmptyLimit }, + } + ); + } } } @@ -374,9 +377,8 @@ export async function validateAnalyticsJob( const messages = await getValidationCheckMessages( client.asCurrentUser, job.analyzed_fields.includes, - job.source.index, job.analysis, - job.source.query + job.source ); return messages; } From d8ef85e85ba18dd0bccc65b6e9edf483d80f287b Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 7 Apr 2021 14:08:28 -0700 Subject: [PATCH 59/65] [globby] normalize paths for windows support (#96476) Co-authored-by: spalger --- .../simple_kibana_platform_plugin_discovery.ts | 3 ++- .../run_failed_tests_reporter_cli.ts | 5 ++++- .../package_json/find_used_dependencies.ts | 18 ++++++++++-------- .../integration_tests/ref_output_cache.test.ts | 6 +++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts index 26b1a6fa2e804..2381faefbff29 100644 --- a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts +++ b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts @@ -9,6 +9,7 @@ import Path from 'path'; import globby from 'globby'; +import normalize from 'normalize-path'; import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin'; @@ -32,7 +33,7 @@ export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPa ), ...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)), ]) - ); + ).map((path) => normalize(path)); const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) => // absolute paths returned from globby are using normalize or diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 8ef11e2dba462..63eca93def64d 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -11,6 +11,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { run, createFailError, createFlagError } from '@kbn/dev-utils'; import globby from 'globby'; +import normalize from 'normalize-path'; import { getFailures, TestFailure } from './get_failures'; import { GithubApi, GithubIssueMini } from './github_api'; @@ -61,7 +62,9 @@ export function runFailedTestsReporterCli() { throw createFlagError('Missing --build-url or process.env.BUILD_URL'); } - const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS; + const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => + normalize(Path.resolve(p)) + ); log.info('Searching for reports at', patterns); const reportPaths = await globby(patterns, { absolute: true, diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts index 3a296ec76f3e6..004e17b87ac8b 100644 --- a/src/dev/build/tasks/package_json/find_used_dependencies.ts +++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ +import Path from 'path'; import globby from 'globby'; +import normalize from 'normalize-path'; // @ts-ignore import { parseEntries, dependenciesParseStrategy } from '@kbn/babel-code-parser'; @@ -21,16 +23,16 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // Define the entry points for the server code in order to // start here later looking for the server side dependencies const mainCodeEntries = [ - `${baseDir}/src/cli/dist.js`, - `${baseDir}/src/cli_keystore/dist.js`, - `${baseDir}/src/cli_plugin/dist.js`, + Path.resolve(baseDir, `src/cli/dist.js`), + Path.resolve(baseDir, `src/cli_keystore/dist.js`), + Path.resolve(baseDir, `src/cli_plugin/dist.js`), ]; const discoveredPluginEntries = await globby([ - `${baseDir}/src/plugins/*/server/index.js`, - `!${baseDir}/src/plugins/**/public`, - `${baseDir}/x-pack/plugins/*/server/index.js`, - `!${baseDir}/x-pack/plugins/**/public`, + normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)), + `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`, + normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)), + `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`, ]); // It will include entries that cannot be discovered @@ -40,7 +42,7 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // Another way would be to include an index file and import all the functions // using named imports const dynamicRequiredEntries = await globby([ - `${baseDir}/src/plugins/vis_type_timelion/server/**/*.js`, + normalize(Path.resolve(baseDir, 'src/plugins/vis_type_timelion/server/**/*.js')), ]); // Compose all the needed entries diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts index 2bc75785ee6a7..7347529239176 100644 --- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts +++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts @@ -12,6 +12,7 @@ import Fs from 'fs'; import del from 'del'; import cpy from 'cpy'; import globby from 'globby'; +import normalize from 'normalize-path'; import { ToolingLog, createAbsolutePathSerializer, @@ -98,7 +99,10 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and const files = Object.fromEntries( globby - .sync(outDirs, { dot: true }) + .sync( + outDirs.map((p) => normalize(p)), + { dot: true } + ) .map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')]) ); From e6e3b16ee1d7a98f2f5b6fcb2f00a62c6a12a301 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 7 Apr 2021 14:18:36 -0700 Subject: [PATCH 60/65] [Enterprise Search] Change last breadcrumb to inactive/non-linked breadcrumb (#96489) * Update our EUI breadcrumb helper to skip generating links for the last breadcrumb in the list * Fix useEuiBreadcrumbs tests - add a describe block to make it clear we're testing link behavior in non-last breadcrumbs - add a helper that automatically adds a last breadcrumb so that link generation still works * Add comment/note as to why I didn't add last-breadcrumb-specific logic to useGenerateBreadcrumbs --- .../generate_breadcrumbs.test.ts | 75 +++++++++---------- .../kibana_chrome/generate_breadcrumbs.ts | 10 ++- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index 0bf7d618c33b3..c05c4dcbdddc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -14,6 +14,7 @@ jest.mock('../react_router_helpers', () => ({ import { letBrowserHandleEvent } from '../react_router_helpers'; import { + Breadcrumb, useGenerateBreadcrumbs, useEuiBreadcrumbs, useEnterpriseSearchBreadcrumbs, @@ -40,6 +41,9 @@ describe('useGenerateBreadcrumbs', () => { { text: 'Groups', path: '/groups' }, { text: 'Example Group Name', path: '/groups/{id}' }, { text: 'Source Prioritization', path: '/groups/{id}/source_prioritization' }, + // Note: We're still generating a path for the last breadcrumb even though useEuiBreadcrumbs + // will not render a link for it. This is because it's easier to keep our last-breadcrumb-specific + // logic in one place, & this way we still have a current path if (for some reason) we need it later. ]); }); @@ -89,48 +93,51 @@ describe('useEuiBreadcrumbs', () => { }, { text: 'World', - href: '/app/enterprise_search/world', - onClick: expect.any(Function), + // Per EUI best practices, the last breadcrumb is inactive/is not a link }, ]); }); - it('prevents default navigation and uses React Router history on click', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/test' }])[0] as any; + describe('link behavior for non-last breadcrumbs', () => { + // Test helper - adds a 2nd dummy breadcrumb so that paths from the first breadcrumb are generated + const useEuiBreadcrumb = (breadcrumb: Breadcrumb) => + useEuiBreadcrumbs([breadcrumb, { text: '' }])[0] as any; - expect(breadcrumb.href).toEqual('/app/enterprise_search/test'); - expect(mockHistory.createHref).toHaveBeenCalled(); + it('prevents default navigation and uses React Router history on click', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test' }); - const event = { preventDefault: jest.fn() }; - breadcrumb.onClick(event); + expect(breadcrumb.href).toEqual('/app/enterprise_search/test'); + expect(mockHistory.createHref).toHaveBeenCalled(); - expect(event.preventDefault).toHaveBeenCalled(); - expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); - }); + const event = { preventDefault: jest.fn() }; + breadcrumb.onClick(event); - it('does not call createHref if shouldNotCreateHref is passed', () => { - const breadcrumb = useEuiBreadcrumbs([ - { text: '', path: '/test', shouldNotCreateHref: true }, - ])[0] as any; + expect(event.preventDefault).toHaveBeenCalled(); + expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled(); + }); - expect(breadcrumb.href).toEqual('/test'); - expect(mockHistory.createHref).not.toHaveBeenCalled(); - }); + it('does not call createHref if shouldNotCreateHref is passed', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/test', shouldNotCreateHref: true }); - it('does not prevent default browser behavior on new tab/window clicks', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/' }])[0] as any; + expect(breadcrumb.href).toEqual('/test'); + expect(mockHistory.createHref).not.toHaveBeenCalled(); + }); - (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); - breadcrumb.onClick(); + it('does not prevent default browser behavior on new tab/window clicks', () => { + const breadcrumb = useEuiBreadcrumb({ text: '', path: '/' }); - expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); - }); + (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); + breadcrumb.onClick(); + + expect(mockKibanaValues.navigateToUrl).not.toHaveBeenCalled(); + }); - it('does not generate link behavior if path is excluded', () => { - const breadcrumb = useEuiBreadcrumbs([{ text: 'Unclickable breadcrumb' }])[0]; + it('does not generate link behavior if path is excluded', () => { + const breadcrumb = useEuiBreadcrumb({ text: 'Unclickable breadcrumb' }); - expect(breadcrumb.href).toBeUndefined(); - expect(breadcrumb.onClick).toBeUndefined(); + expect(breadcrumb.href).toBeUndefined(); + expect(breadcrumb.onClick).toBeUndefined(); + }); }); }); @@ -164,8 +171,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -174,8 +179,6 @@ describe('useEnterpriseSearchBreadcrumbs', () => { expect(useEnterpriseSearchBreadcrumbs()).toEqual([ { text: 'Enterprise Search', - href: '/app/enterprise_search/overview', - onClick: expect.any(Function), }, ]); }); @@ -219,8 +222,6 @@ describe('useAppSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/app_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -234,8 +235,6 @@ describe('useAppSearchBreadcrumbs', () => { }, { text: 'App Search', - href: '/app/enterprise_search/app_search/', - onClick: expect.any(Function), }, ]); }); @@ -279,8 +278,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => { }, { text: 'Page 2', - href: '/app/enterprise_search/workplace_search/page2', - onClick: expect.any(Function), }, ]); }); @@ -294,8 +291,6 @@ describe('useWorkplaceSearchBreadcrumbs', () => { }, { text: 'Workplace Search', - href: '/app/enterprise_search/workplace_search/', - onClick: expect.any(Function), }, ]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 908cc0601ab9c..5855dc6990f6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -24,7 +24,7 @@ import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; * Types */ -interface Breadcrumb { +export interface Breadcrumb { text: string; path?: string; // Used to navigate outside of the React Router basename, @@ -64,16 +64,20 @@ export const useGenerateBreadcrumbs = (trail: BreadcrumbTrail): Breadcrumbs => { /** * Convert IBreadcrumb objects to React-Router-friendly EUI breadcrumb objects * https://elastic.github.io/eui/#/navigation/breadcrumbs + * + * NOTE: Per EUI best practices, we remove the link behavior and + * generate an inactive breadcrumb for the last breadcrumb in the list. */ export const useEuiBreadcrumbs = (breadcrumbs: Breadcrumbs): EuiBreadcrumb[] => { const { navigateToUrl, history } = useValues(KibanaLogic); const { http } = useValues(HttpLogic); - return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => { + return breadcrumbs.map(({ text, path, shouldNotCreateHref }, i) => { const breadcrumb: EuiBreadcrumb = { text }; + const isLastBreadcrumb = i === breadcrumbs.length - 1; - if (path) { + if (path && !isLastBreadcrumb) { breadcrumb.href = createHref(path, { history, http }, { shouldNotCreateHref }); breadcrumb.onClick = (event) => { if (letBrowserHandleEvent(event)) return; From 8d2d2ad864fb4761725f118e67f050b73d7df454 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 7 Apr 2021 16:51:05 -0500 Subject: [PATCH 61/65] Replace `EuiPanel` with `EuiCard` when using beta badges (#96147) In elastic/eui#4649 the `betaBadgeLabel` and related props have been removed from `EuiPanel` and it's now recommended to use an `EuiCard` instead. Replace these in APM and Observability plugins and update stories so examples can be viewed. --- .../components/app/ServiceMap/index.tsx | 2 +- .../LinkPreview.tsx | 133 ---------------- .../CreateEditCustomLinkFlyout/index.tsx | 2 +- .../link_preview.stories.tsx | 39 +++++ .../link_preview.test.tsx | 2 +- .../link_preview.tsx | 147 ++++++++++++++++++ .../Settings/CustomizeUI/CustomLink/index.tsx | 2 +- .../app/Settings/anomaly_detection/index.tsx | 2 +- .../components/app/correlations/index.tsx | 2 +- .../components/shared/LicensePrompt/index.tsx | 63 -------- .../shared/license_prompt/index.tsx | 59 +++++++ .../license_prompt.stories.tsx} | 20 ++- .../components/app/fleet_panel/index.tsx | 59 +++---- .../public/pages/landing/landing.stories.tsx | 41 +++++ 14 files changed, 327 insertions(+), 246 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx delete mode 100644 x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx rename x-pack/plugins/apm/public/components/shared/{LicensePrompt/LicensePrompt.stories.tsx => license_prompt/license_prompt.stories.tsx} (61%) create mode 100644 x-pack/plugins/observability/public/pages/landing/landing.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 7ef3cbca3ad2f..b338d1e4ab03d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -19,7 +19,7 @@ import { useLicenseContext } from '../../../context/license/use_license_context' import { useTheme } from '../../../hooks/use_theme'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { DatePicker } from '../../shared/DatePicker'; -import { LicensePrompt } from '../../shared/LicensePrompt'; +import { LicensePrompt } from '../../shared/license_prompt'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscape_options'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx deleted file mode 100644 index 0312b802df173..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx +++ /dev/null @@ -1,133 +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, { useEffect, useState } from 'react'; -import { - EuiPanel, - EuiText, - EuiSpacer, - EuiLink, - EuiToolTip, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; -import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; -import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; -import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; - -interface Props { - label: string; - url: string; - filters: Filter[]; -} - -const fetchTransaction = debounce( - async (filters: Filter[], callback: (transaction: Transaction) => void) => { - const transaction = await callApmApi({ - signal: null, - endpoint: 'GET /api/apm/settings/custom_links/transaction', - params: { query: convertFiltersToQuery(filters) }, - }); - callback(transaction); - }, - 1000 -); - -const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); - -export function LinkPreview({ label, url, filters }: Props) { - const [transaction, setTransaction] = useState(); - - useEffect(() => { - /* - React throwns "Can't perform a React state update on an unmounted component" - It happens when the Custom Link flyout is closed before the return of the api request. - To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction. - */ - let isUnmounted = false; - fetchTransaction(filters, (_transaction: Transaction) => { - if (!isUnmounted) { - setTransaction(_transaction); - } - }); - return () => { - isUnmounted = true; - }; - }, [filters]); - - const { formattedUrl, error } = replaceTemplateVariables(url, transaction); - - return ( - - - {label - ? label - : i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.default.label', - { defaultMessage: 'Elastic.co' } - )} - - - - {url ? ( - - {formattedUrl} - - ) : ( - i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.default.url', - { defaultMessage: 'https://www.elastic.co' } - ) - )} - - - - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition', - { - defaultMessage: - 'Test your link with values from an example transaction document based on the filters above.', - } - )} - - - - - {error && ( - - - - )} - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index ccd2b0d425743..dfe768735d19b 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -22,7 +22,7 @@ import { FiltersSection } from './FiltersSection'; import { FlyoutFooter } from './FlyoutFooter'; import { LinkSection } from './LinkSection'; import { saveCustomLink } from './saveCustomLink'; -import { LinkPreview } from './LinkPreview'; +import { LinkPreview } from './link_preview'; import { Documentation } from './Documentation'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx new file mode 100644 index 0000000000000..3bf17a733bf8a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.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, { ComponentProps } from 'react'; +import { CoreStart } from 'kibana/public'; +import { createCallApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { LinkPreview } from './link_preview'; + +export default { + title: + 'app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview', + component: LinkPreview, +}; + +export function Example({ + filters, + label, + url, +}: ComponentProps) { + const coreMock = ({ + http: { + get: async () => ({ transaction: { id: '0' } }), + }, + uiSettings: { get: () => false }, + } as unknown) as CoreStart; + + createCallApmApi(coreMock); + + return ; +} +Example.args = { + filters: [], + label: 'Example label', + url: 'https://example.com', +} as ComponentProps; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx index 6348157104287..6a6db40892e10 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview'; +import { LinkPreview } from '../CreateEditCustomLinkFlyout/link_preview'; import { render, getNodeText, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx new file mode 100644 index 0000000000000..726d4ba0d65ee --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx @@ -0,0 +1,147 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { + EuiPanel, + EuiText, + EuiSpacer, + EuiLink, + EuiToolTip, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; +import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; + +export interface LinkPreviewProps { + label: string; + url: string; + filters: Filter[]; +} + +const fetchTransaction = debounce( + async (filters: Filter[], callback: (transaction: Transaction) => void) => { + const transaction = await callApmApi({ + signal: null, + endpoint: 'GET /api/apm/settings/custom_links/transaction', + params: { query: convertFiltersToQuery(filters) }, + }); + callback(transaction); + }, + 1000 +); + +const getTextColor = (value?: string) => (value ? 'default' : 'subdued'); + +export function LinkPreview({ label, url, filters }: LinkPreviewProps) { + const [transaction, setTransaction] = useState(); + + useEffect(() => { + /* + React throwns "Can't perform a React state update on an unmounted component" + It happens when the Custom Link flyout is closed before the return of the api request. + To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction. + */ + let isUnmounted = false; + fetchTransaction(filters, (_transaction: Transaction) => { + if (!isUnmounted) { + setTransaction(_transaction); + } + }); + return () => { + isUnmounted = true; + }; + }, [filters]); + + const { formattedUrl, error } = replaceTemplateVariables(url, transaction); + + return ( + <> + +

    + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.previewSectionTitle', + { + defaultMessage: 'Preview', + } + )} +

    +
    + + + + {label + ? label + : i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.default.label', + { defaultMessage: 'Elastic.co' } + )} + + + + {url ? ( + + {formattedUrl} + + ) : ( + i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.default.url', + { defaultMessage: 'https://www.elastic.co' } + ) + )} + + + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition', + { + defaultMessage: + 'Test your link with values from an example transaction document based on the filters above.', + } + )} + + + + + {error && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 49fa3eab47862..ab18a31e76917 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -20,7 +20,7 @@ import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; import { useLicenseContext } from '../../../../../context/license/use_license_context'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; +import { LicensePrompt } from '../../../../shared/license_prompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 72f0249f07bf6..62b39664cf63d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -14,7 +14,7 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { LicensePrompt } from '../../../shared/LicensePrompt'; +import { LicensePrompt } from '../../../shared/license_prompt'; import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index e0651edbeb79b..62c547aa69e0d 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -34,7 +34,7 @@ import { } from '../../../../../observability/public'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useLicenseContext } from '../../../context/license/use_license_context'; -import { LicensePrompt } from '../../shared/LicensePrompt'; +import { LicensePrompt } from '../../shared/license_prompt'; import { IUrlParams } from '../../../context/url_params_context/types'; const latencyTab = { diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx deleted file mode 100644 index 97a48a61e47cc..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx +++ /dev/null @@ -1,63 +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 { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; - -interface Props { - text: string; - showBetaBadge?: boolean; -} - -export function LicensePrompt({ text, showBetaBadge = false }: Props) { - const licensePageUrl = useKibanaUrl( - '/app/management/stack/license_management' - ); - - const renderLicenseBody = ( - - {i18n.translate('xpack.apm.license.title', { - defaultMessage: 'Start free 30-day trial', - })} -

    - } - body={

    {text}

    } - actions={ - - {i18n.translate('xpack.apm.license.button', { - defaultMessage: 'Start trial', - })} - - } - /> - ); - - const renderWithBetaBadge = ( - - {renderLicenseBody} - - ); - - return <>{showBetaBadge ? renderWithBetaBadge : renderLicenseBody}; -} diff --git a/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx new file mode 100644 index 0000000000000..0950cff5127fc --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx @@ -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 { EuiButton, EuiCard, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; + +export interface LicensePromptProps { + text: string; + showBetaBadge?: boolean; +} + +export function LicensePrompt({ + text, + showBetaBadge = false, +}: LicensePromptProps) { + const licensePageUrl = useKibanaUrl( + '/app/management/stack/license_management' + ); + + return ( + {text}} + footer={ + + {i18n.translate('xpack.apm.license.button', { + defaultMessage: 'Start trial', + })} + + } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx rename to x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx index 57f782a020082..35e22b50306d9 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ComponentType } from 'react'; +import React, { ComponentProps, ComponentType } from 'react'; import { LicensePrompt } from '.'; import { ApmPluginContext, @@ -17,19 +17,25 @@ const contextMock = ({ } as unknown) as ApmPluginContextValue; export default { - title: 'app/LicensePrompt', + title: 'shared/LicensePrompt', component: LicensePrompt, decorators: [ (Story: ComponentType) => ( - {' '} + ), ], }; -export function Example() { - return ( - - ); +export function Example({ + showBetaBadge, + text, +}: ComponentProps) { + return ; } +Example.args = { + showBetaBadge: false, + text: + 'To create Feature name, you must be subscribed to an Elastic X license or above.', +} as ComponentProps; diff --git a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx index b1ca3c614fc70..fce1cde38f587 100644 --- a/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx @@ -5,53 +5,38 @@ * 2.0. */ -import React from 'react'; -import { EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiTitle } from '@elastic/eui'; +import { EuiCard, EuiLink, EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiText } from '@elastic/eui'; -import { EuiLink } from '@elastic/eui'; +import React from 'react'; import { usePluginContext } from '../../../hooks/use_plugin_context'; export function FleetPanel() { const { core } = usePluginContext(); return ( - - - - -

    - {i18n.translate('xpack.observability.fleet.title', { - defaultMessage: 'Have you seen our new Fleet?', - })} -

    -
    -
    - - - {i18n.translate('xpack.observability.fleet.text', { - defaultMessage: - 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', - })} - - - - - {i18n.translate('xpack.observability.fleet.button', { - defaultMessage: 'Try Fleet Beta', - })} - - -
    -
    + description={ + + {i18n.translate('xpack.observability.fleet.text', { + defaultMessage: + 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', + })} + + } + footer={ + + {i18n.translate('xpack.observability.fleet.button', { + defaultMessage: 'Try Fleet Beta', + })} + + } + title={i18n.translate('xpack.observability.fleet.title', { + defaultMessage: 'Have you seen our new Fleet?', + })} + /> ); } diff --git a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx new file mode 100644 index 0000000000000..86922b045c742 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx @@ -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 React, { ComponentType } from 'react'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; +import { PluginContext, PluginContextValue } from '../../context/plugin_context'; +import { LandingPage } from './'; + +export default { + title: 'app/Landing', + component: LandingPage, + decorators: [ + (Story: ComponentType) => { + const pluginContextValue = ({ + appMountParameters: { setHeaderActionMenu: () => {} }, + core: { + http: { + basePath: { + prepend: () => '', + }, + }, + }, + } as unknown) as PluginContextValue; + return ( + + + + + + ); + }, + ], +}; + +export function Example() { + return ; +} From c2d5fa1dda95d6689c064f48bfa6bbab1605f494 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 7 Apr 2021 15:06:44 -0700 Subject: [PATCH 62/65] [Actions] Added action configuration settings `maxResponseContentLength` and `responseTimeout`. (#96355) * [Actions] Added action configuration settings `maxResponseContentLength` and `responseTimeout` which define max response content size (in bytes) and awaiting timeout for action executions based on axios requests. * replaced pasceDuration with moment * fixed due to comments * renamed internal options --- docs/settings/alert-action-settings.asciidoc | 7 +++++ .../resources/base/bin/kibana-docker | 2 ++ .../actions/server/actions_client.test.ts | 4 +++ .../actions/server/actions_config.mock.ts | 4 +++ .../actions/server/actions_config.test.ts | 16 ++++++++++ .../plugins/actions/server/actions_config.ts | 11 ++++++- .../server/builtin_action_types/email.test.ts | 2 ++ .../lib/axios_utils.test.ts | 19 ++++++++++++ .../builtin_action_types/lib/axios_utils.ts | 3 ++ .../server/builtin_action_types/teams.test.ts | 2 ++ .../builtin_action_types/webhook.test.ts | 29 +++++++++++++++++++ .../server/builtin_action_types/webhook.ts | 5 +++- x-pack/plugins/actions/server/config.test.ts | 8 +++++ x-pack/plugins/actions/server/config.ts | 2 ++ x-pack/plugins/actions/server/plugin.test.ts | 6 ++++ x-pack/plugins/actions/server/types.ts | 5 ++++ 16 files changed, 123 insertions(+), 2 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 08cbee8851b98..20bbbcf874c05 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -77,6 +77,13 @@ a|`xpack.actions.` + As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates. +| `xpack.actions.maxResponseContentLength` {ess-icon} + | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB). + +| `xpack.actions.responseTimeout` {ess-icon} + | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as [ms|s|m|h|d|w|M|Y], for example, '20m', '24h', '7d', '1w'. Defaults to 60s. + + |=== [float] diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index e0fd649a43df7..6cc94208fbcce 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -166,6 +166,8 @@ kibana_vars=( xpack.actions.proxyBypassHosts xpack.actions.proxyOnlyHosts xpack.actions.rejectUnauthorized + xpack.actions.maxResponseContentLength + xpack.actions.responseTimeout xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 92d3b4f29d967..6544a3c426e42 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -6,6 +6,8 @@ */ import { schema } from '@kbn/config-schema'; +import moment from 'moment'; +import { ByteSizeValue } from '@kbn/config-schema'; import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionsClient } from './actions_client'; @@ -408,6 +410,8 @@ describe('create()', () => { rejectUnauthorized: true, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration('60s'), }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index 012cd63be2702..76f6a62ce6597 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -17,6 +17,10 @@ const createActionsConfigMock = () => { ensureActionTypeEnabled: jest.fn().mockReturnValue({}), isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true), getProxySettings: jest.fn().mockReturnValue(undefined), + getResponseSettings: jest.fn().mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 36899f7661ba4..c81f1f4a4bf2e 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { ActionsConfig } from './config'; import { getActionsConfigurationUtilities, AllowedHosts, EnabledActionTypes, } from './actions_config'; +import moment from 'moment'; const defaultActionsConfig: ActionsConfig = { enabled: false, @@ -19,6 +21,8 @@ const defaultActionsConfig: ActionsConfig = { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }; describe('ensureUriAllowed', () => { @@ -254,6 +258,18 @@ describe('ensureActionTypeEnabled', () => { }); }); +describe('getResponseSettingsFromConfig', () => { + test('returns expected parsed values for default config for responseTimeout and maxResponseContentLength', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + }; + expect(getActionsConfigurationUtilities(config).getResponseSettings()).toEqual({ + timeout: 60000, + maxContentLength: 1000000, + }); + }); +}); + describe('getProxySettings', () => { test('returns undefined when no proxy URL set', () => { const config: ActionsConfig = { diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index b35a4a0d7b6c5..4c73cab76f9e8 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -13,7 +13,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config'; import { ActionTypeDisabledError } from './lib'; -import { ProxySettings } from './types'; +import { ProxySettings, ResponseSettings } from './types'; export { AllowedHosts, EnabledActionTypes } from './config'; @@ -31,6 +31,7 @@ export interface ActionsConfigurationUtilities { ensureActionTypeEnabled: (actionType: string) => void; isRejectUnauthorizedCertificatesEnabled: () => boolean; getProxySettings: () => undefined | ProxySettings; + getResponseSettings: () => ResponseSettings; } function allowListErrorMessage(field: AllowListingField, value: string) { @@ -99,6 +100,13 @@ function arrayAsSet(arr: T[] | undefined): Set | undefined { return new Set(arr); } +function getResponseSettingsFromConfig(config: ActionsConfig): ResponseSettings { + return { + maxContentLength: config.maxResponseContentLength.getValueInBytes(), + timeout: config.responseTimeout.asMilliseconds(), + }; +} + export function getActionsConfigurationUtilities( config: ActionsConfig ): ActionsConfigurationUtilities { @@ -110,6 +118,7 @@ export function getActionsConfigurationUtilities( isUriAllowed, isActionTypeEnabled, getProxySettings: () => getProxySettingsFromConfig(config), + getResponseSettings: () => getResponseSettingsFromConfig(config), isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized, ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index b858d5491a6bd..4596619c50940 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -283,6 +283,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -342,6 +343,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index a932b38ede2bb..edc9429e4fac6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -42,6 +42,10 @@ describe('request', () => { headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, })); + configurationUtilities.getResponseSettings.mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }); }); test('it fetch correctly with defaults', async () => { @@ -58,6 +62,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -88,6 +94,8 @@ describe('request', () => { httpAgent, httpsAgent, proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -116,6 +124,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -224,6 +234,8 @@ describe('request', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); expect(res).toEqual({ status: 200, @@ -235,10 +247,15 @@ describe('request', () => { describe('patch', () => { beforeEach(() => { + jest.resetAllMocks(); axiosMock.mockImplementation(() => ({ status: 200, headers: { 'content-type': 'application/json' }, })); + configurationUtilities.getResponseSettings.mockReturnValue({ + maxContentLength: 1000000, + timeout: 360000, + }); }); test('it fetch correctly', async () => { @@ -249,6 +266,8 @@ describe('patch', () => { httpAgent: undefined, httpsAgent: expect.any(HttpsAgent), proxy: false, + maxContentLength: 1000000, + timeout: 360000, }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index edce369096142..af353e1d1da5a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -31,6 +31,7 @@ export const request = async ({ auth?: AxiosBasicCredentials; }): Promise => { const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url); + const { maxContentLength, timeout } = configurationUtilities.getResponseSettings(); return await axios(url, { ...rest, @@ -40,6 +41,8 @@ export const request = async ({ httpAgent, httpsAgent, proxy: false, + maxContentLength, + timeout, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index c31adddc5a57e..8a185d353de02 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -168,6 +168,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -230,6 +231,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index c468453247809..d3f059eede615 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -291,6 +291,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], @@ -329,6 +330,33 @@ describe('execute()', () => { `); }); + test('execute with exception maxContentLength size exceeded should log the proper error', async () => { + const config: ActionTypeConfigType = { + url: 'https://abc.def/my-webhook', + method: WebhookMethods.POST, + headers: { + aheader: 'a value', + }, + hasAuth: true, + }; + requestMock.mockReset(); + requestMock.mockRejectedValueOnce({ + tag: 'err', + isAxiosError: true, + message: 'maxContentLength size of 1000000 exceeded', + }); + await actionType.executor({ + actionId: 'some-id', + services, + config, + secrets: { user: 'abc', password: '123' }, + params: { body: 'some data' }, + }); + expect(mockedLogger.error).toBeCalledWith( + 'error on some-id webhook event: maxContentLength size of 1000000 exceeded' + ); + }); + test('execute without username/password sends request without basic auth', async () => { const config: ActionTypeConfigType = { url: 'https://abc.def/my-webhook', @@ -355,6 +383,7 @@ describe('execute()', () => { "ensureHostnameAllowed": [MockFunction], "ensureUriAllowed": [MockFunction], "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isRejectUnauthorizedCertificatesEnabled": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 269449686acf0..93c9bbdbab18a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -180,7 +180,6 @@ export async function executor( return successResult(actionId, data); } else { const { error } = result; - if (error.response) { const { status, @@ -211,6 +210,10 @@ export async function executor( const message = `[${error.code}] ${error.message}`; logger.error(`error on ${actionId} webhook event: ${message}`); return errorResultRequestFailed(actionId, message); + } else if (error.isAxiosError) { + const message = `${error.message}`; + logger.error(`error on ${actionId} webhook event: ${message}`); + return errorResultRequestFailed(actionId, message); } logger.error(`error on ${actionId} webhook action: unexpected error`); diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 0d270512d1dee..2eecaa19da0c5 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -27,9 +27,13 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, "preconfigured": Object {}, "proxyRejectUnauthorizedCertificates": true, "rejectUnauthorized": true, + "responseTimeout": "PT1M", } `); }); @@ -57,6 +61,9 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, "preconfigured": Object { "mySlack1": Object { "actionTypeId": ".slack", @@ -69,6 +76,7 @@ describe('config validation', () => { }, "proxyRejectUnauthorizedCertificates": false, "rejectUnauthorized": false, + "responseTimeout": "PT1M", } `); }); diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 450f03308ab0b..4aa77ded315b8 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -47,6 +47,8 @@ export const configSchema = schema.object({ proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), rejectUnauthorized: schema.boolean({ defaultValue: true }), + maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }), + responseTimeout: schema.duration({ defaultValue: '60s' }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index b8f83e91239e2..30bbedbedbe9c 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment'; +import { ByteSizeValue } from '@kbn/config-schema'; import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; @@ -37,6 +39,8 @@ describe('Actions Plugin', () => { preconfigured: {}, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -197,6 +201,8 @@ describe('Actions Plugin', () => { }, proxyRejectUnauthorizedCertificates: true, rejectUnauthorized: true, + maxResponseContentLength: new ByteSizeValue(1000000), + responseTimeout: moment.duration(60000), }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 6830f013ade5f..b7a6750a520ea 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -138,3 +138,8 @@ export interface ProxySettings { proxyHeaders?: Record; proxyRejectUnauthorizedCertificates: boolean; } + +export interface ResponseSettings { + maxContentLength: number; + timeout: number; +} From fc9f97e03bf4ad32493565e0a49bbb564fd057e5 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 7 Apr 2021 16:04:13 -0700 Subject: [PATCH 63/65] skip suites failing es promotion (#96515) (cherry picked from commit 7fdf7e1d7913c3f5ab5af1388d02a8a880702999) --- x-pack/test/fleet_api_integration/apis/epm/list.ts | 3 ++- .../security_solution_endpoint_api_int/apis/artifacts/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/epm/list.ts b/x-pack/test/fleet_api_integration/apis/epm/list.ts index 5a991e52bdba4..0a7002764a54c 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/list.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/list.ts @@ -19,7 +19,8 @@ export default function (providerContext: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions - describe('EPM - list', async function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + describe.skip('EPM - list', async function () { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.load('fleet/empty_fleet_server'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index e1edeb7808697..8ee028ae3f56b 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -19,7 +19,8 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; - describe('artifact download', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 + describe.skip('artifact download', () => { const esArchiverSnapshots = [ 'endpoint/artifacts/fleet_artifacts', 'endpoint/artifacts/api_feature', From a6b2a8477534b67d840f3569cd08d80ee9738dff Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 7 Apr 2021 18:33:18 -0500 Subject: [PATCH 64/65] fix index pattern field editor console error (#96497) --- .../public/components/field_editor_flyout_content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index e0ca654c956c6..13830f9233b5e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -143,7 +143,7 @@ const FieldEditorFlyoutContentComponent = ({ const [isValidating, setIsValidating] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); - const [confirmContent, setConfirmContent] = useState(); + const [confirmContent, setConfirmContent] = useState(''); const { submit, isValid: isFormValid, isSubmitted } = formState; const { fields } = indexPattern; From d63fbb19cd9c16a4f56bb6499789c77e25cd496c Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 7 Apr 2021 18:57:24 -0600 Subject: [PATCH 65/65] [Security Solution][Detections]Fixes Rule Management Cypress Tests (#96505) ## Summary Fixes two cypress tests: > Deleting prebuilt rules "before each" hook for "Does not allow to delete one rule when more than one is selected" https://github.com/elastic/kibana/issues/68607 This one is more of a drive around the pot-hole fix as we were waiting for the Alerts Table to load when we really didn't need to. Removed unnecessary check.

    > Alerts rules, prebuilt rules Loads prebuilt rules https://github.com/elastic/kibana/issues/71300 This one was fixed with a `.pipe()` and `.should('not.be.visible')` to ensure the click was successful. Also removed unnecessary check on the Alerts Table loading that was present here as well too..

    --- .../integration/detection_rules/prebuilt_rules.spec.ts | 8 +------- .../cypress/tasks/alerts_detection_rules.ts | 5 ++++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index d290773d425e2..fb0a01bd1c7d3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -14,11 +14,7 @@ import { SHOWING_RULES_TEXT, } from '../../screens/alerts_detection_rules'; -import { - goToManageAlertsDetectionRules, - waitForAlertsIndexToBeCreated, - waitForAlertsPanelToBeLoaded, -} from '../../tasks/alerts'; +import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { changeRowsPerPageTo300, deleteFirstRule, @@ -47,7 +43,6 @@ describe('Alerts rules, prebuilt rules', () => { const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); @@ -79,7 +74,6 @@ describe('Deleting prebuilt rules', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 10644e046a68b..d66b839267ea0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -191,7 +191,10 @@ export const resetAllRulesIdleModalTimeout = () => { export const changeRowsPerPageTo = (rowsCount: number) => { cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); - cy.get(rowsPerPageSelector(rowsCount)).click(); + cy.get(rowsPerPageSelector(rowsCount)) + .pipe(($el) => $el.trigger('click')) + .should('not.be.visible'); + waitForRulesTableToBeRefreshed(); };