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 1508cab69a048..599c32137c553 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 0000000000000..33f7ca12abc53 --- /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]."` + ); + }); + }); +}); 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 0000000000000..e8e55ad2827c5 --- /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 a82f9e9b6113b..a15d39c447126 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 c41781deafb95..09f499e540e93 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 29cb2b7589220..7f7b26e380c55 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 ( -
+ <> - -
- -
-
+ + ); }; 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 a9712cc4e1dc0..2cf867446b7fb 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 ff3e1e83925d0..2e6a3c65597ea 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 463468d1304b6..528065da23af6 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 b14ea4ebd7a73..cd20e32def16d 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 ; 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 66c9a2462736a..9ec6cbcb5d4ac 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 f79f9bdffe366..5a2a98a24fca1 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'); 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 edbfc11343e38..3123a43594c93 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 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 1fcc301fbdba7..588d85f24a023 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 4d0f432375330..4c8740cc76b6f 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 0bf3b951f4246..aa7bd2f5ecf6d 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?: { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 45665b2026db5..e33c09932daab 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 ? ( diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 8fa0629d013cf..71dc4919237e5 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)); + }); }, }, }; 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 ea17b03082751..b9dd782d8a653 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 9ce8ec0cb6fd3..70e095c88576f 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 4f12ec2e5de2d..21791952fec06 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 0a7df66f6c1d5..3e690e50b04b1 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 7884c7bd00263..19c706b86577d 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 f66b060b166bf..1c17f95bb6ba0 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 542369fdf5aa3..702a532949428 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 2afe14644f5e9..b1cd14fa039b5 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 5632dd0ed03be..788ce00ba1b1d 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 90a848329c013..520cfef74ce41 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 2b224f1bb6125..25c0f5411f25c 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 e8a13a1cc183e..27f584bb17248 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 a797d56835ae7..f5a3c75747e52 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 198757e9ceade..513fed36f678c 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 679aac71c6fdf..004a904828ecf 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 91b53c11ddda1..c17d227428391 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 a527123fffb4a..dd4da78db4e8e 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 dc6747510a3ea..e8cd501816afe 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 00ecbca338b70..74adc8d36b0aa 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 4532d3427375b..6a527ca00f525 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 f3c90ae12ae33..b7ef04c79d3da 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 7fcefe02cfe33..8969843f61a1c 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 0074808057ca7..d6d6dec6edc6a 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 e9a807d772d8b..d50ef49593f40 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 8107d1ca84fda..9ea8cee106052 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 3fbda3e4533ea..be474bbdc4fd8 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 4b062bee6176b..3c87a20dea6bb 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 5ecc7904871a4..4e5480a921493 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 c773cac5dcfef..96a8b00bf4966 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 1f221c9abc798..e3e2351b40a32 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 f9488caaa9132..e177d36057b1d 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 5681b076aa6bb..5f16cb593a516 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 c000870e8e51f..3b16d0266e566 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 046702323db38..a5953b6ec3e65 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 5cfa5ecd225ec..146b7e8470718 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 3ea6000401872..a84a60af51b39 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 5b443b73f11e2..9622610f3c637 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 5f485dcaa0195..e7cdfbe268fe6 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 0ffeaa4224544..bcd5ccdc0b5ac 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 f1efdd2e3c432..c31094b5778d5 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 1eaa89575de26..dd55bdb4c6948 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 380e6b05471a8..a3703ab64beda 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 383c4c233914f..7bf681092c075 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 ad3c7e0e829fb..6a3323da4fb44 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 8ed1aaecb6f0e..5af91539e8be3 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 b34de267f4519..9a72fa1d6cfca 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 1e07b94b55b74..e94873dee5632 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 6bbe7f8f43773..cf7d8e05858d5 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 345aee4de2df2..c835aa6c6a3e3 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 6dd3df1055f9c..2e4f3b83e6708 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 dfc7d0a28db79..b9d3e8639c560 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 08c4d917f5da3..2699d63144be1 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 a532f4f11a301..488c526134525 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 5ce31bada520b..47e60f27a7dbd 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 d1ff9da1fa6c2..98105f5cac25a 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 405957d98055e..e7f3cf3f2675a 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 9c6a4b3d1147f..3cbaf0fbc976c 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 49a7064113c30..754f0cac8868c 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 e000981733eed..d4be09f97591d 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 8b17a7288eae3..52b58439af0ab 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 cf0774a02db3b..846c40994aac2 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 7e4924eacda4b..37fdd5a444b2b 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 0000000000000..d11984b967db9 --- /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 496107e910d76..1032d0ec1672a 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 83b511f95bc2a..92199336b978c 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 cf5f44a65ab96..4a6eab13ba4f1 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 0000000000000..1a487ef8127f2 --- /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', + } +); diff --git a/x-pack/test/functional/services/ml/alerting.ts b/x-pack/test/functional/services/ml/alerting.ts index 8d27a75b7b485..327a0e574f0fd 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 cc0dcff528663..ee30f3a9eab00 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', () => {