From 4278e560ef75e0b33204f2ca384a27138a3bad3b Mon Sep 17 00:00:00 2001 From: Fabien Baligand Date: Thu, 16 Jul 2020 22:13:35 +0200 Subject: [PATCH 01/13] [Visualizations] Pass 'aggs' parameter to custom request handlers (#71423) --- .../public/expressions/visualization_function.ts | 14 +++++++++++++- .../visualizations/public/legacy/build_pipeline.ts | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/plugins/visualizations/public/expressions/visualization_function.ts b/src/plugins/visualizations/public/expressions/visualization_function.ts index 222479158934b..68a153f4272a3 100644 --- a/src/plugins/visualizations/public/expressions/visualization_function.ts +++ b/src/plugins/visualizations/public/expressions/visualization_function.ts @@ -21,7 +21,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { VisResponseValue, PersistedState } from '../../../../plugins/visualizations/public'; import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public'; -import { getTypes, getIndexPatterns, getFilterManager } from '../services'; +import { getTypes, getIndexPatterns, getFilterManager, getSearch } from '../services'; interface Arguments { index?: string | null; @@ -31,6 +31,7 @@ interface Arguments { schemas?: string; visConfig?: string; uiState?: string; + aggConfigs?: string; } export type ExpressionFunctionVisualization = ExpressionFunctionDefinition< @@ -84,6 +85,11 @@ export const visualization = (): ExpressionFunctionVisualization => ({ default: '"{}"', help: 'User interface state', }, + aggConfigs: { + types: ['string'], + default: '"{}"', + help: 'Aggregation configurations', + }, }, async fn(input, args, { inspectorAdapters }) { const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {}; @@ -94,6 +100,11 @@ export const visualization = (): ExpressionFunctionVisualization => ({ const uiStateParams = args.uiState ? JSON.parse(args.uiState) : {}; const uiState = new PersistedState(uiStateParams); + const aggConfigsState = args.aggConfigs ? JSON.parse(args.aggConfigs) : []; + const aggs = indexPattern + ? getSearch().aggs.createAggConfigs(indexPattern, aggConfigsState) + : undefined; + if (typeof visType.requestHandler === 'function') { input = await visType.requestHandler({ partialRows: args.partialRows, @@ -107,6 +118,7 @@ export const visualization = (): ExpressionFunctionVisualization => ({ inspectorAdapters, queryFilter: getFilterManager(), forceFetch: true, + aggs, }); } diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 2ef07bf18c91c..e74a83d91fabf 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -535,7 +535,10 @@ export const buildPipeline = async ( metricsAtAllLevels=${vis.isHierarchical()} partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} `; if (indexPattern) { - pipeline += `${prepareString('index', indexPattern.id)}`; + pipeline += `${prepareString('index', indexPattern.id)} `; + if (vis.data.aggs) { + pipeline += `${prepareJson('aggConfigs', vis.data.aggs!.aggs)}`; + } } } From d510263666aa8636c79ff9bc4083c69df22f4174 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 16 Jul 2020 14:25:25 -0600 Subject: [PATCH 02/13] [Maps] fix 'New Map' from getting added to recently accessed (#72125) --- .../public/routing/routes/maps_app/load_map_and_render.js | 6 +++++- .../maps/public/routing/routes/maps_app/maps_app_view.js | 5 ----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js index c87f6eb330531..3eea5b00d324e 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js @@ -7,7 +7,7 @@ import React from 'react'; import { MapsAppView } from '.'; import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; -import { getToasts } from '../../../kibana_services'; +import { getCoreChrome, getToasts } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; @@ -30,6 +30,10 @@ export const LoadMapAndRender = class extends React.Component { try { const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); if (this._isMounted) { + getCoreChrome().docTitle.change(savedMap.title); + if (this.props.savedMapId) { + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); + } this.setState({ savedMap }); } } catch (err) { diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 29fbb5f46e29b..aa7f24155ab43 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -56,11 +56,6 @@ export class MapsAppView extends React.Component { } componentDidMount() { - const { savedMap } = this.props; - - getCoreChrome().docTitle.change(savedMap.title); - getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); - // Init sync utils // eslint-disable-next-line react-hooks/rules-of-hooks this._globalSyncUnsubscribe = useGlobalStateSyncing(); From ba76476110fdcb0dff3d760e82be46a31d48aae0 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 16 Jul 2020 16:54:09 -0400 Subject: [PATCH 03/13] [Ingest Manager] Do not show enrolling and unenrolling agents as online in agent counters (#71921) --- .../common/services/agent_status.ts | 20 +++++++++++-------- .../common/types/rest_spec/agent.ts | 1 + .../sections/fleet/components/donut_chart.tsx | 2 +- .../sections/fleet/components/list_layout.tsx | 1 + .../server/services/agents/status.ts | 6 ++++-- .../policy/store/policy_details/reducer.ts | 1 + .../policy/store/policy_list/index.test.ts | 1 + .../pages/policy/store/policy_list/reducer.ts | 1 + 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index 6489c30308771..536003b0f743d 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AGENT_POLLING_THRESHOLD_MS, - AGENT_TYPE_PERMANENT, - AGENT_SAVED_OBJECT_TYPE, -} from '../constants'; +import { AGENT_POLLING_THRESHOLD_MS, AGENT_SAVED_OBJECT_TYPE } from '../constants'; import { Agent, AgentStatus } from '../types'; export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { @@ -41,8 +37,16 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta return 'online'; } +export function buildKueryForEnrollingAgents() { + return `not ${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*`; +} + +export function buildKueryForUnenrollingAgents() { + return `${AGENT_SAVED_OBJECT_TYPE}.unenrollment_started_at:*`; +} + export function buildKueryForOnlineAgents() { - return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()})`; + return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()}) AND not (${buildKueryForEnrollingAgents()}) AND not (${buildKueryForUnenrollingAgents()})`; } export function buildKueryForErrorAgents() { @@ -50,7 +54,7 @@ export function buildKueryForErrorAgents() { } export function buildKueryForOfflineAgents() { - return `((${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ (4 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s) AND not ( ${buildKueryForErrorAgents()} ))`; + }s AND not (${buildKueryForErrorAgents()})`; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index ed7d73ab0b719..7ec5a8d68311f 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -173,5 +173,6 @@ export interface GetAgentStatusResponse { online: number; error: number; offline: number; + other: number; }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx index bfa9c80f12851..99a4f27b428fe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx @@ -31,7 +31,7 @@ export const DonutChart = ({ height, width, data }: DonutChartProps) => { .ordinal() // @ts-ignore .domain(data) - .range(['#017D73', '#98A2B3', '#BD271E']); + .range(['#017D73', '#98A2B3', '#BD271E', '#F5A700']); const pieGenerator = d3.layout .pie() .value(({ value }: any) => value) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 46190033d4d6b..16acda9dc4afd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -66,6 +66,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { online: agentStatus?.online || 0, offline: agentStatus?.offline || 0, error: agentStatus?.error || 0, + other: agentStatus?.other || 0, }} /> diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 016a2344cf532..86336714a511e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -25,9 +25,10 @@ export async function getAgentStatusForConfig( soClient: SavedObjectsClientContract, configId?: string ) { - const [all, error, offline] = await Promise.all( + const [all, online, error, offline] = await Promise.all( [ undefined, + AgentStatusKueryHelper.buildKueryForOnlineAgents(), AgentStatusKueryHelper.buildKueryForErrorAgents(), AgentStatusKueryHelper.buildKueryForOfflineAgents(), ].map((kuery) => @@ -47,9 +48,10 @@ export async function getAgentStatusForConfig( return { events: await getEventsCount(soClient, configId), total: all.total, - online: all.total - error.total - offline.total, + online: online.total, error: error.total, offline: offline.total, + other: all.total - online.total - error.total - offline.total, }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index b3b74c2ca9dae..e7aa2c8893f8e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -21,6 +21,7 @@ export const initialPolicyDetailsState: () => Immutable = () offline: 0, online: 0, total: 0, + other: 0, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 0a24c9eea71eb..8203aae244f24 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -152,6 +152,7 @@ describe('policy list store concerns', () => { offline: 0, online: 0, total: 0, + other: 0, }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts index 52bed8d850ef4..53954449ab9c3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts @@ -31,6 +31,7 @@ export const initialPolicyListState: () => Immutable = () => ({ offline: 0, online: 0, total: 0, + other: 0, }, }); From 9da8b768bfb5a24053feeaed1c8fb796ac905cba Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 16 Jul 2020 14:43:27 -0700 Subject: [PATCH 04/13] Fixed Webhook connector doesn't retain added HTTP header settings (#71924) * Fixed Webhook connector doesn't retain added HTTP header settings * fixed method --- .../webhook/webhook_connectors.test.tsx | 1 + .../webhook/webhook_connectors.tsx | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 3b7865e59b9e6..3a2afff03c58f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -36,6 +36,7 @@ describe('WebhookActionConnectorFields renders', () => { /> ); expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 57c88607c0884..2321d5b4b5479 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -31,14 +31,16 @@ const HTTP_VERBS = ['post', 'put']; const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + const { user, password } = action.secrets; + const { method, url, headers } = action.config; + const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); const [hasHeaders, setHasHeaders] = useState(false); - const { user, password } = action.secrets; - const { method, url, headers } = action.config; - - editActionConfig('method', 'post'); // set method to POST by default + if (!method) { + editActionConfig('method', 'post'); // set method to POST by default + } const headerErrors = { keyHeader: new Array(), @@ -80,7 +82,7 @@ const WebhookActionConnectorFields: React.FunctionComponent
- {hasHeaders && Object.keys(headers || {}).length > 0 ? ( - + {Object.keys(headers || {}).length > 0 ? ( + <>
@@ -351,10 +353,10 @@ const WebhookActionConnectorFields: React.FunctionComponent {headersList} - + ) : null} - {headerControl} + {hasHeaders && headerControl}
From 78b39e8b9f8d27cd5f4ba28755a9489688748f3f Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 16 Jul 2020 14:51:56 -0700 Subject: [PATCH 05/13] using test_user with minimum privs (#71988) Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/discover/async_scripted_fields.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 46848b1db0ef4..33a64e4f9cdd3 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -14,9 +14,9 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const log = getService('log'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'discover', 'timePicker']); const queryBar = getService('queryBar'); + const security = getService('security'); describe('async search with scripted fields', function () { this.tags(['skipFirefox']); @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { await esArchiver.load('kibana_scripted_fields_on_logstash'); await esArchiver.loadIfNeeded('logstash_functional'); + await security.testUser.setRoles(['test_logstash_reader', 'global_discover_read']); // changing the timepicker default here saves us from having to set it in Discover (~8s) await kibanaServer.uiSettings.update({ 'timepicker:timeDefaults': @@ -36,6 +37,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.update({}); await esArchiver.unload('logstash_functional'); await esArchiver.load('empty_kibana'); + await security.testUser.restoreDefaults(); }); it('query should show failed shards pop up', async function () { From 63e6666b1321ed8d57ec65b0e650249189d160e2 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 16 Jul 2020 17:02:15 -0600 Subject: [PATCH 06/13] [Maps] Fix issue preventing TMS from rendering correctly (#71946) * Ensure getColors selector modifies and returns the same object * Call onSourceConfigChange on CreateSourceEditor mount * Back out selector update Co-authored-by: Elastic Machine --- .../kibana_tilemap_source/create_source_editor.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js index 1cbf4c1a87de3..ee557f6244f49 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui'; @@ -13,10 +13,12 @@ import { i18n } from '@kbn/i18n'; export function CreateSourceEditor({ onSourceConfigChange }) { const tilemap = getKibanaTileMap(); - - if (tilemap.url) { - onSourceConfigChange(); - } + useEffect(() => { + if (tilemap.url) { + onSourceConfigChange(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( @@ -33,7 +35,7 @@ export function CreateSourceEditor({ onSourceConfigChange }) { }) } > - + ); From fead1f2d1d51febe0772cd966b52e5bc633bef24 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 16 Jul 2020 16:42:41 -0700 Subject: [PATCH 07/13] skip flaky suite (#77207) --- .../security_and_spaces/tests/alerting/alerts.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 0f339154bd948..db1e59746162b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -30,7 +30,8 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/72207 + describe.skip('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); From b695d60516ec9551a99ae6ab547657909be2c987 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 16 Jul 2020 17:13:32 -0700 Subject: [PATCH 08/13] [baseline/capture] use high-memory nodes with ramDisks (#71894) Co-authored-by: spalger --- .ci/Jenkinsfile_baseline_capture | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 7c7cc8d98c306..3f90a6bc05af0 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -8,12 +8,12 @@ kibanaPipeline(timeoutMinutes: 120) { catchError { parallel([ 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's', ramDisk: false) { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) } }, 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's', ramDisk: false) { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) } }, From 678dc309afbf2b30d1175374e2e516ec3f02157c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 16 Jul 2020 19:43:15 -0500 Subject: [PATCH 09/13] [Security Solution][Detections,Lists] Miscellaneous post-FF fixes (#71990) * Overview Alerts Histogram stacking defaults to signal.rule.name Since this is now the default for all AlertsHistograms, I've moved this default upstream into the histogram itself. * Replace magic strings with our constant ENDPOINT_LIST_ID Also replaced a few unintentional uses of this string with the non-reserved 'endpoint_list_id'. Co-authored-by: Elastic Machine --- .../schemas/response/exception_list_item_schema.mock.ts | 2 +- .../common/schemas/response/exception_list_schema.mock.ts | 6 ++++-- x-pack/plugins/lists/public/exceptions/api.test.ts | 2 +- .../detections/components/alerts_histogram_panel/index.tsx | 6 +++++- .../pages/detection_engine/rules/create/helpers.ts | 3 ++- .../detections/pages/detection_engine/rules/helpers.tsx | 3 ++- .../overview/components/signals_by_category/index.tsx | 5 ----- .../server/endpoint/lib/artifacts/lists.ts | 3 ++- .../apis/lists/create_exception_list_item.ts | 5 +++-- 9 files changed, 20 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index f7a6af98c8f0e..9e1a88ceb28bd 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -16,7 +16,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ entries: ENTRIES, id: '1', item_id: 'endpoint_list_item', - list_id: 'endpoint_list', + list_id: 'endpoint_list_id', meta: {}, name: 'Sample Endpoint Exception List', namespace_type: 'single', diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 017b959a2baf3..906dcf6560ee5 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ENDPOINT_LIST_ID } from '../..'; + import { ExceptionListSchema } from './exception_list_schema'; export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ @@ -12,10 +14,10 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ created_by: 'user_name', description: 'This is a sample endpoint type exception', id: '1', - list_id: 'endpoint_list', + list_id: ENDPOINT_LIST_ID, meta: {}, name: 'Sample Endpoint Exception List', - namespace_type: 'single', + namespace_type: 'agnostic', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'endpoint', diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 1414d828fa6d4..455670098307f 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -508,7 +508,7 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), - listId: 'endpoint_list', + listId: 'endpoint_list_id', namespaceType: 'single', pagination: { page: 1, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 560c092d12076..3bc84bb7c32ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -80,10 +80,14 @@ const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ const NO_LEGEND_DATA: LegendItem[] = []; +const DEFAULT_STACK_BY = 'signal.rule.name'; +const getDefaultStackByOption = (): AlertsHistogramOption => + alertsHistogramOptions.find(({ text }) => text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; + export const AlertsHistogramPanel = memo( ({ chartHeight, - defaultStackByOption = alertsHistogramOptions[8], // signal.rule.name + defaultStackByOption = getDefaultStackByOption(), deleteQuery, filters, headerChildren, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 226fa5313e34f..38f7836f678f9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -12,6 +12,7 @@ import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/const import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { NewRule } from '../../../../containers/detection_engine/rules'; import { @@ -167,7 +168,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule ...(isAssociatedToEndpointList ? { exceptions_list: [ - { id: 'endpoint_list', namespace_type: 'agnostic', type: 'endpoint' }, + { id: ENDPOINT_LIST_ID, namespace_type: 'agnostic', type: 'endpoint' }, ] as AboutStepRuleJson['exceptions_list'], } : {}), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 3de508dcbb3be..11b779e71b9b2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -13,6 +13,7 @@ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engin import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; +import { ENDPOINT_LIST_ID } from '../../../../shared_imports'; import { Rule } from '../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -137,7 +138,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu return { isNew: false, author, - isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === 'endpoint_list') ?? false, + isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === ENDPOINT_LIST_ID) ?? false, isBuildingBlock: buildingBlockType !== undefined, license: license ?? '', ruleNameOverride: ruleNameOverride ?? '', diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index fbfdefa13d738..0ac136044c06d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -17,7 +17,6 @@ import { UpdateDateRange } from '../../../common/components/charts/common'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; const NO_FILTERS: Filter[] = []; interface Props extends Pick { @@ -62,13 +61,9 @@ const SignalsByCategoryComponent: React.FC = ({ [setAbsoluteRangeDatePicker] ); - const defaultStackByOption = - alertsHistogramOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; - return ( Date: Thu, 16 Jul 2020 20:49:55 -0500 Subject: [PATCH 10/13] [Security Solution][Detections] Disable exceptions for Threshold and ML rules (#72137) * Move isThresholdRule predicate into our common folder This is very similar to isMlRule, which is already used extensively and lives at this level. * Disable endpoint association checkbox for ML and Threshold rules The fullWidth and isDisabled props were not used; what we want is disabled. * Fix react warning about nesting buttons This removes the AdvancedSettingsAccordion in favor of a plain EuiAccordion with buttonContent, as that seems to be all that's needed here. * Disable Exceptions tab on Details for ML or Threshold rules These rule types do not currently support exceptions. * Fix type error Unused import --- .../common/detection_engine/utils.ts | 3 ++ .../rules/select_rule_type/index.tsx | 3 +- .../rules/step_about_rule/index.tsx | 40 ++++++------------ .../detection_engine/rules/details/index.tsx | 42 +++++++++++-------- 4 files changed, 40 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index fa1812235f897..153130fc16d60 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,6 +5,7 @@ */ import { EntriesArray } from '../shared_imports'; +import { RuleType } from './types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); @@ -15,3 +16,5 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'nested'); return found.length > 0; }; + +export const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 6546c1ba59d84..c6ea269e1a355 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -8,14 +8,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; -const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; - interface SelectRuleTypeProps { describedByIds?: string[]; field: FieldHook; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 1fc88da5fd7c7..ec812fa63eadf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleStepProps, RuleStep, @@ -58,26 +60,6 @@ const TagContainer = styled.div` TagContainer.displayName = 'TagContainer'; -const AdvancedSettingsAccordion = styled(EuiAccordion)` - .euiAccordion__iconWrapper { - display: none; - } - - .euiAccordion__childWrapper { - transition-duration: 1ms; /* hack to fire Step accordion to set proper content's height */ - } - - &.euiAccordion-isOpen .euiButtonEmpty__content > svg { - transform: rotate(90deg); - } -`; - -const AdvancedSettingsAccordionButton = ( - - {I18n.ADVANCED_SETTINGS} - -); - const StepAboutRuleComponent: FC = ({ addPadding = false, defaultValues, @@ -94,6 +76,10 @@ const StepAboutRuleComponent: FC = ({ const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( defineRuleData?.index ?? [] ); + const canUseExceptions = + defineRuleData?.ruleType && + !isMlRule(defineRuleData.ruleType) && + !isThresholdRule(defineRuleData.ruleType); const { form } = useForm({ defaultValue: initialState, @@ -193,10 +179,10 @@ const StepAboutRuleComponent: FC = ({ /> - = ({ idAria: 'detectionEngineStepAboutRuleAssociatedToEndpointList', 'data-test-subj': 'detectionEngineStepAboutRuleAssociatedToEndpointList', euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, + disabled: isLoading || !canUseExceptions, }, }} /> @@ -287,8 +272,7 @@ const StepAboutRuleComponent: FC = ({ idAria: 'detectionEngineStepAboutRuleBuildingBlock', 'data-test-subj': 'detectionEngineStepAboutRuleBuildingBlock', euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, + disabled: isLoading, }, }} /> @@ -319,7 +303,7 @@ const StepAboutRuleComponent: FC = ({ placeholder: '', }} /> - + {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 7eb5c3a535377..484c28b4b428c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -37,7 +37,7 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; -import { useRule } from '../../../../containers/detection_engine/rules'; +import { useRule, Rule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { useWithSource } from '../../../../../common/containers/source'; @@ -90,6 +90,8 @@ import { MIN_EVENTS_VIEWER_BODY_HEIGHT, } from '../../../../../timelines/components/timeline/body/helpers'; import { footerHeight } from '../../../../../timelines/components/timeline/footer'; +import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; enum RuleDetailTabs { alerts = 'alerts', @@ -97,23 +99,26 @@ enum RuleDetailTabs { exceptions = 'exceptions', } -const ruleDetailTabs = [ - { - id: RuleDetailTabs.alerts, - name: detectionI18n.ALERT, - disabled: false, - }, - { - id: RuleDetailTabs.exceptions, - name: i18n.EXCEPTIONS_TAB, - disabled: false, - }, - { - id: RuleDetailTabs.failures, - name: i18n.FAILURE_HISTORY_TAB, - disabled: false, - }, -]; +const getRuleDetailsTabs = (rule: Rule | null) => { + const canUseExceptions = rule && !isMlRule(rule.type) && !isThresholdRule(rule.type); + return [ + { + id: RuleDetailTabs.alerts, + name: detectionI18n.ALERT, + disabled: false, + }, + { + id: RuleDetailTabs.exceptions, + name: i18n.EXCEPTIONS_TAB, + disabled: !canUseExceptions, + }, + { + id: RuleDetailTabs.failures, + name: i18n.FAILURE_HISTORY_TAB, + disabled: false, + }, + ]; +}; export const RuleDetailsPageComponent: FC = ({ filters, @@ -160,6 +165,7 @@ export const RuleDetailsPageComponent: FC = ({ // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const ruleDetailTabs = getRuleDetailsTabs(rule); const title = isLoading === true || rule === null ? : rule.name; const subTitle = useMemo( From 55da30d17a7489a95c158b5cea01b5ddb785dbc9 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 16 Jul 2020 19:11:08 -0700 Subject: [PATCH 11/13] skip flaky suite (#64696) --- x-pack/test/api_integration/apis/fleet/unenroll_agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index bc6c44e590cc4..4e1443ad1fc68 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -16,7 +16,8 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); - describe('fleet_unenroll_agent', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64696 + describe.skip('fleet_unenroll_agent', () => { let accessAPIKeyId: string; let outputAPIKeyId: string; before(async () => { From effab78a94524a6ab428b57f407a045f27ae4954 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 16 Jul 2020 23:00:22 -0500 Subject: [PATCH 12/13] [Security Solution][Detections] Better toast errors (#72205) * Add new hook to wrap the toasts service When receiving error responses from our APIs, this gives us better toast messages. * Replace useToasts with useAppToasts in trivial case * WIP: prevent infinite polling when server is unresponsive The crux of this issue was that we had no steady state when the server returned a non-API error (!isApiError), as it would if the server was throwing 500s or just generally misbehaving. The solution, then, is to addresse these non-API errors in our underlying useListsIndex and useListsPrivileges hooks. This also refactors those hooks to: * collapse multiple error states into one (that's all we currently need) * use useAppToasts for better UI TODO: I don't think I need the changes in useListsConfig's useEffect. * Slightly more legible variables The only task in this hook is our readPrivileges task right now, so I'm shortening the variable until we have a need to disambiguate it further. * Remove unnecessary conditions around creating our index If the index hook has an error needsIndex will not be true. * Better toast errors for Kibana API errors Our isApiError predicate does not work for errors coming back from Kibana platform itself, e.g. for a request payload error. I've added a separate predicate for that case, isKibanaError, and then a wrapping isAppError predicate since most of our use cases just care about error.body.message, which is common to both. * Use new toasts hook on our exceptions modals This fixes two issues: * toast appears above modal overlay * Error message from response is now presented in the toast * Fix bug with toasts dependencies Because of the way some of the exception modal's hooks are written, a change to one of its callbacks means that the request will be canceled. Because the toasts service exports instance methods, the context within the function (and thus the function itself) can change leading to a mutable ref. Because we don't want/need this behavior, we store our exported functions in refs to 'freeze' them for react. With our bound functions, we should now be able to declare e.g. `toast.addError` as a dependency, however react cannot determine that it is bound (and thus that toast.addError() is equivalent to addError()), and so we must destructure our functions in order to use them as dependencies. * Alert clipboard toasts through new Toasts service This fixes the z-index issue between modals and toasts. * Fix type errors * Mock external dependency These tests now call out to the Notifications service (in a context) instead of our redux implementation. --- .../exceptions/add_exception_modal/index.tsx | 12 ++-- .../exceptions/edit_exception_modal/index.tsx | 12 ++-- .../viewer/exception_item/index.test.tsx | 2 + .../common/components/toasters/utils.ts | 4 +- .../common/hooks/use_app_toasts.test.ts | 63 +++++++++++++++++ .../public/common/hooks/use_app_toasts.ts | 48 +++++++++++++ .../public/common/lib/clipboard/clipboard.tsx | 23 ++----- .../common/lib/kibana/__mocks__/index.ts | 2 + .../public/common/utils/api/index.ts | 23 ++++++- .../value_lists_management_modal/modal.tsx | 13 ++-- .../alerts/use_signal_index.tsx | 6 +- .../lists/use_lists_config.tsx | 6 +- .../lists/use_lists_index.tsx | 69 ++++++++----------- .../lists/use_lists_privileges.tsx | 29 ++++---- 14 files changed, 211 insertions(+), 101 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index d5eeef0f1e768..79383676266f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -29,8 +29,8 @@ import { } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toasters'; import { ExceptionBuilder } from '../builder'; import { Loader } from '../../loader'; import { useAddOrUpdateException } from '../use_add_exception'; @@ -115,7 +115,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ Array >([]); const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( @@ -124,15 +124,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const onError = useCallback( (error: Error) => { - errorToToaster({ title: i18n.ADD_EXCEPTION_ERROR, error, dispatchToaster }); + addError(error, { title: i18n.ADD_EXCEPTION_ERROR }); onCancel(); }, - [dispatchToaster, onCancel] + [addError, onCancel] ); const onSuccess = useCallback(() => { - displaySuccessToast(i18n.ADD_EXCEPTION_SUCCESS, dispatchToaster); + addSuccess(i18n.ADD_EXCEPTION_SUCCESS); onConfirm(shouldCloseAlert); - }, [dispatchToaster, onConfirm, shouldCloseAlert]); + }, [addSuccess, onConfirm, shouldCloseAlert]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 73933d483e2cb..dbc70dfe21dd0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -30,7 +30,7 @@ import { } from '../../../../../public/lists_plugin_deps'; import * as i18n from './translations'; import { useKibana } from '../../../lib/kibana'; -import { errorToToaster, displaySuccessToast, useStateToaster } from '../../toasters'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; import { ExceptionBuilder } from '../builder'; import { useAddOrUpdateException } from '../use_add_exception'; import { AddExceptionComments } from '../add_exception_comments'; @@ -93,7 +93,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState< Array >([]); - const [, dispatchToaster] = useStateToaster(); + const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( @@ -102,15 +102,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const onError = useCallback( (error) => { - errorToToaster({ title: i18n.EDIT_EXCEPTION_ERROR, error, dispatchToaster }); + addError(error, { title: i18n.EDIT_EXCEPTION_ERROR }); onCancel(); }, - [dispatchToaster, onCancel] + [addError, onCancel] ); const onSuccess = useCallback(() => { - displaySuccessToast(i18n.EDIT_EXCEPTION_SUCCESS, dispatchToaster); + addSuccess(i18n.EDIT_EXCEPTION_SUCCESS); onConfirm(); - }, [dispatchToaster, onConfirm]); + }, [addSuccess, onConfirm]); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx index 0e2908fc34232..90752f9450e4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx @@ -13,6 +13,8 @@ import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock'; +jest.mock('../../../../lib/kibana'); + describe('ExceptionItem', () => { it('it renders ExceptionDetails and ExceptionEntries', () => { const exceptionItem = getExceptionListItemSchemaMock(); 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 e7cc389d4c06b..47c5588a12830 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 @@ -9,7 +9,7 @@ import { isError } from 'lodash/fp'; import { AppToast, ActionToaster } from './'; import { isToasterError } from './errors'; -import { isApiError } from '../../utils/api'; +import { isAppError } from '../../utils/api'; /** * Displays an error toast for the provided title and message @@ -114,7 +114,7 @@ export const errorToToaster = ({ iconType, errors: error.messages, }; - } else if (isApiError(error)) { + } else if (isAppError(error)) { toast = { id, title, 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 new file mode 100644 index 0000000000000..e0e629793952a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useToasts } from '../lib/kibana'; +import { useAppToasts } from './use_app_toasts'; + +jest.mock('../lib/kibana'); + +describe('useDeleteList', () => { + let addErrorMock: jest.Mock; + let addSuccessMock: jest.Mock; + + beforeEach(() => { + addErrorMock = jest.fn(); + addSuccessMock = jest.fn(); + (useToasts as jest.Mock).mockImplementation(() => ({ + addError: addErrorMock, + addSuccess: addSuccessMock, + })); + }); + + it('works normally with a regular error', async () => { + const error = new Error('regular error'); + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(error, { title: 'title' }); + + 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' }, + }; + + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(kibanaApiError, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(kibanaApiError, { + title: 'title', + toastMessage: 'Detailed Message', + }); + }); + + it('converts an unknown error to an Error', () => { + const unknownError = undefined; + + const { result } = renderHook(() => useAppToasts()); + + result.current.addError(unknownError, { title: 'title' }); + + expect(addErrorMock).toHaveBeenCalledWith(Error(`${undefined}`), { + title: 'title', + }); + }); +}); 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 new file mode 100644 index 0000000000000..bc59d87100058 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useRef } from 'react'; + +import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public'; +import { useToasts } from '../lib/kibana'; +import { isAppError, AppError } from '../utils/api'; + +export type UseAppToasts = Pick & { + api: ToastsStart; + addError: (error: unknown, options: ErrorToastOptions) => Toast; +}; + +export const useAppToasts = (): UseAppToasts => { + const toasts = useToasts(); + const addError = useRef(toasts.addError.bind(toasts)).current; + const addSuccess = useRef(toasts.addSuccess.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); + } else { + if (error instanceof Error) { + return addError(error, options); + } else { + return addError(new Error(String(error)), options); + } + } + }, + [addAppError, addError] + ); + + return { api: toasts, addError: _addError, addSuccess }; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx index fdb6ed130a525..75b7308ab61f1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiGlobalToastListToast as Toast, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon } from '@elastic/eui'; import copy from 'copy-to-clipboard'; import React from 'react'; -import uuid from 'uuid'; import * as i18n from './translations'; -import { useStateToaster } from '../../components/toasters'; +import { useAppToasts } from '../../hooks/use_app_toasts'; export type OnCopy = ({ content, @@ -20,17 +19,6 @@ export type OnCopy = ({ isSuccess: boolean; }) => void; -interface GetSuccessToastParams { - titleSummary?: string; -} - -const getSuccessToast = ({ titleSummary }: GetSuccessToastParams): Toast => ({ - id: `copy-success-${uuid.v4()}`, - color: 'success', - iconType: 'copyClipboard', - title: `${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, -}); - interface Props { children?: JSX.Element; content: string | number; @@ -40,7 +28,7 @@ interface Props { } export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTimeMs }: Props) => { - const dispatchToaster = useStateToaster()[1]; + const { addSuccess } = useAppToasts(); const onClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -52,10 +40,7 @@ export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTi } if (isSuccess) { - dispatchToaster({ - type: 'addToaster', - toast: { toastLifeTimeMs, ...getSuccessToast({ titleSummary }) }, - }); + addSuccess(`${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, { toastLifeTimeMs }); } }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index c3e1f35f37356..6ada887ece175 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createKibanaContextProviderMock, createUseUiSettingMock, @@ -19,6 +20,7 @@ export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); +export const useToasts = jest.fn(() => notificationServiceMock.createStartContract().toasts); export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); 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 ab442d0d09cf9..e8934259fe43e 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 @@ -6,14 +6,33 @@ import { has } from 'lodash/fp'; -export interface KibanaApiError { +export interface AppError { name: string; message: string; + body: { + message: string; + }; +} + +export interface KibanaError extends AppError { + body: { + message: string; + statusCode: number; + }; +} + +export interface SecurityAppError extends AppError { body: { message: string; status_code: number; }; } -export const isApiError = (error: unknown): error is KibanaApiError => +export const isKibanaError = (error: unknown): error is KibanaError => + has('message', error) && has('body.message', error) && has('body.statusCode', error); + +export const isSecurityAppError = (error: unknown): error is SecurityAppError => has('message', error) && has('body.message', error) && has('body.status_code', error); + +export const isAppError = (error: unknown): error is AppError => + isKibanaError(error) || isSecurityAppError(error); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index 0a935a9cdb1c4..d7d4be6d951b8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -23,7 +23,8 @@ import { useDeleteList, useCursor, } from '../../../shared_imports'; -import { useToasts, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { GenericDownloader } from '../../../common/components/generic_downloader'; import * as i18n from './translations'; import { ValueListsTable } from './table'; @@ -45,7 +46,7 @@ export const ValueListsModalComponent: React.FC = ({ const { start: findLists, ...lists } = useFindLists(); const { start: deleteList, result: deleteResult } = useDeleteList(); const [exportListId, setExportListId] = useState(); - const toasts = useToasts(); + const { addError, addSuccess } = useAppToasts(); const fetchLists = useCallback(() => { findLists({ cursor, http, pageIndex: pageIndex + 1, pageSize }); @@ -82,21 +83,21 @@ export const ValueListsModalComponent: React.FC = ({ const handleUploadError = useCallback( (error: Error) => { if (error.name !== 'AbortError') { - toasts.addError(error, { title: i18n.UPLOAD_ERROR }); + addError(error, { title: i18n.UPLOAD_ERROR }); } }, - [toasts] + [addError] ); const handleUploadSuccess = useCallback( (response: ListSchema) => { - toasts.addSuccess({ + addSuccess({ text: i18n.uploadSuccessMessage(response.name), title: i18n.UPLOAD_SUCCESS_TITLE, }); fetchLists(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [toasts] + [addSuccess] ); useEffect(() => { 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 65a2721013b5e..14fd9ffa50843 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 @@ -9,7 +9,7 @@ import { useEffect, useState } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { isApiError } from '../../../../common/utils/api'; +import { isSecurityAppError } from '../../../../common/utils/api'; type Func = () => void; @@ -59,7 +59,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { signalIndexName: null, createDeSignalIndex: createIndex, }); - if (isApiError(error) && error.body.status_code !== 404) { + if (isSecurityAppError(error) && error.body.status_code !== 404) { errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster }); } } @@ -81,7 +81,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { } } catch (error) { if (isSubscribed) { - if (isApiError(error) && error.body.status_code === 409) { + if (isSecurityAppError(error) && error.body.status_code === 409) { fetchData(); } else { setSignalIndex({ diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx index e21cbceeaef27..71847e7b7d8cb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_config.tsx @@ -19,16 +19,16 @@ export interface UseListsConfigReturn { } export const useListsConfig = (): UseListsConfigReturn => { - const { createIndex, createIndexError, indexExists, loading: indexLoading } = useListsIndex(); + const { createIndex, indexExists, loading: indexLoading, error: indexError } = useListsIndex(); const { canManageIndex, canWriteIndex, loading: privilegesLoading } = useListsPrivileges(); const { lists } = useKibana().services; const enabled = lists != null; const loading = indexLoading || privilegesLoading; const needsIndex = indexExists === false; - const indexCreationFailed = createIndexError != null; + const hasIndexError = indexError != null; const needsIndexConfiguration = - needsIndex && (canManageIndex === false || (canManageIndex === true && indexCreationFailed)); + needsIndex && (canManageIndex === false || (canManageIndex === true && hasIndexError)); const needsConfiguration = !enabled || canWriteIndex === false || needsIndexConfiguration; useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx index 75f12bd07d3ae..ee1316eb8a1fd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx @@ -7,28 +7,24 @@ import { useEffect, useState, useCallback } from 'react'; import { useReadListIndex, useCreateListIndex } from '../../../../shared_imports'; -import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana'; -import { isApiError } from '../../../../common/utils/api'; +import { useHttp, useKibana } from '../../../../common/lib/kibana'; +import { isSecurityAppError } from '../../../../common/utils/api'; import * as i18n from './translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -export interface UseListsIndexState { +export interface UseListsIndexReturn { + createIndex: () => void; indexExists: boolean | null; -} - -export interface UseListsIndexReturn extends UseListsIndexState { + error: unknown; loading: boolean; - createIndex: () => void; - createIndexError: unknown; - createIndexResult: { acknowledged: boolean } | undefined; } export const useListsIndex = (): UseListsIndexReturn => { - const [state, setState] = useState({ - indexExists: null, - }); + const [indexExists, setIndexExists] = useState(null); + const [error, setError] = useState(null); const { lists } = useKibana().services; const http = useHttp(); - const toasts = useToasts(); + const { addError } = useAppToasts(); const { loading: readLoading, start: readListIndex, ...readListIndexState } = useReadListIndex(); const { loading: createLoading, @@ -51,18 +47,17 @@ export const useListsIndex = (): UseListsIndexReturn => { // initial read list useEffect(() => { - if (!readLoading && state.indexExists === null) { + if (!readLoading && !error && indexExists === null) { readIndex(); } - }, [readIndex, readLoading, state.indexExists]); + }, [error, indexExists, readIndex, readLoading]); // handle read result useEffect(() => { if (readListIndexState.result != null) { - setState({ - indexExists: - readListIndexState.result.list_index && readListIndexState.result.list_item_index, - }); + setIndexExists( + readListIndexState.result.list_index && readListIndexState.result.list_item_index + ); } }, [readListIndexState.result]); @@ -75,34 +70,30 @@ export const useListsIndex = (): UseListsIndexReturn => { // handle read error useEffect(() => { - const error = readListIndexState.error; - if (isApiError(error)) { - setState({ indexExists: false }); - if (error.body.status_code !== 404) { - toasts.addError(error, { - title: i18n.LISTS_INDEX_FETCH_FAILURE, - toastMessage: error.body.message, - }); + const err = readListIndexState.error; + if (err != null) { + if (isSecurityAppError(err) && err.body.status_code === 404) { + setIndexExists(false); + } else { + setError(err); + addError(err, { title: i18n.LISTS_INDEX_FETCH_FAILURE }); } } - }, [readListIndexState.error, toasts]); + }, [addError, readListIndexState.error]); // handle create error useEffect(() => { - const error = createListIndexState.error; - if (isApiError(error)) { - toasts.addError(error, { - title: i18n.LISTS_INDEX_CREATE_FAILURE, - toastMessage: error.body.message, - }); + const err = createListIndexState.error; + if (err != null) { + setError(err); + addError(err, { title: i18n.LISTS_INDEX_CREATE_FAILURE }); } - }, [createListIndexState.error, toasts]); + }, [addError, createListIndexState.error]); return { - loading, createIndex, - createIndexError: createListIndexState.error, - createIndexResult: createListIndexState.result, - ...state, + error, + indexExists, + loading, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx index fbbcff33402c3..f99f62b1948e6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx @@ -7,8 +7,8 @@ import { useEffect, useState, useCallback } from 'react'; import { useReadListPrivileges } from '../../../../shared_imports'; -import { useHttp, useToasts, useKibana } from '../../../../common/lib/kibana'; -import { isApiError } from '../../../../common/utils/api'; +import { useHttp, useKibana } from '../../../../common/lib/kibana'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; export interface UseListsPrivilegesState { @@ -79,8 +79,8 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { }); const { lists } = useKibana().services; const http = useHttp(); - const toasts = useToasts(); - const { loading, start: readListPrivileges, ...privilegesState } = useReadListPrivileges(); + const { addError } = useAppToasts(); + const { loading, start: readListPrivileges, ...readState } = useReadListPrivileges(); const readPrivileges = useCallback(() => { if (lists) { @@ -90,20 +90,20 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { // initRead useEffect(() => { - if (!loading && state.isAuthenticated === null) { + if (!loading && !readState.error && state.isAuthenticated === null) { readPrivileges(); } - }, [loading, readPrivileges, state.isAuthenticated]); + }, [loading, readState.error, readPrivileges, state.isAuthenticated]); // handleReadResult useEffect(() => { - if (privilegesState.result != null) { + if (readState.result != null) { try { const { is_authenticated: isAuthenticated, lists: { index: listsPrivileges }, listItems: { index: listItemsPrivileges }, - } = privilegesState.result as ListPrivileges; + } = readState.result as ListPrivileges; setState({ isAuthenticated, @@ -114,19 +114,18 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => { setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false }); } } - }, [privilegesState.result]); + }, [readState.result]); // handleReadError useEffect(() => { - const error = privilegesState.error; - if (isApiError(error)) { - setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false }); - toasts.addError(error, { + const error = readState.error; + if (error != null) { + setState({ isAuthenticated: false, canManageIndex: false, canWriteIndex: false }); + addError(error, { title: i18n.LISTS_PRIVILEGES_READ_FAILURE, - toastMessage: error.body.message, }); } - }, [privilegesState.error, toasts]); + }, [addError, readState.error]); return { loading, ...state }; }; From 5cd1f6e562a558a6d725806e6a5bc5eb722c4b57 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 16 Jul 2020 22:42:28 -0700 Subject: [PATCH 13/13] Call setupIngest before fleet_install tests (#72214) --- x-pack/test/api_integration/apis/fleet/install.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/fleet/install.ts b/x-pack/test/api_integration/apis/fleet/install.ts index 3a122463fae55..59b040e30fb48 100644 --- a/x-pack/test/api_integration/apis/fleet/install.ts +++ b/x-pack/test/api_integration/apis/fleet/install.ts @@ -6,11 +6,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { setupIngest } from './agents/services'; -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const supertest = getService('supertest'); describe('fleet_install', () => { + setupIngest(providerContext); + it('should return a 400 if we try download an install script for a not supported OS', async () => { await supertest.get(`/api/ingest_manager/fleet/install/gameboy`).expect(400); });