From 4295f8512461816aa6535e36496f40f6364aa572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 22 Apr 2021 14:22:42 +0200 Subject: [PATCH 01/33] [flaky-test] Unskip SOM edit_saved_object tests (#97846) --- .../apps/saved_objects_management/edit_saved_object.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 81569c5bfc498a..89889088bd73ba 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -55,8 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.click(); }; - // Flaky: https://github.com/elastic/kibana/issues/68400 - describe.skip('saved objects edition page', () => { + describe('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); From 649a2e01fc723f549b869607a0993af39a91174d Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 22 Apr 2021 08:01:45 -0500 Subject: [PATCH 02/33] [Workplace Search] PR#3358 to Kibana (#97921) * Render flash message when server sends back an error * Fetch data on all route changes This produced a bug where the nav and loading states were triggered between route changes. Added conditional to prevent resetting between in-source changes --- .../content_sources/source_logic.test.ts | 14 +++++++++++ .../views/content_sources/source_logic.ts | 6 +++++ .../content_sources/source_router.test.tsx | 24 ++++++++++++++++++- .../views/content_sources/source_router.tsx | 11 ++++++--- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index a9712cc4e1dc09..2cf867446b7fb2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -33,6 +33,7 @@ describe('SourceLogic', () => { flashAPIErrors, setSuccessMessage, setQueuedSuccessMessage, + setErrorMessage, } = mockFlashMessageHelpers; const { navigateToUrl } = mockKibanaValues; const { mount, getListeners } = new LogicMounter(SourceLogic); @@ -204,6 +205,19 @@ describe('SourceLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith(NOT_FOUND_PATH); }); + + it('renders error messages passed in success response from server', async () => { + const errors = ['ERROR']; + const promise = Promise.resolve({ + ...contentSource, + errors, + }); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await promise; + + expect(setErrorMessage).toHaveBeenCalledWith(errors); + }); }); describe('initializeFederatedSummary', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index ff3e1e83925d03..2e6a3c65597eac 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -13,6 +13,7 @@ import { DEFAULT_META } from '../../../shared/constants'; import { flashAPIErrors, setSuccessMessage, + setErrorMessage, setQueuedSuccessMessage, clearFlashMessages, } from '../../../shared/flash_messages'; @@ -148,6 +149,11 @@ export const SourceLogic = kea>({ if (response.isFederatedSource) { actions.initializeFederatedSummary(sourceId); } + if (response.errors) { + setErrorMessage(response.errors); + } else { + clearFlashMessages(); + } } catch (e) { if (e.response.status === 404) { KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 463468d1304b6e..528065da23af60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -8,6 +8,8 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; +import { mockLocation } from '../../../__mocks__/react_router_history.mock'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; @@ -30,6 +32,7 @@ import { SourceRouter } from './source_router'; describe('SourceRouter', () => { const initializeSource = jest.fn(); + const resetSourceState = jest.fn(); const contentSource = contentSources[1]; const customSource = contentSources[0]; const mockValues = { @@ -40,10 +43,11 @@ describe('SourceRouter', () => { beforeEach(() => { setMockActions({ initializeSource, + resetSourceState, }); setMockValues({ ...mockValues }); (useParams as jest.Mock).mockImplementationOnce(() => ({ - sourceId: '1', + sourceId: contentSource.id, })); }); @@ -114,4 +118,22 @@ describe('SourceRouter', () => { NAV.DISPLAY_SETTINGS, ]); }); + + describe('reset state', () => { + it('does not reset state when switching between source tree views', () => { + mockLocation.pathname = `/sources/${contentSource.id}`; + shallow(); + unmountHandler(); + + expect(resetSourceState).not.toHaveBeenCalled(); + }); + + it('resets state when leaving source tree', () => { + mockLocation.pathname = '/home'; + shallow(); + unmountHandler(); + + expect(resetSourceState).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index b14ea4ebd7a736..cd20e32def16df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -6,7 +6,8 @@ */ import React, { useEffect } from 'react'; -import { Route, Switch, useParams } from 'react-router-dom'; + +import { Route, Switch, useLocation, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import moment from 'moment'; @@ -47,14 +48,18 @@ import { SourceLogic } from './source_logic'; export const SourceRouter: React.FC = () => { const { sourceId } = useParams() as { sourceId: string }; + const { pathname } = useLocation(); const { initializeSource, resetSourceState } = useActions(SourceLogic); const { contentSource, dataLoading } = useValues(SourceLogic); const { isOrganization } = useValues(AppLogic); useEffect(() => { initializeSource(sourceId); - return resetSourceState; - }, []); + return () => { + // We only want to reset the state when leaving the source section. Otherwise there is an unwanted flash of UI. + if (!pathname.includes(sourceId)) resetSourceState(); + }; + }, [pathname]); if (dataLoading) return ; From ff0276b6a21eee7fcb714f3a593eabd57786ac56 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 22 Apr 2021 15:14:17 +0200 Subject: [PATCH 03/33] Block value `0` for terms.min_doc_count aggregation (#97966) * Block value `0` for terms.min_doc_count aggregation * do the same for histogram --- .../aggregations/aggs_types/bucket_aggs.ts | 4 +- .../aggregations/aggs_types/schemas.test.ts | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.ts diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index 1508cab69a0486..599c32137c553b 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -49,7 +49,7 @@ export const bucketAggsSchemas: Record = { histogram: s.object({ field: s.maybe(s.string()), interval: s.maybe(s.number()), - min_doc_count: s.maybe(s.number()), + min_doc_count: s.maybe(s.number({ min: 1 })), extended_bounds: s.maybe( s.object({ min: s.number(), @@ -78,7 +78,7 @@ export const bucketAggsSchemas: Record = { include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), execution_hint: s.maybe(s.string()), missing: s.maybe(s.number()), - min_doc_count: s.maybe(s.number()), + min_doc_count: s.maybe(s.number({ min: 1 })), size: s.maybe(s.number()), show_term_doc_count_error: s.maybe(s.boolean()), order: s.maybe(s.oneOf([s.literal('asc'), s.literal('desc')])), diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.ts new file mode 100644 index 00000000000000..33f7ca12abc53b --- /dev/null +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/schemas.test.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 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 { bucketAggsSchemas } from './bucket_aggs'; + +describe('bucket aggregation schemas', () => { + describe('terms aggregation schema', () => { + const schema = bucketAggsSchemas.terms; + + it('passes validation when using `1` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 1 })).not.toThrow(); + }); + + // see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_minimum_document_count_4 + // Setting min_doc_count=0 will also return buckets for terms that didn’t match any hit, + // bypassing any filtering perform via `filter` or `query` + // causing a potential security issue as we can return values from other spaces. + it('throws an error when using `0` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 0 })).toThrowErrorMatchingInlineSnapshot( + `"[min_doc_count]: Value must be equal to or greater than [1]."` + ); + }); + }); + + describe('histogram aggregation schema', () => { + const schema = bucketAggsSchemas.histogram; + + it('passes validation when using `1` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 1 })).not.toThrow(); + }); + + it('throws an error when using `0` for `min_doc_count`', () => { + expect(() => schema.validate({ min_doc_count: 0 })).toThrowErrorMatchingInlineSnapshot( + `"[min_doc_count]: Value must be equal to or greater than [1]."` + ); + }); + }); +}); From 158fff3297aafda63f193a2d50ee7d6dc2940b26 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 22 Apr 2021 07:51:30 -0600 Subject: [PATCH 04/33] [Maps] fix cannot read propery 'getImage' of undefined (#97829) --- .../connected_components/mb_map/mb_map.tsx | 29 +++++++++++++------ .../connected_components/mb_map/utils.js | 5 ---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 66c9a2462736af..9ec6cbcb5d4ac0 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -26,7 +26,7 @@ import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { MapSettings } from '../../reducers/map'; -import { Goto } from '../../../common/descriptor_types'; +import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, KBN_TOO_MANY_FEATURES_IMAGE_ID, @@ -35,8 +35,12 @@ import { } from '../../../common/constants'; import { getGlyphUrl, isRetina } from '../../util'; import { syncLayerOrder } from './sort_layers'; -// @ts-expect-error -import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils'; +import { + addSpriteSheetToMapFromImageData, + loadSpriteSheetImageData, + removeOrphanedSourcesAndLayers, + // @ts-expect-error +} from './utils'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; @@ -172,8 +176,7 @@ export class MBMap extends Component { }; } - async _createMbMapInstance(): Promise { - const initialView = await getInitialView(this.props.goto, this.props.settings); + async _createMbMapInstance(initialView: MapCenterAndZoom | null): Promise { return new Promise((resolve) => { const mbStyle = { version: 8, @@ -237,9 +240,14 @@ export class MBMap extends Component { } async _initializeMap() { + const initialView = await getInitialView(this.props.goto, this.props.settings); + if (!this._isMounted) { + return; + } + let mbMap: MapboxMap; try { - mbMap = await this._createMbMapInstance(); + mbMap = await this._createMbMapInstance(initialView); } catch (error) { this.props.setMapInitError(error.message); return; @@ -293,10 +301,13 @@ export class MBMap extends Component { }); } - _loadMakiSprites(mbMap: MapboxMap) { - const sprites = isRetina() ? sprites2 : sprites1; + async _loadMakiSprites(mbMap: MapboxMap) { + const spritesUrl = isRetina() ? sprites2 : sprites1; const json = isRetina() ? spritesheet[2] : spritesheet[1]; - addSpritesheetToMap(json, sprites, mbMap); + const spritesData = await loadSpriteSheetImageData(spritesUrl); + if (this._isMounted) { + addSpriteSheetToMapFromImageData(json, spritesData, mbMap); + } } _syncMbMapWithMapState = () => { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/utils.js b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js index f79f9bdffe366d..5a2a98a24fca15 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/utils.js +++ b/x-pack/plugins/maps/public/connected_components/mb_map/utils.js @@ -51,11 +51,6 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLa mbSourcesToRemove.forEach((mbSourceId) => mbMap.removeSource(mbSourceId)); } -export async function addSpritesheetToMap(json, imgUrl, mbMap) { - const imgData = await loadSpriteSheetImageData(imgUrl); - addSpriteSheetToMapFromImageData(json, imgData, mbMap); -} - function getImageData(img) { const canvas = window.document.createElement('canvas'); const context = canvas.getContext('2d'); From fecdd45d93dc105264017bbe04dbf1f96102bcfb Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 22 Apr 2021 08:59:24 -0500 Subject: [PATCH 05/33] [ML] Fix Data Visualizer event rate chart empty for some indices when using long time range (#97655) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../content_types/document_count_content.tsx | 1 + .../document_count_chart.tsx | 22 ++++++++++++++++--- .../stats_table/types/field_vis_config.ts | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx index 1fcc301fbdba72..588d85f24a0237 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/content_types/document_count_content.tsx @@ -41,6 +41,7 @@ export const DocumentCountContent: FC = ({ config, totalCount }) => { chartPoints={chartPoints} timeRangeEarliest={timeRangeEarliest} timeRangeLatest={timeRangeLatest} + interval={documentCounts.interval} /> ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx index 4d0f4323753303..4c8740cc76b6fc 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/document_count_chart/document_count_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; @@ -29,6 +29,7 @@ interface Props { chartPoints: DocumentCountChartPoint[]; timeRangeEarliest: number; timeRangeLatest: number; + interval?: number; } const SPEC_ID = 'document_count'; @@ -38,6 +39,7 @@ export const DocumentCountChart: FC = ({ chartPoints, timeRangeEarliest, timeRangeLatest, + interval, }) => { const seriesName = i18n.translate('xpack.ml.fieldDataCard.documentCountChart.seriesLabel', { defaultMessage: 'document count', @@ -50,6 +52,21 @@ export const DocumentCountChart: FC = ({ const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); + const adjustedChartPoints = useMemo(() => { + // Display empty chart when no data in range + if (chartPoints.length < 1) return [{ time: timeRangeEarliest, value: 0 }]; + + // If chart has only one bucket + // it won't show up correctly unless we add an extra data point + if (chartPoints.length === 1) { + return [ + ...chartPoints, + { time: interval ? Number(chartPoints[0].time) + interval : timeRangeEarliest, value: 0 }, + ]; + } + return chartPoints; + }, [chartPoints, timeRangeEarliest, timeRangeLatest, interval]); + return (
= ({ yScaleType={ScaleType.Linear} xAccessor="time" yAccessors={['value']} - // Display empty chart when no data in range - data={chartPoints.length > 0 ? chartPoints : [{ time: timeRangeEarliest, value: 0 }]} + data={adjustedChartPoints} />
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts index 0bf3b951f42461..aa7bd2f5ecf6db 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/types/field_vis_config.ts @@ -39,6 +39,7 @@ export interface FieldVisStats { latest?: number; documentCounts?: { buckets?: DocumentCountBuckets; + interval?: number; }; avg?: number; distribution?: { From 98c7d7da0a4806b47a13c156ec788caa4823cd32 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 22 Apr 2021 22:32:02 +0800 Subject: [PATCH 06/33] #95263 Set kbn-href when location changed (#95377) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/public/angular/app_modules.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 8fa0629d013cf7..71dc4919237e5f 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -232,6 +232,11 @@ function createHrefModule(core: CoreStart) { $attr.$set('href', core.http.basePath.prepend(url)); } }); + + _$scope.$on('$locationChangeSuccess', () => { + const url = getSafeForExternalLink($attr.href as string); + $attr.$set('href', core.http.basePath.prepend(url)); + }); }, }, }; From 967b17275b4671de80aa79681284dd52df8162c2 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 22 Apr 2021 10:32:22 -0400 Subject: [PATCH 07/33] ensure progress is updated correctly (#97889) --- .../components/create_step_footer/create_step_footer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx index edbfc11343e38d..3123a43594c937 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx @@ -83,10 +83,12 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => } setCurrentProgress(progressStats); + // Clear if job is completed or stopped (after having started) if ( (progressStats.currentPhase === progressStats.totalPhases && progressStats.progress === 100) || - jobStats.state === DATA_FRAME_TASK_STATE.STOPPED + (jobStats.state === DATA_FRAME_TASK_STATE.STOPPED && + !(progressStats.currentPhase === 1 && progressStats.progress === 0)) ) { clearInterval(interval); // Check job has started. Jobs that fail to start will also have STOPPED state From 65287dffaddf6e6b481ad10c01bf2d3deeac6ee1 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 22 Apr 2021 09:43:53 -0500 Subject: [PATCH 08/33] [Enterprise Search] Fix unstyled UI for Schema Errors (#97776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor main component * Refactor faux view button The original design called for a button-looking component on the accordion header so it looks clickable to reveal the error underneath. Using a button element caused the console to error because the entire header is a button and this is a child. Adding an href fixes the error and allows for styling as a button * Use empty button rather than styles components for view button * Remove extra classNames and fix layout to stretch cells * Add stylesheet Without the width: 100% on .euiIEFlexWrapFix, the header table does not stretch across the screen. Couldn’t find another way around this and happy to take suggestions of a better idea. * Add prop to prevent line break on IDs Existing implementation was causing multi-character IDs to break to a new line. So for 12, its was: 1 2 --- .../schema/schema_errors_accordion.scss | 20 +++++++ .../schema/schema_errors_accordion.test.tsx | 6 +- .../shared/schema/schema_errors_accordion.tsx | 55 +++++++++---------- .../schema/schema_change_errors.tsx | 19 +++---- 4 files changed, 55 insertions(+), 45 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.scss b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.scss new file mode 100644 index 00000000000000..e8e55ad2827c5b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.scss @@ -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. + */ + +.schemaFieldError { + border-top: 1px solid $euiColorLightShade; + + &:last-child { + border-bottom: 1px solid $euiColorLightShade; + } + + // Something about the EuiFlexGroup being inside a button collapses the row of items. + // This wrapper div was injected by EUI and had 'with: auto' on it. + .euiIEFlexWrapFix { + width: 100%; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx index a82f9e9b6113b5..a15d39c4471269 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { EuiAccordion, EuiTableRow } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonEmptyTo } from '../react_router_helpers'; import { SchemaErrorsAccordion } from './schema_errors_accordion'; @@ -40,12 +40,12 @@ describe('SchemaErrorsAccordion', () => { expect(wrapper.find(EuiAccordion)).toHaveLength(1); expect(wrapper.find(EuiTableRow)).toHaveLength(2); - expect(wrapper.find(EuiLinkTo)).toHaveLength(0); + expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(0); }); it('renders document buttons', () => { const wrapper = shallow(); - expect(wrapper.find(EuiLinkTo)).toHaveLength(2); + expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(2); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx index c41781deafb959..09f499e540e932 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiAccordion, + EuiButton, EuiFlexGroup, EuiFlexItem, EuiTable, @@ -19,10 +20,12 @@ import { EuiTableRowCell, } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonEmptyTo } from '../react_router_helpers'; import { TruncatedContent } from '../truncate'; +import './schema_errors_accordion.scss'; + import { ERROR_TABLE_ID_HEADER, ERROR_TABLE_ERROR_HEADER, @@ -60,14 +63,19 @@ export const SchemaErrorsAccordion: React.FC = ({ - - + + + + - {schema[fieldName]} + {schema[fieldName]} - {ERROR_TABLE_REVIEW_CONTROL} + {/* href is needed here because a button cannot be nested in a button or console will error and EuiAccordion uses a button to wrap this. */} + + {ERROR_TABLE_REVIEW_CONTROL} + ); @@ -76,12 +84,12 @@ export const SchemaErrorsAccordion: React.FC = ({ - + {ERROR_TABLE_ID_HEADER} {ERROR_TABLE_ERROR_HEADER} @@ -93,34 +101,21 @@ export const SchemaErrorsAccordion: React.FC = ({ const documentPath = getRoute && itemId ? getRoute(itemId, error.external_id) : ''; const viewButton = showViewButton && ( - - - - {ERROR_TABLE_VIEW_LINK} - - + + {ERROR_TABLE_VIEW_LINK} ); return ( - - -
- -
-
- - {error.error} + + + + {error.error} {showViewButton ? viewButton : } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index 29cb2b7589220b..7f7b26e380c55d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -10,8 +10,6 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiSpacer } from '@elastic/eui'; - import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; @@ -32,16 +30,13 @@ export const SchemaChangeErrors: React.FC = () => { }, []); return ( -
+ <> - -
- -
-
+ + ); }; From 2f679e6df3c349012c34e76708afb13c99ea3419 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 22 Apr 2021 08:57:18 -0600 Subject: [PATCH 09/33] [Security Solutions] Critical bug to add network responses to error toasters (#97945) ## Summary When we updated our codebase to use the newer bsearch/async search, we ended up using error messages which were not the same as we had before which would show the network errors in the full message when the "see full error button" was clicked. This has been bad lately as users and quality assurance people have been posting screen shots that have very little information, blank error messages, and/or stack traces instead of network errors. This makes them think the code has issues when it is a configuration issue or a networking issue that is happening. This PR does the following: * Changes all the bsearch queries to use the use useAppToasts * Modifies the useAppToasts to be able to transform bsearch with the kibana global notification * Cleans up the useAppToasts some * Deprecates the GlobalErrorToaster in favor of the useAppToasts * Fixes and adds a few i18n missing strings found * Removes most of the deprecated error dispatch toasters from detection_engine except for 1 place where it is not a hook. Before screen shot of errors with no buttons and messages that were not pointing to network errors: Screen Shot 2021-04-21 at 4 24 45 PM After screen shot where you have a button and that button will show you the network error: Screen Shot 2021-04-21 at 3 26 12 PM Screen Shot 2021-04-21 at 3 26 21 PM You can manually test this easily by making non ECS indexes to cause errors and then add them as a kibana index and use them in the data sourcer. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../common/components/toasters/index.tsx | 42 +++++++++++ .../common/components/toasters/utils.ts | 16 +++-- .../events/last_event_time/index.test.ts | 5 ++ .../events/last_event_time/index.ts | 12 ++-- .../containers/matrix_histogram/index.ts | 11 +-- .../public/common/containers/source/index.tsx | 21 +++--- .../containers/sourcerer/index.test.tsx | 5 ++ .../common/hooks/eql/use_eql_preview.test.ts | 29 ++++---- .../common/hooks/eql/use_eql_preview.ts | 12 ++-- .../public/common/hooks/translations.ts | 7 ++ .../common/hooks/use_app_toasts.mock.ts | 1 + .../common/hooks/use_app_toasts.test.ts | 38 +++++----- .../public/common/hooks/use_app_toasts.ts | 70 ++++++++++++++----- .../public/common/utils/api/index.ts | 4 +- .../load_empty_prompt.test.tsx | 11 +++ .../alerts/use_privilege_user.test.tsx | 11 +++ .../alerts/use_privilege_user.tsx | 8 +-- .../alerts/use_signal_index.test.tsx | 8 +++ .../alerts/use_signal_index.tsx | 10 +-- .../rules/rules_table/use_rules.test.tsx | 7 ++ .../rules/rules_table/use_rules.tsx | 6 +- .../rules/rules_table/use_rules_table.ts | 6 +- .../rules/use_create_rule.test.tsx | 11 +++ .../rules/use_create_rule.tsx | 8 +-- .../rules/use_pre_packaged_rules.test.tsx | 9 +++ .../rules/use_pre_packaged_rules.tsx | 20 +++--- .../detection_engine/rules/use_rule.test.tsx | 11 +++ .../detection_engine/rules/use_rule.tsx | 8 +-- .../rules/use_rule_status.test.tsx | 6 ++ .../rules/use_rule_status.tsx | 14 ++-- .../detection_engine/rules/use_tags.test.tsx | 11 +++ .../detection_engine/rules/use_tags.tsx | 8 +-- .../rules/use_update_rule.test.tsx | 11 +++ .../rules/use_update_rule.tsx | 8 +-- .../rules/all/exceptions/exceptions_table.tsx | 11 +-- .../rules_table_filters.test.tsx | 11 +++ .../rules/create/index.test.tsx | 10 +++ .../rules/edit/index.test.tsx | 10 +++ .../detection_engine/rules/index.test.tsx | 7 ++ .../containers/authentications/index.tsx | 11 +-- .../hosts/containers/hosts/details/index.tsx | 12 ++-- .../hosts/first_last_seen/index.tsx | 12 ++-- .../public/hosts/containers/hosts/index.tsx | 11 +-- .../kpi_hosts/authentications/index.tsx | 12 ++-- .../containers/kpi_hosts/hosts/index.tsx | 12 ++-- .../containers/kpi_hosts/unique_ips/index.tsx | 12 ++-- .../containers/uncommon_processes/index.tsx | 11 +-- .../network/containers/details/index.tsx | 12 ++-- .../containers/kpi_network/dns/index.tsx | 12 ++-- .../kpi_network/network_events/index.tsx | 12 ++-- .../kpi_network/tls_handshakes/index.tsx | 12 ++-- .../kpi_network/unique_flows/index.tsx | 12 ++-- .../kpi_network/unique_private_ips/index.tsx | 12 ++-- .../network/containers/network_dns/index.tsx | 12 ++-- .../network/containers/network_http/index.tsx | 12 ++-- .../network_top_countries/index.tsx | 12 ++-- .../containers/network_top_n_flow/index.tsx | 12 ++-- .../public/network/containers/tls/index.tsx | 12 ++-- .../public/network/containers/users/index.tsx | 12 ++-- .../containers/overview_host/index.tsx | 12 ++-- .../containers/overview_network/index.tsx | 12 ++-- .../timelines/containers/details/index.tsx | 13 ++-- .../containers/details/translations.ts | 22 ++++++ .../timelines/containers/index.test.tsx | 5 ++ .../public/timelines/containers/index.tsx | 11 +-- .../timelines/containers/kpis/index.tsx | 12 ++-- .../timelines/containers/kpis/translations.ts | 22 ++++++ 67 files changed, 583 insertions(+), 277 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/details/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/kpis/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx index ea17b03082751a..b9dd782d8a6532 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toasters/index.tsx @@ -16,34 +16,58 @@ import * as i18n from './translations'; export * from './utils'; export * from './errors'; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ export interface AppToast extends Toast { errors?: string[]; } +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ interface ToastState { toasts: AppToast[]; } +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ const initialToasterState: ToastState = { toasts: [], }; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ export type ActionToaster = | { type: 'addToaster'; toast: AppToast } | { type: 'deleteToaster'; id: string } | { type: 'toggleWaitToShowNextToast' }; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ export const StateToasterContext = createContext<[ToastState, Dispatch]>([ initialToasterState, () => noop, ]); +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ export const useStateToaster = () => useContext(StateToasterContext); +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ interface ManageGlobalToasterProps { children: React.ReactNode; } +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ export const ManageGlobalToaster = ({ children }: ManageGlobalToasterProps) => { const reducerToaster = (state: ToastState, action: ActionToaster) => { switch (action.type) { @@ -63,16 +87,25 @@ export const ManageGlobalToaster = ({ children }: ManageGlobalToasterProps) => { ); }; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ const GlobalToasterListContainer = styled.div` position: absolute; right: 0; bottom: 0; `; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ interface GlobalToasterProps { toastLifeTimeMs?: number; } +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ export const GlobalToaster = ({ toastLifeTimeMs = 5000 }: GlobalToasterProps) => { const [{ toasts }, dispatch] = useStateToaster(); const [isShowing, setIsShowing] = useState(false); @@ -108,6 +141,9 @@ export const GlobalToaster = ({ toastLifeTimeMs = 5000 }: GlobalToasterProps) => ); }; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ const formatToErrorToastIfNeeded = ( toast: AppToast, toggle: (toast: AppToast) => void @@ -129,8 +165,14 @@ const formatToErrorToastIfNeeded = ( return toast; }; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ const ErrorToastContainer = styled.div` text-align: right; `; +/** + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead + */ ErrorToastContainer.displayName = 'ErrorToastContainer'; diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts index 9ce8ec0cb6fd3c..70e095c88576f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts @@ -15,7 +15,7 @@ import { isAppError } from '../../utils/api'; /** * Displays an error toast for the provided title and message - * + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead * @param errorTitle Title of error to display in toaster and modal * @param errorMessages Message to display in error modal when clicked * @param dispatchToaster provided by useStateToaster() @@ -41,7 +41,7 @@ export const displayErrorToast = ( /** * Displays a warning toast for the provided title and message - * + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead * @param title warning message to display in toaster and modal * @param dispatchToaster provided by useStateToaster() * @param id unique ID if necessary @@ -65,7 +65,7 @@ export const displayWarningToast = ( /** * Displays a success toast for the provided title and message - * + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead * @param title success message to display in toaster and modal * @param dispatchToaster provided by useStateToaster() */ @@ -92,8 +92,16 @@ export type ErrorToToasterArgs = Partial & { }; /** - * Displays an error toast with messages parsed from the error + * Displays an error toast with messages parsed from the error. + * + * This has shortcomings and bugs compared to using the use_app_toasts because it takes naive guesses at the + * underlying data structure and does not display much about the error. This is not compatible with bsearch (async search) + * and sometimes can display to the user blank messages. + * + * The use_app_toasts has more feature rich logic and uses the Kibana toaster system to figure out which type of + * error you have in a more robust way then this function does and supersedes this function. * + * @deprecated Use x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts instead * @param title error message to display in toaster and modal * @param error the error from which messages will be parsed * @param dispatchToaster provided by useStateToaster() diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts index 4f12ec2e5de2d2..21791952fec067 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts @@ -43,6 +43,11 @@ const mockUseKibana = { jest.mock('../../../../common/lib/kibana', () => ({ useKibana: jest.fn(), + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }), })); describe('useTimelineLastEventTime', () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index 0a7df66f6c1d5c..3e690e50b04b14 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -25,6 +25,7 @@ import { } from '../../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { DocValueFields } from '../../../../../common/search_strategy'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; export interface UseTimelineLastEventTimeArgs { lastSeen: string | null; @@ -45,7 +46,7 @@ export const useTimelineLastEventTime = ({ indexNames, details, }: UseTimelineLastEventTimeProps): [boolean, UseTimelineLastEventTimeArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -69,6 +70,7 @@ export const useTimelineLastEventTime = ({ refetch: refetch.current, errorMessage: undefined, }); + const { addError, addWarning } = useAppToasts(); const timelineLastEventTimeSearch = useCallback( (request: TimelineEventsLastEventTimeRequestOptions) => { @@ -96,15 +98,13 @@ export const useTimelineLastEventTime = ({ })); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_LAST_EVENT_TIME); + addWarning(i18n.ERROR_LAST_EVENT_TIME); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_LAST_EVENT_TIME, - text: msg.message, }); setTimelineLastEventTimeResponse((prevResponse) => ({ ...prevResponse, @@ -118,7 +118,7 @@ export const useTimelineLastEventTime = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts] + [data.search, addError, addWarning] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 7884c7bd002636..19c706b86577dd 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -24,6 +24,7 @@ import { isErrorResponse, isCompleteResponse } from '../../../../../../../src/pl import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +import { useAppToasts } from '../../hooks/use_app_toasts'; export type Buckets = Array<{ key: string; @@ -60,7 +61,7 @@ export const useMatrixHistogram = ({ UseMatrixHistogramArgs, (to: string, from: string) => void ] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -83,6 +84,7 @@ export const useMatrixHistogram = ({ ...(isPtrIncluded != null ? { isPtrIncluded } : {}), ...(!isEmpty(docValueFields) ? { docValueFields } : {}), }); + const { addError, addWarning } = useAppToasts(); const [matrixHistogramResponse, setMatrixHistogramResponse] = useState({ data: [], @@ -126,14 +128,13 @@ export const useMatrixHistogram = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_MATRIX_HISTOGRAM); + addWarning(i18n.ERROR_MATRIX_HISTOGRAM); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addError(msg, { + addError(msg, { title: errorMessage ?? i18n.FAIL_MATRIX_HISTOGRAM, }); searchSubscription$.current.unsubscribe(); @@ -145,7 +146,7 @@ export const useMatrixHistogram = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, errorMessage, notifications.toasts] + [data.search, errorMessage, addError, addWarning] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index f66b060b166bf8..1c17f95bb6ba04 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -26,6 +26,7 @@ import * as i18n from './translations'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; import { DocValueFields } from '../../../../common/search_strategy/common'; +import { useAppToasts } from '../../hooks/use_app_toasts'; export { BrowserField, BrowserFields, DocValueFields }; @@ -125,7 +126,7 @@ export const useFetchIndex = ( indexNames: string[], onlyCheckIfIndicesExist: boolean = false ): [boolean, FetchIndexReturn] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); const previousIndexesName = useRef([]); @@ -138,6 +139,7 @@ export const useFetchIndex = ( indexExists: true, indexPatterns: DEFAULT_INDEX_PATTERNS, }); + const { addError, addWarning } = useAppToasts(); const indexFieldsSearch = useCallback( (iNames) => { @@ -168,14 +170,13 @@ export const useFetchIndex = ( searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - notifications.toasts.addWarning(i18n.ERROR_BEAT_FIELDS); + addWarning(i18n.ERROR_BEAT_FIELDS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ - text: msg.message, + addError(msg, { title: i18n.FAIL_BEAT_FIELDS, }); searchSubscription$.current.unsubscribe(); @@ -186,7 +187,7 @@ export const useFetchIndex = ( abortCtrl.current.abort(); asyncSearch(); }, - [data.search, notifications.toasts, onlyCheckIfIndicesExist] + [data.search, addError, addWarning, onlyCheckIfIndicesExist] ); useEffect(() => { @@ -203,7 +204,7 @@ export const useFetchIndex = ( }; export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); const dispatch = useDispatch(); @@ -215,6 +216,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { indexNames: string[]; previousIndexNames: string; }>((state) => indexNamesSelectedSelector(state, sourcererScopeName)); + const { addError, addWarning } = useAppToasts(); const setLoading = useCallback( (loading: boolean) => { @@ -257,14 +259,13 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - notifications.toasts.addWarning(i18n.ERROR_BEAT_FIELDS); + addWarning(i18n.ERROR_BEAT_FIELDS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ - text: msg.message, + addError(msg, { title: i18n.FAIL_BEAT_FIELDS, }); searchSubscription$.current.unsubscribe(); @@ -275,7 +276,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { abortCtrl.current.abort(); asyncSearch(); }, - [data.search, dispatch, notifications.toasts, setLoading, sourcererScopeName] + [data.search, dispatch, addError, addWarning, setLoading, sourcererScopeName] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 542369fdf5aa36..702a532949428d 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -51,6 +51,11 @@ jest.mock('../../utils/route/use_route_spy', () => ({ useRouteSpy: () => [mockRouteSpy], })); jest.mock('../../lib/kibana', () => ({ + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }), useKibana: jest.fn().mockReturnValue({ services: { application: { diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts index 2afe14644f5e94..b1cd14fa039b56 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts @@ -17,8 +17,10 @@ import { EqlSearchResponse } from '../../../../common/detection_engine/types'; import { useKibana } from '../../../common/lib/kibana'; import { useEqlPreview } from '.'; import { getMockEqlResponse } from './eql_search_response.mock'; +import { useAppToasts } from '../use_app_toasts'; jest.mock('../../../common/lib/kibana'); +jest.mock('../use_app_toasts'); describe('useEqlPreview', () => { const params = { @@ -29,10 +31,19 @@ describe('useEqlPreview', () => { from: '2020-10-04T15:00:54.368707900Z', }; - beforeEach(() => { - useKibana().services.notifications.toasts.addError = jest.fn(); + let addErrorMock: jest.Mock; + let addSuccessMock: jest.Mock; + let addWarningMock: jest.Mock; - useKibana().services.notifications.toasts.addWarning = jest.fn(); + beforeEach(() => { + addErrorMock = jest.fn(); + addSuccessMock = jest.fn(); + addWarningMock = jest.fn(); + (useAppToasts as jest.Mock).mockImplementation(() => ({ + addError: addErrorMock, + addWarning: addWarningMock, + addSuccess: addSuccessMock, + })); (useKibana().services.data.search.search as jest.Mock).mockReturnValue( of(getMockEqlResponse()) @@ -134,11 +145,8 @@ describe('useEqlPreview', () => { result.current[1](params); - const mockCalls = (useKibana().services.notifications.toasts.addWarning as jest.Mock).mock - .calls; - expect(result.current[0]).toBeFalsy(); - expect(mockCalls[0][0]).toEqual(i18n.EQL_PREVIEW_FETCH_FAILURE); + expect(addWarningMock.mock.calls[0][0]).toEqual(i18n.EQL_PREVIEW_FETCH_FAILURE); }); }); @@ -166,7 +174,7 @@ describe('useEqlPreview', () => { }); }); - it('should add danger toast if search throws', async () => { + it('should add error toast if search throws', async () => { await act(async () => { (useKibana().services.data.search.search as jest.Mock).mockReturnValue( throwError('This is an error!') @@ -178,11 +186,8 @@ describe('useEqlPreview', () => { result.current[1](params); - const mockCalls = (useKibana().services.notifications.toasts.addError as jest.Mock).mock - .calls; - expect(result.current[0]).toBeFalsy(); - expect(mockCalls[0][0]).toEqual('This is an error!'); + expect(addErrorMock.mock.calls[0][0]).toEqual('This is an error!'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts index 5632dd0ed03bed..788ce00ba1b1d3 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts @@ -27,18 +27,20 @@ import { EqlSearchResponse } from '../../../../common/detection_engine/types'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { inputsModel } from '../../../common/store'; import { EQL_SEARCH_STRATEGY } from '../../../../../data_enhanced/public'; +import { useAppToasts } from '../use_app_toasts'; export const useEqlPreview = (): [ boolean, (arg: EqlPreviewRequest) => void, EqlPreviewResponse ] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const unsubscribeStream = useRef(new Subject()); const [loading, setLoading] = useState(false); const didCancel = useRef(false); + const { addError, addWarning } = useAppToasts(); const [response, setResponse] = useState({ data: [], @@ -53,7 +55,7 @@ export const useEqlPreview = (): [ const searchEql = useCallback( ({ from, to, query, index, interval }: EqlPreviewRequest) => { if (parseScheduleDates(to) == null || parseScheduleDates(from) == null) { - notifications.toasts.addWarning('Time intervals are not defined.'); + addWarning(i18n.EQL_TIME_INTERVAL_NOT_DEFINED); return; } @@ -138,7 +140,7 @@ export const useEqlPreview = (): [ setResponse((prev) => ({ ...prev, inspect: formatInspect(res, index) })); } else if (isErrorResponse(res)) { setLoading(false); - notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE); + addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE); unsubscribeStream.current.next(); } }, @@ -154,7 +156,7 @@ export const useEqlPreview = (): [ refetch: refetch.current, totalCount: 0, }); - notifications.toasts.addError(err, { + addError(err, { title: i18n.EQL_PREVIEW_FETCH_FAILURE, }); } @@ -166,7 +168,7 @@ export const useEqlPreview = (): [ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts] + [data.search, addError, addWarning] ); useEffect((): (() => void) => { diff --git a/x-pack/plugins/security_solution/public/common/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/hooks/translations.ts index 90a848329c0135..520cfef74ce414 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/translations.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/translations.ts @@ -46,3 +46,10 @@ export const EQL_PREVIEW_FETCH_FAILURE = i18n.translate( defaultMessage: 'EQL Preview Error', } ); + +export const EQL_TIME_INTERVAL_NOT_DEFINED = i18n.translate( + 'xpack.securitySolution.components.hooks.errors.timeIntervalsNotDefined', + { + defaultMessage: 'Time intervals are not defined.', + } +); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts index 2b224f1bb61254..25c0f5411f25cf 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts @@ -8,6 +8,7 @@ const createAppToastsMock = () => ({ addError: jest.fn(), addSuccess: jest.fn(), + addWarning: jest.fn(), }); export const useAppToastsMock = { diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts index e8a13a1cc183e8..27f584bb172481 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -6,13 +6,14 @@ */ import { renderHook } from '@testing-library/react-hooks'; +import { IEsError } from 'src/plugins/data/public'; import { useToasts } from '../lib/kibana'; import { useAppToasts } from './use_app_toasts'; jest.mock('../lib/kibana'); -describe('useDeleteList', () => { +describe('useAppToasts', () => { let addErrorMock: jest.Mock; let addSuccessMock: jest.Mock; let addWarningMock: jest.Mock; @@ -37,31 +38,36 @@ describe('useDeleteList', () => { expect(addErrorMock).toHaveBeenCalledWith(error, { title: 'title' }); }); - it("uses a AppError's body.message as the toastMessage", async () => { - const kibanaApiError = { - message: 'Not Found', - body: { status_code: 404, message: 'Detailed Message' }, - }; + it('converts an unknown error to an Error', () => { + const unknownError = undefined; const { result } = renderHook(() => useAppToasts()); - result.current.addError(kibanaApiError, { title: 'title' }); + result.current.addError(unknownError, { title: 'title' }); - expect(addErrorMock).toHaveBeenCalledWith(kibanaApiError, { + expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), { title: 'title', - toastMessage: 'Detailed Message', }); }); - it('converts an unknown error to an Error', () => { - const unknownError = undefined; - + it('works normally with a bsearch type error', async () => { + const error = ({ + message: 'some message', + attributes: {}, + err: { + statusCode: 400, + innerMessages: { somethingElse: 'message' }, + }, + } as unknown) as IEsError; const { result } = renderHook(() => useAppToasts()); - result.current.addError(unknownError, { title: 'title' }); - - expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), { - title: 'title', + result.current.addError(error, { title: 'title' }); + const errorObj = addErrorMock.mock.calls[0][0]; + expect(errorObj).toEqual({ + message: 'some message (400)', + name: 'some message', + stack: + '{\n "statusCode": 400,\n "innerMessages": {\n "somethingElse": "message"\n }\n}', }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts index a797d56835ae79..f5a3c75747e52f 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -6,45 +6,79 @@ */ import { useCallback, useRef } from 'react'; +import { IEsError, isEsError } from '../../../../../../src/plugins/data/public'; import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public'; import { useToasts } from '../lib/kibana'; -import { isAppError, AppError } from '../utils/api'; +import { isAppError } from '../utils/api'; export type UseAppToasts = Pick & { api: ToastsStart; addError: (error: unknown, options: ErrorToastOptions) => Toast; }; +/** + * This gives a better presentation of error data sent from the API (both general platform errors and app-specific errors). + * This uses platform's new Toasts service to prevent modal/toast z-index collision issues. + * This fixes some issues you can see with re-rendering since using a class such as notifications.toasts. + * This also has an adapter and transform for detecting if a bsearch's EsError is present and then adapts that to the + * Kibana error toaster model so that the network error message will be shown rather than a stack trace. + */ export const useAppToasts = (): UseAppToasts => { const toasts = useToasts(); const addError = useRef(toasts.addError.bind(toasts)).current; const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current; const addWarning = useRef(toasts.addWarning.bind(toasts)).current; - const addAppError = useCallback( - (error: AppError, options: ErrorToastOptions) => - addError(error, { - ...options, - toastMessage: error.body.message, - }), - [addError] - ); - const _addError = useCallback( (error: unknown, options: ErrorToastOptions) => { - if (isAppError(error)) { - return addAppError(error, options); + if (error != null && isEsError(error)) { + const err = esErrorToRequestError(error); + return addError(err, options); + } else if (isAppError(error)) { + return addError(error, options); + } else if (error instanceof Error) { + return addError(error, options); } else { - if (error instanceof Error) { - return addError(error, options); - } else { - return addError(new Error(String(error)), options); - } + // Best guess that this is a stringable error. + const err = new Error(String(error)); + return addError(err, options); } }, - [addAppError, addError] + [addError] ); return { api: toasts, addError: _addError, addSuccess, addWarning }; }; + +/** + * See this file, we are not allowed to import files such as es_error. + * So instead we say maybe err is on there so that we can unwrap it and get + * our status code from it if possible within the error in our function. + * src/plugins/data/public/search/errors/es_error.tsx + */ +type MaybeESError = IEsError & { err?: Record }; + +/** + * This attempts its best to map between an IEsError which comes from bsearch to a error_toaster + * See the file: src/core/public/notifications/toasts/error_toast.tsx + * + * NOTE: This is brittle at the moment from bsearch and the hope is that better support between + * the error message and formatting of bsearch and the error_toast.tsx from Kibana core will be + * supported in the future. However, for now, this is _hopefully_ temporary. + * + * Also see the file: + * x-pack/plugins/security_solution/public/app/home/setup.tsx + * + * Where this same technique of overriding and changing the stack is occurring. + */ +export const esErrorToRequestError = (error: IEsError & MaybeESError): Error => { + const maybeUnWrapped = error.err != null ? error.err : error; + const statusCode = error.err?.statusCode != null ? `(${error.err.statusCode})` : ''; + const stringifiedError = JSON.stringify(maybeUnWrapped, null, 2); + return { + message: `${error.attributes?.reason ?? error.message} ${statusCode}`, + name: error.attributes?.reason ?? error.message, + stack: stringifiedError, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/utils/api/index.ts b/x-pack/plugins/security_solution/public/common/utils/api/index.ts index 198757e9ceade5..513fed36f678ca 100644 --- a/x-pack/plugins/security_solution/public/common/utils/api/index.ts +++ b/x-pack/plugins/security_solution/public/common/utils/api/index.ts @@ -7,9 +7,7 @@ import { has } from 'lodash/fp'; -export interface AppError { - name: string; - message: string; +export interface AppError extends Error { body: { message: string; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index 679aac71c6fdfa..004a904828ecf9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -12,6 +12,8 @@ import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { PrePackagedRulesPrompt } from './load_empty_prompt'; import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -37,6 +39,7 @@ jest.mock('../../../containers/detection_engine/rules/api', () => ({ }), createPrepackagedRules: jest.fn(), })); +jest.mock('../../../../common/hooks/use_app_toasts'); const props = { createPrePackagedRules: jest.fn(), @@ -46,6 +49,14 @@ const props = { }; describe('PrePackagedRulesPrompt', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + it('renders correctly', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx index 91b53c11ddda1a..c17d227428391b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx @@ -9,10 +9,21 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { usePrivilegeUser, ReturnPrivilegeUser } from './use_privilege_user'; import * as api from './api'; import { Privilege } from './types'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('usePrivilegeUser', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + test('init', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx index a527123fffb4ac..dd4da78db4e8eb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx @@ -6,8 +6,8 @@ */ import { useEffect, useState } from 'react'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { getUserPrivilege } from './api'; import * as i18n from './translations'; @@ -44,7 +44,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { hasIndexUpdateDelete: null, hasIndexMaintenance: null, }); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -84,7 +84,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { hasIndexUpdateDelete: false, hasIndexMaintenance: false, }); - errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.PRIVILEGE_FETCH_FAILURE }); } } if (isSubscribed) { @@ -97,7 +97,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { isSubscribed = false; abortCtrl.abort(); }; - }, [dispatchToaster]); + }, [addError]); return { loading, ...privilegeUser }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index dc6747510a3ea7..e8cd501816afe9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -8,14 +8,22 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useSignalIndex, ReturnSignalIndex } from './use_signal_index'; import * as api from './api'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('useSignalIndex', () => { + let appToastsMock: jest.Mocked>; + beforeEach(() => { jest.clearAllMocks(); jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); }); + test('init', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 00ecbca338b708..74adc8d36b0aac 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { isSecurityAppError } from '../../../../common/utils/api'; @@ -35,7 +35,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { signalIndexMappingOutdated: null, createDeSignalIndex: null, }); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -63,7 +63,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { createDeSignalIndex: createIndex, }); if (isSecurityAppError(error) && error.body.status_code !== 404) { - errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.SIGNAL_GET_NAME_FAILURE }); } } } @@ -93,7 +93,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { signalIndexMappingOutdated: null, createDeSignalIndex: createIndex, }); - errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.SIGNAL_POST_FAILURE }); } } } @@ -107,7 +107,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { isSubscribed = false; abortCtrl.abort(); }; - }, [dispatchToaster]); + }, [addError]); return { loading, ...signalIndex }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.test.tsx index 4532d3427375b1..6a527ca00f5251 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.test.tsx @@ -8,12 +8,19 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useRules, UseRules, ReturnRules } from './use_rules'; import * as api from '../api'; +import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; jest.mock('../api'); +jest.mock('../../../../../common/hooks/use_app_toasts'); describe('useRules', () => { + let appToastsMock: jest.Mocked>; + beforeEach(() => { jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); }); test('init', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.tsx index f3c90ae12ae33c..b7ef04c79d3da4 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules.tsx @@ -7,8 +7,8 @@ import { useEffect, useState, useRef } from 'react'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from '../types'; -import { errorToToaster, useStateToaster } from '../../../../../common/components/toasters'; import { fetchRules } from '../api'; import * as i18n from '../translations'; @@ -34,7 +34,7 @@ export const useRules = ({ const [rules, setRules] = useState(null); const reFetchRules = useRef<() => Promise>(() => Promise.resolve()); const [loading, setLoading] = useState(true); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); const filterTags = filterOptions.tags.sort().join(); useEffect(() => { @@ -62,7 +62,7 @@ export const useRules = ({ } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); if (dispatchRulesInReducer != null) { dispatchRulesInReducer([], {}); } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts index 7fcefe02cfe33b..8969843f61a1cd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts @@ -8,7 +8,7 @@ import { Dispatch, useMemo, useReducer, useEffect, useRef } from 'react'; import { EuiBasicTable } from '@elastic/eui'; -import { errorToToaster, useStateToaster } from '../../../../../common/components/toasters'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import * as i18n from '../translations'; import { fetchRules } from '../api'; @@ -65,9 +65,9 @@ export const useRulesTable = (params: UseRulesTableParams): UseRulesTableReturn const reducer = useMemo(() => createRulesTableReducer(tableRef), [tableRef]); const [state, dispatch] = useReducer(reducer, initialState); const facade = useRef(createRulesTableFacade(dispatch)); + const { addError } = useAppToasts(); const reFetchRules = useRef<() => Promise>(() => Promise.resolve()); - const [, dispatchToaster] = useStateToaster(); const { pagination, filterOptions } = state; const filterTags = filterOptions.tags.sort().join(); @@ -95,7 +95,7 @@ export const useRulesTable = (params: UseRulesTableParams): UseRulesTableReturn } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); facade.current.setRules([], {}); } } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx index 0074808057ca72..d6d6dec6edc6aa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx @@ -10,10 +10,21 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useCreateRule, ReturnCreateRule } from './use_create_rule'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('useCreateRule', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + test('init', async () => { const { result } = renderHook(() => useCreateRule()); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index e9a807d772d8b4..d50ef49593f409 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -7,7 +7,7 @@ import { useEffect, useState, Dispatch } from 'react'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { createRule } from './api'; @@ -25,7 +25,7 @@ export const useCreateRule = (): ReturnCreateRule => { const [rule, setRule] = useState(null); const [ruleId, setRuleId] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -44,7 +44,7 @@ export const useCreateRule = (): ReturnCreateRule => { } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_ADD_FAILURE }); } } if (isSubscribed) { @@ -58,7 +58,7 @@ export const useCreateRule = (): ReturnCreateRule => { isSubscribed = false; abortCtrl.abort(); }; - }, [rule, dispatchToaster]); + }, [rule, addError]); return [{ isLoading, ruleId }, setRule]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 8107d1ca84fda7..9ea8cee1060522 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -12,6 +12,15 @@ import * as api from './api'; import { shallow } from 'enzyme'; import * as i18n from './translations'; +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn(), + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }), +})); + jest.mock('./api', () => ({ getPrePackagedRulesStatus: jest.fn(), createPrepackagedRules: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 3fbda3e4533ea6..be474bbdc4fd88 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -8,11 +8,7 @@ import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { EuiButton } from '@elastic/eui'; -import { - errorToToaster, - useStateToaster, - displaySuccessToast, -} from '../../../../common/components/toasters'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; import * as i18n from './translations'; @@ -114,7 +110,8 @@ export const usePrePackagedRules = ({ const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); + const getSuccessToastMessage = (result: { rules_installed: number; rules_updated: number; @@ -173,7 +170,7 @@ export const usePrePackagedRules = ({ timelinesNotUpdated: null, }); - errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); } } if (isSubscribed) { @@ -231,7 +228,7 @@ export const usePrePackagedRules = ({ timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed, timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated, }); - displaySuccessToast(getSuccessToastMessage(result), dispatchToaster); + addSuccess(getSuccessToastMessage(result)); stopTimeOut(); resolve(true); } else { @@ -246,10 +243,8 @@ export const usePrePackagedRules = ({ } catch (error) { if (isSubscribed) { setLoadingCreatePrePackagedRules(false); - errorToToaster({ + addError(error, { title: i18n.RULE_AND_TIMELINE_PREPACKAGED_FAILURE, - error, - dispatchToaster, }); resolve(false); } @@ -269,7 +264,8 @@ export const usePrePackagedRules = ({ isAuthenticated, hasEncryptionKey, isSignalIndexExists, - dispatchToaster, + addError, + addSuccess, ]); const prePackagedRuleStatus = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx index 4b062bee6176b0..3c87a20dea6bb9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx @@ -8,10 +8,21 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useRule, ReturnRule } from './use_rule'; import * as api from './api'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('useRule', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + test('init', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx index 5ecc7904871a46..4e5480a9214934 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.tsx @@ -6,8 +6,8 @@ */ import { useEffect, useState } from 'react'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { fetchRuleById } from './api'; import { transformInput } from './transforms'; import * as i18n from './translations'; @@ -24,7 +24,7 @@ export type ReturnRule = [boolean, Rule | null]; export const useRule = (id: string | undefined): ReturnRule => { const [rule, setRule] = useState(null); const [loading, setLoading] = useState(true); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -45,7 +45,7 @@ export const useRule = (id: string | undefined): ReturnRule => { } catch (error) { if (isSubscribed) { setRule(null); - errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); } } if (isSubscribed) { @@ -59,7 +59,7 @@ export const useRule = (id: string | undefined): ReturnRule => { isSubscribed = false; abortCtrl.abort(); }; - }, [id, dispatchToaster]); + }, [id, addError]); return [loading, rule]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx index c773cac5dcfef6..96a8b00bf4966e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx @@ -14,8 +14,11 @@ import { } from './use_rule_status'; import * as api from './api'; import { Rule } from './types'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); const testRule: Rule = { actions: [ @@ -67,10 +70,13 @@ const testRule: Rule = { }; describe('useRuleStatus', () => { + let appToastsMock: jest.Mocked>; beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); jest.clearAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); }); afterEach(async () => { cleanup(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index 1f221c9abc798d..e3e2351b40a32f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -6,8 +6,8 @@ */ import { useEffect, useRef, useState } from 'react'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns'; import { getRuleStatusById, getRulesStatusByIds } from './api'; import * as i18n from './translations'; @@ -30,7 +30,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = const [ruleStatus, setRuleStatus] = useState(null); const fetchRuleStatus = useRef(null); const [loading, setLoading] = useState(true); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -50,7 +50,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = } catch (error) { if (isSubscribed) { setRuleStatus(null); - errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); } } if (isSubscribed) { @@ -65,7 +65,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = isSubscribed = false; abortCtrl.abort(); }; - }, [id, dispatchToaster]); + }, [id, addError]); return [loading, ruleStatus, fetchRuleStatus.current]; }; @@ -79,7 +79,7 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { const [rulesStatuses, setRuleStatuses] = useState([]); const [loading, setLoading] = useState(false); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -106,7 +106,7 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { } catch (error) { if (isSubscribed) { setRuleStatuses([]); - errorToToaster({ title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); } } if (isSubscribed) { @@ -122,7 +122,7 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { isSubscribed = false; abortCtrl.abort(); }; - }, [rules, dispatchToaster]); + }, [rules, addError]); return { loading, rulesStatuses }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx index f9488caaa91329..e177d36057b1d2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.test.tsx @@ -6,11 +6,22 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { useTags, ReturnTags } from './use_tags'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('useTags', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + test('init', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useTags()); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx index 5681b076aa6bbd..5f16cb593a516d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_tags.tsx @@ -7,7 +7,7 @@ import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { fetchTags } from './api'; import * as i18n from './translations'; @@ -20,8 +20,8 @@ export type ReturnTags = [boolean, string[], () => void]; export const useTags = (): ReturnTags => { const [tags, setTags] = useState([]); const [loading, setLoading] = useState(true); - const [, dispatchToaster] = useStateToaster(); const reFetchTags = useRef<() => void>(noop); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -39,7 +39,7 @@ export const useTags = (): ReturnTags => { } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.TAG_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.TAG_FETCH_FAILURE }); } } if (isSubscribed) { @@ -54,7 +54,7 @@ export const useTags = (): ReturnTags => { isSubscribed = false; abortCtrl.abort(); }; - }, [dispatchToaster]); + }, [addError]); return [loading, tags, reFetchTags.current]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx index c000870e8e51f2..3b16d0266e5662 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx @@ -9,10 +9,21 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateRule, ReturnUpdateRule } from './use_update_rule'; import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('useUpdateRule', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + test('init', async () => { const { result } = renderHook(() => useUpdateRule()); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx index 046702323db380..a5953b6ec3e658 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx @@ -7,7 +7,7 @@ import { useEffect, useState, Dispatch } from 'react'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { UpdateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { transformOutput } from './transforms'; @@ -26,7 +26,7 @@ export const useUpdateRule = (): ReturnUpdateRule => { const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); useEffect(() => { let isSubscribed = true; @@ -42,7 +42,7 @@ export const useUpdateRule = (): ReturnUpdateRule => { } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.RULE_ADD_FAILURE }); } } if (isSubscribed) { @@ -56,7 +56,7 @@ export const useUpdateRule = (): ReturnUpdateRule => { isSubscribed = false; abortCtrl.abort(); }; - }, [rule, dispatchToaster]); + }, [rule, addError]); return [{ isLoading, isSaved }, setRule]; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 5cfa5ecd225ecc..146b7e84707180 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -15,6 +15,7 @@ import { } from '@elastic/eui'; import { History } from 'history'; +import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; import { NamespaceType } from '../../../../../../../../lists/common'; import { useKibana } from '../../../../../../common/lib/kibana'; @@ -88,6 +89,7 @@ export const ExceptionListsTable = React.memo( const [deletingListIds, setDeletingListIds] = useState([]); const [exportingListIds, setExportingListIds] = useState([]); const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({}); + const { addError } = useAppToasts(); const handleDeleteSuccess = useCallback( (listId?: string) => () => { @@ -100,12 +102,11 @@ export const ExceptionListsTable = React.memo( const handleDeleteError = useCallback( (err: Error & { body?: { message: string } }): void => { - notifications.toasts.addError(err, { + addError(err, { title: i18n.EXCEPTION_DELETE_ERROR, - toastMessage: err.body != null ? err.body.message : err.message, }); }, - [notifications.toasts] + [addError] ); const handleDelete = useCallback( @@ -170,9 +171,9 @@ export const ExceptionListsTable = React.memo( const handleExportError = useCallback( (err: Error) => { - notifications.toasts.addError(err, { title: i18n.EXCEPTION_EXPORT_ERROR }); + addError(err, { title: i18n.EXCEPTION_EXPORT_ERROR }); }, - [notifications.toasts] + [addError] ); const handleExport = useCallback( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx index 3ea60004018724..a84a60af51b391 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx @@ -10,8 +10,19 @@ import { mount } from 'enzyme'; import { act } from '@testing-library/react'; import { RulesTableFilters } from './rules_table_filters'; +import { useAppToastsMock } from '../../../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; +jest.mock('../../../../../../common/hooks/use_app_toasts'); describe('RulesTableFilters', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + it('renders no numbers next to rule type button filter if none exist', async () => { await act(async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index 5b443b73f11e2a..9622610f3c637f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -12,6 +12,8 @@ import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { CreateRulePage } from './index'; import { useUserData } from '../../../../components/user_info'; +import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -27,8 +29,16 @@ jest.mock('react-router-dom', () => { jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); +jest.mock('../../../../../common/hooks/use_app_toasts'); describe('CreateRulePage', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + it('renders correctly', () => { (useUserData as jest.Mock).mockReturnValue([{}]); const wrapper = shallow(, { wrappingComponent: TestProviders }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index 5f485dcaa01952..e7cdfbe268fe68 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock'; import { EditRulePage } from './index'; import { useUserData } from '../../../../components/user_info'; import { useParams } from 'react-router-dom'; +import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); @@ -26,8 +28,16 @@ jest.mock('react-router-dom', () => { useParams: jest.fn(), }; }); +jest.mock('../../../../../common/hooks/use_app_toasts'); describe('EditRulePage', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + it('renders correctly', () => { (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 0ffeaa42245448..bcd5ccdc0b5ac5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -14,6 +14,8 @@ import { useUserData } from '../../../components/user_info'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -73,10 +75,15 @@ jest.mock('../../../components/rules/pre_packaged_rules/update_callout', () => { UpdatePrePackagedRulesCallOut: jest.fn().mockReturnValue(
), }; }); +jest.mock('../../../../common/hooks/use_app_toasts'); describe('RulesPage', () => { + let appToastsMock: jest.Mocked>; + beforeAll(() => { (useUserData as jest.Mock).mockReturnValue([{}]); + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); }); it('renders AllRules', () => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index f1efdd2e3c4329..c31094b5778d5b 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -33,6 +33,7 @@ import { InspectResponse } from '../../../types'; import { hostsModel, hostsSelectors } from '../../store'; import * as i18n from './translations'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'hostsAuthenticationsQuery'; @@ -71,7 +72,7 @@ export const useAuthentications = ({ const { activePage, limit } = useDeepEqualSelector((state) => pick(['activePage', 'limit'], getAuthenticationsSelector(state, type)) ); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -80,6 +81,7 @@ export const useAuthentications = ({ authenticationsRequest, setAuthenticationsRequest, ] = useState(null); + const { addError, addWarning } = useAppToasts(); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -145,15 +147,14 @@ export const useAuthentications = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - notifications.toasts.addWarning(i18n.ERROR_AUTHENTICATIONS); + addWarning(i18n.ERROR_AUTHENTICATIONS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_AUTHENTICATIONS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -164,7 +165,7 @@ export const useAuthentications = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx index 1eaa89575de260..dd55bdb4c69489 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; import { @@ -55,7 +56,7 @@ export const useHostDetails = ({ skip = false, startDate, }: UseHostDetails): [boolean, HostDetailsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -63,6 +64,7 @@ export const useHostDetails = ({ const [hostDetailsRequest, setHostDetailsRequest] = useState( null ); + const { addError, addWarning } = useAppToasts(); const [hostDetailsResponse, setHostDetailsResponse] = useState({ endDate, @@ -104,16 +106,14 @@ export const useHostDetails = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_HOST_OVERVIEW); + addWarning(i18n.ERROR_HOST_OVERVIEW); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_HOST_OVERVIEW, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -124,7 +124,7 @@ export const useHostDetails = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index 380e6b05471a89..a3703ab64beda4 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -9,6 +9,7 @@ import deepEqual from 'fast-deep-equal'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useKibana } from '../../../../common/lib/kibana'; import { HostsQueries, @@ -45,7 +46,7 @@ export const useFirstLastSeenHost = ({ indexNames, order, }: UseHostFirstLastSeen): [boolean, FirstLastSeenHostArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); const [loading, setLoading] = useState(false); @@ -69,6 +70,7 @@ export const useFirstLastSeenHost = ({ id: ID, } ); + const { addError, addWarning } = useAppToasts(); const firstLastSeenHostSearch = useCallback( (request: HostFirstLastSeenRequestOptions) => { @@ -93,8 +95,7 @@ export const useFirstLastSeenHost = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_FIRST_LAST_SEEN_HOST); + addWarning(i18n.ERROR_FIRST_LAST_SEEN_HOST); searchSubscription$.current.unsubscribe(); } }, @@ -104,9 +105,8 @@ export const useFirstLastSeenHost = ({ ...prevResponse, errorMessage: msg, })); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_FIRST_LAST_SEEN_HOST, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -116,7 +116,7 @@ export const useFirstLastSeenHost = ({ abortCtrl.current.abort(); asyncSearch(); }, - [data.search, notifications.toasts] + [data.search, addError, addWarning] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 383c4c233914f8..7bf681092c075a 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -30,6 +30,7 @@ import * as i18n from './translations'; import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'hostsAllQuery'; @@ -70,12 +71,13 @@ export const useAllHost = ({ const { activePage, direction, limit, sortField } = useDeepEqualSelector((state: State) => getHostsSelector(state, type) ); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription = useRef(new Subscription()); const [loading, setLoading] = useState(false); const [hostsRequest, setHostRequest] = useState(null); + const { addError, addWarning } = useAppToasts(); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -143,14 +145,13 @@ export const useAllHost = ({ searchSubscription.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_ALL_HOST); + addWarning(i18n.ERROR_ALL_HOST); searchSubscription.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ title: i18n.FAIL_ALL_HOST, text: msg.message }); + addError(msg, { title: i18n.FAIL_ALL_HOST }); searchSubscription.current.unsubscribe(); }, }); @@ -160,7 +161,7 @@ export const useAllHost = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index ad3c7e0e829fba..6a3323da4fb440 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -49,7 +50,7 @@ export const useHostsKpiAuthentications = ({ skip = false, startDate, }: UseHostsKpiAuthentications): [boolean, HostsKpiAuthenticationsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -75,6 +76,7 @@ export const useHostsKpiAuthentications = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const hostsKpiAuthenticationsSearch = useCallback( (request: HostsKpiAuthenticationsRequestOptions | null) => { @@ -110,16 +112,14 @@ export const useHostsKpiAuthentications = ({ searchSubscription$.current.unsubscribe(); } else if (response.isPartial && !response.isRunning) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_HOSTS_KPI_AUTHENTICATIONS); + addWarning(i18n.ERROR_HOSTS_KPI_AUTHENTICATIONS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_HOSTS_KPI_AUTHENTICATIONS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -130,7 +130,7 @@ export const useHostsKpiAuthentications = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx index 8ed1aaecb6f0ec..5af91539e8be36 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -48,7 +49,7 @@ export const useHostsKpiHosts = ({ skip = false, startDate, }: UseHostsKpiHosts): [boolean, HostsKpiHostsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -69,6 +70,7 @@ export const useHostsKpiHosts = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const hostsKpiHostsSearch = useCallback( (request: HostsKpiHostsRequestOptions | null) => { @@ -98,16 +100,14 @@ export const useHostsKpiHosts = ({ searchSubscription$.current.unsubscribe(); } else if (response.isPartial && !response.isRunning) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_HOSTS_KPI_HOSTS); + addWarning(i18n.ERROR_HOSTS_KPI_HOSTS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_HOSTS_KPI_HOSTS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -118,7 +118,7 @@ export const useHostsKpiHosts = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx index b34de267f45192..9a72fa1d6cfca7 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -49,7 +50,7 @@ export const useHostsKpiUniqueIps = ({ skip = false, startDate, }: UseHostsKpiUniqueIps): [boolean, HostsKpiUniqueIpsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -74,6 +75,7 @@ export const useHostsKpiUniqueIps = ({ refetch: refetch.current, } ); + const { addError, addWarning } = useAppToasts(); const hostsKpiUniqueIpsSearch = useCallback( (request: HostsKpiUniqueIpsRequestOptions | null) => { @@ -105,16 +107,14 @@ export const useHostsKpiUniqueIps = ({ searchSubscription$.current.unsubscribe(); } else if (response.isPartial && !response.isRunning) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_HOSTS_KPI_UNIQUE_IPS); + addWarning(i18n.ERROR_HOSTS_KPI_UNIQUE_IPS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_HOSTS_KPI_UNIQUE_IPS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -125,7 +125,7 @@ export const useHostsKpiUniqueIps = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index 1e07b94b55b740..e94873dee5632e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -32,6 +32,7 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'hostsUncommonProcessesQuery'; @@ -72,7 +73,7 @@ export const useUncommonProcesses = ({ const { activePage, limit } = useDeepEqualSelector((state: State) => getUncommonProcessesSelector(state, type) ); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -81,6 +82,7 @@ export const useUncommonProcesses = ({ uncommonProcessesRequest, setUncommonProcessesRequest, ] = useState(null); + const { addError, addWarning } = useAppToasts(); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -150,15 +152,14 @@ export const useUncommonProcesses = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - notifications.toasts.addWarning(i18n.ERROR_UNCOMMON_PROCESSES); + addWarning(i18n.ERROR_UNCOMMON_PROCESSES); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_UNCOMMON_PROCESSES, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -169,7 +170,7 @@ export const useUncommonProcesses = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx index 6bbe7f8f437739..cf7d8e05858d5f 100644 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -24,6 +24,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkDetailsQuery'; @@ -52,7 +53,7 @@ export const useNetworkDetails = ({ skip, ip, }: UseNetworkDetails): [boolean, NetworkDetailsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -73,6 +74,7 @@ export const useNetworkDetails = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const networkDetailsSearch = useCallback( (request: NetworkDetailsRequestOptions | null) => { @@ -100,16 +102,14 @@ export const useNetworkDetails = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_DETAILS); + addWarning(i18n.ERROR_NETWORK_DETAILS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_DETAILS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -120,7 +120,7 @@ export const useNetworkDetails = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 345aee4de2df20..c835aa6c6a3e36 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -53,7 +54,7 @@ export const useNetworkKpiDns = ({ skip = false, startDate, }: UseNetworkKpiDns): [boolean, NetworkKpiDnsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -73,6 +74,7 @@ export const useNetworkKpiDns = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const networkKpiDnsSearch = useCallback( (request: NetworkKpiDnsRequestOptions | null) => { @@ -102,16 +104,14 @@ export const useNetworkKpiDns = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_KPI_DNS); + addWarning(i18n.ERROR_NETWORK_KPI_DNS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_KPI_DNS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -122,7 +122,7 @@ export const useNetworkKpiDns = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 6dd3df1055f9ce..2e4f3b83e67083 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -53,7 +54,7 @@ export const useNetworkKpiNetworkEvents = ({ skip = false, startDate, }: UseNetworkKpiNetworkEvents): [boolean, NetworkKpiNetworkEventsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -76,6 +77,7 @@ export const useNetworkKpiNetworkEvents = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const networkKpiNetworkEventsSearch = useCallback( (request: NetworkKpiNetworkEventsRequestOptions | null) => { @@ -108,16 +110,14 @@ export const useNetworkKpiNetworkEvents = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_KPI_NETWORK_EVENTS); + addWarning(i18n.ERROR_NETWORK_KPI_NETWORK_EVENTS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_KPI_NETWORK_EVENTS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -128,7 +128,7 @@ export const useNetworkKpiNetworkEvents = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index dfc7d0a28db79c..b9d3e8639c5603 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -53,7 +54,7 @@ export const useNetworkKpiTlsHandshakes = ({ skip = false, startDate, }: UseNetworkKpiTlsHandshakes): [boolean, NetworkKpiTlsHandshakesArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -76,6 +77,7 @@ export const useNetworkKpiTlsHandshakes = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const networkKpiTlsHandshakesSearch = useCallback( (request: NetworkKpiTlsHandshakesRequestOptions | null) => { @@ -107,16 +109,14 @@ export const useNetworkKpiTlsHandshakes = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_KPI_TLS_HANDSHAKES); + addWarning(i18n.ERROR_NETWORK_KPI_TLS_HANDSHAKES); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_KPI_TLS_HANDSHAKES, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -127,7 +127,7 @@ export const useNetworkKpiTlsHandshakes = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 08c4d917f5da3f..2699d63144be14 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -53,7 +54,7 @@ export const useNetworkKpiUniqueFlows = ({ skip = false, startDate, }: UseNetworkKpiUniqueFlows): [boolean, NetworkKpiUniqueFlowsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -76,6 +77,7 @@ export const useNetworkKpiUniqueFlows = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const networkKpiUniqueFlowsSearch = useCallback( (request: NetworkKpiUniqueFlowsRequestOptions | null) => { @@ -108,16 +110,14 @@ export const useNetworkKpiUniqueFlows = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_KPI_UNIQUE_FLOWS); + addWarning(i18n.ERROR_NETWORK_KPI_UNIQUE_FLOWS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_KPI_UNIQUE_FLOWS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -128,7 +128,7 @@ export const useNetworkKpiUniqueFlows = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index a532f4f11a3015..488c526134525b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Subscription } from 'rxjs'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -57,7 +58,7 @@ export const useNetworkKpiUniquePrivateIps = ({ skip = false, startDate, }: UseNetworkKpiUniquePrivateIps): [boolean, NetworkKpiUniquePrivateIpsArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -83,6 +84,7 @@ export const useNetworkKpiUniquePrivateIps = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const networkKpiUniquePrivateIpsSearch = useCallback( (request: NetworkKpiUniquePrivateIpsRequestOptions | null) => { @@ -119,16 +121,14 @@ export const useNetworkKpiUniquePrivateIps = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_KPI_UNIQUE_PRIVATE_IPS); + addWarning(i18n.ERROR_NETWORK_KPI_UNIQUE_PRIVATE_IPS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_KPI_UNIQUE_PRIVATE_IPS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -139,7 +139,7 @@ export const useNetworkKpiUniquePrivateIps = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 5ce31bada520b1..47e60f27a7dbdf 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -30,6 +30,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkDnsQuery'; @@ -68,7 +69,7 @@ export const useNetworkDns = ({ }: UseNetworkDns): [boolean, NetworkDnsArgs] => { const getNetworkDnsSelector = useMemo(() => networkSelectors.dnsSelector(), []); const { activePage, sort, isPtrIncluded, limit } = useDeepEqualSelector(getNetworkDnsSelector); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -110,6 +111,7 @@ export const useNetworkDns = ({ refetch: refetch.current, totalCount: -1, }); + const { addError, addWarning } = useAppToasts(); const networkDnsSearch = useCallback( (request: NetworkDnsRequestOptions | null) => { @@ -142,16 +144,14 @@ export const useNetworkDns = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_DNS); + addWarning(i18n.ERROR_NETWORK_DNS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_DNS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -162,7 +162,7 @@ export const useNetworkDns = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index d1ff9da1fa6c22..98105f5cac25a5 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -29,6 +29,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import * as i18n from './translations'; import { InspectResponse } from '../../../types'; import { getInspectResponse } from '../../../helpers'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkHttpQuery'; @@ -67,7 +68,7 @@ export const useNetworkHttp = ({ }: UseNetworkHttp): [boolean, NetworkHttpArgs] => { const getHttpSelector = useMemo(() => networkSelectors.httpSelector(), []); const { activePage, limit, sort } = useDeepEqualSelector((state) => getHttpSelector(state, type)); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -108,6 +109,7 @@ export const useNetworkHttp = ({ refetch: refetch.current, totalCount: -1, }); + const { addError, addWarning } = useAppToasts(); const networkHttpSearch = useCallback( (request: NetworkHttpRequestOptions | null) => { @@ -139,16 +141,14 @@ export const useNetworkHttp = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_HTTP); + addWarning(i18n.ERROR_NETWORK_HTTP); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_HTTP, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -159,7 +159,7 @@ export const useNetworkHttp = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index 405957d98055e0..e7f3cf3f2675a0 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -29,6 +29,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkTopCountriesQuery'; @@ -68,7 +69,7 @@ export const useNetworkTopCountries = ({ const { activePage, limit, sort } = useDeepEqualSelector((state) => getTopCountriesSelector(state, type, flowTarget) ); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -95,6 +96,7 @@ export const useNetworkTopCountries = ({ }, [limit] ); + const { addError, addWarning } = useAppToasts(); const [ networkTopCountriesResponse, @@ -147,16 +149,14 @@ export const useNetworkTopCountries = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_TOP_COUNTRIES); + addWarning(i18n.ERROR_NETWORK_TOP_COUNTRIES); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_TOP_COUNTRIES, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -167,7 +167,7 @@ export const useNetworkTopCountries = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addWarning, addError, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 9c6a4b3d1147f6..3cbaf0fbc976ce 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -29,6 +29,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkTopNFlowQuery'; @@ -68,7 +69,7 @@ export const useNetworkTopNFlow = ({ const { activePage, limit, sort } = useDeepEqualSelector((state) => getTopNFlowSelector(state, type, flowTarget) ); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -112,6 +113,7 @@ export const useNetworkTopNFlow = ({ refetch: refetch.current, totalCount: -1, }); + const { addError, addWarning } = useAppToasts(); const networkTopNFlowSearch = useCallback( (request: NetworkTopNFlowRequestOptions | null) => { @@ -143,16 +145,14 @@ export const useNetworkTopNFlow = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_TOP_N_FLOW); + addWarning(i18n.ERROR_NETWORK_TOP_N_FLOW); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_TOP_N_FLOW, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -163,7 +163,7 @@ export const useNetworkTopNFlow = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 49a7064113c307..754f0cac8868c1 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -27,6 +27,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { FlowTargetSourceDest, PageInfoPaginated } from '../../../../common/search_strategy'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkTlsQuery'; @@ -68,7 +69,7 @@ export const useNetworkTls = ({ const { activePage, limit, sort } = useDeepEqualSelector((state) => getTlsSelector(state, type, flowTarget) ); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -109,6 +110,7 @@ export const useNetworkTls = ({ refetch: refetch.current, totalCount: -1, }); + const { addError, addWarning } = useAppToasts(); const networkTlsSearch = useCallback( (request: NetworkTlsRequestOptions | null) => { @@ -141,16 +143,14 @@ export const useNetworkTls = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_TLS); + addWarning(i18n.ERROR_NETWORK_TLS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_TLS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -161,7 +161,7 @@ export const useNetworkTls = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index e000981733eedb..d4be09f97591d6 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -29,6 +29,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import { PageInfoPaginated } from '../../../../common/search_strategy'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; const ID = 'networkUsersQuery'; @@ -65,7 +66,7 @@ export const useNetworkUsers = ({ }: UseNetworkUsers): [boolean, NetworkUsersArgs] => { const getNetworkUsersSelector = useMemo(() => networkSelectors.usersSelector(), []); const { activePage, sort, limit } = useDeepEqualSelector(getNetworkUsersSelector); - const { data, notifications, uiSettings } = useKibana().services; + const { data, uiSettings } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -109,6 +110,7 @@ export const useNetworkUsers = ({ refetch: refetch.current, totalCount: -1, }); + const { addError, addWarning } = useAppToasts(); const networkUsersSearch = useCallback( (request: NetworkUsersRequestOptions | null) => { @@ -140,16 +142,14 @@ export const useNetworkUsers = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_USERS); + addWarning(i18n.ERROR_NETWORK_USERS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_USERS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -160,7 +160,7 @@ export const useNetworkUsers = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index 8b17a7288eae3d..52b58439af0ab8 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -23,6 +23,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; export const ID = 'overviewHostQuery'; @@ -49,7 +50,7 @@ export const useHostOverview = ({ skip = false, startDate, }: UseHostOverview): [boolean, HostOverviewArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -66,6 +67,7 @@ export const useHostOverview = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const overviewHostSearch = useCallback( (request: HostOverviewRequestOptions | null) => { @@ -95,16 +97,14 @@ export const useHostOverview = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_HOST_OVERVIEW); + addWarning(i18n.ERROR_HOST_OVERVIEW); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_HOST_OVERVIEW, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -115,7 +115,7 @@ export const useHostOverview = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index cf0774a02db3b5..846c40994aac21 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -23,6 +23,7 @@ import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/pl import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; export const ID = 'overviewNetworkQuery'; @@ -49,7 +50,7 @@ export const useNetworkOverview = ({ skip = false, startDate, }: UseNetworkOverview): [boolean, NetworkOverviewArgs] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -69,6 +70,7 @@ export const useNetworkOverview = ({ isInspected: false, refetch: refetch.current, }); + const { addError, addWarning } = useAppToasts(); const overviewNetworkSearch = useCallback( (request: NetworkOverviewRequestOptions | null) => { @@ -98,16 +100,14 @@ export const useNetworkOverview = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning(i18n.ERROR_NETWORK_OVERVIEW); + addWarning(i18n.ERROR_NETWORK_OVERVIEW); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_NETWORK_OVERVIEW, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -118,7 +118,7 @@ export const useNetworkOverview = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts] + [data.search, addError, addWarning] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index 7e4924eacda4b5..37fdd5a444b2b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -20,6 +20,9 @@ import { TimelineEventsDetailsStrategyResponse, } from '../../../../common/search_strategy'; import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/public'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; + export interface EventsArgs { detailsData: TimelineEventsDetailsItem[] | null; } @@ -37,7 +40,7 @@ export const useTimelineEventsDetails = ({ eventId, skip, }: UseTimelineEventsDetailsProps): [boolean, EventsArgs['detailsData']] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -46,6 +49,7 @@ export const useTimelineEventsDetails = ({ timelineDetailsRequest, setTimelineDetailsRequest, ] = useState(null); + const { addError, addWarning } = useAppToasts(); const [timelineDetailsResponse, setTimelineDetailsResponse] = useState( null @@ -77,14 +81,13 @@ export const useTimelineEventsDetails = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - // TODO: Make response error status clearer - notifications.toasts.addWarning('An error has occurred'); + addWarning(i18n.FAIL_TIMELINE_DETAILS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ title: 'Failed to run search', text: msg.message }); + addError(msg, { title: i18n.FAIL_TIMELINE_SEARCH_DETAILS }); searchSubscription$.current.unsubscribe(); }, }); @@ -94,7 +97,7 @@ export const useTimelineEventsDetails = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts, skip] + [data.search, addError, addWarning, skip] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/translations.ts b/x-pack/plugins/security_solution/public/timelines/containers/details/translations.ts new file mode 100644 index 00000000000000..d11984b967db98 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/translations.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 { i18n } from '@kbn/i18n'; + +export const FAIL_TIMELINE_DETAILS = i18n.translate( + 'xpack.securitySolution.timeline.failDescription', + { + defaultMessage: 'An error has occurred', + } +); + +export const FAIL_TIMELINE_SEARCH_DETAILS = i18n.translate( + 'xpack.securitySolution.timeline.failSearchDescription', + { + defaultMessage: 'Failed to run search', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index 496107e910d76c..1032d0ec1672ac 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -27,6 +27,11 @@ const mockEvents = mockTimelineData.filter((i, index) => index <= 11); const mockSearch = jest.fn(); jest.mock('../../common/lib/kibana', () => ({ + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }), useKibana: jest.fn().mockReturnValue({ services: { application: { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 83b511f95bc2a0..92199336b978c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -40,6 +40,7 @@ import { TimelineEqlRequestOptions, TimelineEqlResponse, } from '../../../common/search_strategy/timeline/events/eql'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; export interface TimelineArgs { events: TimelineItem[]; @@ -138,7 +139,7 @@ export const useTimelineEvents = ({ }: UseTimelineEventsProps): [boolean, TimelineArgs] => { const [{ pageName }] = useRouteSpy(); const dispatch = useDispatch(); - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -194,6 +195,7 @@ export const useTimelineEvents = ({ loadPage: wrappedLoadPage, updatedAt: 0, }); + const { addError, addWarning } = useAppToasts(); const timelineSearch = useCallback( (request: TimelineRequest | null) => { @@ -242,15 +244,14 @@ export const useTimelineEvents = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - notifications.toasts.addWarning(i18n.ERROR_TIMELINE_EVENTS); + addWarning(i18n.ERROR_TIMELINE_EVENTS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger({ + addError(msg, { title: i18n.FAIL_TIMELINE_EVENTS, - text: msg.message, }); searchSubscription$.current.unsubscribe(); }, @@ -300,7 +301,7 @@ export const useTimelineEvents = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, id, notifications.toasts, pageName, refetchGrid, skip, wrappedLoadPage] + [data.search, id, addWarning, addError, pageName, refetchGrid, skip, wrappedLoadPage] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx index cf5f44a65ab968..4a6eab13ba4f1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx @@ -21,6 +21,8 @@ import { } from '../../../../common/search_strategy'; import { ESQuery } from '../../../../common/typed_json'; import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/public'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; export interface UseTimelineKpiProps { timerange: TimerangeInput; @@ -37,7 +39,7 @@ export const useTimelineKpis = ({ defaultIndex, isBlankTimeline, }: UseTimelineKpiProps): [boolean, TimelineKpiStrategyResponse | null] => { - const { data, notifications } = useKibana().services; + const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); @@ -49,6 +51,8 @@ export const useTimelineKpis = ({ timelineKpiResponse, setTimelineKpiResponse, ] = useState(null); + const { addError, addWarning } = useAppToasts(); + const timelineKpiSearch = useCallback( (request: TimelineRequestBasicOptions | null) => { if (request == null) { @@ -71,13 +75,13 @@ export const useTimelineKpis = ({ searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); - notifications.toasts.addWarning('An error has occurred'); + addWarning(i18n.FAIL_TIMELINE_KPI_DETAILS); searchSubscription$.current.unsubscribe(); } }, error: (msg) => { setLoading(false); - notifications.toasts.addDanger('Failed to load KPIs'); + addError(msg, { title: i18n.FAIL_TIMELINE_KPI_SEARCH_DETAILS }); searchSubscription$.current.unsubscribe(); }, }); @@ -87,7 +91,7 @@ export const useTimelineKpis = ({ asyncSearch(); refetch.current = asyncSearch; }, - [data.search, notifications.toasts] + [data.search, addError, addWarning] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/kpis/translations.ts b/x-pack/plugins/security_solution/public/timelines/containers/kpis/translations.ts new file mode 100644 index 00000000000000..1a487ef8127f2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/kpis/translations.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 { i18n } from '@kbn/i18n'; + +export const FAIL_TIMELINE_KPI_DETAILS = i18n.translate( + 'xpack.securitySolution.timeline.kpiFailDescription', + { + defaultMessage: 'An error has occurred', + } +); + +export const FAIL_TIMELINE_KPI_SEARCH_DETAILS = i18n.translate( + 'xpack.securitySolution.timeline.kpiFailSearchDescription', + { + defaultMessage: 'Failed to load KPIs', + } +); From 49cdc9066df35d7fae0e5fc55cf3c4be81f9f40d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 22 Apr 2021 17:16:54 +0200 Subject: [PATCH 10/33] [ML] Improve functional tests for Anomaly detection alert rule (#97998) * [ML] ensureAdvancedSectionOpen for assertion * [ML] delete alert rules after tests execution * [ML] add isAdvancedSectionOpened --- .../test/functional/services/ml/alerting.ts | 30 ++++++++++++++++--- .../apps/ml/alert_flyout.ts | 1 + 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/services/ml/alerting.ts b/x-pack/test/functional/services/ml/alerting.ts index 8d27a75b7b485b..327a0e574f0fde 100644 --- a/x-pack/test/functional/services/ml/alerting.ts +++ b/x-pack/test/functional/services/ml/alerting.ts @@ -8,6 +8,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlCommonUI } from './common_ui'; +import { ML_ALERT_TYPES } from '../../../../plugins/ml/common/constants/alerts'; +import { Alert } from '../../../../plugins/alerting/common'; +import { MlAnomalyDetectionAlertParams } from '../../../../plugins/ml/common/types/alerts'; export function MachineLearningAlertingProvider( { getService }: FtrProviderContext, @@ -17,6 +20,7 @@ export function MachineLearningAlertingProvider( const comboBox = getService('comboBox'); const testSubjects = getService('testSubjects'); const find = getService('find'); + const supertest = getService('supertest'); return { async selectAnomalyDetectionAlertType() { @@ -103,6 +107,7 @@ export function MachineLearningAlertingProvider( }, async assertLookbackInterval(expectedValue: string) { + await this.ensureAdvancedSectionOpen(); const actualValue = await testSubjects.getAttribute( 'mlAnomalyAlertLookbackInterval', 'value' @@ -114,6 +119,7 @@ export function MachineLearningAlertingProvider( }, async assertTopNBuckets(expectedNumberOfBuckets: number) { + await this.ensureAdvancedSectionOpen(); const actualValue = await testSubjects.getAttribute('mlAnomalyAlertTopNBuckets', 'value'); expect(actualValue).to.eql( expectedNumberOfBuckets, @@ -133,15 +139,31 @@ export function MachineLearningAlertingProvider( await this.assertTopNBuckets(numberOfBuckets); }, + async isAdvancedSectionOpened() { + return await find.existsByDisplayedByCssSelector('#mlAnomalyAlertAdvancedSettings'); + }, + async ensureAdvancedSectionOpen() { await retry.tryForTime(5000, async () => { - const isVisible = await find.existsByDisplayedByCssSelector( - '#mlAnomalyAlertAdvancedSettings' - ); - if (!isVisible) { + if (!(await this.isAdvancedSectionOpened())) { await testSubjects.click('mlAnomalyAlertAdvancedSettingsTrigger'); + expect(await this.isAdvancedSectionOpened()).to.eql(true); } }); }, + + async cleanAnomalyDetectionRules() { + const { body: anomalyDetectionRules } = await supertest + .get(`/api/alerting/rules/_find`) + .query({ filter: `alert.attributes.alertTypeId:${ML_ALERT_TYPES.ANOMALY_DETECTION}` }) + .set('kbn-xsrf', 'foo') + .expect(200); + + for (const rule of anomalyDetectionRules.data as Array< + Alert + >) { + await supertest.delete(`/api/alerting/rule/${rule.id}`).set('kbn-xsrf', 'foo').expect(204); + } + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts index cc0dcff5286635..ee30f3a9eab00e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts @@ -93,6 +93,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => { await ml.api.cleanMlIndices(); + await ml.alerting.cleanAnomalyDetectionRules(); }); describe('overview page alert flyout controls', () => { From 5c9491154362a0486f8b1c1be514eaafe0085bd6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 22 Apr 2021 08:18:49 -0700 Subject: [PATCH 11/33] [ML] Adds tooltip for top influencers (#97762) Co-authored-by: Dima Arnautov --- .../plugins/ml/public/application/explorer/explorer.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 45665b2026db51..e33c09932daab1 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -328,6 +328,15 @@ export class ExplorerUI extends React.Component { id="xpack.ml.explorer.topInfuencersTitle" defaultMessage="Top influencers" /> + + } + position="right" + /> {loading ? ( From 8bbf9c0e280fe3108c0397433ea844b751de3120 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 22 Apr 2021 11:50:38 -0400 Subject: [PATCH 12/33] [App Search] Disabled Save button when nothing selected (#97896) --- .../result_settings/result_settings.test.tsx | 11 +++++++ .../result_settings/result_settings.tsx | 12 ++++--- .../result_settings_logic.test.ts | 15 +++++++++ .../result_settings/result_settings_logic.ts | 5 +++ .../components/result_settings/utils.test.ts | 32 +++++++++++++++++++ .../components/result_settings/utils.ts | 7 ++++ 6 files changed, 78 insertions(+), 4 deletions(-) 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 70bc49421a4f1f..48a25d4f1f4bd6 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 @@ -86,6 +86,17 @@ describe('ResultSettings', () => { expect(saveButton.prop('disabled')).toBe(true); }); + it('renders the "save" button as disabled if everything is disabled', () => { + setMockValues({ + ...values, + stagedUpdates: true, + resultFieldsEmpty: true, + }); + const buttons = findButtons(subject()); + const saveButton = shallow(buttons[0]); + expect(saveButton.prop('disabled')).toBe(true); + }); + it('renders a "restore defaults" button that will reset all values to their defaults', () => { const buttons = findButtons(subject()); expect(buttons.length).toBe(3); 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 bea5bcc548fab5..51cdc3aea21f24 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 @@ -46,9 +46,13 @@ const UNSAVED_MESSAGE = i18n.translate( ); export const ResultSettings: React.FC = () => { - const { dataLoading, schema, stagedUpdates, resultFieldsAtDefaultSettings } = useValues( - ResultSettingsLogic - ); + const { + dataLoading, + schema, + stagedUpdates, + resultFieldsAtDefaultSettings, + resultFieldsEmpty, + } = useValues(ResultSettingsLogic); const { initializeResultSettingsData, saveResultSettings, @@ -81,7 +85,7 @@ export const ResultSettings: React.FC = () => { color="primary" fill onClick={saveResultSettings} - disabled={!stagedUpdates} + disabled={resultFieldsEmpty || !stagedUpdates} > {SAVE_BUTTON_LABEL} , 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 437949982cb5aa..e432ba6956094f 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 @@ -46,6 +46,7 @@ describe('ResultSettingsLogic', () => { const SELECTORS = { serverResultFields: {}, reducedServerResultFields: {}, + resultFieldsEmpty: true, resultFieldsAtDefaultSettings: true, stagedUpdates: false, nonTextResultFields: {}, @@ -333,6 +334,20 @@ describe('ResultSettingsLogic', () => { }); }); + describe('resultFieldsEmpty', () => { + it('should return true if no raw or snippet fields are enabled', () => { + mount({ + resultFields: { + foo: { raw: false }, + bar: {}, + baz: { raw: false, snippet: false }, + }, + }); + + expect(ResultSettingsLogic.values.resultFieldsEmpty).toEqual(true); + }); + }); + describe('stagedUpdates', () => { it('should return true if changes have been made since the last save', () => { mount({ 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 af78543cda2b23..4e738961f5e58e 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 @@ -24,6 +24,7 @@ import { import { areFieldsAtDefaultSettings, + areFieldsEmpty, clearAllFields, convertServerResultFieldsToResultFields, convertToServerFieldResultSetting, @@ -197,6 +198,10 @@ export const ResultSettingsLogic = kea [selectors.resultFields], (resultFields) => areFieldsAtDefaultSettings(resultFields), ], + resultFieldsEmpty: [ + () => [selectors.resultFields], + (resultFields) => areFieldsEmpty(resultFields), + ], stagedUpdates: [ () => [selectors.lastSavedResultFields, selectors.resultFields], (lastSavedResultFields, resultFields) => !isEqual(lastSavedResultFields, resultFields), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts index 6fee0a25003575..7e1d3d96c6d3f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.test.ts @@ -14,6 +14,7 @@ import { clearAllFields, resetAllFields, splitResultFields, + areFieldsEmpty, } from './utils'; describe('clearAllFields', () => { @@ -144,6 +145,37 @@ describe('splitResultFields', () => { }); }); +describe('areFieldsEmpty', () => { + it('should return true if all fields are empty or have all properties disabled', () => { + expect( + areFieldsEmpty({ + foo: {}, + bar: { raw: false, snippet: false }, + baz: { raw: false }, + }) + ).toBe(true); + }); + + it('should return false otherwise', () => { + expect( + areFieldsEmpty({ + foo: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + bar: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }) + ).toBe(false); + }); +}); + describe('areFieldsAtDefaultSettings', () => { it('will return true if all settings for all fields are at their defaults', () => { expect( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts index ff88aaac193d78..a67f092a5e7f72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/utils.ts @@ -112,6 +112,13 @@ export const splitResultFields = (resultFields: FieldResultSettingObject, schema return { textResultFields, nonTextResultFields }; }; +export const areFieldsEmpty = (fields: FieldResultSettingObject) => { + const anyNonEmptyField = Object.values(fields).find((field) => { + return (field as FieldResultSetting).raw || (field as FieldResultSetting).snippet; + }); + return !anyNonEmptyField; +}; + export const areFieldsAtDefaultSettings = (fields: FieldResultSettingObject) => { const anyNonDefaultSettingsValue = Object.values(fields).find((resultSettings) => { return !isEqual(resultSettings, DEFAULT_FIELD_SETTINGS); From d25bd680eb7ce479c11efeaf1b9d7b94f0d59b42 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 22 Apr 2021 12:01:45 -0400 Subject: [PATCH 13/33] Switch Tinymath to use peggy instead of pegjs (#97906) --- package.json | 1 + packages/kbn-tinymath/BUILD.bazel | 8 ++++---- .../kbn-tinymath/grammar/{grammar.pegjs => grammar.peggy} | 0 yarn.lock | 5 +++++ 4 files changed, 10 insertions(+), 4 deletions(-) rename packages/kbn-tinymath/grammar/{grammar.pegjs => grammar.peggy} (100%) diff --git a/package.json b/package.json index 992433e17e6c15..d4d3706d8b6b8c 100644 --- a/package.json +++ b/package.json @@ -300,6 +300,7 @@ "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pdfmake": "^0.1.65", + "peggy": "^1.0.0", "pegjs": "0.10.0", "pluralize": "3.1.0", "pngjs": "^3.4.0", diff --git a/packages/kbn-tinymath/BUILD.bazel b/packages/kbn-tinymath/BUILD.bazel index 9d521776fb4919..ae029c88774e84 100644 --- a/packages/kbn-tinymath/BUILD.bazel +++ b/packages/kbn-tinymath/BUILD.bazel @@ -1,5 +1,5 @@ load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("@npm//pegjs:index.bzl", "pegjs") +load("@npm//peggy:index.bzl", "peggy") PKG_BASE_NAME = "kbn-tinymath" PKG_REQUIRE_NAME = "@kbn/tinymath" @@ -30,16 +30,16 @@ DEPS = [ "@npm//lodash", ] -pegjs( +peggy( name = "grammar", data = [ - ":grammar/grammar.pegjs" + ":grammar/grammar.peggy" ], output_dir = True, args = [ "-o", "$(@D)/index.js", - "./%s/grammar/grammar.pegjs" % package_name() + "./%s/grammar/grammar.peggy" % package_name() ], ) diff --git a/packages/kbn-tinymath/grammar/grammar.pegjs b/packages/kbn-tinymath/grammar/grammar.peggy similarity index 100% rename from packages/kbn-tinymath/grammar/grammar.pegjs rename to packages/kbn-tinymath/grammar/grammar.peggy diff --git a/yarn.lock b/yarn.lock index f4d76841749673..465667230b6399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21252,6 +21252,11 @@ pdfmake@^0.1.65: pdfkit "^0.11.0" svg-to-pdfkit "^0.1.8" +peggy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/peggy/-/peggy-1.0.0.tgz#df6c7816c9df0ef35e071aaf96836cb866fe7eb4" + integrity sha512-lH12sxAXj4Aug+vH6IGoByIQOREIlhH+x4Uzb9kce6DD8wcGeidkC0JYEOwHormKrLt5BFLTbR4PuD/tiMOirQ== + pegjs@0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/pegjs/-/pegjs-0.10.0.tgz#cf8bafae6eddff4b5a7efb185269eaaf4610ddbd" From 97ebe11aac997e9df8638a1cfd1191f78f5e8993 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 22 Apr 2021 11:02:24 -0500 Subject: [PATCH 14/33] [ML] Fix Anomaly Detection influencer filter icons spacing too big (#97713) * [ML] Fix entity button too big * [ML] Consolidate sizing to eui size Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/components/entity_cell/entity_cell.scss | 5 +++-- .../components/influencers_list/_influencers_list.scss | 2 +- .../explorer_charts/_explorer_chart_tooltip.scss | 2 +- .../explorer_chart_label/_explorer_chart_label.scss | 4 +++- .../{entity_filter.scss => _entity_filter.scss} | 9 ++++++--- .../explorer_chart_label/entity_filter/entity_filter.tsx | 4 +--- 6 files changed, 15 insertions(+), 11 deletions(-) rename x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/{entity_filter.scss => _entity_filter.scss} (51%) diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.scss b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.scss index 60c1a0820fbc90..fc9cf149101380 100644 --- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.scss +++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.scss @@ -9,8 +9,9 @@ } .filter-button { - opacity: .3; - min-width: 14px; + opacity: .5; + width: $euiSize; + height: $euiSize; -webkit-transform: translateY(-1px); transform: translateY(-1px); diff --git a/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss b/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss index d2d57544de41a8..e33811aa9a8ccc 100644 --- a/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss +++ b/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss @@ -97,7 +97,7 @@ line-height: 14px; border-radius: $euiBorderRadius; padding: $euiSizeXS / 2; - margin-top: 3px; + margin-top: $euiSizeXS; display: inline-block; border: $euiBorderThin; } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss b/x-pack/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss index d291ff3d3cade1..80ac69d5b72894 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss @@ -8,7 +8,7 @@ } .ml-explorer-chart-info-tooltip .mlDescriptionList > * { - margin-top: 3px; + margin-top: $euiSizeXS; } .ml-explorer-chart-info-tooltip .mlDescriptionList { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss index bbe134af0202b4..011937c27758bd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss @@ -3,5 +3,7 @@ } .ml-explorer-chart-label-badges { - margin-top: 3px; + margin-top: $euiSizeXS; + display: flex; + align-items: center; } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.scss b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/_entity_filter.scss similarity index 51% rename from x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.scss rename to x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/_entity_filter.scss index 732b71d0565364..800c33e50689d6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.scss +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/_entity_filter.scss @@ -1,7 +1,10 @@ .filter-button { - opacity: .3; - min-width: 14px; - padding-right: 0; + opacity: .5; + width: $euiSize; + height: $euiSize; + + -webkit-transform: translateY(-1px); + transform: translateY(-1px); .euiIcon { width: $euiFontSizeXS; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx index 079af5827a4b51..2ede9d380f3bf9 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx @@ -12,7 +12,7 @@ import { ENTITY_FIELD_OPERATIONS, EntityFieldOperation, } from '../../../../../../../common/util/anomaly_utils'; -import './entity_filter.scss'; +import './_entity_filter.scss'; interface EntityFilterProps { onFilter: (params: { @@ -39,7 +39,6 @@ export const EntityFilter: FC = ({ } > @@ -65,7 +64,6 @@ export const EntityFilter: FC = ({ } > From 57f84f85934e02ac10b3c1fbcbb89e414ab04097 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 22 Apr 2021 11:25:10 -0500 Subject: [PATCH 15/33] [Fleet] Refactor setup to load default packages/policies with preconfiguration (#97328) Co-authored-by: Nicolas Chaulet Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/common/constants/agent_policy.ts | 35 --- x-pack/plugins/fleet/common/constants/epm.ts | 1 + .../common/constants/preconfiguration.ts | 60 +++++ .../common/types/models/preconfiguration.ts | 4 +- .../common/types/rest_spec/ingest_setup.ts | 5 +- .../fleet/public/applications/fleet/app.tsx | 11 +- .../plugins/fleet/server/constants/index.ts | 4 + x-pack/plugins/fleet/server/index.ts | 4 +- .../server/routes/setup/handlers.test.ts | 5 +- .../fleet/server/routes/setup/handlers.ts | 16 +- .../fleet/server/services/agent_policy.ts | 88 +++---- .../epm/packages/bulk_install_packages.ts | 59 ++++- .../ensure_installed_default_packages.test.ts | 147 ------------ .../server/services/epm/packages/install.ts | 102 +++----- .../server/services/preconfiguration.test.ts | 32 ++- .../fleet/server/services/preconfiguration.ts | 226 +++++++++++------- x-pack/plugins/fleet/server/services/setup.ts | 140 ++--------- .../server/types/models/preconfiguration.ts | 19 +- .../apis/preconfiguration/preconfiguration.ts | 1 + 19 files changed, 412 insertions(+), 547 deletions(-) delete mode 100644 x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index 859a96801595a4..d0bde39e9f2d71 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -5,44 +5,9 @@ * 2.0. */ -import type { AgentPolicy } from '../types'; - -import { defaultPackages } from './epm'; - export const AGENT_POLICY_SAVED_OBJECT_TYPE = 'ingest-agent-policies'; export const AGENT_POLICY_INDEX = '.fleet-policies'; export const agentPolicyStatuses = { Active: 'active', Inactive: 'inactive', } as const; - -export const DEFAULT_AGENT_POLICY: Omit< - AgentPolicy, - 'id' | 'updated_at' | 'updated_by' | 'revision' -> = { - name: 'Default policy', - namespace: 'default', - description: 'Default agent policy created by Kibana', - status: agentPolicyStatuses.Active, - package_policies: [], - is_default: true, - is_managed: false, - monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, -}; - -export const DEFAULT_FLEET_SERVER_AGENT_POLICY: Omit< - AgentPolicy, - 'id' | 'updated_at' | 'updated_by' | 'revision' -> = { - name: 'Default Fleet Server policy', - namespace: 'default', - description: 'Default Fleet Server agent policy created by Kibana', - status: agentPolicyStatuses.Active, - package_policies: [], - is_default: false, - is_default_fleet_server: true, - is_managed: false, - monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, -}; - -export const DEFAULT_AGENT_POLICIES_PACKAGES = [defaultPackages.System]; diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 7bf3c3e6205ec8..436eaf7cb8ae81 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -15,6 +15,7 @@ export const requiredPackages = { System: 'system', Endpoint: 'endpoint', ElasticAgent: 'elastic_agent', + FleetServer: FLEET_SERVER_PACKAGE, SecurityDetectionEngine: 'security_detection_engine', } as const; diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts index da011f31783c34..88ae8530244ca0 100644 --- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts @@ -5,7 +5,67 @@ * 2.0. */ +import type { PreconfiguredAgentPolicy } from '../types'; + +import { defaultPackages } from './epm'; + export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE = 'fleet-preconfiguration-deletion-record'; export const PRECONFIGURATION_LATEST_KEYWORD = 'latest'; + +type PreconfiguredAgentPolicyWithDefaultInputs = Omit< + PreconfiguredAgentPolicy, + 'package_policies' | 'id' +> & { + package_policies: Array>; +}; + +export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { + name: 'Default policy', + namespace: 'default', + description: 'Default agent policy created by Kibana', + package_policies: [ + { + name: `${defaultPackages.System}-1`, + package: { + name: defaultPackages.System, + }, + }, + ], + is_default: true, + is_managed: false, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, +}; + +export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { + name: 'Default Fleet Server policy', + namespace: 'default', + description: 'Default Fleet Server agent policy created by Kibana', + package_policies: [ + { + name: `${defaultPackages.FleetServer}-1`, + package: { + name: defaultPackages.FleetServer, + }, + }, + ], + is_default: false, + is_default_fleet_server: true, + is_managed: false, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, +}; + +export const DEFAULT_PACKAGES = Object.values(defaultPackages).map((name) => ({ + name, + version: PRECONFIGURATION_LATEST_KEYWORD, +})); + +// these are currently identical. we can separate if they later diverge +export const REQUIRED_PACKAGES = DEFAULT_PACKAGES; + +export interface PreconfigurationError { + package?: { name: string; version: string }; + agentPolicy?: { name: string }; + error: Error; +} diff --git a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts index 61a5cb63400a01..6087c910510cc7 100644 --- a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts @@ -20,9 +20,9 @@ export interface PreconfiguredAgentPolicy extends Omit> & { + Partial> & { name: string; - package: Partial; + package: Partial & { name: string }; inputs?: InputsOverride[]; } >; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts b/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts index 6f64f1c48336d1..91a1915c4c5187 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/ingest_setup.ts @@ -5,10 +5,7 @@ * 2.0. */ -import type { DefaultPackagesInstallationError } from '../models/epm'; - export interface PostIngestSetupResponse { isInitialized: boolean; - preconfigurationError?: { name: string; message: string }; - nonFatalPackageUpgradeErrors?: DefaultPackagesInstallationError[]; + nonFatalErrors?: Array<{ error: Error }>; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 4a47d39b77934d..5327d4b7cc4a4c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -83,20 +83,13 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { if (setupResponse.error) { setInitializationError(setupResponse.error); } - if (setupResponse.data?.preconfigurationError) { - notifications.toasts.addError(setupResponse.data.preconfigurationError, { + if (setupResponse.data?.nonFatalErrors?.length) { + notifications.toasts.addError(setupResponse.data.nonFatalErrors[0], { title: i18n.translate('xpack.fleet.setup.uiPreconfigurationErrorTitle', { defaultMessage: 'Configuration error', }), }); } - if (setupResponse.data?.nonFatalPackageUpgradeErrors) { - notifications.toasts.addError(setupResponse.data.nonFatalPackageUpgradeErrors, { - title: i18n.translate('xpack.fleet.setup.nonFatalPackageErrorsTitle', { - defaultMessage: 'One or more packages could not be successfully upgraded', - }), - }); - } } catch (err) { setInitializationError(err); } diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index aa4fbd9cfeb97b..e2f800f67705db 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -47,11 +47,15 @@ export { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, // Defaults DEFAULT_AGENT_POLICY, + DEFAULT_FLEET_SERVER_AGENT_POLICY, DEFAULT_OUTPUT, + DEFAULT_PACKAGES, + REQUIRED_PACKAGES, // Fleet Server index FLEET_SERVER_SERVERS_INDEX, ENROLLMENT_API_KEYS_INDEX, AGENTS_INDEX, + // Preconfiguration PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, PRECONFIGURATION_LATEST_KEYWORD, } from '../../common'; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 25298d991230de..e83617413b7442 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -58,8 +58,8 @@ export const config: PluginConfigDescriptor = { }) ), }), - packages: schema.maybe(PreconfiguredPackagesSchema), - agentPolicies: schema.maybe(PreconfiguredAgentPoliciesSchema), + packages: PreconfiguredPackagesSchema, + agentPolicies: PreconfiguredAgentPoliciesSchema, }), }; diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index fd32d699ae45e2..809a045478b032 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -48,13 +48,12 @@ describe('FleetSetupHandler', () => { mockSetupIngestManager.mockImplementation(() => Promise.resolve({ isInitialized: true, - preconfigurationError: undefined, - nonFatalPackageUpgradeErrors: [], + nonFatalErrors: [], }) ); await fleetSetupHandler(context, request, response); - const expectedBody: PostIngestSetupResponse = { isInitialized: true }; + const expectedBody: PostIngestSetupResponse = { isInitialized: true, nonFatalErrors: [] }; expect(response.customError).toHaveBeenCalledTimes(0); expect(response.ok).toHaveBeenCalledWith({ body: expectedBody }); }); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index 627f628f7b9fc1..370196cc202cd7 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -48,12 +48,18 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon const esClient = context.core.elasticsearch.client.asCurrentUser; const body: PostIngestSetupResponse = await setupIngestManager(soClient, esClient); - if (body.nonFatalPackageUpgradeErrors?.length === 0) { - delete body.nonFatalPackageUpgradeErrors; - } - return response.ok({ - body, + body: { + ...body, + nonFatalErrors: body.nonFatalErrors?.map((e) => { + // JSONify the error object so it can be displayed properly in the UI + const error = e.error ?? e; + return { + name: error.name, + message: error.message, + }; + }), + }, }); } catch (error) { return defaultIngestErrorHandler({ error, response }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index c0a2e80af4bf12..2b9cc4e0723044 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -16,7 +16,6 @@ import type { import type { AuthenticatedUser } from '../../../security/server'; import { - DEFAULT_AGENT_POLICY, AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, @@ -37,7 +36,6 @@ import { dataTypes, packageToPackagePolicy, AGENT_POLICY_INDEX, - DEFAULT_FLEET_SERVER_AGENT_POLICY, } from '../../common'; import type { DeleteAgentPolicyResponse, @@ -106,39 +104,6 @@ class AgentPolicyService { return (await this.get(soClient, id)) as AgentPolicy; } - public async ensureDefaultAgentPolicy( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient - ): Promise<{ - created: boolean; - policy: AgentPolicy; - }> { - const searchParams = { - searchFields: ['is_default'], - search: 'true', - }; - return await this.ensureAgentPolicy(soClient, esClient, DEFAULT_AGENT_POLICY, searchParams); - } - - public async ensureDefaultFleetServerAgentPolicy( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient - ): Promise<{ - created: boolean; - policy: AgentPolicy; - }> { - const searchParams = { - searchFields: ['is_default_fleet_server'], - search: 'true', - }; - return await this.ensureAgentPolicy( - soClient, - esClient, - DEFAULT_FLEET_SERVER_AGENT_POLICY, - searchParams - ); - } - public async ensurePreconfiguredAgentPolicy( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -148,22 +113,44 @@ class AgentPolicyService { policy?: AgentPolicy; }> { const { id, ...preconfiguredAgentPolicy } = omit(config, 'package_policies'); - const preconfigurationId = String(id); - const searchParams = { - searchFields: ['preconfiguration_id'], - search: escapeSearchQueryPhrase(preconfigurationId), - }; - const newAgentPolicyDefaults: Partial = { namespace: 'default', monitoring_enabled: ['logs', 'metrics'], }; - const newAgentPolicy = { - ...newAgentPolicyDefaults, - ...preconfiguredAgentPolicy, - preconfiguration_id: preconfigurationId, - } as NewAgentPolicy; + let searchParams; + let newAgentPolicy; + if (id) { + const preconfigurationId = String(id); + searchParams = { + searchFields: ['preconfiguration_id'], + search: escapeSearchQueryPhrase(preconfigurationId), + }; + + newAgentPolicy = { + ...newAgentPolicyDefaults, + ...preconfiguredAgentPolicy, + preconfiguration_id: preconfigurationId, + } as NewAgentPolicy; + } else if ( + preconfiguredAgentPolicy.is_default || + preconfiguredAgentPolicy.is_default_fleet_server + ) { + searchParams = { + searchFields: [ + preconfiguredAgentPolicy.is_default_fleet_server + ? 'is_default_fleet_server' + : 'is_default', + ], + search: 'true', + }; + + newAgentPolicy = { + ...newAgentPolicyDefaults, + ...preconfiguredAgentPolicy, + } as NewAgentPolicy; + } + if (!newAgentPolicy || !searchParams) throw new Error('Missing ID'); return await this.ensureAgentPolicy(soClient, esClient, newAgentPolicy, searchParams); } @@ -554,13 +541,14 @@ class AgentPolicyService { throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`); } - const { - policy: { id: defaultAgentPolicyId }, - } = await this.ensureDefaultAgentPolicy(soClient, esClient); - if (id === defaultAgentPolicyId) { + if (agentPolicy.is_default) { throw new Error('The default agent policy cannot be deleted'); } + if (agentPolicy.is_default_fleet_server) { + throw new Error('The default fleet server agent policy cannot be deleted'); + } + const { total } = await getAgentsByKuery(esClient, { showInactive: false, perPage: 0, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index 2c5b072aa39796..c77e2a0a22a0a7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -11,38 +11,68 @@ import { appContextService } from '../../app_context'; import * as Registry from '../registry'; import { installIndexPatterns } from '../kibana/index_pattern/install'; -import { installPackage } from './install'; +import type { InstallResult } from '../../../types'; + +import { installPackage, isPackageVersionOrLaterInstalled } from './install'; import type { BulkInstallResponse, IBulkInstallPackageError } from './install'; interface BulkInstallPackagesParams { savedObjectsClient: SavedObjectsClientContract; - packagesToInstall: string[]; + packagesToInstall: Array; esClient: ElasticsearchClient; + force?: boolean; } export async function bulkInstallPackages({ savedObjectsClient, packagesToInstall, esClient, + force, }: BulkInstallPackagesParams): Promise { const logger = appContextService.getLogger(); const installSource = 'registry'; - const latestPackagesResults = await Promise.allSettled( - packagesToInstall.map((packageName) => Registry.fetchFindLatestPackage(packageName)) + const packagesResults = await Promise.allSettled( + packagesToInstall.map((pkg) => { + if (typeof pkg === 'string') return Registry.fetchFindLatestPackage(pkg); + return Promise.resolve(pkg); + }) ); logger.debug(`kicking off bulk install of ${packagesToInstall.join(', ')} from registry`); const bulkInstallResults = await Promise.allSettled( - latestPackagesResults.map(async (result, index) => { - const packageName = packagesToInstall[index]; + packagesResults.map(async (result, index) => { + const packageName = getNameFromPackagesToInstall(packagesToInstall, index); if (result.status === 'fulfilled') { - const latestPackage = result.value; + const pkgKeyProps = result.value; + const installedPackageResult = await isPackageVersionOrLaterInstalled({ + savedObjectsClient, + pkgName: pkgKeyProps.name, + pkgVersion: pkgKeyProps.version, + }); + if (installedPackageResult) { + const { + name, + version, + installed_es: installedEs, + installed_kibana: installedKibana, + } = installedPackageResult.package; + return { + name, + version, + result: { + assets: [...installedEs, ...installedKibana], + status: 'already_installed', + installType: installedPackageResult.installType, + } as InstallResult, + }; + } const installResult = await installPackage({ savedObjectsClient, esClient, - pkgkey: Registry.pkgToPkgKey(latestPackage), + pkgkey: Registry.pkgToPkgKey(pkgKeyProps), installSource, skipPostInstall: true, + force, }); if (installResult.error) { return { @@ -53,7 +83,7 @@ export async function bulkInstallPackages({ } else { return { name: packageName, - version: latestPackage.version, + version: pkgKeyProps.version, result: installResult, }; } @@ -76,7 +106,7 @@ export async function bulkInstallPackages({ } return bulkInstallResults.map((result, index) => { - const packageName = packagesToInstall[index]; + const packageName = getNameFromPackagesToInstall(packagesToInstall, index); if (result.status === 'fulfilled') { if (result.value && result.value.error) { return { @@ -98,3 +128,12 @@ export function isBulkInstallError( ): installResponse is IBulkInstallPackageError { return 'error' in installResponse && installResponse.error instanceof Error; } + +function getNameFromPackagesToInstall( + packagesToInstall: BulkInstallPackagesParams['packagesToInstall'], + index: number +) { + const entry = packagesToInstall[index]; + if (typeof entry === 'string') return entry; + return entry.name; +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts deleted file mode 100644 index 60e2e5ea2cbf8b..00000000000000 --- a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ /dev/null @@ -1,147 +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 { SavedObject, SavedObjectsClientContract } from 'src/core/server'; - -import { ElasticsearchAssetType, KibanaSavedObjectType } from '../../../types'; -import type { Installation } from '../../../types'; - -jest.mock('./install'); -jest.mock('./bulk_install_packages'); -jest.mock('./get'); - -const { ensureInstalledDefaultPackages } = jest.requireActual('./install'); -const { isBulkInstallError: actualIsBulkInstallError } = jest.requireActual( - './bulk_install_packages' -); -// eslint-disable-next-line import/order -import { savedObjectsClientMock } from 'src/core/server/mocks'; - -import { appContextService } from '../../app_context'; -import { createAppContextStartContractMock } from '../../../mocks'; - -import { getInstallation } from './get'; -import { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; - -// if we add this assertion, TS will type check the return value -// and the editor will also know about .mockImplementation, .mock.calls, etc -const mockedBulkInstallPackages = bulkInstallPackages as jest.MockedFunction< - typeof bulkInstallPackages ->; -const mockedIsBulkInstallError = isBulkInstallError as jest.MockedFunction< - typeof isBulkInstallError ->; -const mockedGetInstallation = getInstallation as jest.MockedFunction; - -// I was unable to get the actual implementation set in the `jest.mock()` call at the top to work -// so this will set the `isBulkInstallError` function back to the actual implementation -mockedIsBulkInstallError.mockImplementation(actualIsBulkInstallError); - -const mockInstallation: SavedObject = { - id: 'test-pkg', - references: [], - type: 'epm-packages', - attributes: { - id: 'test-pkg', - installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], - installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], - package_assets: [], - es_index_patterns: { pattern: 'pattern-name' }, - name: 'test package', - version: '1.0.0', - install_status: 'installed', - install_version: '1.0.0', - install_started_at: new Date().toISOString(), - install_source: 'registry', - }, -}; - -describe('ensureInstalledDefaultPackages', () => { - let soClient: jest.Mocked; - beforeEach(async () => { - soClient = savedObjectsClientMock.create(); - appContextService.start(createAppContextStartContractMock()); - }); - afterEach(async () => { - appContextService.stop(); - }); - it('should return an array of Installation objects when successful', async () => { - mockedGetInstallation.mockImplementation(async () => { - return mockInstallation.attributes; - }); - mockedBulkInstallPackages.mockImplementationOnce(async function () { - return [ - { - name: mockInstallation.attributes.name, - result: { assets: [], status: 'installed', installType: 'install' }, - version: '', - statusCode: 200, - }, - ]; - }); - const resp = await ensureInstalledDefaultPackages(soClient, jest.fn()); - expect(resp.installations).toEqual([mockInstallation.attributes]); - }); - it('should throw the first Error it finds', async () => { - class SomeCustomError extends Error {} - mockedGetInstallation.mockImplementation(async () => { - return mockInstallation.attributes; - }); - mockedBulkInstallPackages.mockImplementationOnce(async function () { - return [ - { - name: 'success one', - result: { assets: [], status: 'installed', installType: 'install' }, - version: '', - statusCode: 200, - }, - { - name: 'success two', - result: { assets: [], status: 'installed', installType: 'install' }, - version: '', - statusCode: 200, - }, - { - name: 'failure one', - error: new SomeCustomError('abc 123'), - }, - { - name: 'success three', - result: { assets: [], status: 'installed', installType: 'install' }, - version: '', - statusCode: 200, - }, - { - name: 'failure two', - error: new Error('zzz'), - }, - ]; - }); - const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); - expect.assertions(2); - expect(installPromise).rejects.toThrow(SomeCustomError); - expect(installPromise).rejects.toThrow('abc 123'); - }); - it('should throw an error when get installation returns undefined', async () => { - mockedGetInstallation.mockImplementation(async () => { - return undefined; - }); - mockedBulkInstallPackages.mockImplementationOnce(async function () { - return [ - { - name: 'undefined package', - result: { assets: [], status: 'installed', installType: 'install' }, - version: '', - statusCode: 200, - }, - ]; - }); - const installPromise = ensureInstalledDefaultPackages(soClient, jest.fn()); - expect.assertions(1); - expect(installPromise).rejects.toThrow(); - }); -}); 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 ec1cc322475b03..48d66f06e17b94 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -5,19 +5,14 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import semverLt from 'semver/functions/lt'; import type Boom from '@hapi/boom'; import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'src/core/server'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { defaultPackages } from '../../../../common'; -import type { - BulkInstallPackageInfo, - InstallablePackage, - InstallSource, - DefaultPackagesInstallationError, -} from '../../../../common'; +import type { BulkInstallPackageInfo, InstallablePackage, InstallSource } from '../../../../common'; import { IngestManagerError, PackageOperationNotSupportedError, @@ -39,71 +34,30 @@ import { toAssetReference } from '../kibana/assets/install'; import type { ArchiveAsset } from '../kibana/assets/install'; import { installIndexPatterns } from '../kibana/index_pattern/install'; -import { - isRequiredPackage, - getInstallation, - getInstallationObject, - bulkInstallPackages, - isBulkInstallError, -} from './index'; +import { isRequiredPackage, getInstallation, getInstallationObject } from './index'; import { removeInstallation } from './remove'; import { getPackageSavedObjects } from './get'; import { _installPackage } from './_install_package'; -export interface DefaultPackagesInstallationResult { - installations: Installation[]; - nonFatalPackageUpgradeErrors: DefaultPackagesInstallationError[]; -} - -export async function ensureInstalledDefaultPackages( - savedObjectsClient: SavedObjectsClientContract, - esClient: ElasticsearchClient -): Promise { - const installations = []; - const nonFatalPackageUpgradeErrors = []; - const bulkResponse = await bulkInstallPackages({ - savedObjectsClient, - packagesToInstall: Object.values(defaultPackages), - esClient, - }); - - for (const resp of bulkResponse) { - if (isBulkInstallError(resp)) { - if (resp.installType && (resp.installType === 'update' || resp.installType === 'reupdate')) { - nonFatalPackageUpgradeErrors.push({ installType: resp.installType, error: resp.error }); - } else { - throw resp.error; - } - } else { - installations.push(getInstallation({ savedObjectsClient, pkgName: resp.name })); - } - } - - const retrievedInstallations = await Promise.all(installations); - const verifiedInstallations = retrievedInstallations.map((installation, index) => { - if (!installation) { - throw new Error(`could not get installation ${bulkResponse[index].name}`); - } - return installation; - }); - return { - installations: verifiedInstallations, - nonFatalPackageUpgradeErrors, - }; -} - -async function isPackageVersionOrLaterInstalled(options: { +export async function isPackageVersionOrLaterInstalled(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; -}): Promise { +}): Promise<{ package: Installation; installType: InstallType } | false> { const { savedObjectsClient, pkgName, pkgVersion } = options; - const installedPackage = await getInstallation({ savedObjectsClient, pkgName }); + const installedPackageObject = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedPackage = installedPackageObject?.attributes; if ( installedPackage && (installedPackage.version === pkgVersion || semverLt(pkgVersion, installedPackage.version)) ) { - return installedPackage; + let installType: InstallType; + try { + installType = getInstallType({ pkgVersion, installedPkg: installedPackageObject }); + } catch (e) { + installType = 'unknown'; + } + return { package: installedPackage, installType }; } return false; } @@ -121,16 +75,16 @@ export async function ensureInstalledPackage(options: { ? { name: pkgName, version: pkgVersion } : await Registry.fetchFindLatestPackage(pkgName); - const installedPackage = await isPackageVersionOrLaterInstalled({ + const installedPackageResult = await isPackageVersionOrLaterInstalled({ savedObjectsClient, pkgName: pkgKeyProps.name, pkgVersion: pkgKeyProps.version, }); - if (installedPackage) { - return installedPackage; + if (installedPackageResult) { + return installedPackageResult.package; } const pkgkey = Registry.pkgToPkgKey(pkgKeyProps); - await installPackage({ + const installResult = await installPackage({ installSource: 'registry', savedObjectsClient, pkgkey, @@ -138,6 +92,26 @@ export async function ensureInstalledPackage(options: { force: true, // Always force outdated packages to be installed if a later version isn't installed }); + if (installResult.error) { + const errorPrefix = + installResult.installType === 'update' || installResult.installType === 'reupdate' + ? i18n.translate('xpack.fleet.epm.install.packageUpdateError', { + defaultMessage: 'Error updating {pkgName} to {pkgVersion}', + values: { + pkgName: pkgKeyProps.name, + pkgVersion: pkgKeyProps.version, + }, + }) + : i18n.translate('xpack.fleet.epm.install.packageInstallError', { + defaultMessage: 'Error installing {pkgName} {pkgVersion}', + values: { + pkgName: pkgKeyProps.name, + pkgVersion: pkgKeyProps.version, + }, + }); + throw new Error(`${errorPrefix}: ${installResult.error.message}`); + } + const installation = await getInstallation({ savedObjectsClient, pkgName }); if (!installation) throw new Error(`could not get installation ${pkgName}`); return installation; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index d60b8fde2aa8d5..f7a4c6d9e670f1 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -71,15 +71,8 @@ function getPutPreconfiguredPackagesMock() { } jest.mock('./epm/packages/install', () => ({ - ensureInstalledPackage({ - pkgName, - pkgVersion, - force, - }: { - pkgName: string; - pkgVersion: string; - force?: boolean; - }) { + installPackage({ pkgkey, force }: { pkgkey: string; force?: boolean }) { + const [pkgName, pkgVersion] = pkgkey.split('-'); const installedPackage = mockInstalledPackages.get(pkgName); if (installedPackage) { if (installedPackage.version === pkgVersion) return installedPackage; @@ -87,8 +80,15 @@ jest.mock('./epm/packages/install', () => ({ const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName }; mockInstalledPackages.set(pkgName, packageInstallation); + return packageInstallation; }, + ensurePackagesCompletedInstall() { + return []; + }, + isPackageVersionOrLaterInstalled() { + return false; + }, })); jest.mock('./epm/packages/get', () => ({ @@ -117,6 +117,20 @@ jest.mock('./package_policy', () => ({ }, })); +jest.mock('./app_context', () => ({ + appContextService: { + getLogger: () => + new Proxy( + {}, + { + get() { + return jest.fn(); + }, + } + ), + }, +})); + describe('policy preconfiguration', () => { beforeEach(() => { mockInstalledPackages.clear(); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index ffb16d286c45ae..77230c01cdcb88 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -18,6 +18,7 @@ import type { NewPackagePolicyInputStream, PreconfiguredAgentPolicy, PreconfiguredPackage, + PreconfigurationError, } from '../../common'; import { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, @@ -28,9 +29,16 @@ import { escapeSearchQueryPhrase } from './saved_object'; import { pkgToPkgKey } from './epm/registry'; import { getInstallation } from './epm/packages'; -import { ensureInstalledPackage } from './epm/packages/install'; +import { ensurePackagesCompletedInstall } from './epm/packages/install'; +import { bulkInstallPackages } from './epm/packages/bulk_install_packages'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; +interface PreconfigurationResult { + policies: Array<{ id: string; updated_at: string }>; + packages: string[]; + nonFatalErrors?: PreconfigurationError[]; +} + export type InputsOverride = Partial & { vars?: Array; }; @@ -41,7 +49,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( policies: PreconfiguredAgentPolicy[] = [], packages: PreconfiguredPackage[] = [], defaultOutput: Output -) { +): Promise { // Validate configured packages to ensure there are no version conflicts const packageNames = groupBy(packages, (pkg) => pkg.name); const duplicatePackages = Object.entries(packageNames).filter( @@ -66,28 +74,64 @@ export async function ensurePreconfiguredPackagesAndPolicies( } // Preinstall packages specified in Kibana config - const preconfiguredPackages = await Promise.all( - packages.map(({ name, version }) => - ensureInstalledPreconfiguredPackage(soClient, esClient, name, version) - ) - ); + const preconfiguredPackages = await bulkInstallPackages({ + savedObjectsClient: soClient, + esClient, + packagesToInstall: packages.map((pkg) => + pkg.version === PRECONFIGURATION_LATEST_KEYWORD ? pkg.name : pkg + ), + force: true, // Always force outdated packages to be installed if a later version isn't installed + }); + + const fulfilledPackages = []; + const rejectedPackages = []; + for (let i = 0; i < preconfiguredPackages.length; i++) { + const packageResult = preconfiguredPackages[i]; + if ('error' in packageResult) + rejectedPackages.push({ + package: { name: packages[i].name, version: packages[i].version }, + error: packageResult.error, + } as PreconfigurationError); + else fulfilledPackages.push(packageResult); + } + + // Keeping this outside of the Promise.all because it introduces a race condition. + // If one of the required packages fails to install/upgrade it might get stuck in the installing state. + // On the next call to the /setup API, if there is a upgrade available for one of the required packages a race condition + // will occur between upgrading the package and reinstalling the previously failed package. + // By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any + // packages that are stuck in the installing state. + await ensurePackagesCompletedInstall(soClient, esClient); // Create policies specified in Kibana config - const preconfiguredPolicies = await Promise.all( + const preconfiguredPolicies = await Promise.allSettled( policies.map(async (preconfiguredAgentPolicy) => { - // Check to see if a preconfigured policy with the same preconfigurationId was already deleted by the user - const preconfigurationId = String(preconfiguredAgentPolicy.id); - const searchParams = { - searchFields: ['preconfiguration_id'], - search: escapeSearchQueryPhrase(preconfigurationId), - }; - const deletionRecords = await soClient.find({ - type: PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, - ...searchParams, - }); - const wasDeleted = deletionRecords.total > 0; - if (wasDeleted) { - return { created: false, deleted: preconfigurationId }; + if (preconfiguredAgentPolicy.id) { + // Check to see if a preconfigured policy with the same preconfigurationId was already deleted by the user + const preconfigurationId = String(preconfiguredAgentPolicy.id); + const searchParams = { + searchFields: ['preconfiguration_id'], + search: escapeSearchQueryPhrase(preconfigurationId), + }; + const deletionRecords = await soClient.find({ + type: PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, + ...searchParams, + }); + const wasDeleted = deletionRecords.total > 0; + if (wasDeleted) { + return { created: false, deleted: preconfigurationId }; + } + } else if ( + !preconfiguredAgentPolicy.is_default && + !preconfiguredAgentPolicy.is_default_fleet_server + ) { + throw new Error( + i18n.translate('xpack.fleet.preconfiguration.missingIDError', { + defaultMessage: + '{agentPolicyName} is missing an `id` field. `id` is required, except for policies marked is_default or is_default_fleet_server.', + values: { agentPolicyName: preconfiguredAgentPolicy.name }, + }) + ); } const { created, policy } = await agentPolicyService.ensurePreconfiguredAgentPolicy( @@ -132,13 +176,24 @@ export async function ensurePreconfiguredPackagesAndPolicies( }) ); - for (const preconfiguredPolicy of preconfiguredPolicies) { + const fulfilledPolicies = []; + const rejectedPolicies = []; + for (let i = 0; i < preconfiguredPolicies.length; i++) { + const policyResult = preconfiguredPolicies[i]; + if (policyResult.status === 'rejected') { + rejectedPolicies.push({ + error: policyResult.reason as Error, + agentPolicy: { name: policies[i].name }, + } as PreconfigurationError); + continue; + } + fulfilledPolicies.push(policyResult.value); const { created, policy, installedPackagePolicies, shouldAddIsManagedFlag, - } = preconfiguredPolicy; + } = policyResult.value; if (created) { await addPreconfiguredPolicyPackages( soClient, @@ -155,21 +210,22 @@ export async function ensurePreconfiguredPackagesAndPolicies( } return { - policies: preconfiguredPolicies.map((p) => + policies: fulfilledPolicies.map((p) => p.policy ? { - id: p.policy.id, + id: p.policy.id!, updated_at: p.policy.updated_at, } : { - id: p.deleted, + id: p.deleted!, updated_at: i18n.translate('xpack.fleet.preconfiguration.policyDeleted', { defaultMessage: 'Preconfigured policy {id} was deleted; skipping creation', values: { id: p.deleted }, }), } ), - packages: preconfiguredPackages.map((pkg) => pkgToPkgKey(pkg)), + packages: fulfilledPackages.map((pkg) => pkgToPkgKey(pkg)), + nonFatalErrors: [...rejectedPackages, ...rejectedPolicies], }; } @@ -201,21 +257,6 @@ async function addPreconfiguredPolicyPackages( } } -async function ensureInstalledPreconfiguredPackage( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - pkgName: string, - pkgVersion: string -) { - const isLatest = pkgVersion === PRECONFIGURATION_LATEST_KEYWORD; - return ensureInstalledPackage({ - savedObjectsClient: soClient, - pkgName, - esClient, - pkgVersion: isLatest ? undefined : pkgVersion, - }); -} - function overridePackageInputs( basePackagePolicy: NewPackagePolicy, inputsOverride?: InputsOverride[] @@ -228,15 +269,19 @@ function overridePackageInputs( for (const override of inputsOverride) { const originalInput = inputs.find((i) => i.type === override.type); if (!originalInput) { - throw new Error( - i18n.translate('xpack.fleet.packagePolicyInputOverrideError', { - defaultMessage: 'Input type {inputType} does not exist on package {packageName}', - values: { - inputType: override.type, - packageName, - }, - }) - ); + const e = { + error: new Error( + i18n.translate('xpack.fleet.packagePolicyInputOverrideError', { + defaultMessage: 'Input type {inputType} does not exist on package {packageName}', + values: { + inputType: override.type, + packageName, + }, + }) + ), + package: { name: packageName, version: basePackagePolicy.package!.version }, + }; + throw e; } if (typeof override.enabled !== 'undefined') originalInput.enabled = override.enabled; @@ -245,16 +290,21 @@ function overridePackageInputs( try { deepMergeVars(override, originalInput); } catch (e) { - throw new Error( - i18n.translate('xpack.fleet.packagePolicyVarOverrideError', { - defaultMessage: 'Var {varName} does not exist on {inputType} of package {packageName}', - values: { - varName: e.message, - inputType: override.type, - packageName, - }, - }) - ); + const err = { + error: new Error( + i18n.translate('xpack.fleet.packagePolicyVarOverrideError', { + defaultMessage: + 'Var {varName} does not exist on {inputType} of package {packageName}', + values: { + varName: e.message, + inputType: override.type, + packageName, + }, + }) + ), + package: { name: packageName, version: basePackagePolicy.package!.version }, + }; + throw err; } } @@ -264,17 +314,21 @@ function overridePackageInputs( (s) => s.data_stream.dataset === stream.data_stream.dataset ); if (!originalStream) { - throw new Error( - i18n.translate('xpack.fleet.packagePolicyStreamOverrideError', { - defaultMessage: - 'Data stream {streamSet} does not exist on {inputType} of package {packageName}', - values: { - streamSet: stream.data_stream.dataset, - inputType: override.type, - packageName, - }, - }) - ); + const e = { + error: new Error( + i18n.translate('xpack.fleet.packagePolicyStreamOverrideError', { + defaultMessage: + 'Data stream {streamSet} does not exist on {inputType} of package {packageName}', + values: { + streamSet: stream.data_stream.dataset, + inputType: override.type, + packageName, + }, + }) + ), + package: { name: packageName, version: basePackagePolicy.package!.version }, + }; + throw e; } if (typeof stream.enabled !== 'undefined') originalStream.enabled = stream.enabled; @@ -283,18 +337,22 @@ function overridePackageInputs( try { deepMergeVars(stream as InputsOverride, originalStream); } catch (e) { - throw new Error( - i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', { - defaultMessage: - 'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}', - values: { - varName: e.message, - streamSet: stream.data_stream.dataset, - inputType: override.type, - packageName, - }, - }) - ); + const err = { + error: new Error( + i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', { + defaultMessage: + 'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}', + values: { + varName: e.message, + streamSet: stream.data_stream.dataset, + inputType: override.type, + packageName, + }, + }) + ), + package: { name: packageName, version: basePackagePolicy.package!.version }, + }; + throw err; } } } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index de6876c7f6fda8..0723186569df8a 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -6,23 +6,15 @@ */ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; -import { DEFAULT_AGENT_POLICIES_PACKAGES, FLEET_SERVER_PACKAGE } from '../../common'; - -import type { PackagePolicy, DefaultPackagesInstallationError } from '../../common'; - -import { SO_SEARCH_LIMIT } from '../constants'; +import type { DefaultPackagesInstallationError, PreconfigurationError } from '../../common'; +import { SO_SEARCH_LIMIT, REQUIRED_PACKAGES } from '../constants'; import { appContextService } from './app_context'; -import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; +import { agentPolicyService } from './agent_policy'; import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration'; import { outputService } from './output'; -import { - ensureInstalledDefaultPackages, - ensureInstalledPackage, - ensurePackagesCompletedInstall, -} from './epm/packages/install'; + import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys'; import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; @@ -32,8 +24,7 @@ import { awaitIfFleetServerSetupPending } from './fleet_server'; export interface SetupStatus { isInitialized: boolean; - preconfigurationError: { name: string; message: string } | undefined; - nonFatalPackageUpgradeErrors: DefaultPackagesInstallationError[]; + nonFatalErrors?: Array; } export async function setupIngestManager( @@ -47,9 +38,7 @@ async function createSetupSideEffects( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { - const [defaultPackagesResult, defaultOutput] = await Promise.all([ - // packages installed by default - ensureInstalledDefaultPackages(soClient, esClient), + const [defaultOutput] = await Promise.all([ outputService.ensureDefaultOutput(soClient), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { @@ -61,122 +50,35 @@ async function createSetupSideEffects( }), ]); - // Keeping this outside of the Promise.all because it introduces a race condition. - // If one of the required packages fails to install/upgrade it might get stuck in the installing state. - // On the next call to the /setup API, if there is a upgrade available for one of the required packages a race condition - // will occur between upgrading the package and reinstalling the previously failed package. - // By moving this outside of the Promise.all, the upgrade will occur first, and then we'll attempt to reinstall any - // packages that are stuck in the installing state. - await ensurePackagesCompletedInstall(soClient, esClient); - await awaitIfFleetServerSetupPending(); - const fleetServerPackage = await ensureInstalledPackage({ - savedObjectsClient: soClient, - pkgName: FLEET_SERVER_PACKAGE, - esClient, - }); - const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } = appContextService.getConfig() ?? {}; const policies = policiesOrUndefined ?? []; - const packages = packagesOrUndefined ?? []; - let preconfigurationError; - - try { - await ensurePreconfiguredPackagesAndPolicies( - soClient, - esClient, - policies, - packages, - defaultOutput - ); - } catch (e) { - preconfigurationError = { name: e.name, message: e.message }; - } - // Ensure the predefined default policies AFTER loading preconfigured policies. This allows the kibana config - // to override the default agent policies. - - const [ - { created: defaultAgentPolicyCreated, policy: defaultAgentPolicy }, - { created: defaultFleetServerPolicyCreated, policy: defaultFleetServerPolicy }, - ] = await Promise.all([ - agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), - agentPolicyService.ensureDefaultFleetServerAgentPolicy(soClient, esClient), - ]); + let packages = packagesOrUndefined ?? []; + // Ensure that required packages are always installed even if they're left out of the config + const preconfiguredPackageNames = new Set(packages.map((pkg) => pkg.name)); + packages = [ + ...packages, + ...REQUIRED_PACKAGES.filter((pkg) => !preconfiguredPackageNames.has(pkg.name)), + ]; - // If we just created the default fleet server policy add the fleet server package - if (defaultFleetServerPolicyCreated) { - await addPackageToAgentPolicy( - soClient, - esClient, - fleetServerPackage, - defaultFleetServerPolicy, - defaultOutput - ); - } - - // If we just created the default policy, ensure default packages are added to it - if (defaultAgentPolicyCreated) { - const agentPolicyWithPackagePolicies = await agentPolicyService.get( - soClient, - defaultAgentPolicy.id, - true - ); - if (!agentPolicyWithPackagePolicies) { - throw new Error( - i18n.translate('xpack.fleet.setup.policyNotFoundError', { - defaultMessage: 'Policy not found', - }) - ); - } - if ( - agentPolicyWithPackagePolicies.package_policies.length && - typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string' - ) { - throw new Error( - i18n.translate('xpack.fleet.setup.policyNotFoundError', { - defaultMessage: 'Policy not found', - }) - ); - } - - for (const installedPackage of defaultPackagesResult.installations) { - const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some( - (packageName) => installedPackage.name === packageName - ); - if (!packageShouldBeInstalled) { - continue; - } - - const isInstalled = agentPolicyWithPackagePolicies.package_policies.some( - (d: PackagePolicy | string) => { - return typeof d !== 'string' && d.package?.name === installedPackage.name; - } - ); - - if (!isInstalled) { - await addPackageToAgentPolicy( - soClient, - esClient, - installedPackage, - agentPolicyWithPackagePolicies, - defaultOutput - ); - } - } - } + const { nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + policies, + packages, + defaultOutput + ); await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); - await ensureAgentActionPolicyChangeExists(soClient, esClient); return { isInitialized: true, - preconfigurationError, - nonFatalPackageUpgradeErrors: defaultPackagesResult.nonFatalPackageUpgradeErrors, + nonFatalErrors, }; } diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 11336af6c2635c..5b871b80a6bbd6 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -8,7 +8,12 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import semverValid from 'semver/functions/valid'; -import { PRECONFIGURATION_LATEST_KEYWORD } from '../../constants'; +import { + PRECONFIGURATION_LATEST_KEYWORD, + DEFAULT_AGENT_POLICY, + DEFAULT_FLEET_SERVER_AGENT_POLICY, + DEFAULT_PACKAGES, +} from '../../constants'; import { AgentPolicyBaseSchema } from './agent_policy'; import { NamespaceSchema } from './package_policy'; @@ -36,14 +41,17 @@ export const PreconfiguredPackagesSchema = schema.arrayOf( } }, }), - }) + }), + { + defaultValue: DEFAULT_PACKAGES, + } ); export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( schema.object({ ...AgentPolicyBaseSchema, namespace: schema.maybe(NamespaceSchema), - id: schema.oneOf([schema.string(), schema.number()]), + id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), is_default: schema.maybe(schema.boolean()), is_default_fleet_server: schema.maybe(schema.boolean()), package_policies: schema.arrayOf( @@ -77,5 +85,8 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( ), }) ), - }) + }), + { + defaultValue: [DEFAULT_AGENT_POLICY, DEFAULT_FLEET_SERVER_AGENT_POLICY], + } ); diff --git a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts index a6ae2d34ed8dae..7d9534cae364a6 100644 --- a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts +++ b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts @@ -41,6 +41,7 @@ export default function (providerContext: FtrProviderContext) { expect(body).to.eql({ packages: [], policies: [], + nonFatalErrors: [], }); }); }); From a8a8289b2005c49848062daf32c6e283ff7f7753 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 22 Apr 2021 17:55:38 +0100 Subject: [PATCH 16/33] chore(NA): chore(NA): moving @kbn/utils into bazel (#97833) * chore(NA): chore(NA): moving @kbn/utils into bazel * chore(NA): run kbn-test integration test with preserve-symlinks --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-apm-config-loader/package.json | 3 +- packages/kbn-cli-dev-mode/package.json | 3 +- packages/kbn-config-schema/BUILD.bazel | 3 +- packages/kbn-dev-utils/package.json | 3 - packages/kbn-docs-utils/package.json | 1 - packages/kbn-es-archiver/package.json | 3 +- packages/kbn-legacy-logging/package.json | 3 - packages/kbn-pm/package.json | 3 - packages/kbn-test/package.json | 3 +- .../integration_tests/junit_reporter.test.ts | 9 +- packages/kbn-utils/BUILD.bazel | 82 +++++++++++++++++++ packages/kbn-utils/package.json | 7 +- packages/kbn-utils/tsconfig.json | 2 +- yarn.lock | 2 +- 17 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 packages/kbn-utils/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 610d78bacccd4b..86f9f7562434e4 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -68,4 +68,5 @@ yarn kbn watch-bazel - @kbn/std - @kbn/tinymath - @kbn/utility-types +- @kbn/utils diff --git a/package.json b/package.json index d4d3706d8b6b8c..b21ad0021656e1 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "@kbn/ui-framework": "link:packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:packages/kbn-ui-shared-deps", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types/npm_module", - "@kbn/utils": "link:packages/kbn-utils", + "@kbn/utils": "link:bazel-bin/packages/kbn-utils/npm_module", "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", "@mapbox/geojson-rewind": "^0.5.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 552eed64d418cd..5c3172a6c636a2 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -10,5 +10,6 @@ filegroup( "//packages/kbn-std:build", "//packages/kbn-tinymath:build", "//packages/kbn-utility-types:build", + "//packages/kbn-utils:build", ], ) diff --git a/packages/kbn-apm-config-loader/package.json b/packages/kbn-apm-config-loader/package.json index 214b8510ba69c6..d198ee57c619d4 100644 --- a/packages/kbn-apm-config-loader/package.json +++ b/packages/kbn-apm-config-loader/package.json @@ -11,7 +11,6 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@elastic/safer-lodash-set": "link:../elastic-safer-lodash-set", - "@kbn/utils": "link:../kbn-utils" + "@elastic/safer-lodash-set": "link:../elastic-safer-lodash-set" } } \ No newline at end of file diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index 2ffa09d7e1604a..cc91be0df45508 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -18,7 +18,6 @@ "@kbn/logging": "link:../kbn-logging", "@kbn/server-http-tools": "link:../kbn-server-http-tools", "@kbn/optimizer": "link:../kbn-optimizer", - "@kbn/dev-utils": "link:../kbn-dev-utils", - "@kbn/utils": "link:../kbn-utils" + "@kbn/dev-utils": "link:../kbn-dev-utils" } } \ No newline at end of file diff --git a/packages/kbn-config-schema/BUILD.bazel b/packages/kbn-config-schema/BUILD.bazel index 5dcbd9e5a802a2..0c6b3c10db4fd9 100644 --- a/packages/kbn-config-schema/BUILD.bazel +++ b/packages/kbn-config-schema/BUILD.bazel @@ -63,7 +63,7 @@ ts_project( js_library( name = PKG_BASE_NAME, - srcs = [], + srcs = NPM_MODULE_EXTRA_FILES, deps = [":tsc"] + DEPS, package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], @@ -71,7 +71,6 @@ js_library( pkg_npm( name = "npm_module", - srcs = NPM_MODULE_EXTRA_FILES, deps = [ ":%s" % PKG_BASE_NAME, ] diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 87e142c3bece7d..4ce2880afbbdad 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -13,9 +13,6 @@ "kibana": { "devOnly": true }, - "dependencies": { - "@kbn/utils": "link:../kbn-utils" - }, "devDependencies": { "@kbn/expect": "link:../kbn-expect" } diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 26a7fa0e8c9576..e2db07001b5432 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -13,7 +13,6 @@ "kbn:watch": "../../node_modules/.bin/tsc --watch" }, "dependencies": { - "@kbn/utils": "link:../kbn-utils", "@kbn/config": "link:../kbn-config", "@kbn/dev-utils": "link:../kbn-dev-utils" } diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 047d1dd675d263..0e4c9884d2c390 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -14,7 +14,6 @@ }, "dependencies": { "@kbn/dev-utils": "link:../kbn-dev-utils", - "@kbn/test": "link:../kbn-test", - "@kbn/utils": "link:../kbn-utils" + "@kbn/test": "link:../kbn-test" } } \ No newline at end of file diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json index 9450fd39607ea9..8c26535b9b48f8 100644 --- a/packages/kbn-legacy-logging/package.json +++ b/packages/kbn-legacy-logging/package.json @@ -9,8 +9,5 @@ "build": "tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" - }, - "dependencies": { - "@kbn/utils": "link:../kbn-utils" } } diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 050aadd402d8a5..c46906112b2e22 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -14,8 +14,5 @@ }, "devDependencies": { "@kbn/dev-utils": "link:../kbn-dev-utils" - }, - "dependencies": { - "@kbn/utils": "link:../kbn-utils" } } \ No newline at end of file diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 2afbe41e0e00ec..9bf8a01e031cc0 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -20,7 +20,6 @@ }, "devDependencies": { "@kbn/dev-utils": "link:../kbn-dev-utils", - "@kbn/expect": "link:../kbn-expect", - "@kbn/utils": "link:../kbn-utils" + "@kbn/expect": "link:../kbn-expect" } } \ No newline at end of file diff --git a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts index 9a94ff41eb6b98..f2bf25067a9bdf 100644 --- a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts +++ b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts @@ -30,8 +30,13 @@ it( 'produces a valid junit report for failures', async () => { const result = await execa( - './node_modules/.bin/jest', - ['--config', 'packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js'], + 'node', + [ + '--preserve-symlinks', + './node_modules/.bin/jest', + '--config', + 'packages/kbn-test/src/jest/integration_tests/__fixtures__/jest.config.js', + ], { cwd: REPO_ROOT, env: { diff --git a/packages/kbn-utils/BUILD.bazel b/packages/kbn-utils/BUILD.bazel new file mode 100644 index 00000000000000..57aee048746b44 --- /dev/null +++ b/packages/kbn-utils/BUILD.bazel @@ -0,0 +1,82 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-utils" +PKG_REQUIRE_NAME = "@kbn/utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "@npm//load-json-file", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-utils/package.json b/packages/kbn-utils/package.json index 2c3c0c11b65ab8..c404613d2c8e10 100644 --- a/packages/kbn-utils/package.json +++ b/packages/kbn-utils/package.json @@ -4,10 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "rm -rf target && ../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "private": true } \ No newline at end of file diff --git a/packages/kbn-utils/tsconfig.json b/packages/kbn-utils/tsconfig.json index e6c83767c30dc0..0c7657ba55ee60 100644 --- a/packages/kbn-utils/tsconfig.json +++ b/packages/kbn-utils/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "declaration": true, "declarationMap": true, diff --git a/yarn.lock b/yarn.lock index 465667230b6399..bc87ff50c55e8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2721,7 +2721,7 @@ version "0.0.0" uid "" -"@kbn/utils@link:packages/kbn-utils": +"@kbn/utils@link:bazel-bin/packages/kbn-utils/npm_module": version "0.0.0" uid "" From 55f3b8975e168be930b75eada0533985b3245254 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 22 Apr 2021 18:02:21 +0100 Subject: [PATCH 17/33] [Discover Index Pattern management] Add more unit tests (#97749) * [Discover Index Pattern management] Add more unit tests * [Discover Index Pattern management] Fix typo * Fix failing unit test --- ...ver_index_pattern_management.test.tsx.snap | 79 ++++++++++++++++--- ...discover_index_pattern_management.test.tsx | 55 +++++++++++-- .../discover_index_pattern_management.tsx | 1 + 3 files changed, 116 insertions(+), 19 deletions(-) diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index 44b8cbb8b839a3..f94a73ae05a554 100644 --- a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -634,21 +634,16 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` "show": true, }, }, + "core": Object { + "application": Object { + "navigateToApp": [MockFunction], + }, + }, "history": [Function], "indexPatternFieldEditor": Object { "openEditor": [MockFunction], "userPermissions": Object { - "editIndexPattern": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, + "editIndexPattern": [Function], }, }, "uiSettings": Object { @@ -657,5 +652,65 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` } } useNewFieldsApi={true} -/> +> + + } + closePopover={[Function]} + data-test-subj="discover-addRuntimeField-popover" + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="s" + > +
+
+ + + +
+
+
+ `; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx index 88644dc213fd66..5a954270fdf587 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx @@ -14,6 +14,8 @@ import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; +import { EuiContextMenuPanel, EuiPopover, EuiContextMenuItem } from '@elastic/eui'; +import { findTestSubject } from '@kbn/test/jest'; const mockServices = ({ history: () => ({ @@ -29,6 +31,11 @@ const mockServices = ({ save: false, }, }, + core: { + application: { + navigateToApp: jest.fn(), + }, + }, uiSettings: { get: (key: string) => { if (key === 'fields:popularLimit') { @@ -39,15 +46,13 @@ const mockServices = ({ indexPatternFieldEditor: { openEditor: jest.fn(), userPermissions: { - editIndexPattern: jest.fn(), + editIndexPattern: () => { + return true; + }, }, }, } as unknown) as DiscoverServices; -jest.mock('../../../kibana_services', () => ({ - getServices: () => mockServices, -})); - describe('Discover IndexPattern Management', () => { const indexPattern = getStubIndexPattern( 'logstash-*', @@ -59,8 +64,8 @@ describe('Discover IndexPattern Management', () => { const editField = jest.fn(); - test('renders correctly', () => { - const component = mountWithIntl( + const mountComponent = () => { + return mountWithIntl( { useNewFieldsApi={true} /> ); + }; + + test('renders correctly', () => { + const component = mountComponent(); expect(component).toMatchSnapshot(); + expect(component.find(EuiPopover).length).toBe(1); + }); + + test('click on a button opens popover', () => { + const component = mountComponent(); + expect(component.find(EuiContextMenuPanel).length).toBe(0); + + const button = findTestSubject(component, 'discoverIndexPatternActions'); + button.simulate('click'); + + expect(component.find(EuiContextMenuPanel).length).toBe(1); + expect(component.find(EuiContextMenuItem).length).toBe(2); + }); + + test('click on an add button executes editField callback', () => { + const component = mountComponent(); + const button = findTestSubject(component, 'discoverIndexPatternActions'); + button.simulate('click'); + + const addButton = findTestSubject(component, 'indexPattern-add-field'); + addButton.simulate('click'); + expect(editField).toHaveBeenCalledWith(undefined); + }); + + test('click on a manage button navigates away from discover', () => { + const component = mountComponent(); + const button = findTestSubject(component, 'discoverIndexPatternActions'); + button.simulate('click'); + + const manageButton = findTestSubject(component, 'indexPattern-manage-field'); + manageButton.simulate('click'); + expect(mockServices.core.application.navigateToApp).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx index 38681d75a4e1d0..9a9dfd579b96d5 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx @@ -89,6 +89,7 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage { setIsAddIndexPatternFieldPopoverOpen(false); core.application.navigateToApp('management', { From dc8786604a749b54bfa4e7eff1d5cf025e0b9910 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 22 Apr 2021 13:08:26 -0400 Subject: [PATCH 18/33] Create privilege action to allow for decrypted telemetry payload (#96571) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../privileges/privileges.test.ts | 885 ++++++++++-------- .../authorization/privileges/privileges.ts | 8 +- 2 files changed, 484 insertions(+), 409 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 4da0020e94b158..ecbbb637f4da08 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -212,438 +212,485 @@ describe('features', () => { expectManageSpaces: true, expectGetFeatures: true, expectEnterpriseSearch: true, + expectDecryptedTelemetry: true, }, { group: 'space', expectManageSpaces: false, expectGetFeatures: false, expectEnterpriseSearch: false, + expectDecryptedTelemetry: false, }, -].forEach(({ group, expectManageSpaces, expectGetFeatures, expectEnterpriseSearch }) => { - describe(`${group}`, () => { - test('actions defined in any feature privilege are included in `all`', () => { - const features: KibanaFeature[] = [ - new KibanaFeature({ - id: 'foo', - name: 'Foo KibanaFeature', - app: [], - category: { id: 'foo', label: 'foo' }, - catalogue: ['ignore-me-1', 'ignore-me-2'], - management: { - foo: ['ignore-me-1', 'ignore-me-2'], - }, - privileges: { - all: { - management: { - 'all-management': ['all-management-1', 'all-management-2'], - }, - catalogue: ['all-catalogue-1', 'all-catalogue-2'], - savedObject: { - all: ['all-savedObject-all-1', 'all-savedObject-all-2'], - read: ['all-savedObject-read-1', 'all-savedObject-read-2'], - }, - ui: ['all-ui-1', 'all-ui-2'], +].forEach( + ({ + group, + expectManageSpaces, + expectGetFeatures, + expectEnterpriseSearch, + expectDecryptedTelemetry, + }) => { + describe(`${group}`, () => { + test('actions defined in any feature privilege are included in `all`', () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], }, - read: { - management: { - 'read-management': ['read-management-1', 'read-management-2'], + privileges: { + all: { + management: { + 'all-management': ['all-management-1', 'all-management-2'], + }, + catalogue: ['all-catalogue-1', 'all-catalogue-2'], + savedObject: { + all: ['all-savedObject-all-1', 'all-savedObject-all-2'], + read: ['all-savedObject-read-1', 'all-savedObject-read-2'], + }, + ui: ['all-ui-1', 'all-ui-2'], }, - catalogue: ['read-catalogue-1', 'read-catalogue-2'], - savedObject: { - all: ['read-savedObject-all-1', 'read-savedObject-all-2'], - read: ['read-savedObject-read-1', 'read-savedObject-read-2'], + read: { + management: { + 'read-management': ['read-management-1', 'read-management-2'], + }, + catalogue: ['read-catalogue-1', 'read-catalogue-2'], + savedObject: { + all: ['read-savedObject-all-1', 'read-savedObject-all-2'], + read: ['read-savedObject-read-1', 'read-savedObject-read-2'], + }, + ui: ['read-ui-1', 'read-ui-2'], }, - ui: ['read-ui-1', 'read-ui-2'], }, - }, - }), - ]; - - const mockFeaturesPlugin = { - getKibanaFeatures: jest.fn().mockReturnValue(features), - }; - const mockLicenseService = { - getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), - getType: jest.fn().mockReturnValue('basic'), - }; - const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); - - const actual = privileges.get(); - expect(actual).toHaveProperty(`${group}.all`, [ - actions.login, - actions.version, - ...(expectGetFeatures ? [actions.api.get('features')] : []), - ...(expectManageSpaces - ? [ - actions.space.manage, - actions.ui.get('spaces', 'manage'), - actions.ui.get('management', 'kibana', 'spaces'), - actions.ui.get('catalogue', 'spaces'), - ] - : []), - ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), - actions.ui.get('catalogue', 'all-catalogue-1'), - actions.ui.get('catalogue', 'all-catalogue-2'), - actions.ui.get('management', 'all-management', 'all-management-1'), - actions.ui.get('management', 'all-management', 'all-management-2'), - actions.savedObject.get('all-savedObject-all-1', 'bulk_get'), - actions.savedObject.get('all-savedObject-all-1', 'get'), - actions.savedObject.get('all-savedObject-all-1', 'find'), - actions.savedObject.get('all-savedObject-all-1', 'open_point_in_time'), - actions.savedObject.get('all-savedObject-all-1', 'close_point_in_time'), - actions.savedObject.get('all-savedObject-all-1', 'create'), - actions.savedObject.get('all-savedObject-all-1', 'bulk_create'), - actions.savedObject.get('all-savedObject-all-1', 'update'), - actions.savedObject.get('all-savedObject-all-1', 'bulk_update'), - actions.savedObject.get('all-savedObject-all-1', 'delete'), - actions.savedObject.get('all-savedObject-all-1', 'share_to_space'), - actions.savedObject.get('all-savedObject-all-2', 'bulk_get'), - actions.savedObject.get('all-savedObject-all-2', 'get'), - actions.savedObject.get('all-savedObject-all-2', 'find'), - actions.savedObject.get('all-savedObject-all-2', 'open_point_in_time'), - actions.savedObject.get('all-savedObject-all-2', 'close_point_in_time'), - actions.savedObject.get('all-savedObject-all-2', 'create'), - actions.savedObject.get('all-savedObject-all-2', 'bulk_create'), - actions.savedObject.get('all-savedObject-all-2', 'update'), - actions.savedObject.get('all-savedObject-all-2', 'bulk_update'), - actions.savedObject.get('all-savedObject-all-2', 'delete'), - actions.savedObject.get('all-savedObject-all-2', 'share_to_space'), - actions.savedObject.get('all-savedObject-read-1', 'bulk_get'), - actions.savedObject.get('all-savedObject-read-1', 'get'), - actions.savedObject.get('all-savedObject-read-1', 'find'), - actions.savedObject.get('all-savedObject-read-1', 'open_point_in_time'), - actions.savedObject.get('all-savedObject-read-1', 'close_point_in_time'), - actions.savedObject.get('all-savedObject-read-2', 'bulk_get'), - actions.savedObject.get('all-savedObject-read-2', 'get'), - actions.savedObject.get('all-savedObject-read-2', 'find'), - actions.savedObject.get('all-savedObject-read-2', 'open_point_in_time'), - actions.savedObject.get('all-savedObject-read-2', 'close_point_in_time'), - actions.ui.get('foo', 'all-ui-1'), - actions.ui.get('foo', 'all-ui-2'), - actions.ui.get('catalogue', 'read-catalogue-1'), - actions.ui.get('catalogue', 'read-catalogue-2'), - actions.ui.get('management', 'read-management', 'read-management-1'), - actions.ui.get('management', 'read-management', 'read-management-2'), - actions.savedObject.get('read-savedObject-all-1', 'bulk_get'), - actions.savedObject.get('read-savedObject-all-1', 'get'), - actions.savedObject.get('read-savedObject-all-1', 'find'), - actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'), - actions.savedObject.get('read-savedObject-all-1', 'create'), - actions.savedObject.get('read-savedObject-all-1', 'bulk_create'), - actions.savedObject.get('read-savedObject-all-1', 'update'), - actions.savedObject.get('read-savedObject-all-1', 'bulk_update'), - actions.savedObject.get('read-savedObject-all-1', 'delete'), - actions.savedObject.get('read-savedObject-all-1', 'share_to_space'), - actions.savedObject.get('read-savedObject-all-2', 'bulk_get'), - actions.savedObject.get('read-savedObject-all-2', 'get'), - actions.savedObject.get('read-savedObject-all-2', 'find'), - actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'), - actions.savedObject.get('read-savedObject-all-2', 'create'), - actions.savedObject.get('read-savedObject-all-2', 'bulk_create'), - actions.savedObject.get('read-savedObject-all-2', 'update'), - actions.savedObject.get('read-savedObject-all-2', 'bulk_update'), - actions.savedObject.get('read-savedObject-all-2', 'delete'), - actions.savedObject.get('read-savedObject-all-2', 'share_to_space'), - actions.savedObject.get('read-savedObject-read-1', 'bulk_get'), - actions.savedObject.get('read-savedObject-read-1', 'get'), - actions.savedObject.get('read-savedObject-read-1', 'find'), - actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'), - actions.savedObject.get('read-savedObject-read-2', 'bulk_get'), - actions.savedObject.get('read-savedObject-read-2', 'get'), - actions.savedObject.get('read-savedObject-read-2', 'find'), - actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'), - actions.ui.get('foo', 'read-ui-1'), - actions.ui.get('foo', 'read-ui-2'), - ]); - }); - - test('actions defined in a feature privilege with name `read` are included in `read`', () => { - const features: KibanaFeature[] = [ - new KibanaFeature({ - id: 'foo', - name: 'Foo KibanaFeature', - app: [], - category: { id: 'foo', label: 'foo' }, - catalogue: ['ignore-me-1', 'ignore-me-2'], - management: { - foo: ['ignore-me-1', 'ignore-me-2'], - }, - privileges: { - all: { - management: { - 'ignore-me': ['ignore-me-1', 'ignore-me-2'], - }, - catalogue: ['ignore-me-1', 'ignore-me-2'], - savedObject: { - all: ['ignore-me-1', 'ignore-me-2'], - read: ['ignore-me-1', 'ignore-me-2'], - }, - ui: ['ignore-me-1', 'ignore-me-2'], + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), + }; + const privileges = privilegesFactory( + actions, + mockFeaturesPlugin as any, + mockLicenseService + ); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ...(expectGetFeatures ? [actions.api.get('features')] : []), + ...(expectManageSpaces + ? [ + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), + ] + : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), + actions.ui.get('catalogue', 'all-catalogue-1'), + actions.ui.get('catalogue', 'all-catalogue-2'), + actions.ui.get('management', 'all-management', 'all-management-1'), + actions.ui.get('management', 'all-management', 'all-management-2'), + actions.savedObject.get('all-savedObject-all-1', 'bulk_get'), + actions.savedObject.get('all-savedObject-all-1', 'get'), + actions.savedObject.get('all-savedObject-all-1', 'find'), + actions.savedObject.get('all-savedObject-all-1', 'open_point_in_time'), + actions.savedObject.get('all-savedObject-all-1', 'close_point_in_time'), + actions.savedObject.get('all-savedObject-all-1', 'create'), + actions.savedObject.get('all-savedObject-all-1', 'bulk_create'), + actions.savedObject.get('all-savedObject-all-1', 'update'), + actions.savedObject.get('all-savedObject-all-1', 'bulk_update'), + actions.savedObject.get('all-savedObject-all-1', 'delete'), + actions.savedObject.get('all-savedObject-all-1', 'share_to_space'), + actions.savedObject.get('all-savedObject-all-2', 'bulk_get'), + actions.savedObject.get('all-savedObject-all-2', 'get'), + actions.savedObject.get('all-savedObject-all-2', 'find'), + actions.savedObject.get('all-savedObject-all-2', 'open_point_in_time'), + actions.savedObject.get('all-savedObject-all-2', 'close_point_in_time'), + actions.savedObject.get('all-savedObject-all-2', 'create'), + actions.savedObject.get('all-savedObject-all-2', 'bulk_create'), + actions.savedObject.get('all-savedObject-all-2', 'update'), + actions.savedObject.get('all-savedObject-all-2', 'bulk_update'), + actions.savedObject.get('all-savedObject-all-2', 'delete'), + actions.savedObject.get('all-savedObject-all-2', 'share_to_space'), + actions.savedObject.get('all-savedObject-read-1', 'bulk_get'), + actions.savedObject.get('all-savedObject-read-1', 'get'), + actions.savedObject.get('all-savedObject-read-1', 'find'), + actions.savedObject.get('all-savedObject-read-1', 'open_point_in_time'), + actions.savedObject.get('all-savedObject-read-1', 'close_point_in_time'), + actions.savedObject.get('all-savedObject-read-2', 'bulk_get'), + actions.savedObject.get('all-savedObject-read-2', 'get'), + actions.savedObject.get('all-savedObject-read-2', 'find'), + actions.savedObject.get('all-savedObject-read-2', 'open_point_in_time'), + actions.savedObject.get('all-savedObject-read-2', 'close_point_in_time'), + actions.ui.get('foo', 'all-ui-1'), + actions.ui.get('foo', 'all-ui-2'), + actions.ui.get('catalogue', 'read-catalogue-1'), + actions.ui.get('catalogue', 'read-catalogue-2'), + actions.ui.get('management', 'read-management', 'read-management-1'), + actions.ui.get('management', 'read-management', 'read-management-2'), + actions.savedObject.get('read-savedObject-all-1', 'bulk_get'), + actions.savedObject.get('read-savedObject-all-1', 'get'), + actions.savedObject.get('read-savedObject-all-1', 'find'), + actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'), + actions.savedObject.get('read-savedObject-all-1', 'create'), + actions.savedObject.get('read-savedObject-all-1', 'bulk_create'), + actions.savedObject.get('read-savedObject-all-1', 'update'), + actions.savedObject.get('read-savedObject-all-1', 'bulk_update'), + actions.savedObject.get('read-savedObject-all-1', 'delete'), + actions.savedObject.get('read-savedObject-all-1', 'share_to_space'), + actions.savedObject.get('read-savedObject-all-2', 'bulk_get'), + actions.savedObject.get('read-savedObject-all-2', 'get'), + actions.savedObject.get('read-savedObject-all-2', 'find'), + actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'), + actions.savedObject.get('read-savedObject-all-2', 'create'), + actions.savedObject.get('read-savedObject-all-2', 'bulk_create'), + actions.savedObject.get('read-savedObject-all-2', 'update'), + actions.savedObject.get('read-savedObject-all-2', 'bulk_update'), + actions.savedObject.get('read-savedObject-all-2', 'delete'), + actions.savedObject.get('read-savedObject-all-2', 'share_to_space'), + actions.savedObject.get('read-savedObject-read-1', 'bulk_get'), + actions.savedObject.get('read-savedObject-read-1', 'get'), + actions.savedObject.get('read-savedObject-read-1', 'find'), + actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'), + actions.savedObject.get('read-savedObject-read-2', 'bulk_get'), + actions.savedObject.get('read-savedObject-read-2', 'get'), + actions.savedObject.get('read-savedObject-read-2', 'find'), + actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'), + actions.ui.get('foo', 'read-ui-1'), + actions.ui.get('foo', 'read-ui-2'), + ]); + }); + + test('actions defined in a feature privilege with name `read` are included in `read`', () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], }, - read: { - management: { - 'read-management': ['read-management-1', 'read-management-2'], + privileges: { + all: { + management: { + 'ignore-me': ['ignore-me-1', 'ignore-me-2'], + }, + catalogue: ['ignore-me-1', 'ignore-me-2'], + savedObject: { + all: ['ignore-me-1', 'ignore-me-2'], + read: ['ignore-me-1', 'ignore-me-2'], + }, + ui: ['ignore-me-1', 'ignore-me-2'], }, - catalogue: ['read-catalogue-1', 'read-catalogue-2'], - savedObject: { - all: ['read-savedObject-all-1', 'read-savedObject-all-2'], - read: ['read-savedObject-read-1', 'read-savedObject-read-2'], + read: { + management: { + 'read-management': ['read-management-1', 'read-management-2'], + }, + catalogue: ['read-catalogue-1', 'read-catalogue-2'], + savedObject: { + all: ['read-savedObject-all-1', 'read-savedObject-all-2'], + read: ['read-savedObject-read-1', 'read-savedObject-read-2'], + }, + ui: ['read-ui-1', 'read-ui-2'], }, - ui: ['read-ui-1', 'read-ui-2'], }, - }, - }), - ]; - - const mockFeaturesPlugin = { - getKibanaFeatures: jest.fn().mockReturnValue(features), - }; - const mockLicenseService = { - getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), - getType: jest.fn().mockReturnValue('basic'), - }; - const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); - - const actual = privileges.get(); - expect(actual).toHaveProperty(`${group}.read`, [ - actions.login, - actions.version, - actions.ui.get('catalogue', 'read-catalogue-1'), - actions.ui.get('catalogue', 'read-catalogue-2'), - actions.ui.get('management', 'read-management', 'read-management-1'), - actions.ui.get('management', 'read-management', 'read-management-2'), - actions.savedObject.get('read-savedObject-all-1', 'bulk_get'), - actions.savedObject.get('read-savedObject-all-1', 'get'), - actions.savedObject.get('read-savedObject-all-1', 'find'), - actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'), - actions.savedObject.get('read-savedObject-all-1', 'create'), - actions.savedObject.get('read-savedObject-all-1', 'bulk_create'), - actions.savedObject.get('read-savedObject-all-1', 'update'), - actions.savedObject.get('read-savedObject-all-1', 'bulk_update'), - actions.savedObject.get('read-savedObject-all-1', 'delete'), - actions.savedObject.get('read-savedObject-all-1', 'share_to_space'), - actions.savedObject.get('read-savedObject-all-2', 'bulk_get'), - actions.savedObject.get('read-savedObject-all-2', 'get'), - actions.savedObject.get('read-savedObject-all-2', 'find'), - actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'), - actions.savedObject.get('read-savedObject-all-2', 'create'), - actions.savedObject.get('read-savedObject-all-2', 'bulk_create'), - actions.savedObject.get('read-savedObject-all-2', 'update'), - actions.savedObject.get('read-savedObject-all-2', 'bulk_update'), - actions.savedObject.get('read-savedObject-all-2', 'delete'), - actions.savedObject.get('read-savedObject-all-2', 'share_to_space'), - actions.savedObject.get('read-savedObject-read-1', 'bulk_get'), - actions.savedObject.get('read-savedObject-read-1', 'get'), - actions.savedObject.get('read-savedObject-read-1', 'find'), - actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'), - actions.savedObject.get('read-savedObject-read-2', 'bulk_get'), - actions.savedObject.get('read-savedObject-read-2', 'get'), - actions.savedObject.get('read-savedObject-read-2', 'find'), - actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'), - actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'), - actions.ui.get('foo', 'read-ui-1'), - actions.ui.get('foo', 'read-ui-2'), - ]); - }); - - test('actions defined in a reserved privilege are not included in `all` or `read`', () => { - const features: KibanaFeature[] = [ - new KibanaFeature({ - id: 'foo', - name: 'Foo KibanaFeature', - app: [], - category: { id: 'foo', label: 'foo' }, - catalogue: ['ignore-me-1', 'ignore-me-2'], - management: { - foo: ['ignore-me-1', 'ignore-me-2'], - }, - privileges: null, - reserved: { - privileges: [ - { - id: 'reserved', - privilege: { - savedObject: { - all: ['ignore-me-1', 'ignore-me-2'], - read: ['ignore-me-1', 'ignore-me-2'], + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), + }; + const privileges = privilegesFactory( + actions, + mockFeaturesPlugin as any, + mockLicenseService + ); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.read`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + actions.ui.get('catalogue', 'read-catalogue-1'), + actions.ui.get('catalogue', 'read-catalogue-2'), + actions.ui.get('management', 'read-management', 'read-management-1'), + actions.ui.get('management', 'read-management', 'read-management-2'), + actions.savedObject.get('read-savedObject-all-1', 'bulk_get'), + actions.savedObject.get('read-savedObject-all-1', 'get'), + actions.savedObject.get('read-savedObject-all-1', 'find'), + actions.savedObject.get('read-savedObject-all-1', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-all-1', 'close_point_in_time'), + actions.savedObject.get('read-savedObject-all-1', 'create'), + actions.savedObject.get('read-savedObject-all-1', 'bulk_create'), + actions.savedObject.get('read-savedObject-all-1', 'update'), + actions.savedObject.get('read-savedObject-all-1', 'bulk_update'), + actions.savedObject.get('read-savedObject-all-1', 'delete'), + actions.savedObject.get('read-savedObject-all-1', 'share_to_space'), + actions.savedObject.get('read-savedObject-all-2', 'bulk_get'), + actions.savedObject.get('read-savedObject-all-2', 'get'), + actions.savedObject.get('read-savedObject-all-2', 'find'), + actions.savedObject.get('read-savedObject-all-2', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-all-2', 'close_point_in_time'), + actions.savedObject.get('read-savedObject-all-2', 'create'), + actions.savedObject.get('read-savedObject-all-2', 'bulk_create'), + actions.savedObject.get('read-savedObject-all-2', 'update'), + actions.savedObject.get('read-savedObject-all-2', 'bulk_update'), + actions.savedObject.get('read-savedObject-all-2', 'delete'), + actions.savedObject.get('read-savedObject-all-2', 'share_to_space'), + actions.savedObject.get('read-savedObject-read-1', 'bulk_get'), + actions.savedObject.get('read-savedObject-read-1', 'get'), + actions.savedObject.get('read-savedObject-read-1', 'find'), + actions.savedObject.get('read-savedObject-read-1', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-read-1', 'close_point_in_time'), + actions.savedObject.get('read-savedObject-read-2', 'bulk_get'), + actions.savedObject.get('read-savedObject-read-2', 'get'), + actions.savedObject.get('read-savedObject-read-2', 'find'), + actions.savedObject.get('read-savedObject-read-2', 'open_point_in_time'), + actions.savedObject.get('read-savedObject-read-2', 'close_point_in_time'), + actions.ui.get('foo', 'read-ui-1'), + actions.ui.get('foo', 'read-ui-2'), + ]); + }); + + test('actions defined in a reserved privilege are not included in `all` or `read`', () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], + }, + privileges: null, + reserved: { + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: ['ignore-me-1', 'ignore-me-2'], + read: ['ignore-me-1', 'ignore-me-2'], + }, + ui: ['ignore-me-1'], }, - ui: ['ignore-me-1'], }, - }, - ], - description: '', - }, - }), - ]; - - const mockFeaturesPlugin = { - getKibanaFeatures: jest.fn().mockReturnValue(features), - }; - const mockLicenseService = { - getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), - getType: jest.fn().mockReturnValue('basic'), - }; - const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); - - const actual = privileges.get(); - expect(actual).toHaveProperty(`${group}.all`, [ - actions.login, - actions.version, - ...(expectGetFeatures ? [actions.api.get('features')] : []), - ...(expectManageSpaces - ? [ - actions.space.manage, - actions.ui.get('spaces', 'manage'), - actions.ui.get('management', 'kibana', 'spaces'), - actions.ui.get('catalogue', 'spaces'), - ] - : []), - ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), - ]); - expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); - }); - - test('actions defined in a feature with excludeFromBasePrivileges are not included in `all` or `read', () => { - const features: KibanaFeature[] = [ - new KibanaFeature({ - id: 'foo', - name: 'Foo KibanaFeature', - excludeFromBasePrivileges: true, - app: [], - category: { id: 'foo', label: 'foo' }, - catalogue: ['ignore-me-1', 'ignore-me-2'], - management: { - foo: ['ignore-me-1', 'ignore-me-2'], - }, - privileges: { - all: { - management: { - 'all-management': ['all-management-1'], - }, - catalogue: ['all-catalogue-1'], - savedObject: { - all: ['all-savedObject-all-1'], - read: ['all-savedObject-read-1'], - }, - ui: ['all-ui-1'], + ], + description: '', }, - read: { - management: { - 'read-management': ['read-management-1'], - }, - catalogue: ['read-catalogue-1'], - savedObject: { - all: ['read-savedObject-all-1'], - read: ['read-savedObject-read-1'], - }, - ui: ['read-ui-1'], + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), + }; + const privileges = privilegesFactory( + actions, + mockFeaturesPlugin as any, + mockLicenseService + ); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ...(expectGetFeatures ? [actions.api.get('features')] : []), + ...(expectManageSpaces + ? [ + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), + ] + : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), + ]); + expect(actual).toHaveProperty(`${group}.read`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ]); + }); + + test('actions defined in a feature with excludeFromBasePrivileges are not included in `all` or `read', () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + excludeFromBasePrivileges: true, + app: [], + category: { id: 'foo', label: 'foo' }, + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], }, - }, - }), - ]; - - const mockFeaturesPlugin = { - getKibanaFeatures: jest.fn().mockReturnValue(features), - }; - const mockLicenseService = { - getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), - getType: jest.fn().mockReturnValue('basic'), - }; - const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); - - const actual = privileges.get(); - expect(actual).toHaveProperty(`${group}.all`, [ - actions.login, - actions.version, - ...(expectGetFeatures ? [actions.api.get('features')] : []), - ...(expectManageSpaces - ? [ - actions.space.manage, - actions.ui.get('spaces', 'manage'), - actions.ui.get('management', 'kibana', 'spaces'), - actions.ui.get('catalogue', 'spaces'), - ] - : []), - ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), - ]); - expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); - }); - - test('actions defined in an individual feature privilege with excludeFromBasePrivileges are not included in `all` or `read`', () => { - const features: KibanaFeature[] = [ - new KibanaFeature({ - id: 'foo', - name: 'Foo KibanaFeature', - app: [], - category: { id: 'foo', label: 'foo' }, - catalogue: ['ignore-me-1', 'ignore-me-2'], - management: { - foo: ['ignore-me-1', 'ignore-me-2'], - }, - privileges: { - all: { - excludeFromBasePrivileges: true, - management: { - 'all-management': ['all-management-1'], + privileges: { + all: { + management: { + 'all-management': ['all-management-1'], + }, + catalogue: ['all-catalogue-1'], + savedObject: { + all: ['all-savedObject-all-1'], + read: ['all-savedObject-read-1'], + }, + ui: ['all-ui-1'], }, - catalogue: ['all-catalogue-1'], - savedObject: { - all: ['all-savedObject-all-1'], - read: ['all-savedObject-read-1'], + read: { + management: { + 'read-management': ['read-management-1'], + }, + catalogue: ['read-catalogue-1'], + savedObject: { + all: ['read-savedObject-all-1'], + read: ['read-savedObject-read-1'], + }, + ui: ['read-ui-1'], }, - ui: ['all-ui-1'], }, - read: { - excludeFromBasePrivileges: true, - management: { - 'read-management': ['read-management-1'], + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), + }; + const privileges = privilegesFactory( + actions, + mockFeaturesPlugin as any, + mockLicenseService + ); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ...(expectGetFeatures ? [actions.api.get('features')] : []), + ...(expectManageSpaces + ? [ + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), + ] + : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), + ]); + expect(actual).toHaveProperty(`${group}.read`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ]); + }); + + test('actions defined in an individual feature privilege with excludeFromBasePrivileges are not included in `all` or `read`', () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], + }, + privileges: { + all: { + excludeFromBasePrivileges: true, + management: { + 'all-management': ['all-management-1'], + }, + catalogue: ['all-catalogue-1'], + savedObject: { + all: ['all-savedObject-all-1'], + read: ['all-savedObject-read-1'], + }, + ui: ['all-ui-1'], }, - catalogue: ['read-catalogue-1'], - savedObject: { - all: ['read-savedObject-all-1'], - read: ['read-savedObject-read-1'], + read: { + excludeFromBasePrivileges: true, + management: { + 'read-management': ['read-management-1'], + }, + catalogue: ['read-catalogue-1'], + savedObject: { + all: ['read-savedObject-all-1'], + read: ['read-savedObject-read-1'], + }, + ui: ['read-ui-1'], }, - ui: ['read-ui-1'], }, - }, - }), - ]; - - const mockFeaturesPlugin = { - getKibanaFeatures: jest.fn().mockReturnValue(features), - }; - const mockLicenseService = { - getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), - getType: jest.fn().mockReturnValue('basic'), - }; - const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); - - const actual = privileges.get(); - expect(actual).toHaveProperty(`${group}.all`, [ - actions.login, - actions.version, - ...(expectGetFeatures ? [actions.api.get('features')] : []), - ...(expectManageSpaces - ? [ - actions.space.manage, - actions.ui.get('spaces', 'manage'), - actions.ui.get('management', 'kibana', 'spaces'), - actions.ui.get('catalogue', 'spaces'), - ] - : []), - ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), - ]); - expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), + }; + const privileges = privilegesFactory( + actions, + mockFeaturesPlugin as any, + mockLicenseService + ); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ...(expectGetFeatures ? [actions.api.get('features')] : []), + ...(expectManageSpaces + ? [ + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), + ] + : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), + ]); + expect(actual).toHaveProperty(`${group}.read`, [ + actions.login, + actions.version, + ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), + ]); + }); }); - }); -}); + } +); describe('reserved', () => { test('actions defined at the feature do not cascade to the privileges', () => { @@ -911,6 +958,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -922,6 +970,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.read', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.ui.get('foo', 'foo'), ]); @@ -1080,6 +1129,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -1108,6 +1158,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.read', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), @@ -1316,6 +1367,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -1323,7 +1375,11 @@ describe('subFeatures', () => { actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ]); - expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); + expect(actual).toHaveProperty('global.read', [ + actions.login, + actions.version, + actions.api.get('decryptedTelemetry'), + ]); expect(actual).toHaveProperty('space.all', [actions.login, actions.version]); expect(actual).toHaveProperty('space.read', [actions.login, actions.version]); @@ -1455,6 +1511,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -1483,6 +1540,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.read', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.ui.get('foo', 'foo'), ]); @@ -1640,6 +1698,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -1647,7 +1706,11 @@ describe('subFeatures', () => { actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ]); - expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); + expect(actual).toHaveProperty('global.read', [ + actions.login, + actions.version, + actions.api.get('decryptedTelemetry'), + ]); expect(actual).toHaveProperty('space.all', [actions.login, actions.version]); expect(actual).toHaveProperty('space.read', [actions.login, actions.version]); @@ -1768,6 +1831,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -1796,6 +1860,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.read', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), @@ -2002,6 +2067,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -2030,6 +2096,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.read', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), @@ -2270,6 +2337,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.all', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -2315,6 +2383,7 @@ describe('subFeatures', () => { expect(actual).toHaveProperty('global.read', [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 28d3ddefc62b59..1826b853ce6687 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -105,6 +105,7 @@ export function privilegesFactory( all: [ actions.login, actions.version, + actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.space.manage, actions.ui.get('spaces', 'manage'), @@ -113,7 +114,12 @@ export function privilegesFactory( actions.ui.get('enterpriseSearch', 'all'), ...allActions, ], - read: [actions.login, actions.version, ...readActions], + read: [ + actions.login, + actions.version, + actions.api.get('decryptedTelemetry'), + ...readActions, + ], }, space: { all: [actions.login, actions.version, ...allActions], From 2f25047cbd789887733084035c086315fce76186 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 22 Apr 2021 13:25:16 -0400 Subject: [PATCH 19/33] [ML] DF Analytics map: deselect node after node action or flyout close (#97922) * deselect node after node action or flyout close * remove unnecessary wrapping function for deselect --- .../pages/job_map/components/controls.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index 7093c3f7a88c74..eb4aa8e4f09fe0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -249,6 +249,9 @@ export const Controls: FC = React.memo( icon="branch" onClick={() => { getNodeData({ id: nodeLabel, type: nodeType }); + if (cy) { + cy.elements().unselect(); + } setShowFlyout(false); setPopover(false); }} @@ -264,12 +267,7 @@ export const Controls: FC = React.memo( return ( - setShowFlyout(false)} - data-test-subj="mlAnalyticsJobMapFlyout" - > + From 6bb289368b313430cfb45d4144205220e10484ee Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 22 Apr 2021 11:30:27 -0700 Subject: [PATCH 20/33] Revert skips added while triaging ES OOMs (#97876) * Revert "skip flaky suite (#97382)" This reverts commit e321f57f64657ffff91df8ed96f4e9fdbe5dcde7. * Revert "skip flaky suite (#97387)" This reverts commit a89b75671000d6c8431ff150b4f555e1f00f361e. * Revert "Skip test to try and stabilize master" (#97378) This reverts commit 194355fdd3969f567f43ad4b7f63d72dcf7974a9. * upload heap dumps when they are created Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- vars/kibanaPipeline.groovy | 1 + x-pack/test/api_integration/apis/lens/existing_fields.ts | 3 +-- .../apis/security_solution/matrix_dns_histogram.ts | 3 +-- .../test/api_integration/apis/short_urls/feature_controls.ts | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index b8afdb9cde3ef7..76ed71ebbf2708 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -201,6 +201,7 @@ def withGcsArtifactUpload(workerName, closure) { 'x-pack/test/**/screenshots/session/*.png', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', 'x-pack/test/functional/failure_debug/html/*.html', + '.es/**/*.hprof' ] withEnv([ diff --git a/x-pack/test/api_integration/apis/lens/existing_fields.ts b/x-pack/test/api_integration/apis/lens/existing_fields.ts index 03587869939196..88949401f102ad 100644 --- a/x-pack/test/api_integration/apis/lens/existing_fields.ts +++ b/x-pack/test/api_integration/apis/lens/existing_fields.ts @@ -160,8 +160,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - // FLAKY: https://github.com/elastic/kibana/issues/97387 - describe.skip('existing_fields apis', () => { + describe('existing_fields apis', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('visualize/default'); diff --git a/x-pack/test/api_integration/apis/security_solution/matrix_dns_histogram.ts b/x-pack/test/api_integration/apis/security_solution/matrix_dns_histogram.ts index 27a7a5a5396077..69beb65dec670f 100644 --- a/x-pack/test/api_integration/apis/security_solution/matrix_dns_histogram.ts +++ b/x-pack/test/api_integration/apis/security_solution/matrix_dns_histogram.ts @@ -33,8 +33,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - // FIX: https://github.com/elastic/kibana/issues/97378 - describe.skip('Matrix DNS Histogram', () => { + describe('Matrix DNS Histogram', () => { describe('Large data set', () => { before(() => esArchiver.load('security_solution/matrix_dns_histogram/large_dns_query')); after(() => esArchiver.unload('security_solution/matrix_dns_histogram/large_dns_query')); diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index e55fcf10b7fac9..a2596e9eaedaf5 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -12,8 +12,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const supertest = getService('supertestWithoutAuth'); const security = getService('security'); - // FLAKY: https://github.com/elastic/kibana/issues/97382 - describe.skip('feature controls', () => { + describe('feature controls', () => { const kibanaUsername = 'kibana_admin'; const kibanaUserRoleName = 'kibana_admin'; From 49a18483d38c7430d63e6fb774efd7de47997603 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 22 Apr 2021 20:36:25 +0200 Subject: [PATCH 21/33] Refactor execution service to use observables underneath (#96065) * Refactor execution service to use observables underneath * Fix canvas plugin to initialize workspace after assets * Update expression functions implementations to resolve observables instead of promises --- ...-expressions-public.execution.interpret.md | 4 +- ...xpressions-public.execution.invokechain.md | 4 +- ...essions-public.execution.invokefunction.md | 4 +- ...in-plugins-expressions-public.execution.md | 2 +- ...xpressions-public.execution.resolveargs.md | 4 +- ...ins-expressions-public.execution.result.md | 4 +- ...gins-expressions-public.execution.start.md | 4 +- ...plugins-expressions-public.executor.run.md | 4 +- ...n-plugins-expressions-public.typestring.md | 2 +- ...-expressions-server.execution.interpret.md | 4 +- ...xpressions-server.execution.invokechain.md | 4 +- ...essions-server.execution.invokefunction.md | 4 +- ...in-plugins-expressions-server.execution.md | 2 +- ...xpressions-server.execution.resolveargs.md | 4 +- ...ins-expressions-server.execution.result.md | 4 +- ...gins-expressions-server.execution.start.md | 4 +- ...plugins-expressions-server.executor.run.md | 4 +- ...n-plugins-expressions-server.typestring.md | 2 +- .../execution/execution.abortion.test.ts | 11 +- .../common/execution/execution.test.ts | 314 ++++++++++-- .../expressions/common/execution/execution.ts | 463 +++++++++--------- .../execution/execution_contract.test.ts | 5 +- .../common/execution/execution_contract.ts | 29 +- .../expressions/common/executor/executor.ts | 11 +- .../expression_function.ts | 4 +- .../expression_functions/specs/map_column.ts | 10 +- .../specs/tests/map_column.test.ts | 176 ++++--- .../common/service/expressions_services.ts | 3 +- .../expression_functions/index.ts | 2 + .../test_helpers/expression_functions/sum.ts | 23 + .../expressions/common/types/common.ts | 12 +- src/plugins/expressions/public/loader.test.ts | 7 +- src/plugins/expressions/public/public.api.md | 19 +- src/plugins/expressions/server/server.api.md | 19 +- .../functions/common/case.test.js | 33 +- .../functions/common/case.ts | 21 +- .../functions/common/filterrows.test.js | 27 +- .../functions/common/filterrows.ts | 6 +- .../functions/common/if.test.js | 67 ++- .../canvas_plugin_src/functions/common/if.ts | 20 +- .../functions/common/ply.test.js | 75 +-- .../canvas_plugin_src/functions/common/ply.ts | 6 +- .../functions/common/switch.test.js | 25 +- .../functions/common/switch.ts | 16 +- .../canvas/public/apps/workpad/routes.ts | 2 +- 45 files changed, 904 insertions(+), 566 deletions(-) create mode 100644 src/plugins/expressions/common/test_helpers/expression_functions/sum.ts diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md index 24dee04861b4ee..46934e119aee09 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md @@ -7,7 +7,7 @@ Signature: ```typescript -interpret(ast: ExpressionAstNode, input: T): Promise; +interpret(ast: ExpressionAstNode, input: T): Observable; ``` ## Parameters @@ -19,5 +19,5 @@ interpret(ast: ExpressionAstNode, input: T): Promise; Returns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokechain.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokechain.md index 5078baf2ca526e..99768f0ddd533d 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokechain.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokechain.md @@ -7,7 +7,7 @@ Signature: ```typescript -invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; +invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable; ``` ## Parameters @@ -19,5 +19,5 @@ invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; Returns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokefunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokefunction.md index e90cee8b626d61..2c3c2173e08331 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokefunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.invokefunction.md @@ -7,7 +7,7 @@ Signature: ```typescript -invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Promise; +invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Observable; ``` ## Parameters @@ -20,5 +20,5 @@ invokeFunction(fn: ExpressionFunction, input: unknown, args: RecordReturns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md index 56b14e005adfb8..30fe9f497f7eeb 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md @@ -26,7 +26,7 @@ export declare class Executionstring | | | [input](./kibana-plugin-plugins-expressions-public.execution.input.md) | | Input | Initial input of the execution.N.B. It is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.execution.inspectoradapters.md) | | InspectorAdapters | | -| [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | Promise<Output | ExpressionValueError> | | +| [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | Observable<Output | ExpressionValueError> | Future that tracks result or error of this execution. | | [state](./kibana-plugin-plugins-expressions-public.execution.state.md) | | ExecutionContainer<Output | ExpressionValueError> | Dynamic state of the execution. | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.resolveargs.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.resolveargs.md index ab67dff604a867..fc11af42c5febd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.resolveargs.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.resolveargs.md @@ -7,7 +7,7 @@ Signature: ```typescript -resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise; +resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable; ``` ## Parameters @@ -20,5 +20,5 @@ resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): PromiseReturns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md index e0167a3a378fef..94f60ccee0f009 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md @@ -4,8 +4,10 @@ ## Execution.result property +Future that tracks result or error of this execution. + Signature: ```typescript -get result(): Promise; +readonly result: Observable; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md index c6edc43d423dc4..64cf81b376948a 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md @@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons Signature: ```typescript -start(input?: Input): void; +start(input?: Input): Observable; ``` ## Parameters @@ -22,5 +22,5 @@ start(input?: Input): void; Returns: -`void` +`Observable` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md index 81fc8aa8658ca0..307e6b6bcd5c80 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md @@ -9,7 +9,7 @@ Execute expression and return result. Signature: ```typescript -run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; +run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; ``` ## Parameters @@ -22,5 +22,5 @@ run(ast: string | ExpressionAstExpression, input: Input, params?: Returns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.typestring.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.typestring.md index 1e85625907bb07..08dc2d6208d341 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.typestring.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.typestring.md @@ -11,5 +11,5 @@ If the type extends a Promise, we still need to return the string representation Signature: ```typescript -export declare type TypeString = KnownTypeToString>; +export declare type TypeString = KnownTypeToString ? UnwrapObservable : UnwrapPromiseOrReturn>; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md index e425bdc70e3491..936e98be589a35 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md @@ -7,7 +7,7 @@ Signature: ```typescript -interpret(ast: ExpressionAstNode, input: T): Promise; +interpret(ast: ExpressionAstNode, input: T): Observable; ``` ## Parameters @@ -19,5 +19,5 @@ interpret(ast: ExpressionAstNode, input: T): Promise; Returns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokechain.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokechain.md index 9ada611f32bf2e..003702ff845b20 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokechain.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokechain.md @@ -7,7 +7,7 @@ Signature: ```typescript -invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; +invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable; ``` ## Parameters @@ -19,5 +19,5 @@ invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; Returns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokefunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokefunction.md index 4519d21ee250af..91839172c31f42 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokefunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.invokefunction.md @@ -7,7 +7,7 @@ Signature: ```typescript -invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Promise; +invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Observable; ``` ## Parameters @@ -20,5 +20,5 @@ invokeFunction(fn: ExpressionFunction, input: unknown, args: RecordReturns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md index c94ae9bcfe9466..a4e324eef6674c 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md @@ -26,7 +26,7 @@ export declare class Executionstring | | | [input](./kibana-plugin-plugins-expressions-server.execution.input.md) | | Input | Initial input of the execution.N.B. It is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-server.execution.inspectoradapters.md) | | InspectorAdapters | | -| [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | Promise<Output | ExpressionValueError> | | +| [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | Observable<Output | ExpressionValueError> | Future that tracks result or error of this execution. | | [state](./kibana-plugin-plugins-expressions-server.execution.state.md) | | ExecutionContainer<Output | ExpressionValueError> | Dynamic state of the execution. | ## Methods diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.resolveargs.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.resolveargs.md index 48cc43b2d7767b..784818f2fb8e3d 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.resolveargs.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.resolveargs.md @@ -7,7 +7,7 @@ Signature: ```typescript -resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise; +resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable; ``` ## Parameters @@ -20,5 +20,5 @@ resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): PromiseReturns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md index be0134cd2542e3..06cf047ac4160f 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md @@ -4,8 +4,10 @@ ## Execution.result property +Future that tracks result or error of this execution. + Signature: ```typescript -get result(): Promise; +readonly result: Observable; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md index 9a4e93fe6a9af0..dd0456ac09950e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md @@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons Signature: ```typescript -start(input?: Input): void; +start(input?: Input): Observable; ``` ## Parameters @@ -22,5 +22,5 @@ start(input?: Input): void; Returns: -`void` +`Observable` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md index de5ce1ed37f12a..2ab534eac2f3a2 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md @@ -9,7 +9,7 @@ Execute expression and return result. Signature: ```typescript -run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; +run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; ``` ## Parameters @@ -22,5 +22,5 @@ run(ast: string | ExpressionAstExpression, input: Input, params?: Returns: -`Promise` +`Observable` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.typestring.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.typestring.md index af4d5ae0bf8149..adf2c52490de44 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.typestring.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.typestring.md @@ -11,5 +11,5 @@ If the type extends a Promise, we still need to return the string representation Signature: ```typescript -export declare type TypeString = KnownTypeToString>; +export declare type TypeString = KnownTypeToString ? UnwrapObservable : UnwrapPromiseOrReturn>; ``` diff --git a/src/plugins/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts index 33bb7826917473..514086e9b19eee 100644 --- a/src/plugins/expressions/common/execution/execution.abortion.test.ts +++ b/src/plugins/expressions/common/execution/execution.abortion.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { first } from 'rxjs/operators'; import { waitFor } from '@testing-library/react'; import { Execution } from './execution'; import { parseExpression } from '../ast'; @@ -39,7 +40,7 @@ describe('Execution abortion tests', () => { execution.start(); execution.cancel(); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toMatchObject({ type: 'error', @@ -57,7 +58,7 @@ describe('Execution abortion tests', () => { jest.advanceTimersByTime(100); execution.cancel(); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toMatchObject({ type: 'error', @@ -75,7 +76,7 @@ describe('Execution abortion tests', () => { execution.start(); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); execution.cancel(); @@ -130,12 +131,12 @@ describe('Execution abortion tests', () => { params: {}, }); - execution.start(); + execution.start().toPromise(); await waitFor(() => expect(started).toHaveBeenCalledTimes(1)); execution.cancel(); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toMatchObject({ type: 'error', error: { diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index b9fa693c1056b1..343ea9ef7f03c8 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { of } from 'rxjs'; +import { first, scan } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; import { Execution } from './execution'; import { parseExpression, ExpressionAstExpression } from '../ast'; import { createUnitTestExecutor } from '../test_helpers'; @@ -42,10 +45,18 @@ const run = async ( ) => { const execution = createExecution(expression, context); execution.start(input); - return await execution.result; + return await execution.result.pipe(first()).toPromise(); }; +let testScheduler: TestScheduler; + describe('Execution', () => { + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => { + return expect(actual).toStrictEqual(expected); + }); + }); + test('can instantiate', () => { const execution = createExecution('foo bar=123'); expect(execution.state.get().ast.chain[0].arguments.bar).toEqual([123]); @@ -73,7 +84,7 @@ describe('Execution', () => { /* eslint-enable no-console */ execution.start(123); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toBe(123); expect(spy).toHaveBeenCalledTimes(1); @@ -91,7 +102,7 @@ describe('Execution', () => { value: -1, }); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toEqual({ type: 'num', @@ -106,7 +117,7 @@ describe('Execution', () => { value: 0, }); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toEqual({ type: 'num', @@ -114,16 +125,102 @@ describe('Execution', () => { }); }); - test('casts input to correct type', async () => { - const execution = createExecution('add val=1'); + describe('.input', () => { + test('casts input to correct type', async () => { + const execution = createExecution('add val=1'); - // Below 1 is cast to { type: 'num', value: 1 }. - execution.start(1); - const result = await execution.result; + // Below 1 is cast to { type: 'num', value: 1 }. + execution.start(1); + const result = await execution.result.pipe(first()).toPromise(); - expect(result).toEqual({ - type: 'num', - value: 2, + expect(result).toEqual({ + type: 'num', + value: 2, + }); + }); + + test('supports promises on input', async () => { + const execution = createExecution('add val=1'); + + execution.start(Promise.resolve(1)); + const result = await execution.result.pipe(first()).toPromise(); + + expect(result).toEqual({ + type: 'num', + value: 2, + }); + }); + + test('supports observables on input', async () => { + const execution = createExecution('add val=1'); + + execution.start(of(1)); + const result = await execution.result.pipe(first()).toPromise(); + + expect(result).toEqual({ + type: 'num', + value: 2, + }); + }); + + test('handles observables on input', () => { + const execution = createExecution('add val=1'); + + testScheduler.run(({ cold, expectObservable }) => { + const input = cold(' -a--b-c-', { a: 1, b: 2, c: 3 }); + const subscription = ' ---^---!'; + const expected = ' ---ab-c-'; + + expectObservable(execution.start(input), subscription).toBe(expected, { + a: { type: 'num', value: 2 }, + b: { type: 'num', value: 3 }, + c: { type: 'num', value: 4 }, + }); + }); + }); + + test('stops when input errors', () => { + const execution = createExecution('add val=1'); + + testScheduler.run(({ cold, expectObservable }) => { + const input = cold('-a-#-b-', { a: 1, b: 2 }); + const expected = ' -a-#'; + + expectObservable(execution.start(input)).toBe(expected, { + a: { type: 'num', value: 2 }, + }); + }); + }); + + test('does not complete when input completes', () => { + const execution = createExecution('add val=1'); + + testScheduler.run(({ cold, expectObservable }) => { + const input = cold('-a-b|', { a: 1, b: 2 }); + const expected = ' -a-b-'; + + expectObservable(execution.start(input)).toBe(expected, { + a: { type: 'num', value: 2 }, + b: { type: 'num', value: 3 }, + }); + }); + }); + + test('handles partial results', () => { + const execution = createExecution('sum'); + + testScheduler.run(({ cold, expectObservable }) => { + const items = cold(' -a--b-c-', { a: 1, b: 2, c: 3 }); + const subscription = ' ---^---!'; + const expected = ' ---ab-c-'; + const input = items.pipe(scan((result, value) => [...result, value], new Array())); + + expectObservable(execution.start(input), subscription).toBe(expected, { + a: { type: 'num', value: 1 }, + b: { type: 'num', value: 3 }, + c: { type: 'num', value: 6 }, + }); + }); }); }); @@ -251,7 +348,7 @@ describe('Execution', () => { value: 0, }); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toEqual({ type: 'num', @@ -267,13 +364,32 @@ describe('Execution', () => { test('result is undefined until execution completes', async () => { const execution = createExecution('sleep 10'); expect(execution.state.get().result).toBe(undefined); - execution.start(null); + execution.start(null).subscribe(jest.fn()); expect(execution.state.get().result).toBe(undefined); await new Promise((r) => setTimeout(r, 1)); expect(execution.state.get().result).toBe(undefined); await new Promise((r) => setTimeout(r, 11)); expect(execution.state.get().result).toBe(null); }); + + test('handles functions returning observables', () => { + testScheduler.run(({ cold, expectObservable }) => { + const arg = cold(' -a-b-c|', { a: 1, b: 2, c: 3 }); + const expected = ' -a-b-c-'; + const observable: ExpressionFunctionDefinition<'observable', any, {}, any> = { + name: 'observable', + args: {}, + help: '', + fn: () => arg, + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable); + + const result = executor.run('observable', null, {}); + + expectObservable(result).toBe(expected, { a: 1, b: 2, c: 3 }); + }); + }); }); describe('when function throws', () => { @@ -309,7 +425,7 @@ describe('Execution', () => { const execution = await createExecution('error "foo"'); execution.start(null); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toMatchObject({ type: 'error', @@ -330,7 +446,7 @@ describe('Execution', () => { const executor = createUnitTestExecutor(); executor.registerFunction(spy); - await executor.run('error "..." | spy', null); + await executor.run('error "..." | spy', null).pipe(first()).toPromise(); expect(spy.fn).toHaveBeenCalledTimes(0); }); @@ -360,14 +476,14 @@ describe('Execution', () => { test('execution state is "result" when execution successfully completes', async () => { const execution = createExecution('sleep 1'); execution.start(null); - await execution.result; + await execution.result.pipe(first()).toPromise(); expect(execution.state.get().state).toBe('result'); }); test('execution state is "result" when execution successfully completes - 2', async () => { const execution = createExecution('var foo'); execution.start(null); - await execution.result; + await execution.result.pipe(first()).toPromise(); expect(execution.state.get().state).toBe('result'); }); }); @@ -413,10 +529,142 @@ describe('Execution', () => { expect(result).toBe(66); }); + + test('supports observables in arguments', () => { + const observable = { + name: 'observable', + args: {}, + help: '', + fn: () => of(1), + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable); + + expect( + executor.run('add val={observable}', 1, {}).pipe(first()).toPromise() + ).resolves.toEqual({ + type: 'num', + value: 2, + }); + }); + + test('supports observables in arguments emitting multiple values', () => { + testScheduler.run(({ cold, expectObservable }) => { + const arg = cold('-a-b-c-', { a: 1, b: 2, c: 3 }); + const expected = '-a-b-c-'; + const observable = { + name: 'observable', + args: {}, + help: '', + fn: () => arg, + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable); + + const result = executor.run('add val={observable}', 1, {}); + + expectObservable(result).toBe(expected, { + a: { type: 'num', value: 2 }, + b: { type: 'num', value: 3 }, + c: { type: 'num', value: 4 }, + }); + }); + }); + + test('combines multiple observables in arguments', () => { + testScheduler.run(({ cold, expectObservable }) => { + const arg1 = cold('--ab-c-', { a: 0, b: 2, c: 4 }); + const arg2 = cold('-a--bc-', { a: 1, b: 3, c: 5 }); + const expected = ' --abc(de)-'; + const observable1 = { + name: 'observable1', + args: {}, + help: '', + fn: () => arg1, + }; + const observable2 = { + name: 'observable2', + args: {}, + help: '', + fn: () => arg2, + }; + const max: ExpressionFunctionDefinition<'max', any, { val1: number; val2: number }, any> = { + name: 'max', + args: { + val1: { help: '', types: ['number'] }, + val2: { help: '', types: ['number'] }, + }, + help: '', + fn: (input, { val1, val2 }) => ({ type: 'num', value: Math.max(val1, val2) }), + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable1); + executor.registerFunction(observable2); + executor.registerFunction(max); + + const result = executor.run('max val1={observable1} val2={observable2}', {}); + + expectObservable(result).toBe(expected, { + a: { type: 'num', value: 1 }, + b: { type: 'num', value: 2 }, + c: { type: 'num', value: 3 }, + d: { type: 'num', value: 4 }, + e: { type: 'num', value: 5 }, + }); + }); + }); + + test('does not complete when an argument completes', () => { + testScheduler.run(({ cold, expectObservable }) => { + const arg = cold('-a|', { a: 1 }); + const expected = '-a-'; + const observable = { + name: 'observable', + args: {}, + help: '', + fn: () => arg, + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable); + + const result = executor.run('add val={observable}', 1, {}); + + expectObservable(result).toBe(expected, { + a: { type: 'num', value: 2 }, + }); + }); + }); + + test('handles error in observable arguments', () => { + testScheduler.run(({ cold, expectObservable }) => { + const arg = cold('-a-#', { a: 1 }, new Error('some error')); + const expected = '-a-b'; + const observable = { + name: 'observable', + args: {}, + help: '', + fn: () => arg, + }; + const executor = createUnitTestExecutor(); + executor.registerFunction(observable); + + const result = executor.run('add val={observable}', 1, {}); + + expectObservable(result).toBe(expected, { + a: { type: 'num', value: 2 }, + b: { + error: expect.objectContaining({ + message: '[add] > [observable] > some error', + }), + type: 'error', + }, + }); + }); + }); }); describe('when arguments are missing', () => { - test('when required argument is missing and has not alias, returns error', async () => { + it('when required argument is missing and has not alias, returns error', async () => { const requiredArg: ExpressionFunctionDefinition<'requiredArg', any, { arg: any }, any> = { name: 'requiredArg', args: { @@ -430,7 +678,7 @@ describe('Execution', () => { }; const executor = createUnitTestExecutor(); executor.registerFunction(requiredArg); - const result = await executor.run('requiredArg', null, {}); + const result = await executor.run('requiredArg', null, {}).pipe(first()).toPromise(); expect(result).toMatchObject({ type: 'error', @@ -456,7 +704,7 @@ describe('Execution', () => { test('can execute expression in debug mode', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toEqual({ type: 'num', @@ -471,7 +719,7 @@ describe('Execution', () => { true ); execution.start(0); - const result = await execution.result; + const result = await execution.result.pipe(first()).toPromise(); expect(result).toEqual({ type: 'num', @@ -483,7 +731,7 @@ describe('Execution', () => { test('sets "success" flag on all functions to true', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); for (const node of execution.state.get().ast.chain) { expect(node.debug?.success).toBe(true); @@ -493,7 +741,7 @@ describe('Execution', () => { test('stores "fn" reference to the function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); for (const node of execution.state.get().ast.chain) { expect(node.debug?.fn).toBe('add'); @@ -503,7 +751,7 @@ describe('Execution', () => { test('saves duration it took to execute each function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); for (const node of execution.state.get().ast.chain) { expect(typeof node.debug?.duration).toBe('number'); @@ -515,7 +763,7 @@ describe('Execution', () => { test('adds .debug field in expression AST on each executed function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); for (const node of execution.state.get().ast.chain) { expect(typeof node.debug).toBe('object'); @@ -526,7 +774,7 @@ describe('Execution', () => { test('stores input of each function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); const { chain } = execution.state.get().ast; @@ -544,7 +792,7 @@ describe('Execution', () => { test('stores output of each function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); const { chain } = execution.state.get().ast; @@ -569,7 +817,7 @@ describe('Execution', () => { true ); execution.start(-1); - await execution.result; + await execution.result.pipe(first()).toPromise(); const { chain } = execution.state.get().ast; @@ -592,7 +840,7 @@ describe('Execution', () => { true ); execution.start(0); - await execution.result; + await execution.result.pipe(first()).toPromise(); const { chain } = execution.state.get().ast.chain[0].arguments .val[0] as ExpressionAstExpression; @@ -627,7 +875,7 @@ describe('Execution', () => { params: { debug: true }, }); execution.start(0); - await execution.result; + await execution.result.pipe(first()).toPromise(); const node1 = execution.state.get().ast.chain[0]; const node2 = execution.state.get().ast.chain[1]; @@ -645,7 +893,7 @@ describe('Execution', () => { params: { debug: true }, }); execution.start(0); - await execution.result; + await execution.result.pipe(first()).toPromise(); const node2 = execution.state.get().ast.chain[1]; @@ -666,7 +914,7 @@ describe('Execution', () => { params: { debug: true }, }); execution.start(0); - await execution.result; + await execution.result.pipe(first()).toPromise(); const node2 = execution.state.get().ast.chain[1]; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index bf545a0075bed7..b70f261ea4b201 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -7,14 +7,28 @@ */ import { i18n } from '@kbn/i18n'; +import { isPromise } from '@kbn/std'; import { keys, last, mapValues, reduce, zipObject } from 'lodash'; +import { + combineLatest, + defer, + from, + isObservable, + of, + race, + throwError, + Observable, + ReplaySubject, +} from 'rxjs'; +import { catchError, finalize, map, shareReplay, switchMap, tap } from 'rxjs/operators'; import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; -import { abortSignalToPromise, Defer, now } from '../../../kibana_utils/common'; +import { abortSignalToPromise, now } from '../../../kibana_utils/common'; import { RequestAdapter, Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { + ExpressionAstArgument, ExpressionAstExpression, ExpressionAstFunction, parse, @@ -23,8 +37,8 @@ import { ExpressionAstNode, } from '../ast'; import { ExecutionContext, DefaultInspectorAdapters } from './types'; -import { getType, ExpressionValue, Datatable } from '../expression_types'; -import { ArgumentType, ExpressionFunction } from '../expression_functions'; +import { getType, Datatable } from '../expression_types'; +import { ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; import { ExpressionExecutionParams } from '../service'; @@ -87,6 +101,11 @@ export class Execution< */ public input: Input = null as any; + /** + * Input of the started execution. + */ + private input$ = new ReplaySubject(1); + /** * Execution context - object that allows to do side-effects. Context is passed * to every function. @@ -104,10 +123,10 @@ export class Execution< private readonly abortRejection = abortSignalToPromise(this.abortController.signal); /** - * Races a given promise against the "abort" event of `abortController`. + * Races a given observable against the "abort" event of `abortController`. */ - private race(promise: Promise): Promise { - return Promise.race([this.abortRejection.promise, promise]); + private race(observable: Observable): Observable { + return race(from(this.abortRejection.promise), observable); } /** @@ -118,7 +137,7 @@ export class Execution< /** * Future that tracks result or error of this execution. */ - private readonly firstResultFuture = new Defer(); + public readonly result: Observable; /** * Keeping track of any child executions @@ -139,10 +158,6 @@ export class Execution< public readonly expression: string; - public get result(): Promise { - return this.firstResultFuture.promise; - } - public get inspectorAdapters(): InspectorAdapters { return this.context.inspectorAdapters; } @@ -184,6 +199,28 @@ export class Execution< isSyncColorsEnabled: () => execution.params.syncColors, ...(execution.params as any).extraContext, }; + + this.result = this.input$.pipe( + switchMap((input) => this.race(this.invokeChain(this.state.get().ast.chain, input))), + catchError((error) => { + if (this.abortController.signal.aborted) { + this.childExecutions.forEach((childExecution) => childExecution.cancel()); + + return of(createAbortErrorValue()); + } + + return throwError(error); + }), + tap({ + next: (result) => { + this.context.inspectorAdapters.expression?.logAST(this.state.get().ast); + this.state.transitions.setResult(result); + }, + error: (error) => this.state.transitions.setError(error), + }), + finalize(() => this.abortRejection.cleanup()), + shareReplay(1) + ); } /** @@ -199,150 +236,139 @@ export class Execution< * N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons, * because in legacy interpreter it was set to `null` by default. */ - public start(input: Input = null as any) { + public start(input: Input = null as any): Observable { if (this.hasStarted) throw new Error('Execution already started.'); this.hasStarted = true; - this.input = input; this.state.transitions.start(); - const { resolve, reject } = this.firstResultFuture; - const chainPromise = this.invokeChain(this.state.get().ast.chain, input); - - this.race(chainPromise).then(resolve, (error) => { - if (this.abortController.signal.aborted) { - this.childExecutions.forEach((ex) => ex.cancel()); - resolve(createAbortErrorValue()); - } else reject(error); - }); + if (isObservable(input)) { + // `input$` should never complete + input.subscribe( + (value) => this.input$.next(value), + (error) => this.input$.error(error) + ); + } else if (isPromise(input)) { + input.then( + (value) => this.input$.next(value), + (error) => this.input$.error(error) + ); + } else { + this.input$.next(input); + } - this.firstResultFuture.promise - .then( - (result) => { - if (this.context.inspectorAdapters.expression) { - this.context.inspectorAdapters.expression.logAST(this.state.get().ast); - } - this.state.transitions.setResult(result); - }, - (error) => { - this.state.transitions.setError(error); - } - ) - .finally(() => { - this.abortRejection.cleanup(); - }); + return this.result; } - async invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise { - if (!chainArr.length) return input; - - for (const link of chainArr) { - const { function: fnName, arguments: fnArgs } = link; - const fn = getByAlias(this.state.get().functions, fnName); - - if (!fn) { - return createError({ - name: 'fn not found', - message: i18n.translate('expressions.execution.functionNotFound', { - defaultMessage: `Function {fnName} could not be found.`, - values: { - fnName, - }, - }), - }); - } - - if (fn.disabled) { - return createError({ - name: 'fn is disabled', - message: i18n.translate('expressions.execution.functionDisabled', { - defaultMessage: `Function {fnName} is disabled.`, - values: { - fnName, - }, - }), - }); - } - - let args: Record = {}; - let timeStart: number | undefined; - - try { - // `resolveArgs` returns an object because the arguments themselves might - // actually have a `then` function which would be treated as a `Promise`. - const { resolvedArgs } = await this.race(this.resolveArgs(fn, input, fnArgs)); - args = resolvedArgs; - timeStart = this.execution.params.debug ? now() : 0; - const output = await this.race(this.invokeFunction(fn, input, resolvedArgs)); - - if (this.execution.params.debug) { - const timeEnd: number = now(); - (link as ExpressionAstFunction).debug = { - success: true, - fn: fn.name, - input, - args: resolvedArgs, - output, - duration: timeEnd - timeStart, - }; - } + invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable { + return of(input).pipe( + ...(chainArr.map((link) => + switchMap((currentInput) => { + const { function: fnName, arguments: fnArgs } = link; + const fn = getByAlias(this.state.get().functions, fnName); + + if (!fn) { + throw createError({ + name: 'fn not found', + message: i18n.translate('expressions.execution.functionNotFound', { + defaultMessage: `Function {fnName} could not be found.`, + values: { + fnName, + }, + }), + }); + } - if (getType(output) === 'error') return output; - input = output; - } catch (rawError) { - const timeEnd: number = this.execution.params.debug ? now() : 0; - const error = createError(rawError) as ExpressionValueError; - error.error.message = `[${fnName}] > ${error.error.message}`; - - if (this.execution.params.debug) { - (link as ExpressionAstFunction).debug = { - success: false, - fn: fn.name, - input, - args, - error, - rawError, - duration: timeStart ? timeEnd - timeStart : undefined, - }; - } + if (fn.disabled) { + throw createError({ + name: 'fn is disabled', + message: i18n.translate('expressions.execution.functionDisabled', { + defaultMessage: `Function {fnName} is disabled.`, + values: { + fnName, + }, + }), + }); + } - return error; - } - } + if (this.execution.params.debug) { + link.debug = { + args: {}, + duration: 0, + fn: fn.name, + input: currentInput, + success: true, + }; + } - return input; + const timeStart = this.execution.params.debug ? now() : 0; + + // `resolveArgs` returns an object because the arguments themselves might + // actually have `then` or `subscribe` methods which would be treated as a `Promise` + // or an `Observable` accordingly. + return this.race(this.resolveArgs(fn, currentInput, fnArgs)).pipe( + tap((args) => this.execution.params.debug && Object.assign(link.debug, { args })), + switchMap((args) => this.race(this.invokeFunction(fn, currentInput, args))), + switchMap((output) => (getType(output) === 'error' ? throwError(output) : of(output))), + tap((output) => this.execution.params.debug && Object.assign(link.debug, { output })), + catchError((rawError) => { + const error = createError(rawError); + error.error.message = `[${fnName}] > ${error.error.message}`; + + if (this.execution.params.debug) { + Object.assign(link.debug, { error, rawError, success: false }); + } + + return throwError(error); + }), + finalize(() => { + if (this.execution.params.debug) { + Object.assign(link.debug, { duration: now() - timeStart }); + } + }) + ); + }) + ) as Parameters['pipe']>), + catchError((error) => of(error)) + ); } - async invokeFunction( + invokeFunction( fn: ExpressionFunction, input: unknown, args: Record - ): Promise { - const normalizedInput = this.cast(input, fn.inputTypes); - const output = await this.race(fn.fn(normalizedInput, args, this.context)); - - // Validate that the function returned the type it said it would. - // This isn't required, but it keeps function developers honest. - const returnType = getType(output); - const expectedType = fn.type; - if (expectedType && returnType !== expectedType) { - throw new Error( - `Function '${fn.name}' should return '${expectedType}',` + - ` actually returned '${returnType}'` - ); - } + ): Observable { + return of(input).pipe( + map((currentInput) => this.cast(currentInput, fn.inputTypes)), + switchMap((normalizedInput) => this.race(of(fn.fn(normalizedInput, args, this.context)))), + switchMap((fnResult: any) => + isObservable(fnResult) ? fnResult : from(isPromise(fnResult) ? fnResult : [fnResult]) + ), + map((output) => { + // Validate that the function returned the type it said it would. + // This isn't required, but it keeps function developers honest. + const returnType = getType(output); + const expectedType = fn.type; + if (expectedType && returnType !== expectedType) { + throw new Error( + `Function '${fn.name}' should return '${expectedType}',` + + ` actually returned '${returnType}'` + ); + } - // Validate the function output against the type definition's validate function. - const type = this.context.types[fn.type]; - if (type && type.validate) { - try { - type.validate(output); - } catch (e) { - throw new Error(`Output of '${fn.name}' is not a valid type '${fn.type}': ${e}`); - } - } + // Validate the function output against the type definition's validate function. + const type = this.context.types[fn.type]; + if (type && type.validate) { + try { + type.validate(output); + } catch (e) { + throw new Error(`Output of '${fn.name}' is not a valid type '${fn.type}': ${e}`); + } + } - return output; + return output; + }) + ); } public cast(value: any, toTypeNames?: string[]) { @@ -371,98 +397,96 @@ export class Execution< } // Processes the multi-valued AST argument values into arguments that can be passed to the function - async resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise { - const argDefs = fnDef.args; - - // Use the non-alias name from the argument definition - const dealiasedArgAsts = reduce( - argAsts, - (acc, argAst, argName) => { - const argDef = getByAlias(argDefs, argName); - if (!argDef) { - throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`); - } - acc[argDef.name] = (acc[argDef.name] || []).concat(argAst); - return acc; - }, - {} as any - ); + resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable { + return defer(() => { + const { args: argDefs } = fnDef; + + // Use the non-alias name from the argument definition + const dealiasedArgAsts = reduce( + argAsts as Record, + (acc, argAst, argName) => { + const argDef = getByAlias(argDefs, argName); + if (!argDef) { + throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`); + } + acc[argDef.name] = (acc[argDef.name] || []).concat(argAst); + return acc; + }, + {} as Record + ); - // Check for missing required arguments. - for (const argDef of Object.values(argDefs)) { - const { - aliases, - default: argDefault, - name: argName, - required, - } = argDef as ArgumentType & { name: string }; - if ( - typeof argDefault !== 'undefined' || - !required || - typeof dealiasedArgAsts[argName] !== 'undefined' - ) - continue; - - if (!aliases || aliases.length === 0) { - throw new Error(`${fnDef.name} requires an argument`); - } + // Check for missing required arguments. + for (const { aliases, default: argDefault, name, required } of Object.values(argDefs)) { + if (!(name in dealiasedArgAsts) && typeof argDefault !== 'undefined') { + dealiasedArgAsts[name] = [parse(argDefault, 'argument')]; + } - // use an alias if _ is the missing arg - const errorArg = argName === '_' ? aliases[0] : argName; - throw new Error(`${fnDef.name} requires an "${errorArg}" argument`); - } + if (!required || name in dealiasedArgAsts) { + continue; + } - // Fill in default values from argument definition - const argAstsWithDefaults = reduce( - argDefs, - (acc: any, argDef: any, argName: any) => { - if (typeof acc[argName] === 'undefined' && typeof argDef.default !== 'undefined') { - acc[argName] = [parse(argDef.default, 'argument')]; + if (!aliases?.length) { + throw new Error(`${fnDef.name} requires an argument`); } - return acc; - }, - dealiasedArgAsts - ); + // use an alias if _ is the missing arg + const errorArg = name === '_' ? aliases[0] : name; + throw new Error(`${fnDef.name} requires an "${errorArg}" argument`); + } - // Create the functions to resolve the argument ASTs into values - // These are what are passed to the actual functions if you opt out of resolving - const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { - return asts.map((item: ExpressionAstExpression) => { - return async (subInput = input) => { - const output = await this.interpret(item, subInput); - if (isExpressionValueError(output)) throw output.error; - const casted = this.cast(output, argDefs[argName as any].types); - return casted; - }; - }); - }); + // Create the functions to resolve the argument ASTs into values + // These are what are passed to the actual functions if you opt out of resolving + const resolveArgFns = mapValues(dealiasedArgAsts, (asts, argName) => + asts.map((item) => (subInput = input) => + this.interpret(item, subInput).pipe( + map((output) => { + if (isExpressionValueError(output)) { + throw output.error; + } + + return this.cast(output, argDefs[argName].types); + }) + ) + ) + ); - const argNames = keys(resolveArgFns); + const argNames = keys(resolveArgFns); - // Actually resolve unless the argument definition says not to - const resolvedArgValues = await Promise.all( - argNames.map((argName) => { - const interpretFns = resolveArgFns[argName]; - if (!argDefs[argName].resolve) return interpretFns; - return Promise.all(interpretFns.map((fn: any) => fn())); - }) - ); + if (!argNames.length) { + return from([[]]); + } - const resolvedMultiArgs = zipObject(argNames, resolvedArgValues); + const resolvedArgValuesObservable = combineLatest( + argNames.map((argName) => { + const interpretFns = resolveArgFns[argName]; - // Just return the last unless the argument definition allows multiple - const resolvedArgs = mapValues(resolvedMultiArgs, (argValues, argName) => { - if (argDefs[argName as any].multi) return argValues; - return last(argValues as any); - }); + // `combineLatest` does not emit a value on an empty collection + // @see https://github.com/ReactiveX/RxSwift/issues/1879 + if (!interpretFns.length) { + return of([]); + } - // Return an object here because the arguments themselves might actually have a 'then' - // function which would be treated as a promise - return { resolvedArgs }; + return argDefs[argName].resolve + ? combineLatest(interpretFns.map((fn) => fn())) + : of(interpretFns); + }) + ); + + return resolvedArgValuesObservable.pipe( + map((resolvedArgValues) => + mapValues( + // Return an object here because the arguments themselves might actually have a 'then' + // function which would be treated as a promise + zipObject(argNames, resolvedArgValues), + // Just return the last unless the argument definition allows multiple + (argValues, argName) => (argDefs[argName].multi ? argValues : last(argValues)) + ) + ) + ); + }); } - public async interpret(ast: ExpressionAstNode, input: T): Promise { + public interpret(ast: ExpressionAstNode, input: T): Observable { switch (getType(ast)) { case 'expression': const execution = this.execution.executor.createExecution( @@ -470,15 +494,14 @@ export class Execution< this.execution.params ); this.childExecutions.push(execution); - execution.start(input); - return await execution.result; + return execution.start(input); case 'string': case 'number': case 'null': case 'boolean': - return ast; + return of(ast); default: - throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`); + return throwError(new Error(`Unknown AST object: ${JSON.stringify(ast)}`)); } } } diff --git a/src/plugins/expressions/common/execution/execution_contract.test.ts b/src/plugins/expressions/common/execution/execution_contract.test.ts index 111dc0d735ebb7..99a5c80de3c462 100644 --- a/src/plugins/expressions/common/execution/execution_contract.test.ts +++ b/src/plugins/expressions/common/execution/execution_contract.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { first } from 'rxjs/operators'; import { Execution } from './execution'; import { parseExpression } from '../ast'; import { createUnitTestExecutor } from '../test_helpers'; @@ -108,7 +109,7 @@ describe('ExecutionContract', () => { const contract = new ExecutionContract(execution); execution.start(); - await execution.result; + await execution.result.pipe(first()).toPromise(); expect(contract.isPending).toBe(false); expect(execution.state.get().state).toBe('result'); @@ -119,7 +120,7 @@ describe('ExecutionContract', () => { const contract = new ExecutionContract(execution); execution.start(); - await execution.result; + await execution.result.pipe(first()).toPromise(); execution.state.get().state = 'error'; expect(contract.isPending).toBe(false); diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts index 9ef2aa79f520c8..3cad9cef5e09ae 100644 --- a/src/plugins/expressions/common/execution/execution_contract.ts +++ b/src/plugins/expressions/common/execution/execution_contract.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { of } from 'rxjs'; +import { catchError, take } from 'rxjs/operators'; import { Execution } from './execution'; import { ExpressionValueError } from '../expression_types/specs'; import { ExpressionAstExpression } from '../ast'; @@ -38,18 +40,21 @@ export class ExecutionContract => { - try { - return await this.execution.result; - } catch (e) { - return { - type: 'error', - error: { - name: e.name, - message: e.message, - stack: e.stack, - }, - }; - } + return this.execution.result + .pipe( + take(1), + catchError(({ name, message, stack }) => + of({ + type: 'error', + error: { + name, + message, + stack, + }, + } as ExpressionValueError) + ) + ) + .toPromise(); }; /** diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 255de31f7239b1..1eea51a0e1ec45 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -9,6 +9,7 @@ /* eslint-disable max-classes-per-file */ import { cloneDeep, mapValues } from 'lodash'; +import { Observable } from 'rxjs'; import { ExecutorState, ExecutorContainer } from './container'; import { createExecutorContainer } from './container'; import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; @@ -17,7 +18,7 @@ import { IRegistry } from '../types'; import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; import { ExpressionAstExpression, ExpressionAstFunction } from '../ast'; -import { typeSpecs } from '../expression_types/specs'; +import { ExpressionValueError, typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; @@ -156,14 +157,12 @@ export class Executor = Record( + public run( ast: string | ExpressionAstExpression, input: Input, params: ExpressionExecutionParams = {} - ) { - const execution = this.createExecution(ast, params); - execution.start(input); - return (await execution.result) as Output; + ): Observable { + return this.createExecution(ast, params).start(input); } public createExecution( diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 6eb1762a5d3c39..a4cb1141104984 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -10,7 +10,6 @@ import { identity } from 'lodash'; import { AnyExpressionFunctionDefinition } from './types'; import { ExpressionFunctionParameter } from './expression_function_parameter'; import { ExpressionValue } from '../expression_types/types'; -import { ExecutionContext } from '../execution'; import { ExpressionAstFunction } from '../ast'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableState, SerializableState } from '../../../kibana_utils/common'; @@ -89,8 +88,7 @@ export class ExpressionFunction implements PersistableState - Promise.resolve(fn(input, params, handlers as ExecutionContext)); + this.fn = fn as ExpressionFunction['fn']; this.help = help || ''; this.inputTypes = inputTypes || context?.types; this.disabled = disabled || false; diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts index e2605e5ddf38d2..c570206670dde5 100644 --- a/src/plugins/expressions/common/expression_functions/specs/map_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; import { Datatable, getType } from '../../expression_types'; @@ -13,7 +15,7 @@ import { Datatable, getType } from '../../expression_types'; export interface MapColumnArguments { id?: string | null; name: string; - expression?: (datatable: Datatable) => Promise; + expression?(datatable: Datatable): Observable; copyMetaFrom?: string | null; } @@ -79,7 +81,11 @@ export const mapColumn: ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const expression = args.expression || (() => Promise.resolve(null)); + const expression = (...params: Parameters['expression']>) => + args + .expression?.(...params) + .pipe(take(1)) + .toPromise() ?? Promise.resolve(null); const columnId = args.id != null ? args.id : args.name; const columns = [...input.columns]; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts index 6b0dce4ff9a2a1..b2966b010b4790 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts @@ -6,85 +6,80 @@ * Side Public License, v 1. */ +import { of } from 'rxjs'; import { Datatable } from '../../../expression_types'; import { mapColumn, MapColumnArguments } from '../map_column'; import { emptyTable, functionWrapper, testTable } from './utils'; -const pricePlusTwo = (datatable: Datatable) => Promise.resolve(datatable.rows[0].price + 2); +const pricePlusTwo = (datatable: Datatable) => of(datatable.rows[0].price + 2); describe('mapColumn', () => { const fn = functionWrapper(mapColumn); const runFn = (input: Datatable, args: MapColumnArguments) => fn(input, args) as Promise; - it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => { - return runFn(testTable, { + it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', async () => { + const arbitraryRowIndex = 2; + const result = await runFn(testTable, { id: 'pricePlusTwo', name: 'pricePlusTwo', expression: pricePlusTwo, - }).then((result) => { - const arbitraryRowIndex = 2; - - expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([ - ...testTable.columns, - { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } }, - ]); - expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo'); }); + + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual([ + ...testTable.columns, + { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } }, + ]); + expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo'); + expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo'); }); - it('overwrites existing column with the new column if an existing column name is provided', () => { - return runFn(testTable, { name: 'name', expression: pricePlusTwo }).then((result) => { - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); - const arbitraryRowIndex = 4; + it('overwrites existing column with the new column if an existing column name is provided', async () => { + const result = await runFn(testTable, { name: 'name', expression: pricePlusTwo }); + const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); + const arbitraryRowIndex = 4; - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(testTable.columns.length); - expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name'); - expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202); - }); + expect(result.type).toBe('datatable'); + expect(result.columns).toHaveLength(testTable.columns.length); + expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name'); + expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); + expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202); }); - it('adds a column to empty tables', () => { - return runFn(emptyTable, { name: 'name', expression: pricePlusTwo }).then((result) => { - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); - }); + it('adds a column to empty tables', async () => { + const result = await runFn(emptyTable, { name: 'name', expression: pricePlusTwo }); + + expect(result.type).toBe('datatable'); + expect(result.columns).toHaveLength(1); + expect(result.columns[0]).toHaveProperty('name', 'name'); + expect(result.columns[0].meta).toHaveProperty('type', 'null'); }); - it('should assign specific id, different from name, when id arg is passed for new columns', () => { - return runFn(emptyTable, { name: 'name', id: 'myid', expression: pricePlusTwo }).then( - (result) => { - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('id', 'myid'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); - } - ); + it('should assign specific id, different from name, when id arg is passed for new columns', async () => { + const result = await runFn(emptyTable, { name: 'name', id: 'myid', expression: pricePlusTwo }); + + expect(result.type).toBe('datatable'); + expect(result.columns).toHaveLength(1); + expect(result.columns[0]).toHaveProperty('name', 'name'); + expect(result.columns[0]).toHaveProperty('id', 'myid'); + expect(result.columns[0].meta).toHaveProperty('type', 'null'); }); - it('should assign specific id, different from name, when id arg is passed for copied column', () => { - return runFn(testTable, { name: 'name', id: 'myid', expression: pricePlusTwo }).then( - (result) => { - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); - expect(result.type).toBe('datatable'); - expect(result.columns[nameColumnIndex]).toEqual({ - id: 'myid', - name: 'name', - meta: { type: 'number' }, - }); - } - ); + it('should assign specific id, different from name, when id arg is passed for copied column', async () => { + const result = await runFn(testTable, { name: 'name', id: 'myid', expression: pricePlusTwo }); + const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); + + expect(result.type).toBe('datatable'); + expect(result.columns[nameColumnIndex]).toEqual({ + id: 'myid', + name: 'name', + meta: { type: 'number' }, + }); }); - it('should copy over the meta information from the specified column', () => { - return runFn( + it('should copy over the meta information from the specified column', async () => { + const result = await runFn( { ...testTable, columns: [ @@ -99,52 +94,53 @@ describe('mapColumn', () => { rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })), }, { name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo } - ).then((result) => { - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); - expect(result.type).toBe('datatable'); - expect(result.columns[nameColumnIndex]).toEqual({ - id: 'name', - name: 'name', - meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, - }); + ); + const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); + + expect(result.type).toBe('datatable'); + expect(result.columns[nameColumnIndex]).toEqual({ + id: 'name', + name: 'name', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, }); }); - it('should be resilient if the references column for meta information does not exists', () => { - return runFn(emptyTable, { name: 'name', copyMetaFrom: 'time', expression: pricePlusTwo }).then( - (result) => { - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('id', 'name'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); - } - ); + it('should be resilient if the references column for meta information does not exists', async () => { + const result = await runFn(emptyTable, { + name: 'name', + copyMetaFrom: 'time', + expression: pricePlusTwo, + }); + + expect(result.type).toBe('datatable'); + expect(result.columns).toHaveLength(1); + expect(result.columns[0]).toHaveProperty('name', 'name'); + expect(result.columns[0]).toHaveProperty('id', 'name'); + expect(result.columns[0].meta).toHaveProperty('type', 'null'); }); - it('should correctly infer the type fromt he first row if the references column for meta information does not exists', () => { - return runFn( + it('should correctly infer the type fromt he first row if the references column for meta information does not exists', async () => { + const result = await runFn( { ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] }, { name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo } - ).then((result) => { - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'value'); - expect(result.columns[0]).toHaveProperty('id', 'value'); - expect(result.columns[0].meta).toHaveProperty('type', 'number'); - }); + ); + + expect(result.type).toBe('datatable'); + expect(result.columns).toHaveLength(1); + expect(result.columns[0]).toHaveProperty('name', 'value'); + expect(result.columns[0]).toHaveProperty('id', 'value'); + expect(result.columns[0].meta).toHaveProperty('type', 'number'); }); describe('expression', () => { - it('maps null values to the new column', () => { - return runFn(testTable, { name: 'empty' }).then((result) => { - const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty'); - const arbitraryRowIndex = 8; - - expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty'); - expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null); - }); + it('maps null values to the new column', async () => { + const result = await runFn(testTable, { name: 'empty' }); + const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty'); + const arbitraryRowIndex = 8; + + expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty'); + expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null'); + expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null); }); }); }); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 28df6d69818299..d57c1748954abf 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { take } from 'rxjs/operators'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; @@ -228,7 +229,7 @@ export class ExpressionsService implements PersistableStateService this.renderers.register(definition); public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => - this.executor.run(ast, input, params); + this.executor.run(ast, input, params).pipe(take(1)).toPromise(); public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/index.ts b/src/plugins/expressions/common/test_helpers/expression_functions/index.ts index 3e5f93e5830817..b73d1e0208f892 100644 --- a/src/plugins/expressions/common/test_helpers/expression_functions/index.ts +++ b/src/plugins/expressions/common/test_helpers/expression_functions/index.ts @@ -12,6 +12,7 @@ import { error } from './error'; import { introspectContext } from './introspect_context'; import { mult } from './mult'; import { sleep } from './sleep'; +import { sum } from './sum'; import { AnyExpressionFunctionDefinition } from '../../expression_functions'; export const functionTestSpecs: AnyExpressionFunctionDefinition[] = [ @@ -21,4 +22,5 @@ export const functionTestSpecs: AnyExpressionFunctionDefinition[] = [ introspectContext, mult, sleep, + sum, ]; diff --git a/src/plugins/expressions/common/test_helpers/expression_functions/sum.ts b/src/plugins/expressions/common/test_helpers/expression_functions/sum.ts new file mode 100644 index 00000000000000..54b0207364ffd9 --- /dev/null +++ b/src/plugins/expressions/common/test_helpers/expression_functions/sum.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 { ExpressionFunctionDefinition } from '../../expression_functions'; +import { ExpressionValueNum } from '../../expression_types'; + +export const sum: ExpressionFunctionDefinition<'sum', unknown[], {}, ExpressionValueNum> = { + name: 'sum', + help: 'This function summarizes the input', + inputTypes: [], + args: {}, + fn: (values) => { + return { + type: 'num', + value: Array.isArray(values) ? values.reduce((a, b) => a + b) : values, + }; + }, +}; diff --git a/src/plugins/expressions/common/types/common.ts b/src/plugins/expressions/common/types/common.ts index a2f1ed9a0d569f..d8d1a9a4b256a1 100644 --- a/src/plugins/expressions/common/types/common.ts +++ b/src/plugins/expressions/common/types/common.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; +import { ObservableLike, UnwrapObservable, UnwrapPromiseOrReturn } from '@kbn/utility-types'; /** * This can convert a type into a known Expression string representation of @@ -23,9 +23,9 @@ export type TypeToString = KnownTypeToString | UnmappedTypeStrings; * the `type` key as a string literal type for it. */ // prettier-ignore -export type KnownTypeToString = - T extends string ? 'string' : - T extends boolean ? 'boolean' : +export type KnownTypeToString = + T extends string ? 'string' : + T extends boolean ? 'boolean' : T extends number ? 'number' : T extends null ? 'null' : T extends { type: string } ? T['type'] : @@ -36,7 +36,9 @@ export type KnownTypeToString = * * `someArgument: Promise` results in `types: ['boolean', 'string']` */ -export type TypeString = KnownTypeToString>; +export type TypeString = KnownTypeToString< + T extends ObservableLike ? UnwrapObservable : UnwrapPromiseOrReturn +>; /** * Types used in Expressions that don't map to a primitive cleanly: diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 2177966fb4d7a3..98adec285afd5e 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -43,6 +43,11 @@ jest.mock('./services', () => { }; service.registerFunction(testFn); + // eslint-disable-next-line @typescript-eslint/no-var-requires + for (const func of require('../common/test_helpers/expression_functions').functionTestSpecs) { + service.registerFunction(func); + } + const moduleMock = { __execution: undefined, __getLastExecution: () => moduleMock.__execution, @@ -144,7 +149,7 @@ describe('ExpressionLoader', () => { }); it('cancels the previous request when the expression is updated', () => { - const expressionLoader = new ExpressionLoader(element, 'var foo', {}); + const expressionLoader = new ExpressionLoader(element, 'sleep 10', {}); const execution = __getLastExecution(); jest.spyOn(execution, 'cancel'); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index b3e7803f97c38c..9c17a2753cfc93 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -11,10 +11,12 @@ import { EnvironmentMode } from '@kbn/config'; import { EventEmitter } from 'events'; import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; +import { ObservableLike } from '@kbn/utility-types'; import { PackageInfo } from '@kbn/config'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import React from 'react'; +import { UnwrapObservable } from '@kbn/utility-types'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; // Warning: (ae-missing-release-tag) "AnyExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -110,16 +112,15 @@ export class Execution(ast: ExpressionAstNode, input: T): Promise; + interpret(ast: ExpressionAstNode, input: T): Observable; // (undocumented) - invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; + invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable; // (undocumented) - invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Promise; + invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Observable; // (undocumented) - resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise; - // (undocumented) - get result(): Promise; - start(input?: Input): void; + resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable; + readonly result: Observable; + start(input?: Input): Observable; readonly state: ExecutionContainer; } @@ -229,7 +230,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; + run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -1160,7 +1161,7 @@ export class TypesRegistry implements IRegistry { // Warning: (ae-missing-release-tag) "TypeString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type TypeString = KnownTypeToString>; +export type TypeString = KnownTypeToString ? UnwrapObservable : UnwrapPromiseOrReturn>; // Warning: (ae-missing-release-tag) "TypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 2d873fa5183069..12af0480fac93f 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -10,8 +10,10 @@ import { Ensure } from '@kbn/utility-types'; import { EventEmitter } from 'events'; import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; +import { ObservableLike } from '@kbn/utility-types'; import { Plugin as Plugin_2 } from 'src/core/server'; import { PluginInitializerContext } from 'src/core/server'; +import { UnwrapObservable } from '@kbn/utility-types'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; // Warning: (ae-missing-release-tag) "AnyExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -108,16 +110,15 @@ export class Execution(ast: ExpressionAstNode, input: T): Promise; + interpret(ast: ExpressionAstNode, input: T): Observable; // (undocumented) - invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; + invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable; // (undocumented) - invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Promise; + invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Observable; // (undocumented) - resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise; - // (undocumented) - get result(): Promise; - start(input?: Input): void; + resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable; + readonly result: Observable; + start(input?: Input): Observable; readonly state: ExecutionContainer; } @@ -211,7 +212,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; + run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -919,7 +920,7 @@ export class TypesRegistry implements IRegistry { // Warning: (ae-missing-release-tag) "TypeString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type TypeString = KnownTypeToString>; +export type TypeString = KnownTypeToString ? UnwrapObservable : UnwrapPromiseOrReturn>; // Warning: (ae-missing-release-tag) "TypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js index 997afdbddeafab..fc5d03190d4f8d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js @@ -5,6 +5,7 @@ * 2.0. */ +import { of } from 'rxjs'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { caseFn } from './case'; @@ -19,10 +20,10 @@ describe('case', () => { describe('function', () => { describe('no args', () => { - it('should return a case object that matches with the result as the context', async () => { + it('should return a case object that matches with the result as the context', () => { const context = null; const args = {}; - expect(await fn(context, args)).toEqual({ + expect(fn(context, args)).resolves.toEqual({ type: 'case', matches: true, result: context, @@ -31,24 +32,24 @@ describe('case', () => { }); describe('no if or value', () => { - it('should return the result if provided', async () => { + it('should return the result if provided', () => { const context = null; const args = { - then: () => 'foo', + then: () => of('foo'), }; - expect(await fn(context, args)).toEqual({ + expect(fn(context, args)).resolves.toEqual({ type: 'case', matches: true, - result: args.then(), + result: 'foo', }); }); }); describe('with if', () => { - it('should return as the matches prop', async () => { + it('should return as the matches prop', () => { const context = null; const args = { if: false }; - expect(await fn(context, args)).toEqual({ + expect(fn(context, args)).resolves.toEqual({ type: 'case', matches: args.if, result: context, @@ -57,17 +58,17 @@ describe('case', () => { }); describe('with value', () => { - it('should return whether it matches the context as the matches prop', async () => { + it('should return whether it matches the context as the matches prop', () => { const args = { - when: () => 'foo', - then: () => 'bar', + when: () => of('foo'), + then: () => of('bar'), }; - expect(await fn('foo', args)).toEqual({ + expect(fn('foo', args)).resolves.toEqual({ type: 'case', matches: true, - result: args.then(), + result: 'bar', }); - expect(await fn('bar', args)).toEqual({ + expect(fn('bar', args)).resolves.toEqual({ type: 'case', matches: false, result: null, @@ -76,13 +77,13 @@ describe('case', () => { }); describe('with if and value', () => { - it('should return the if as the matches prop', async () => { + it('should return the if as the matches prop', () => { const context = null; const args = { when: () => 'foo', if: true, }; - expect(await fn(context, args)).toEqual({ + expect(fn(context, args)).resolves.toEqual({ type: 'case', matches: args.if, result: context, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts index ce2d6586acc3ff..7fba5b74e9b207 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - when: () => any; - if: boolean; - then: () => any; + when?(): Observable; + if?: boolean; + then?(): Observable; } interface Case { @@ -31,16 +33,16 @@ export function caseFn(): ExpressionFunctionDefinition<'case', any, Arguments, P when: { aliases: ['_'], resolve: false, - help: argHelp.when, + help: argHelp.when!, }, if: { types: ['boolean'], - help: argHelp.if, + help: argHelp.if!, }, then: { resolve: false, required: true, - help: argHelp.then, + help: argHelp.then!, }, }, fn: async (input, args) => { @@ -56,14 +58,11 @@ async function doesMatch(context: any, args: Arguments) { return args.if; } if (typeof args.when !== 'undefined') { - return (await args.when()) === context; + return (await args.when().pipe(take(1)).toPromise()) === context; } return true; } async function getResult(context: any, args: Arguments) { - if (typeof args.then !== 'undefined') { - return await args.then(); - } - return context; + return args.then?.().pipe(take(1)).toPromise() ?? context; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js index 9be713e0154476..fdea4faa4ece28 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js @@ -5,35 +5,38 @@ * 2.0. */ +import { of } from 'rxjs'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { testTable } from './__fixtures__/test_tables'; import { filterrows } from './filterrows'; -const inStock = (datatable) => datatable.rows[0].in_stock; -const returnFalse = () => false; +const inStock = (datatable) => of(datatable.rows[0].in_stock); +const returnFalse = () => of(false); describe('filterrows', () => { const fn = functionWrapper(filterrows); it('returns a datable', () => { - return fn(testTable, { fn: inStock }).then((result) => { - expect(result).toHaveProperty('type', 'datatable'); - }); + expect(fn(testTable, { fn: inStock })).resolves.toHaveProperty('type', 'datatable'); }); it('keeps rows that evaluate to true and removes rows that evaluate to false', () => { const inStockRows = testTable.rows.filter((row) => row.in_stock); - return fn(testTable, { fn: inStock }).then((result) => { - expect(result.columns).toEqual(testTable.columns); - expect(result.rows).toEqual(inStockRows); - }); + expect(fn(testTable, { fn: inStock })).resolves.toEqual( + expect.objectContaining({ + columns: testTable.columns, + rows: inStockRows, + }) + ); }); it('returns datatable with no rows when no rows meet function condition', () => { - return fn(testTable, { fn: returnFalse }).then((result) => { - expect(result.rows).toEqual([]); - }); + expect(fn(testTable, { fn: returnFalse })).resolves.toEqual( + expect.objectContaining({ + rows: [], + }) + ); }); it('throws when no function is provided', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts index 90bf668f4503c3..082506f58e86fa 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.ts @@ -5,11 +5,13 @@ * 2.0. */ +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { Datatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - fn: (datatable: Datatable) => Promise; + fn: (datatable: Datatable) => Observable; } export function filterrows(): ExpressionFunctionDefinition< @@ -41,6 +43,8 @@ export function filterrows(): ExpressionFunctionDefinition< ...input, rows: [row], }) + .pipe(take(1)) + .toPromise() ); return Promise.all(checks) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js index 458447ca07c4b7..8e1106644105e4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js @@ -5,6 +5,7 @@ * 2.0. */ +import { of } from 'rxjs'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { ifFn } from './if'; @@ -19,52 +20,68 @@ describe('if', () => { describe('function', () => { describe('condition passed', () => { - it('with then', async () => { - expect(await fn(null, { condition: true, then: () => 'foo' })).toBe('foo'); - expect(await fn(null, { condition: true, then: () => 'foo', else: () => 'bar' })).toBe( - 'foo' - ); + it('with then', () => { + expect(fn(null, { condition: true, then: () => of('foo') })).resolves.toBe('foo'); + expect( + fn(null, { condition: true, then: () => of('foo'), else: () => of('bar') }) + ).resolves.toBe('foo'); }); - it('without then', async () => { - expect(await fn(null, { condition: true })).toBe(null); - expect(await fn('some context', { condition: true })).toBe('some context'); + it('without then', () => { + expect(fn(null, { condition: true })).resolves.toBe(null); + expect(fn('some context', { condition: true })).resolves.toBe('some context'); }); }); describe('condition failed', () => { - it('with else', async () => + it('with else', () => expect( - await fn('some context', { condition: false, then: () => 'foo', else: () => 'bar' }) - ).toBe('bar')); + fn('some context', { + condition: false, + then: () => of('foo'), + else: () => of('bar'), + }) + ).resolves.toBe('bar')); - it('without else', async () => - expect(await fn('some context', { condition: false, then: () => 'foo' })).toBe( + it('without else', () => + expect(fn('some context', { condition: false, then: () => of('foo') })).resolves.toBe( 'some context' )); }); describe('falsy values', () => { describe('for then', () => { - it('with null', async () => - expect(await fn('some context', { condition: true, then: () => null })).toBe(null)); + it('with null', () => { + expect(fn('some context', { condition: true, then: () => of(null) })).resolves.toBe(null); + }); - it('with false', async () => - expect(await fn('some context', { condition: true, then: () => false })).toBe(false)); + it('with false', () => { + expect(fn('some context', { condition: true, then: () => of(false) })).resolves.toBe( + false + ); + }); - it('with 0', async () => - expect(await fn('some context', { condition: true, then: () => 0 })).toBe(0)); + it('with 0', () => { + expect(fn('some context', { condition: true, then: () => of(0) })).resolves.toBe(0); + }); }); describe('for else', () => { - it('with null', async () => - expect(await fn('some context', { condition: false, else: () => null })).toBe(null)); + it('with null', () => { + expect(fn('some context', { condition: false, else: () => of(null) })).resolves.toBe( + null + ); + }); - it('with false', async () => - expect(await fn('some context', { condition: false, else: () => false })).toBe(false)); + it('with false', () => { + expect(fn('some context', { condition: false, else: () => of(false) })).resolves.toBe( + false + ); + }); - it('with 0', async () => - expect(await fn('some context', { condition: false, else: () => 0 })).toBe(0)); + it('with 0', () => { + expect(fn('some context', { condition: false, else: () => of(0) })).resolves.toBe(0); + }); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts index fca98d3b9a725b..6d7665db551e4b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { condition: boolean | null; - then: () => Promise; - else: () => Promise; + then?(): Observable; + else?(): Observable; } export function ifFn(): ExpressionFunctionDefinition<'if', unknown, Arguments, unknown> { @@ -29,24 +31,18 @@ export function ifFn(): ExpressionFunctionDefinition<'if', unknown, Arguments, u }, then: { resolve: false, - help: argHelp.then, + help: argHelp.then!, }, else: { resolve: false, - help: argHelp.else, + help: argHelp.else!, }, }, fn: async (input, args) => { if (args.condition) { - if (typeof args.then === 'undefined') { - return input; - } - return await args.then(); + return args.then?.().pipe(take(1)).toPromise() ?? input; } else { - if (typeof args.else === 'undefined') { - return input; - } - return await args.else(); + return args.else?.().pipe(take(1)).toPromise() ?? input; } }, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js index 588a897453be4f..74eca79395a103 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js @@ -5,6 +5,7 @@ * 2.0. */ +import { of } from 'rxjs'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; import { testTable } from './__fixtures__/test_tables'; @@ -15,7 +16,7 @@ const errors = getFunctionErrors().ply; const averagePrice = (datatable) => { const average = datatable.rows.reduce((sum, row) => sum + row.price, 0) / datatable.rows.length; - return Promise.resolve({ + return of({ type: 'datatable', columns: [{ id: 'average_price', name: 'average_price', meta: { type: 'number' } }], rows: [{ average_price: average }], @@ -23,17 +24,17 @@ const averagePrice = (datatable) => { }; const doublePrice = (datatable) => { - const newRows = datatable.rows.map((row) => ({ double_price: row.price * 2 })); + const newRows = datatable.rows.map((row) => of({ double_price: row.price * 2 })); - return Promise.resolve({ + return of({ type: 'datatable', columns: [{ id: 'double_price', name: 'double_price', meta: { type: 'number' } }], rows: newRows, }); }; -const rowCount = (datatable) => { - return Promise.resolve({ +const rowCount = (datatable) => + of({ type: 'datatable', columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], rows: [ @@ -42,43 +43,40 @@ const rowCount = (datatable) => { }, ], }); -}; describe('ply', () => { const fn = functionWrapper(ply); - it('maps a function over sub datatables grouped by specified columns and merges results into one datatable', () => { + it('maps a function over sub datatables grouped by specified columns and merges results into one datatable', async () => { const arbitaryRowIndex = 0; + const result = await fn(testTable, { + by: ['name', 'in_stock'], + expression: [averagePrice, rowCount], + }); - return fn(testTable, { by: ['name', 'in_stock'], expression: [averagePrice, rowCount] }).then( - (result) => { - expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([ - { id: 'name', name: 'name', meta: { type: 'string' } }, - { id: 'in_stock', name: 'in_stock', meta: { type: 'boolean' } }, - { id: 'average_price', name: 'average_price', meta: { type: 'number' } }, - { id: 'row_count', name: 'row_count', meta: { type: 'number' } }, - ]); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('average_price'); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('row_count'); - } - ); + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual([ + { id: 'name', name: 'name', meta: { type: 'string' } }, + { id: 'in_stock', name: 'in_stock', meta: { type: 'boolean' } }, + { id: 'average_price', name: 'average_price', meta: { type: 'number' } }, + { id: 'row_count', name: 'row_count', meta: { type: 'number' } }, + ]); + expect(result.rows[arbitaryRowIndex]).toHaveProperty('average_price'); + expect(result.rows[arbitaryRowIndex]).toHaveProperty('row_count'); }); describe('missing args', () => { it('returns the original datatable if both args are missing', () => { - return fn(testTable).then((result) => expect(result).toEqual(testTable)); + expect(fn(testTable)).resolves.toEqual(testTable); }); describe('by', () => { it('passes the entire context into the expression when no columns are provided', () => { - return fn(testTable, { expression: [rowCount] }).then((result) => - expect(result).toEqual({ - type: 'datatable', - rows: [{ row_count: testTable.rows.length }], - columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], - }) - ); + expect(fn(testTable, { expression: [rowCount] })).resolves.toEqual({ + type: 'datatable', + rows: [{ row_count: testTable.rows.length }], + columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], + }); }); it('throws when by is an invalid column', () => { @@ -93,20 +91,23 @@ describe('ply', () => { }); describe('expression', () => { - it('returns the original datatable grouped by the specified columns', () => { + it('returns the original datatable grouped by the specified columns', async () => { const arbitaryRowIndex = 6; + const result = await fn(testTable, { by: ['price', 'quantity'] }); - return fn(testTable, { by: ['price', 'quantity'] }).then((result) => { - expect(result.columns[0]).toHaveProperty('name', 'price'); - expect(result.columns[1]).toHaveProperty('name', 'quantity'); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('price'); - expect(result.rows[arbitaryRowIndex]).toHaveProperty('quantity'); - }); + expect(result.columns[0]).toHaveProperty('name', 'price'); + expect(result.columns[1]).toHaveProperty('name', 'quantity'); + expect(result.rows[arbitaryRowIndex]).toHaveProperty('price'); + expect(result.rows[arbitaryRowIndex]).toHaveProperty('quantity'); }); it('throws when row counts do not match across resulting datatables', () => { - return fn(testTable, { by: ['name'], expression: [doublePrice, rowCount] }).catch((e) => - expect(e.message).toBe(errors.rowCountMismatch().message) + expect( + fn(testTable, { by: ['name'], expression: [doublePrice, rowCount] }) + ).rejects.toEqual( + expect.objectContaining({ + message: errors.rowCountMismatch().message, + }) ); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts index 0dd6590ce1b3f3..514d7f73d48e47 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { groupBy, flatten, pick, map } from 'lodash'; import { Datatable, DatatableColumn, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { by: string[]; - expression: Array<(datatable: Datatable) => Promise>; + expression: Array<(datatable: Datatable) => Observable>; } type Output = Datatable | Promise; @@ -73,7 +75,7 @@ export function ply(): ExpressionFunctionDefinition<'ply', Datatable, Arguments, if (args.expression) { expressionResultPromises = args.expression.map((expression) => - expression(originalDatatable) + expression(originalDatatable).pipe(take(1)).toPromise() ); } else { expressionResultPromises.push(Promise.resolve(originalDatatable)); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js index 6371198b2db92d..6d9a20dfeb4873 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js @@ -5,12 +5,13 @@ * 2.0. */ +import { of } from 'rxjs'; import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { switchFn } from './switch'; describe('switch', () => { const fn = functionWrapper(switchFn); - const getter = (value) => () => value; + const getter = (value) => () => of(value); const mockCases = [ { type: 'case', @@ -48,32 +49,32 @@ describe('switch', () => { describe('function', () => { describe('with no cases', () => { - it('should return the context if no default is provided', async () => { + it('should return the context if no default is provided', () => { const context = 'foo'; - expect(await fn(context, {})).toBe(context); + expect(fn(context, {})).resolves.toBe(context); }); - it('should return the default if provided', async () => { + it('should return the default if provided', () => { const context = 'foo'; - const args = { default: () => 'bar' }; - expect(await fn(context, args)).toBe(args.default()); + const args = { default: () => of('bar') }; + expect(fn(context, args)).resolves.toBe('bar'); }); }); describe('with no matching cases', () => { - it('should return the context if no default is provided', async () => { + it('should return the context if no default is provided', () => { const context = 'foo'; const args = { case: nonMatchingCases.map(getter) }; - expect(await fn(context, args)).toBe(context); + expect(fn(context, args)).resolves.toBe(context); }); - it('should return the default if provided', async () => { + it('should return the default if provided', () => { const context = 'foo'; const args = { case: nonMatchingCases.map(getter), - default: () => 'bar', + default: () => of('bar'), }; - expect(await fn(context, args)).toBe(args.default()); + expect(fn(context, args)).resolves.toBe('bar'); }); }); @@ -82,7 +83,7 @@ describe('switch', () => { const context = 'foo'; const args = { case: mockCases.map(getter) }; const firstMatch = mockCases.find((c) => c.matches); - expect(await fn(context, args)).toBe(firstMatch.result); + expect(fn(context, args)).resolves.toBe(firstMatch.result); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts index 57cfca4ab2787a..4258f56ec4cf5f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Case } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - case: Array<() => Promise>; - default: () => any; + case: Array<() => Observable>; + default?(): Observable; } export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Arguments, unknown> { @@ -32,25 +34,21 @@ export function switchFn(): ExpressionFunctionDefinition<'switch', unknown, Argu default: { aliases: ['finally'], resolve: false, - help: argHelp.default, + help: argHelp.default!, }, }, fn: async (input, args) => { const cases = args.case || []; for (let i = 0; i < cases.length; i++) { - const { matches, result } = await cases[i](); + const { matches, result } = await cases[i]().pipe(take(1)).toPromise(); if (matches) { return result; } } - if (typeof args.default !== 'undefined') { - return await args.default(); - } - - return input; + return args.default?.().pipe(take(1)).toPromise() ?? input; }, }; } diff --git a/x-pack/plugins/canvas/public/apps/workpad/routes.ts b/x-pack/plugins/canvas/public/apps/workpad/routes.ts index d2586b1d89ec9d..8ecba5e1833434 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/routes.ts +++ b/x-pack/plugins/canvas/public/apps/workpad/routes.ts @@ -70,8 +70,8 @@ export const routes = [ const fetchedWorkpad = await workpadService.get(params.id); const { assets, ...workpad } = fetchedWorkpad; - dispatch(setWorkpad(workpad)); dispatch(setAssets(assets)); + dispatch(setWorkpad(workpad)); // reset transient properties when changing workpads dispatch(setZoomScale(1)); From aee7787db9d25690dba861049045a906b0415090 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 22 Apr 2021 15:31:04 -0400 Subject: [PATCH 22/33] [Uptime] open synthetics beta disclaimer in a new tab (#97822) --- .../components/monitor/monitor_title.test.tsx | 14 ++++++++++---- .../public/components/monitor/monitor_title.tsx | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx index 4bf4e9193de7e6..5e77e68720c528 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx @@ -139,7 +139,7 @@ describe('MonitorTitle component', () => { state: { monitorStatus: { status: defaultBrowserMonitorStatus, loading: false } }, }); const betaLink = screen.getByRole('link', { - name: 'See more External link', + name: 'See more External link (opens in a new tab or window)', }) as HTMLAnchorElement; expect(betaLink).toBeInTheDocument(); expect(betaLink.href).toBe('https://www.elastic.co/what-is/synthetic-monitoring'); @@ -152,7 +152,9 @@ describe('MonitorTitle component', () => { }); expect(screen.getByText('HTTP ping')).toBeInTheDocument(); expect(screen.queryByText(/BETA/)).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'See more External link' })).not.toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: 'See more External link (opens in a new tab or window)' }) + ).not.toBeInTheDocument(); }); it('does not render beta disclaimer for tcp', () => { @@ -161,7 +163,9 @@ describe('MonitorTitle component', () => { }); expect(screen.getByText('TCP ping')).toBeInTheDocument(); expect(screen.queryByText(/BETA/)).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'See more External link' })).not.toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: 'See more External link (opens in a new tab or window)' }) + ).not.toBeInTheDocument(); }); it('renders badge and does not render beta disclaimer for icmp', () => { @@ -170,6 +174,8 @@ describe('MonitorTitle component', () => { }); expect(screen.getByText('ICMP ping')).toBeInTheDocument(); expect(screen.queryByText(/BETA/)).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'See more External link' })).not.toBeInTheDocument(); + expect( + screen.queryByRole('link', { name: 'See more External link (opens in a new tab or window)' }) + ).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx index d25d7eca333cf0..d6f8b23229f6c5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx @@ -112,7 +112,7 @@ export const MonitorPageTitle: React.FC = () => { {isBrowser && ( - + Date: Thu, 22 Apr 2021 15:31:35 -0400 Subject: [PATCH 23/33] [Uptime] - monitors - update ExpandRowColumn to use isDisabled prop (#97883) --- .../ping_list/columns/expand_row.test.tsx | 80 +++++++++++++++++++ .../monitor/ping_list/columns/expand_row.tsx | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.test.tsx new file mode 100644 index 00000000000000..e3ac1f2e171254 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; + +import { makePing } from '../../../../../common/runtime_types'; + +import { render } from '../../../../lib/helper/rtl_helpers'; +import { ExpandRowColumn } from './expand_row'; + +import { Ping } from '../../../../../common/runtime_types/ping'; + +describe('ExpandRowColumn', () => { + const defaultPing = makePing({ + docId: 'test', + }); + const pingWithError = { + ...defaultPing, + error: true, + }; + const pingWithoutResponseBody = { + ...defaultPing, + http: { + response: { + body: { + bytes: 0, + }, + }, + }, + }; + const pingWithResponseBody = { + ...defaultPing, + http: { + response: { + body: { + bytes: 1, + }, + }, + }, + }; + const browserPing = { + ...defaultPing, + type: 'browser', + }; + const onChange = jest.fn(); + const defaultExpandedRows = { + test:

Test row

, + }; + + it.each([ + [defaultExpandedRows, 'Collapse'], + [{}, 'Expand'], + ])('renders correctly', (expandedRows, labelText) => { + render( + + ); + expect(screen.getByRole('button', { name: labelText })); + }); + + it.each([[defaultPing], [pingWithoutResponseBody], [browserPing]])( + 'disables expand button for pings without error, without response body, or browser pings', + (ping) => { + render(); + expect(screen.getByRole('button', { name: 'Expand' })).toHaveAttribute('disabled'); + } + ); + + it.each([[pingWithError], [pingWithResponseBody]])( + 'enables expand button for pings with error and response body', + (ping) => { + render(); + expect(screen.getByRole('button', { name: 'Expand' })).not.toHaveAttribute('disabled'); + } + ); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx index 4655efc77729e1..b0453dd8f26808 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx @@ -47,7 +47,7 @@ export const ExpandRowColumn = ({ item, expandedRows, setExpandedRows }: Props) toggleDetails(item, expandedRows, setExpandedRows)} - disabled={!rowShouldExpand(item)} + isDisabled={!rowShouldExpand(item)} aria-label={ expandedRows[item.docId] ? i18n.translate('xpack.uptime.pingList.collapseRow', { From 94e5acd1476df6ee5e8795f132a03c986df98a2d Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 22 Apr 2021 13:24:36 -0700 Subject: [PATCH 24/33] Update synonyms API routes (#98046) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/routes/app_search/index.ts | 2 + .../server/routes/app_search/synonyms.test.ts | 86 ------------------- .../server/routes/app_search/synonyms.ts | 17 ++-- 3 files changed, 7 insertions(+), 98 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 1d48614e733741..6b6886cbbb75dd 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -18,6 +18,7 @@ import { registerResultSettingsRoutes } from './result_settings'; import { registerRoleMappingsRoutes } from './role_mappings'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSettingsRoutes } from './settings'; +import { registerSynonymsRoutes } from './synonyms'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); @@ -27,6 +28,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); registerCurationsRoutes(dependencies); + registerSynonymsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); registerRoleMappingsRoutes(dependencies); registerResultSettingsRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts index 26b44b5ad88896..53ceefc736d207 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts @@ -71,49 +71,6 @@ describe('synonyms routes', () => { path: '/as/engines/:engineName/synonyms/collection', }); }); - - describe('validates', () => { - it('with synonyms', () => { - const request = { - body: { - synonyms: ['a', 'b', 'c'], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('empty synonyms array', () => { - const request = { - body: { - queries: [], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('only one synonym', () => { - const request = { - body: { - queries: ['a'], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('empty synonym strings', () => { - const request = { - body: { - queries: ['', '', ''], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('missing synonyms', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); }); describe('PUT /api/app_search/engines/{engineName}/synonyms/{synonymId}', () => { @@ -137,49 +94,6 @@ describe('synonyms routes', () => { path: '/as/engines/:engineName/synonyms/:synonymId', }); }); - - describe('validates', () => { - it('with synonyms', () => { - const request = { - body: { - synonyms: ['a', 'b', 'c'], - }, - }; - mockRouter.shouldValidate(request); - }); - - it('empty synonyms array', () => { - const request = { - body: { - queries: [], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('only one synonym', () => { - const request = { - body: { - queries: ['a'], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('empty synonym strings', () => { - const request = { - body: { - queries: ['', '', ''], - }, - }; - mockRouter.shouldThrow(request); - }); - - it('missing synonyms', () => { - const request = { body: {} }; - mockRouter.shouldThrow(request); - }); - }); }); describe('DELETE /api/app_search/engines/{engineName}/synonyms/{synonymId}', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts index 1be58f00c476a4..d9b0cf1b9289fb 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts @@ -7,10 +7,9 @@ import { schema } from '@kbn/config-schema'; +import { skipBodyValidation } from '../../lib/route_config_helpers'; import { RouteDependencies } from '../../plugin'; -const synonymsSchema = schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 2 }); - export function registerSynonymsRoutes({ router, enterpriseSearchRequestHandler, @@ -34,35 +33,29 @@ export function registerSynonymsRoutes({ ); router.post( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/synonyms', validate: { params: schema.object({ engineName: schema.string(), }), - body: schema.object({ - synonyms: synonymsSchema, - }), }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/synonyms/collection', }) ); router.put( - { + skipBodyValidation({ path: '/api/app_search/engines/{engineName}/synonyms/{synonymId}', validate: { params: schema.object({ engineName: schema.string(), synonymId: schema.string(), }), - body: schema.object({ - synonyms: synonymsSchema, - }), }, - }, + }), enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:engineName/synonyms/:synonymId', }) From fc45de9fd0158667e83b942ef8546b0c738cc379 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 22 Apr 2021 16:25:07 -0400 Subject: [PATCH 25/33] Don't hungry mock within mocks (#98040) --- .../public/applications/__mocks__/kibana_logic.mock.ts | 4 ++-- .../public/applications/__mocks__/mount_async.mock.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index 2325ddcf2b2704..1ebd61df388c5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; +import { mockHistory } from './react_router_history.mock'; -import { mockHistory } from './'; +import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; export const mockKibanaValues = { config: { host: 'http://localhost:3002' }, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx index 2b5c06df37e8cd..886effcd540573 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx @@ -5,13 +5,13 @@ * 2.0. */ +import { mountWithIntl } from './mount_with_i18n.mock'; + import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; -import { mountWithIntl } from './'; - /** * This helper is intended for components that have async effects * (e.g. http fetches) on mount. It mostly adds act/update boilerplate From fde12bf1451d1b5c842462e90727bd576f6a7c03 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 22 Apr 2021 16:22:42 -0500 Subject: [PATCH 26/33] skip flaky suite. #97954 --- .../test/api_integration/apis/searchprofiler/searchprofiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts b/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts index f0478ea41b044c..ab965054146480 100644 --- a/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts +++ b/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts @@ -10,10 +10,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; const API_BASE_PATH = '/api/searchprofiler'; +// Flaky https://github.com/elastic/kibana/issues/97954 export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Profile', () => { + describe.skip('Profile', () => { it('should return profile results for a valid index', async () => { const payload = { index: '_all', From 9e8e6b227c08a7092c3cccbf84d4db29c42509a8 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 22 Apr 2021 15:40:44 -0600 Subject: [PATCH 27/33] [Maps] Add geo alerts dirs to codeowners (#98076) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cae64a24ec2cd5..bafa023cf3f35f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -144,6 +144,8 @@ /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis +/x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis +/x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis From 4eb6c22bcceb6e1400e34ff72bcfd2dc821603ac Mon Sep 17 00:00:00 2001 From: Davey Holler Date: Thu, 22 Apr 2021 18:43:14 -0700 Subject: [PATCH 28/33] [App Search] UI polish - empty states (#97875) * Empty State in Curations * Adds Empty State to Documents View * Updates Empty State in Engines Overview * Updates Relevance Tuning empty state * Updates the Result Settings empty state * Updating test files. * Display the empty state in the table only * Copy changes Small copy changes to bring it more inline with Elastic guidelines * Update api_logs_table test * Copy adjustments * Fixing failing type check * Copy adjustments on credentials empty state * Copy changes on meta engines table * Removes emptyState__prompt class * Removes emptyState__prompt class Forgot to save a file * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx * Adding the popout icon to docs links * Removes the need for the engineIcon class Also allows EuiIcon props to be passed to metaEngineIcon * Updates meta engine empty state button to docs link * Adds a separate constant for the empty meta engine button * Meta Engines empty state refactor - pull it out to its own component - simplify tests - simplify constants, they're only in 1 location so prob don't need to be in DRY'd out - fix some i18n IDs * Curations refactor * Documents empty state refactor - create components/ folder, move document creation button to there (feels like it belongs) - move empty state there - remove other empty state prompt from SearchExperienceContent??? Not sure why that wasn't showing up, but fixed tests/returns * Relevance Tuning refactor - create components/ folder - move empty state + shared boost icon & value badge to it - write test for missing value badge coverage MISC - Fix loading screen to an early return (matches UI of all other views) - Remove extra EuiSpacer in layout (matches UI of all other views) Sorry for the extra noise in your PR Davey! * Result Settings refactor - move empty state to its own component - fix broken .html.html link * Fix missing popout icon on documentation button link * EnginesOverview cleanup - Remove unnecessary CSS - fix responsive behavior of icons - standardize responsive behavior of create button actions - standardize spacing between content headers & tables - Tweak gap between 2 tables Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance Co-authored-by: Constance Chen --- .../analytics_tables/analytics_table.tsx | 2 +- .../components/api_logs_table.test.tsx | 2 +- .../api_logs/components/api_logs_table.tsx | 2 +- .../credentials_list/credentials_list.tsx | 2 +- .../curations/components/empty_state.test.tsx | 27 ++++++ .../curations/components/empty_state.tsx | 48 +++++++++++ .../components/index.ts} | 14 +-- .../curations/views/curations.test.tsx | 10 ++- .../components/curations/views/curations.tsx | 23 +---- .../document_creation_button.test.tsx | 4 +- .../document_creation_button.tsx | 2 +- .../documents/components/empty_state.test.tsx | 27 ++++++ .../documents/components/empty_state.tsx | 49 +++++++++++ .../components/documents/components/index.ts | 9 ++ .../components/documents/documents.test.tsx | 2 +- .../components/documents/documents.tsx | 2 +- .../search_experience.test.tsx | 17 +++- .../search_experience/search_experience.tsx | 9 +- .../search_experience_content.test.tsx | 86 +++---------------- .../search_experience_content.tsx | 37 +------- .../engine_overview/engine_overview_empty.tsx | 2 +- .../empty_meta_engines_state.test.tsx | 27 ++++++ .../components/empty_meta_engines_state.tsx | 49 +++++++++++ .../engines/components/empty_state.tsx | 14 ++- .../components/engines/components/index.ts | 1 + .../components/engines/constants.ts | 30 ++----- .../components/engines/engines_overview.scss | 23 ----- .../engines/engines_overview.test.tsx | 22 ----- .../components/engines/engines_overview.tsx | 71 ++++++++------- .../boosts/boost_item.test.tsx | 4 +- .../relevance_tuning/boosts/boost_item.tsx | 3 +- .../relevance_tuning/boosts/boosts.tsx | 2 +- .../{ => components}/boost_icon.test.tsx | 7 +- .../{ => components}/boost_icon.tsx | 4 +- .../components/empty_state.test.tsx | 27 ++++++ .../components/empty_state.tsx | 48 +++++++++++ .../relevance_tuning/components/index.ts | 10 +++ .../{ => components}/value_badge.scss | 0 .../components/value_badge.test.tsx | 25 ++++++ .../{ => components}/value_badge.tsx | 9 +- .../relevance_tuning.test.tsx | 7 +- .../relevance_tuning/relevance_tuning.tsx | 78 ++++------------- .../relevance_tuning_item.test.tsx | 3 +- .../relevance_tuning_item.tsx | 3 +- .../relevance_tuning_layout.tsx | 3 +- .../components/empty_state.test.tsx | 27 ++++++ .../components/empty_state.tsx | 48 +++++++++++ .../result_settings/components/index.ts | 8 ++ .../result_settings/result_settings.test.tsx | 7 +- .../result_settings/result_settings.tsx | 32 +------ .../sample_engine_creation_cta.tsx | 4 +- .../app_search/icons/engine_icon.tsx | 9 +- .../app_search/icons/icons.test.tsx | 4 +- .../app_search/icons/meta_engine_icon.tsx | 9 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 56 files changed, 586 insertions(+), 414 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{engines/components/empty_state.scss => curations/components/index.ts} (54%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/{ => components}/document_creation_button.test.tsx (88%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/{ => components}/document_creation_button.tsx (97%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss rename x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/{ => components}/boost_icon.test.tsx (87%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/{ => components}/boost_icon.tsx (86%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/index.ts rename x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/{ => components}/value_badge.scss (100%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.test.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/{ => components}/value_badge.tsx (76%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx index 737b87816ba461..53fa6312b9c498 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx @@ -68,7 +68,7 @@ export const AnalyticsTable: React.FC = ({ items, hasClicks, isSmall }) =

{i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.table.empty.noQueriesTitle', - { defaultMessage: 'No queries' } + { defaultMessage: 'No queries to display' } )}

} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx index 768295ec1389c6..780c198a9fac52 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -95,7 +95,7 @@ describe('ApiLogsTable', () => { const wrapper = mountWithIntl(); const promptContent = wrapper.find(EuiEmptyPrompt).text(); - expect(promptContent).toContain('No recent logs'); + expect(promptContent).toContain('Perform your first API call'); }); describe('hasPagination', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index 5ecf8e1ba33307..bb1327ce2da30b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -115,7 +115,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => { title={

{i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { - defaultMessage: 'No recent logs', + defaultMessage: 'Perform your first API call', })}

} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 386ea09d5da0b7..619d9d07bf5f35 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -116,7 +116,7 @@ export const CredentialsList: React.FC = () => { title={

{i18n.translate('xpack.enterpriseSearch.appSearch.credentials.empty.title', { - defaultMessage: 'No API Keys have been created yet.', + defaultMessage: 'Create your first API key', })}

} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx new file mode 100644 index 00000000000000..60ae386bea58ed --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Create your first curation'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/curations-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.tsx new file mode 100644 index 00000000000000..872a7282136e39 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/empty_state.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.empty.noCurationsTitle', + { defaultMessage: 'Create your first curation' } + )} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.empty.description', { + defaultMessage: + 'Use curations to promote and hide documents. Help people discover what you would most like them to discover.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.empty.buttonLabel', { + defaultMessage: 'Read the curations guide', + })} + + } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts similarity index 54% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts index 026d7e4ffa72af..8c9e58e6ba0f44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts @@ -5,16 +5,4 @@ * 2.0. */ -/** - * Empty/Error UI states - */ -.emptyState { - min-height: $euiSizeXXL * 11.25; - display: flex; - flex-direction: column; - justify-content: center; - - &__prompt > .euiIcon { - margin-bottom: $euiSizeS; - } -} +export { EmptyState } from './empty_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index e680579f7b0b71..1be21c97c623bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -17,9 +17,10 @@ import React from 'react'; import { shallow, ReactWrapper } from 'enzyme'; -import { EuiPageHeader, EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiPageHeader, EuiBasicTable } from '@elastic/eui'; import { Loading } from '../../../../shared/loading'; +import { EmptyState } from '../components'; import { Curations, CurationsTable } from './curations'; @@ -82,11 +83,12 @@ describe('Curations', () => { }); describe('CurationsTable', () => { - it('renders an EuiEmptyPrompt if curations is empty', () => { + it('renders an empty state', () => { setMockValues({ ...values, curations: [] }); - const wrapper = shallow(); + const table = shallow().find(EuiBasicTable); + const noItemsMessage = table.prop('noItemsMessage') as React.ReactElement; - expect(wrapper.find(EuiBasicTable).prop('noItemsMessage').type).toEqual(EuiEmptyPrompt); + expect(noItemsMessage.type).toEqual(EmptyState); }); it('passes loading prop based on dataLoading', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 624790c8471679..80de9aba772585 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -9,13 +9,7 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiPageHeader, - EuiPageContent, - EuiBasicTable, - EuiBasicTableColumn, - EuiEmptyPrompt, -} from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiPageContent, EuiPageHeader } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; @@ -29,6 +23,7 @@ import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes import { FormattedDateTime } from '../../../utils/formatted_date_time'; import { generateEnginePath } from '../../engine'; +import { EmptyState } from '../components'; import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants'; import { CurationsLogic } from '../curations_logic'; import { Curation } from '../types'; @@ -144,19 +139,7 @@ export const CurationsTable: React.FC = () => { responsive hasActions loading={dataLoading} - noItemsMessage={ - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.curations.table.empty.noCurationsTitle', - { defaultMessage: 'No curations yet' } - )} - - } - /> - } + noItemsMessage={} pagination={{ ...convertMetaToPagination(meta), hidePerPageOptions: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.test.tsx similarity index 88% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.test.tsx index 82fa9d3c82ce97..a6ccdc180b415d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { setMockActions } from '../../../__mocks__/kea.mock'; +import { setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; @@ -13,7 +13,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButton } from '@elastic/eui'; -import { DocumentCreationFlyout } from '../document_creation'; +import { DocumentCreationFlyout } from '../../document_creation'; import { DocumentCreationButton } from './document_creation_button'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx similarity index 97% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx index 687f589d375941..cded18094c5f2f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx @@ -12,7 +12,7 @@ import { useActions } from 'kea'; import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DocumentCreationLogic, DocumentCreationFlyout } from '../document_creation'; +import { DocumentCreationLogic, DocumentCreationFlyout } from '../../document_creation'; export const DocumentCreationButton: React.FC = () => { const { showCreationModes } = useActions(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx new file mode 100644 index 00000000000000..907dcf8c9c208b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Add your first documents'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/indexing-documents-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx new file mode 100644 index 00000000000000..0f9455a3b9228c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx @@ -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 React from 'react'; + +import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState = () => ( + + + {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', { + defaultMessage: 'Add your first documents', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', { + defaultMessage: + 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', { + defaultMessage: 'Read the documents guide', + })} + + } + /> +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/index.ts new file mode 100644 index 00000000000000..af6425d7c610ad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DocumentCreationButton } from './document_creation_button'; +export { EmptyState } from './empty_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx index ba971a3405e910..88f5b6a1c49e65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx @@ -14,7 +14,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiPageHeader } from '@elastic/eui'; -import { DocumentCreationButton } from './document_creation_button'; +import { DocumentCreationButton } from './components'; import { SearchExperience } from './search_experience'; import { Documents } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx index 58aa6acc59783a..b4122a715f9270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx @@ -18,8 +18,8 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { AppLogic } from '../../app_logic'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; +import { DocumentCreationButton } from './components'; import { DOCUMENTS_TITLE } from './constants'; -import { DocumentCreationButton } from './document_creation_button'; import { SearchExperience } from './search_experience'; export const Documents: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx index bfa5c8264fecee..e5f2978e5ba925 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx @@ -20,9 +20,11 @@ jest.mock('../../../../shared/use_local_storage', () => ({ })); import { useLocalStorage } from '../../../../shared/use_local_storage'; +import { EmptyState } from '../components'; + import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; - +import { SearchExperienceContent } from './search_experience_content'; import { Fields } from './types'; import { SearchExperience } from './'; @@ -32,6 +34,7 @@ describe('SearchExperience', () => { engine: { name: 'some-engine', apiKey: '1234', + document_count: 50, }, }; const mockSetFields = jest.fn(); @@ -50,7 +53,17 @@ describe('SearchExperience', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(SearchProvider).length).toBe(1); + + expect(wrapper.find(SearchProvider)).toHaveLength(1); + expect(wrapper.find(SearchExperienceContent)).toHaveLength(1); + }); + + it('renders an empty state when the engine does not have documents', () => { + setMockValues({ ...values, engine: { ...values.engine, document_count: 0 } }); + const wrapper = shallow(); + + expect(wrapper.find(EmptyState)).toHaveLength(1); + expect(wrapper.find(SearchExperienceContent)).toHaveLength(0); }); describe('when there are no selected filter fields', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index 6fbc6305edb257..22029956601a65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; import { useValues } from 'kea'; -import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet; import { SearchProvider, SearchBox, Sorting, Facet } from '@elastic/react-search-ui'; // @ts-expect-error types are not available for this package yet @@ -21,6 +21,7 @@ import './search_experience.scss'; import { externalUrl } from '../../../../shared/enterprise_search_url'; import { useLocalStorage } from '../../../../shared/use_local_storage'; import { EngineLogic } from '../../engine'; +import { EmptyState } from '../components'; import { buildSearchUIConfig } from './build_search_ui_config'; import { buildSortOptions } from './build_sort_options'; @@ -140,7 +141,11 @@ export const SearchExperience: React.FC = () => { )}
- + {engine.document_count && engine.document_count > 0 ? ( + + ) : ( + + )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 96fcd8997f6740..0905000f55139b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -10,7 +10,7 @@ import { setMockSearchContextState } from './__mocks__/hooks.mock'; import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { shallow } from 'enzyme'; // @ts-expect-error types are not available for this package yet import { Results } from '@elastic/react-search-ui'; @@ -113,81 +113,15 @@ describe('SearchExperienceContent', () => { expect(wrapper.find('[data-test-subj="documentsSearchNoResults"]').length).toBe(1); }); - describe('when an empty search was performed and there are no results, meaning there are no documents indexed', () => { - beforeEach(() => { - setMockSearchContextState({ - ...searchState, - resultSearchTerm: '', - wasSearched: true, - totalResults: 0, - }); - }); - - it('renders a no documents message', () => { - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="documentsSearchResults"]').length).toBe(0); - expect(wrapper.find('[data-test-subj="documentsSearchNoDocuments"]').length).toBe(1); - }); - - it('will include a button to index new documents', () => { - const wrapper = mount(); - expect( - wrapper - .find( - '[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="IndexDocumentsButton"]' - ) - .exists() - ).toBe(true); - }); - - it('will include a button to documentation if this is a meta engine', () => { - setMockValues({ - ...values, - isMetaEngine: true, - }); - - const wrapper = mount(); - - expect( - wrapper - .find( - '[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="IndexDocumentsButton"]' - ) - .exists() - ).toBe(false); - - expect( - wrapper - .find( - '[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="documentsSearchDocsLink"]' - ) - .exists() - ).toBe(true); - }); - - it('will include a button to documentation if the user cannot manage documents', () => { - setMockValues({ - ...values, - myRole: { canManageEngineDocuments: false }, - }); - - const wrapper = mount(); - - expect( - wrapper - .find( - '[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="IndexDocumentsButton"]' - ) - .exists() - ).toBe(false); - - expect( - wrapper - .find( - '[data-test-subj="documentsSearchNoDocuments"] [data-test-subj="documentsSearchDocsLink"]' - ) - .exists() - ).toBe(true); + it('renders empty if an empty search was performed and there are no results', () => { + // In a real world scenario this does not happen - wasSearched returns false before this final branch + setMockSearchContextState({ + ...searchState, + resultSearchTerm: '', + totalResults: 0, + wasSearched: true, }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx index 91db26ac676c94..84fe721f9eb7f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx @@ -9,16 +9,13 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiFlexGroup, EuiSpacer, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiFlexGroup, EuiSpacer, EuiEmptyPrompt } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui'; import { i18n } from '@kbn/i18n'; -import { AppLogic } from '../../../app_logic'; -import { DOCS_PREFIX } from '../../../routes'; import { EngineLogic } from '../../engine'; import { Result } from '../../result/types'; -import { DocumentCreationButton } from '../document_creation_button'; import { useSearchContextState } from './hooks'; import { Pagination } from './pagination'; @@ -27,7 +24,6 @@ import { ResultView } from './views'; export const SearchExperienceContent: React.FC = () => { const { resultSearchTerm, totalResults, wasSearched } = useSearchContextState(); - const { myRole } = useValues(AppLogic); const { isMetaEngine, engine } = useValues(EngineLogic); if (!wasSearched) return null; @@ -84,34 +80,5 @@ export const SearchExperienceContent: React.FC = () => { ); } - // If we have no results AND no search term, show a CTA for the user to index documents - return ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.indexDocumentsTitle', { - defaultMessage: 'No documents yet!', - })} - - } - body={i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.indexDocuments', { - defaultMessage: 'Indexed documents will show up here.', - })} - actions={ - !isMetaEngine && myRole.canManageEngineDocuments ? ( - - ) : ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.documents.search.indexingGuide', { - defaultMessage: 'Read the indexing guide', - })} - - ) - } - /> - ); + return null; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index d48664febb5f75..959d544a673243 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -23,7 +23,7 @@ export const EmptyEngineOverview: React.FC = () => { { defaultMessage: 'Engine setup' } )} rightSideItems={[ - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction', { defaultMessage: 'View documentation' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx new file mode 100644 index 00000000000000..1eab32d64b77f1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyMetaEnginesState } from './'; + +describe('EmptyMetaEnginesState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Create your first meta engine'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/meta-engines-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx new file mode 100644 index 00000000000000..58bf3f0a0195ea --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx @@ -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 React from 'react'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyMetaEnginesState: React.FC = () => ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptTitle', { + defaultMessage: 'Create your first meta engine', + })} + + } + body={ +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptDescription', + { + defaultMessage: + 'Meta engines allow you to combine multiple engines into one searchable engine.', + } + )} +

+ } + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptButtonLabel', + { defaultMessage: 'Learn more about meta engines' } + )} + + } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index 6911015e39d4ae..6b4a081d2c5151 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -15,14 +15,13 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../../shared/telemetry'; import { AppLogic } from '../../../app_logic'; +import { EngineIcon } from '../../../icons'; import { ENGINE_CREATION_PATH } from '../../../routes'; import { SampleEngineCreationCta } from '../../sample_engine_creation_cta/sample_engine_creation_cta'; import { EnginesOverviewHeader } from './header'; -import './empty_state.scss'; - export const EmptyState: React.FC = () => { const { myRole: { canManageEngines }, @@ -32,12 +31,11 @@ export const EmptyState: React.FC = () => { return ( <> - + {canManageEngines ? ( {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', { @@ -57,6 +55,7 @@ export const EmptyState: React.FC = () => { actions={ <> { { defaultMessage: 'Create an engine' } )} - + } @@ -80,8 +79,7 @@ export const EmptyState: React.FC = () => { ) : ( {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts index 89c57672fb5294..234d3ba31f44bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts @@ -8,3 +8,4 @@ export { EnginesOverviewHeader } from './header'; export { LoadingState } from './loading_state'; export { EmptyState } from './empty_state'; +export { EmptyMetaEnginesState } from './empty_meta_engines_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts index c6c077e984efe7..8d03e3d23ae237 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts @@ -22,11 +22,17 @@ export const SOURCE_ENGINES_TITLE = i18n.translate( ); export const CREATE_AN_ENGINE_BUTTON_LABEL = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engines.createAnEngineButton.ButtonLabel', + 'xpack.enterpriseSearch.appSearch.engines.createEngineButtonLabel', { defaultMessage: 'Create an engine', } ); +export const CREATE_A_META_ENGINE_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engines.createMetaEngineButtonLabel', + { + defaultMessage: 'Create a meta engine', + } +); export const DELETE_ENGINE_MESSAGE = (engineName: string) => i18n.translate( @@ -38,25 +44,3 @@ export const DELETE_ENGINE_MESSAGE = (engineName: string) => }, } ); - -export const CREATE_A_META_ENGINE_BUTTON_LABEL = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engines.createAMetaEngineButton.ButtonLabel', - { - defaultMessage: 'Create a meta engine', - } -); - -export const META_ENGINE_EMPTY_PROMPT_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPrompTitle', - { - defaultMessage: 'No meta engines yet', - } -); - -export const META_ENGINE_EMPTY_PROMPT_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptDescription', - { - defaultMessage: - 'Meta engines allow you to combine multiple engines into one searchable engine.', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss deleted file mode 100644 index 04a6033c5078a6..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -.enginesOverview { - padding: $euiSize; - - @include euiBreakpoint('m', 'l', 'xl') { - padding: $euiSizeXL; - } -} - -.engineIcon { - display: inline-block; - width: $euiSize; - // Use line-height of EuiTitle - SVG will vertically center accordingly - height: map-get(map-get($euiTitles, 's'), 'line-height'); - vertical-align: top; - margin-right: $euiSizeXS; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index c47b169ede3644..b326a7d3ee0759 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -12,8 +12,6 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiEmptyPrompt } from '@elastic/eui'; - import { LoadingState, EmptyState } from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; @@ -142,26 +140,6 @@ describe('EnginesOverview', () => { wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') ).toEqual('/meta_engine_creation'); }); - - describe('when metaEngines is empty', () => { - it('contains an EuiEmptyPrompt that takes users to the create meta engine page', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - myRole: { canManageEngines: true }, - metaEngines: [], - }); - const wrapper = shallow(); - const metaEnginesTable = wrapper.find(MetaEnginesTable).dive(); - const emptyPrompt = metaEnginesTable.dive().find(EuiEmptyPrompt).dive(); - - expect( - emptyPrompt - .find('[data-test-subj="appSearchMetaEnginesEmptyStateCreationButton"]') - .prop('to') - ).toEqual('/meta_engine_creation'); - }); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 4e17278d25d1a3..7001ecada999a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -10,13 +10,14 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; import { + EuiFlexGroup, + EuiFlexItem, EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, EuiPageContentBody, EuiTitle, EuiSpacer, - EuiEmptyPrompt, } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; @@ -28,21 +29,22 @@ import { AppLogic } from '../../app_logic'; import { EngineIcon, MetaEngineIcon } from '../../icons'; import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes'; -import { EnginesOverviewHeader, LoadingState, EmptyState } from './components'; +import { + EnginesOverviewHeader, + LoadingState, + EmptyState, + EmptyMetaEnginesState, +} from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { CREATE_AN_ENGINE_BUTTON_LABEL, CREATE_A_META_ENGINE_BUTTON_LABEL, ENGINES_TITLE, - META_ENGINE_EMPTY_PROMPT_DESCRIPTION, - META_ENGINE_EMPTY_PROMPT_TITLE, META_ENGINES_TITLE, } from './constants'; import { EnginesLogic } from './engines_logic'; -import './engines_overview.scss'; - export const EnginesOverview: React.FC = () => { const { hasPlatinumLicense } = useValues(LicensingLogic); const { @@ -79,15 +81,20 @@ export const EnginesOverview: React.FC = () => { - + - + - -

- {ENGINES_TITLE} -

-
+ + + + + + +

{ENGINES_TITLE}

+
+
+
{canManageEngines && ( @@ -104,7 +111,7 @@ export const EnginesOverview: React.FC = () => {
- + { {hasPlatinumLicense && ( <> - + - -

- {META_ENGINES_TITLE} -

-
+ + + + + + +

{META_ENGINES_TITLE}

+
+
+
{canManageEngines && ( @@ -142,6 +154,7 @@ export const EnginesOverview: React.FC = () => {
+ { ...convertMetaToPagination(metaEnginesMeta), hidePerPageOptions: true, }} - noItemsMessage={ - {META_ENGINE_EMPTY_PROMPT_TITLE}} - body={

{META_ENGINE_EMPTY_PROMPT_DESCRIPTION}

} - actions={ - canManageEngines && ( - - {CREATE_A_META_ENGINE_BUTTON_LABEL} - - ) - } - /> - } + noItemsMessage={} onChange={handlePageChange(onMetaEnginesPagination)} />
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.test.tsx index 7ee921eef0e096..0af20982bb9705 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.test.tsx @@ -11,11 +11,9 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiAccordion } from '@elastic/eui'; -import { BoostIcon } from '../boost_icon'; +import { BoostIcon, ValueBadge } from '../components'; import { BoostType } from '../types'; -import { ValueBadge } from '../value_badge'; - import { BoostItem } from './boost_item'; import { BoostItemContent } from './boost_item_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx index 98d83619866d18..2954ffc5e8c2ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx @@ -9,10 +9,9 @@ import React, { useMemo } from 'react'; import { EuiFlexItem, EuiAccordion, EuiFlexGroup, EuiHideFor } from '@elastic/eui'; -import { BoostIcon } from '../boost_icon'; +import { BoostIcon, ValueBadge } from '../components'; import { BOOST_TYPE_TO_DISPLAY_MAP } from '../constants'; import { Boost } from '../types'; -import { ValueBadge } from '../value_badge'; import { BoostItemContent } from './boost_item_content'; import { getBoostSummary } from './get_boost_summary'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx index 297365896bb6d8..7a407491ffef3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { GEOLOCATION, TEXT, DATE } from '../../../../shared/constants/field_types'; import { SchemaTypes } from '../../../../shared/types'; -import { BoostIcon } from '../boost_icon'; +import { BoostIcon } from '../components'; import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../constants'; import { RelevanceTuningLogic } from '../relevance_tuning_logic'; import { Boost, BoostType } from '../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/boost_icon.test.tsx similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/boost_icon.test.tsx index fd567f52ada249..15281be9ddd6cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/boost_icon.test.tsx @@ -11,14 +11,11 @@ import { shallow } from 'enzyme'; import { EuiToken } from '@elastic/eui'; +import { BoostType } from '../types'; + import { BoostIcon } from './boost_icon'; -import { BoostType } from './types'; describe('BoostIcon', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('renders a token according to the provided type', () => { const wrapper = shallow(); expect(wrapper.find(EuiToken).prop('iconType')).toBe('tokenNumber'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/boost_icon.tsx similarity index 86% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/boost_icon.tsx index 2570a29274d069..8af03fd63b4382 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/boost_icon.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { EuiToken } from '@elastic/eui'; -import { BOOST_TYPE_TO_ICON_MAP } from './constants'; -import { BoostType } from './types'; +import { BOOST_TYPE_TO_ICON_MAP } from '../constants'; +import { BoostType } from '../types'; interface Props { type: BoostType; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx new file mode 100644 index 00000000000000..a60f68c19f6dce --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Add documents to tune relevance'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/relevance-tuning-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx new file mode 100644 index 00000000000000..e6a14d7b5cd725 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => ( + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', { + defaultMessage: 'Add documents to tune relevance', + })} + + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description', + { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', + } + )} + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel', + { defaultMessage: 'Read the relevance tuning guide' } + )} + + } + /> + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/index.ts new file mode 100644 index 00000000000000..f456e11b160f67 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/index.ts @@ -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 { BoostIcon } from './boost_icon'; +export { ValueBadge } from './value_badge'; +export { EmptyState } from './empty_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.test.tsx new file mode 100644 index 00000000000000..0d09148d3de3fb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { ValueBadge } from './'; + +describe('ValueBadge', () => { + it('renders', () => { + const wrapper = shallow(Hello world); + expect(wrapper.hasClass('valueBadge')).toBe(true); + expect(wrapper.text()).toEqual('Hello world'); + }); + + it('renders a disabled class', () => { + const wrapper = shallow(Test); + expect(wrapper.hasClass('valueBadge--disabled')).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.tsx similarity index 76% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.tsx index 8397087ca69b46..a5e86892b4c6cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/value_badge.tsx @@ -11,10 +11,11 @@ import classNames from 'classnames'; import './value_badge.scss'; -export const ValueBadge: React.FC<{ children: React.ReactNode; disabled?: boolean }> = ({ - children, - disabled = false, -}) => { +interface Props { + children: React.ReactNode; + disabled?: boolean; +} +export const ValueBadge: React.FC = ({ children, disabled = false }) => { const className = classNames('valueBadge', { 'valueBadge--disabled': disabled, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx index c76c50094aeddc..574d2ae02af999 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx @@ -18,6 +18,7 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { EmptyState } from './components'; import { RelevanceTuning } from './relevance_tuning'; import { RelevanceTuningForm } from './relevance_tuning_form'; @@ -51,7 +52,7 @@ describe('RelevanceTuning', () => { const wrapper = subject(); expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true); expect(wrapper.find(Loading).exists()).toBe(false); - expect(wrapper.find('EmptyCallout').exists()).toBe(false); + expect(wrapper.find(EmptyState).exists()).toBe(false); }); it('initializes relevance tuning data', () => { @@ -65,7 +66,7 @@ describe('RelevanceTuning', () => { engineHasSchemaFields: false, }); const wrapper = subject(); - expect(wrapper.find('EmptyCallout').dive().find(EuiEmptyPrompt).exists()).toBe(true); + expect(wrapper.find(EmptyState).dive().find(EuiEmptyPrompt).exists()).toBe(true); expect(wrapper.find(Loading).exists()).toBe(false); expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); }); @@ -77,7 +78,7 @@ describe('RelevanceTuning', () => { }); const wrapper = subject(); expect(wrapper.find(Loading).exists()).toBe(true); - expect(wrapper.find('EmptyCallout').exists()).toBe(false); + expect(wrapper.find(EmptyState).exists()).toBe(false); expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index ab9bbaa9a1773e..b98541a9638901 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -9,58 +9,18 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; -import { DOCS_PREFIX } from '../../routes'; +import { EmptyState } from './components'; import { RelevanceTuningForm } from './relevance_tuning_form'; import { RelevanceTuningLayout } from './relevance_tuning_layout'; - import { RelevanceTuningPreview } from './relevance_tuning_preview'; import { RelevanceTuningLogic } from '.'; -const EmptyCallout: React.FC = () => { - return ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyErrorMessageTitle', - { - defaultMessage: 'Tuning requires schema fields', - } - )} - - } - body={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyErrorMessage', - { - defaultMessage: 'Index documents to tune relevance.', - } - )} - actions={ - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.emptyButtonLabel', - { - defaultMessage: 'Read the relevance tuning guide', - } - )} - - } - /> - ); -}; - export const RelevanceTuning: React.FC = () => { const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic); const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic); @@ -69,31 +29,23 @@ export const RelevanceTuning: React.FC = () => { initializeRelevanceTuning(); }, []); - const body = () => { - if (dataLoading) { - return ; - } - - if (!engineHasSchemaFields) { - return ; - } - - return ( - - - - - - - - - ); - }; + if (dataLoading) return ; return ( - {body()} + {engineHasSchemaFields ? ( + + + + + + + + + ) : ( + + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx index 97913ed49fefd9..b6061a326365b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx @@ -11,9 +11,8 @@ import { shallow } from 'enzyme'; import { SchemaTypes } from '../../../../shared/types'; -import { BoostIcon } from '../boost_icon'; +import { BoostIcon, ValueBadge } from '../components'; import { Boost, BoostType, SearchField } from '../types'; -import { ValueBadge } from '../value_badge'; import { RelevanceTuningItem } from './relevance_tuning_item'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx index f7f4c64622fa6c..9264078ca40f55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx @@ -11,9 +11,8 @@ import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiTextColor, EuiIcon } f import { SchemaTypes } from '../../../../shared/types'; -import { BoostIcon } from '../boost_icon'; +import { BoostIcon, ValueBadge } from '../components'; import { Boost, SearchField } from '../types'; -import { ValueBadge } from '../value_badge'; interface Props { name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx index 69043d80bd8d00..4fa694300a7795 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiSpacer, EuiButton } from '@elastic/eui'; +import { EuiPageHeader, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -67,7 +67,6 @@ export const RelevanceTuningLayout: React.FC = ({ children }) => { {pageHeader()} - {children} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx new file mode 100644 index 00000000000000..537fd9ec6a0d44 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Add documents to adjust settings'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/result-settings-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx new file mode 100644 index 00000000000000..299156984724ef --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => ( + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.title', { + defaultMessage: 'Add documents to adjust settings', + })} + + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.description', + { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', + } + )} + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.buttonLabel', + { defaultMessage: 'Read the result settings guide' } + )} + + } + /> + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/index.ts new file mode 100644 index 00000000000000..8c9e58e6ba0f44 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/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 { EmptyState } from './empty_state'; 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 48a25d4f1f4bd6..dd99dc6c505dab 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 @@ -13,10 +13,11 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { EmptyState } from './components'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; import { SampleResponse } from './sample_response'; @@ -151,8 +152,8 @@ describe('ResultSettings', () => { expect(wrapper.find(SampleResponse).exists()).toBe(false); }); - it('will render an "empty" message', () => { - expect(wrapper.find(EuiEmptyPrompt).exists()).toBe(true); + it('will render an empty state', () => { + expect(wrapper.find(EmptyState).exists()).toBe(true); }); }); }); 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 51cdc3aea21f24..45cb9ea1cfcb4c 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 @@ -9,15 +9,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { - EuiPageHeader, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiEmptyPrompt, - EuiPanel, -} from '@elastic/eui'; +import { EuiPageHeader, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -29,6 +21,7 @@ import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; import { getEngineBreadcrumbs } from '../engine'; +import { EmptyState } from './components'; import { RESULT_SETTINGS_TITLE } from './constants'; import { ResultSettingsTable } from './result_settings_table'; import { SampleResponse } from './sample_response'; @@ -115,26 +108,7 @@ export const ResultSettings: React.FC = () => { ) : ( - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.noSchemaTitle', - { defaultMessage: 'Engine does not have a schema' } - )} - - } - body={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.noSchemaDescription', - { - defaultMessage: - 'You need one! A schema is created for you after you index some documents.', - } - )} - /> - + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx index 6f1ccd1ae2b532..3a292792fbca00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx @@ -23,10 +23,10 @@ export const SampleEngineCreationCta: React.FC = () => { const { createSampleEngine } = useActions(SampleEngineCreationCtaLogic); return ( - + - +

{SAMPLE_ENGINE_CREATION_CTA_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/icons/engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/icons/engine_icon.tsx index 9e502b623ae435..13c948787c77fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/icons/engine_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/icons/engine_icon.tsx @@ -7,13 +7,14 @@ import React from 'react'; -export const EngineIcon: React.FC = () => ( +export const EngineIcon: React.FC = ({ ...props }) => (
)} - {fieldExamples !== null && loadingData === false && ( + {ccsVersionFailure === false && fieldExamples !== null && loadingData === false && ( <> = ({ setIsValid }) => { )} + {ccsVersionFailure === true && } diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index 4faa77b2fbc34c..9d705c8cd725fb 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -146,3 +146,11 @@ export function timeBasedIndexCheck(indexPattern: IndexPattern, showNotification return true; } } + +/** + * Returns true if the index pattern contains a : + * which means it is cross-cluster + */ +export function isCcsIndexPattern(indexPatternTitle: string) { + return indexPatternTitle.includes(':'); +} From bda0a934d3f64b0c4f20c8bf57419917af231313 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 23 Apr 2021 14:11:03 +0100 Subject: [PATCH 33/33] [ML] removing unused notification settings endpoint (#98109) * [ML] removing unused notification settings endpoint * removing route init function * test commit * removing test comment --- .../services/ml_api_service/index.ts | 7 --- x-pack/plugins/ml/server/plugin.ts | 2 - x-pack/plugins/ml/server/routes/apidoc.json | 3 -- .../ml/server/routes/notification_settings.ts | 45 ------------------- 4 files changed, 57 deletions(-) delete mode 100644 x-pack/plugins/ml/server/routes/notification_settings.ts diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 3b6b948132c43a..bf6b752faa8da6 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -361,13 +361,6 @@ export function mlApiServicesProvider(httpService: HttpService) { }); }, - getNotificationSettings() { - return httpService.http({ - path: `${basePath()}/notification_settings`, - method: 'GET', - }); - }, - checkIndexExists({ index }: { index: string }) { const body = JSON.stringify({ index }); diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 746d2909507041..213be9421c41df 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -42,7 +42,6 @@ import { jobRoutes } from './routes/anomaly_detectors'; import { jobServiceRoutes } from './routes/job_service'; import { savedObjectsRoutes } from './routes/saved_objects'; import { jobValidationRoutes } from './routes/job_validation'; -import { notificationRoutes } from './routes/notification_settings'; import { resultsServiceRoutes } from './routes/results_service'; import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; @@ -181,7 +180,6 @@ export class MlServerPlugin jobAuditMessagesRoutes(routeInit); jobRoutes(routeInit); jobServiceRoutes(routeInit); - notificationRoutes(routeInit); resultsServiceRoutes(routeInit); jobValidationRoutes(routeInit); savedObjectsRoutes(routeInit, { diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 55f66b354df277..7b54e48099d606 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -127,9 +127,6 @@ "ValidateCardinality", "ValidateJob", - "NotificationSettings", - "GetNotificationSettings", - "DatafeedService", "CreateDatafeed", "PreviewDatafeed", diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts deleted file mode 100644 index 3e0a74a32ec8ef..00000000000000 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ /dev/null @@ -1,45 +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 { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../types'; - -/** - * Routes for notification settings - */ -export function notificationRoutes({ router, routeGuard }: RouteInitialization) { - /** - * @apiGroup NotificationSettings - * - * @api {get} /api/ml/notification_settings Get notification settings - * @apiName GetNotificationSettings - * @apiDescription Returns cluster notification settings - */ - router.get( - { - path: '/api/ml/notification_settings', - validate: false, - options: { - tags: ['access:ml:canAccessML'], - }, - }, - routeGuard.fullLicenseAPIGuard(async ({ client, response }) => { - try { - const { body } = await client.asCurrentUser.cluster.getSettings({ - include_defaults: true, - filter_path: '**.xpack.notification', - }); - - return response.ok({ - body, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - }) - ); -}