From 893d47fb7c89c41b33253c51cbff01f612133e68 Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:19:48 -0500 Subject: [PATCH 01/10] [Enterprise Search] Move healthcolor to shared folder (#149271) ## Summary Based on the PR [comment](https://github.com/elastic/kibana/pull/149241#discussion_r1081847139), Moving healthStatus color map to [public/applications/shared](https://github.com/elastic/kibana/tree/5df280162515c9429cbd172a56eba2baace6baf1/x-pack/plugins/enterprise_search/public/applications/shared) folder --- .../search_index_selectable.tsx | 13 ++--------- .../components/engine/engine_indices.tsx | 8 +------ .../search_indices/indices_table.tsx | 8 +------ .../shared/constants/health_colors.ts | 22 +++++++++++++++++++ 4 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/constants/health_colors.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/search_index_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/search_index_selectable.tsx index 93e045d2ecb26..22add9bdaa518 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/search_index_selectable.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/search_index_selectable.tsx @@ -21,6 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import './engine_creation.scss'; +import { healthColorsMapSelectable } from '../../../shared/constants/health_colors'; export interface SearchIndexSelectableOption { label: string; @@ -47,16 +48,6 @@ export interface SearchIndexSelectableOption { checked?: 'on'; count: number; } - -const healthColorsMap = { - red: 'danger', - RED: 'danger', - green: 'success', - GREEN: 'success', - yellow: 'warning', - YELLOW: 'warning', -}; - interface IndexStatusDetailsProps { option?: SearchIndexSelectableOption; } @@ -67,7 +58,7 @@ export const IndexStatusDetails: React.FC = ({ option } ) : ( - + {option.health ?? '-'} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx index 537c83cac5b1e..bcda0026ddd3c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx @@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n'; import { EnterpriseSearchEngineIndex } from '../../../../../common/types/engines'; import { CANCEL_BUTTON_LABEL } from '../../../shared/constants'; +import { healthColorsMap } from '../../../shared/constants/health_colors'; import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; @@ -33,13 +34,6 @@ import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_temp import { EngineIndicesLogic } from './engine_indices_logic'; import { EngineViewLogic } from './engine_view_logic'; -const healthColorsMap = { - green: 'success', - red: 'danger', - unavailable: '', - yellow: 'warning', -}; - export const EngineIndices: React.FC = () => { const { engineName, isLoadingEngine } = useValues(EngineViewLogic); const { engineData } = useValues(EngineIndicesLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx index 6e506c6bc66a3..fd0bc55fdc7e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx @@ -19,6 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { Meta } from '../../../../../common/types'; +import { healthColorsMap } from '../../../shared/constants/health_colors'; import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; @@ -32,13 +33,6 @@ import { ingestionStatusToText, } from '../../utils/ingestion_status_helpers'; -const healthColorsMap = { - green: 'success', - red: 'danger', - unavailable: '', - yellow: 'warning', -}; - interface IndicesTableProps { indices: ElasticsearchViewIndex[]; isLoading?: boolean; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/health_colors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/health_colors.ts new file mode 100644 index 0000000000000..9c87507ae6994 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/health_colors.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. + */ + +export const healthColorsMap = { + red: 'danger', + green: 'success', + yellow: 'warning', + unavailable: '', +}; + +export const healthColorsMapSelectable = { + red: 'danger', + RED: 'danger', + green: 'success', + GREEN: 'success', + yellow: 'warning', + YELLOW: 'warning', +}; From bdfe63613eb5aeec4265ec15563cd439dcebd18f Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 23 Jan 2023 10:39:43 -0400 Subject: [PATCH 02/10] [Fleet] Move view agent dashboard button (#149315) --- .../components/agent_dashboard_link.test.tsx | 9 +-- .../components/agent_dashboard_link.tsx | 27 ++++++-- .../agent_details/agent_details_overview.tsx | 63 ++++++++++++++----- .../agents/agent_details_page/index.tsx | 4 -- 4 files changed, 73 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx index 3fc27cfbc9263..6e425b53522a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.test.tsx @@ -87,7 +87,7 @@ describe('AgentDashboardLink', () => { expect(result.getByRole('button').hasAttribute('disabled')).toBeTruthy(); }); - it('should not enable the button if elastic_agent package is installed and policy do not have monitoring enabled', async () => { + it('should link to the agent policy settings tab if logs and metrics are not enabled for that policy', async () => { mockedUseGetPackageInfoByKey.mockReturnValue({ isLoading: false, data: { @@ -107,14 +107,15 @@ describe('AgentDashboardLink', () => { } agentPolicy={ { + id: 'policy123', monitoring_enabled: [], } as unknown as AgentPolicy } /> ); - expect(result.queryByRole('link')).toBeNull(); - expect(result.queryByRole('button')).not.toBeNull(); - expect(result.getByRole('button').hasAttribute('disabled')).toBeTruthy(); + const link = result.queryByRole('link'); + expect(link).not.toBeNull(); + expect(link?.getAttribute('href')).toBe('/mock/app/fleet/policies/policy123/settings'); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx index 658a94dc7a564..93be90219cdc9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_dashboard_link.tsx @@ -8,8 +8,9 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; +import styled from 'styled-components'; -import { useGetPackageInfoByKey, useKibanaLink } from '../../../../hooks'; +import { useGetPackageInfoByKey, useKibanaLink, useLink } from '../../../../hooks'; import type { Agent, AgentPolicy } from '../../../../types'; import { FLEET_ELASTIC_AGENT_PACKAGE, @@ -32,11 +33,16 @@ function useAgentDashboardLink(agent: Agent) { }; } +const EuiButtonCompressed = styled(EuiButton)` + height: 32px; +`; + export const AgentDashboardLink: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; }> = ({ agent, agentPolicy }) => { const { isInstalled, link, isLoading } = useAgentDashboardLink(agent); + const { getHref } = useLink(); const isLogAndMetricsEnabled = agentPolicy?.monitoring_enabled?.length ?? 0 > 0; @@ -44,15 +50,15 @@ export const AgentDashboardLink: React.FunctionComponent<{ !isInstalled || isLoading || !isLogAndMetricsEnabled ? { disabled: true } : { href: link }; const button = ( - + - + ); - if (!isLogAndMetricsEnabled) { + if (!isLoading && !isLogAndMetricsEnabled && agentPolicy) { return ( } > - {button} + + + ); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx index b5f53d5640223..f0af73545466f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -27,6 +27,7 @@ import { AgentPolicySummaryLine } from '../../../../../components'; import { AgentHealth } from '../../../components'; import { Tags } from '../../../components/tags'; import { formatAgentCPU, formatAgentMemory } from '../../../services/agent_metrics'; +import { AgentDashboardLink } from '../agent_dashboard_link'; // Allows child text to be truncated const FlexItemWithMinWidth = styled(EuiFlexItem)` @@ -44,23 +45,53 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ + {displayAgentMetrics && ( + + + + {[ + { + title: i18n.translate('xpack.fleet.agentDetails.cpuLabel', { + defaultMessage: 'CPU', + }), + description: formatAgentCPU(agent.metrics, agentPolicy), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.memoryLabel', { + defaultMessage: 'Memory', + }), + description: formatAgentMemory(agent.metrics, agentPolicy), + }, + ].map(({ title, description }) => { + const tooltip = + typeof description === 'string' && description.length > 20 ? description : ''; + return ( + + + {title} + + + + + {description} + + + + + ); + })} + + + + + + + + + + + )} {[ - ...(displayAgentMetrics - ? [ - { - title: i18n.translate('xpack.fleet.agentDetails.cpuLabel', { - defaultMessage: 'CPU', - }), - description: formatAgentCPU(agent.metrics, agentPolicy), - }, - { - title: i18n.translate('xpack.fleet.agentDetails.memoryLabel', { - defaultMessage: 'Memory', - }), - description: formatAgentMemory(agent.metrics, agentPolicy), - }, - ] - : []), { title: i18n.translate('xpack.fleet.agentDetails.statusLabel', { defaultMessage: 'Status', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index 5ff3c8096bc0e..cd5c397bce14e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -32,7 +32,6 @@ import { AgentLogs, AgentDetailsActionMenu, AgentDetailsContent, - AgentDashboardLink, AgentDiagnosticsTab, } from './components'; @@ -131,9 +130,6 @@ export const AgentDetailsPage: React.FunctionComponent = () => { /> )} - - - ) : undefined, From 65c83738393f5080c6a0aae2d09040d9e6c48cf0 Mon Sep 17 00:00:00 2001 From: GitStart <1501599+gitstart@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:49:39 +0100 Subject: [PATCH 03/10] [Fleet]: Flickering is observed on Agent monitoring after navigating to agent details page. (#149110) [Fleet]: Flickering is observed on Agent monitoring after navigating to agent details page Resolves https://github.com/elastic/kibana/issues/142045 ### Loom/Screenshot Demo https://www.loom.com/share/678b4d6765244c40a792cb6845d594fb Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../agent_details/agent_details_overview.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx index f0af73545466f..5f0b64051f26f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -16,6 +16,7 @@ import { EuiPanel, EuiIcon, EuiToolTip, + EuiLoadingContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; @@ -127,7 +128,7 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ description: agentPolicy ? ( ) : ( - agent.policy_id || '-' + ), }, { @@ -199,8 +200,7 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ title: i18n.translate('xpack.fleet.agentDetails.monitorLogsLabel', { defaultMessage: 'Monitor logs', }), - description: - Array.isArray(agentPolicy?.monitoring_enabled) && + description: Array.isArray(agentPolicy?.monitoring_enabled) ? ( agentPolicy?.monitoring_enabled?.includes('logs') ? ( - ), + ) + ) : ( + + ), }, { title: i18n.translate('xpack.fleet.agentDetails.monitorMetricsLabel', { defaultMessage: 'Monitor metrics', }), - description: - Array.isArray(agentPolicy?.monitoring_enabled) && + description: Array.isArray(agentPolicy?.monitoring_enabled) ? ( agentPolicy?.monitoring_enabled?.includes('metrics') ? ( - ), + ) + ) : ( + + ), }, { title: i18n.translate('xpack.fleet.agentDetails.tagsLabel', { From 92e480f4bd7b03d89edaa51c33d98269f69b0667 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 23 Jan 2023 15:53:23 +0100 Subject: [PATCH 04/10] [Synthetics] Fix getting started redirect from overview page (#149308) Fixes https://github.com/elastic/kibana/issues/149117 --- .../components/shared/exploratory_view/embeddable/index.tsx | 1 - .../monitors_page/{monitor_page.tsx => monitors_page.tsx} | 0 .../components/monitors_page/overview/overview_page.tsx | 2 +- x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx | 2 +- 4 files changed, 2 insertions(+), 3 deletions(-) rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/{monitor_page.tsx => monitors_page.tsx} (100%) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx index db0d2c03c3a1d..a8f24ec3ee5c5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx @@ -78,7 +78,6 @@ export function getExploratoryViewEmbeddable( const embedProps = { ...props }; if (props.sparklineMode) { embedProps.axisTitlesVisibility = { x: false, yRight: false, yLeft: false }; - embedProps.gridlinesVisibilitySettings = { x: false, yRight: false, yLeft: false }; embedProps.legendIsVisible = false; embedProps.hideTicks = true; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitor_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitors_page.tsx similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitor_page.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitors_page.tsx diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index b401676032e88..5531f6821445c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -79,7 +79,7 @@ export const OverviewPage: React.FC = () => { if ( !search && - enablementLoading && + !enablementLoading && isEnabled && !monitorsLoading && syntheticsMonitors.length === 0 diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 69cad74504ddd..2379eca420598 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -48,7 +48,7 @@ import { TEST_RUN_DETAILS_ROUTE, } from '../../../common/constants'; import { PLUGIN } from '../../../common/constants/plugin'; -import { MonitorsPageWithServiceAllowed } from './components/monitors_page/monitor_page'; +import { MonitorsPageWithServiceAllowed } from './components/monitors_page/monitors_page'; import { apiService } from '../../utils/api_service'; import { RunTestManually } from './components/monitor_details/run_test_manually'; import { MonitorDetailsStatus } from './components/monitor_details/monitor_details_status'; From 61820b418879f46512e0e2223cdf76792fa32b92 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:56:07 +0000 Subject: [PATCH 05/10] [SecuritySolution] Fix page crashes after editing the filter (#149048) ## Summary Fixes: https://github.com/elastic/kibana/issues/148710 Screenshot 2023-01-11 at 10 15 24 **Steps to verify:** 1. Enable feature flags: Please add this to kibana.dev.yml `xpack.securitySolution.enableExperimental: ['chartEmbeddablesEnabled']` 2. Go to alerts host details page. 3. Click on the donut chart to apply the filter. 4. Click the filter it applied to global search bar, and select `Edit filter`. 5. Should edit the filter successfully. --- .../public/common/containers/sourcerer/index.test.tsx | 1 + .../public/common/containers/sourcerer/index.tsx | 1 + x-pack/plugins/security_solution/public/common/types.ts | 1 + 3 files changed, 3 insertions(+) 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 89d8bc2721f82..d3faed1a28a6b 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 @@ -586,6 +586,7 @@ describe('Sourcerer Hooks', () => { '-filebeat-*', '-packetbeat-*', ]); + expect(result.current.indexPattern).toHaveProperty('getName'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 9e7c14a48026b..43521274cde97 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -434,6 +434,7 @@ export const useSourcererDataView = ( indexPattern: { fields: sourcererDataView.indexFields, title: selectedPatterns.join(','), + getName: () => selectedPatterns.join(','), }, indicesExist, loading: loading || sourcererDataView.loading, diff --git a/x-pack/plugins/security_solution/public/common/types.ts b/x-pack/plugins/security_solution/public/common/types.ts index c639975e499a0..18a6657121723 100644 --- a/x-pack/plugins/security_solution/public/common/types.ts +++ b/x-pack/plugins/security_solution/public/common/types.ts @@ -26,6 +26,7 @@ export interface SecuritySolutionUiConfigType { */ export interface SecuritySolutionDataViewBase extends DataViewBase { fields: FieldSpec[]; + getName?: () => string; } export type AlertWorkflowStatus = 'open' | 'closed' | 'acknowledged'; From ee4276c20dc4b00533b9d4aea87bd64a1444e6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 23 Jan 2023 15:58:54 +0100 Subject: [PATCH 06/10] [Enterprise Search]Add empty prompt for Engines List page (#149317) ## Summary - Add empty prompt for Engines page. ![Screenshot 2023-01-23 at 13 52 09](https://user-images.githubusercontent.com/1410658/214045131-4926210e-e42e-4e5b-b5d1-5837c39fd4a8.png) - Moves page descriptions to page header to match designs. ![Screenshot 2023-01-23 at 13 53 44](https://user-images.githubusercontent.com/1410658/214045218-e2c44680-5823-4c46-8e4f-6114f30d6c20.png) Note on designs: Description page won't extend under right side items per EUI don't allow it. See the screenshot from EUI cc/ @julianrosado ![Screenshot 2023-01-23 at 13 16 30](https://user-images.githubusercontent.com/1410658/214045427-4e41edc8-17c1-44a2-8463-32df5fc77044.png) https://eui.elastic.co/#/layout/page-header - Update isLoading logic for engine list logic. - Update and add tests. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../components/empty_engines_prompt.test.tsx | 23 ++ .../components/empty_engines_prompt.tsx | 36 +++ .../engines/engine_list_logic.test.ts | 4 +- .../components/engines/engines_list.test.tsx | 39 +++- .../components/engines/engines_list.tsx | 218 ++++++++++-------- .../components/engines/engines_list_logic.ts | 2 +- 6 files changed, 216 insertions(+), 106 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.test.tsx new file mode 100644 index 0000000000000..81d81bc8a164c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { EmptyEnginesPrompt } from './empty_engines_prompt'; + +describe('EmptyEnginesPrompt', () => { + it('should pass children to prompt actions', () => { + const dummyEl =
dummy
; + const wrapper = shallow({dummyEl}); + const euiPrompt = wrapper.find(EuiEmptyPrompt); + + expect(euiPrompt.prop('actions')).toEqual(dummyEl); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.tsx new file mode 100644 index 0000000000000..d3b4f1fc2b5f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/components/empty_engines_prompt.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const EmptyEnginesPrompt: React.FC = ({ children }) => { + return ( + + + + } + body={ +

+ +

+ } + actions={children} + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engine_list_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engine_list_logic.test.ts index e6128bab79bb6..bfbbb71d220c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engine_list_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engine_list_logic.test.ts @@ -24,7 +24,7 @@ const DEFAULT_VALUES = { deleteStatus: Status.IDLE, isDeleteLoading: false, isDeleteModalVisible: false, - isLoading: false, + isLoading: true, meta: DEFAULT_META, parameters: { meta: DEFAULT_META }, results: [], @@ -146,6 +146,7 @@ describe('EnginesListLogic', () => { meta: newPageMeta, // searchQuery: 'k', }, + isLoading: false, meta: newPageMeta, parameters: { meta: newPageMeta, @@ -224,6 +225,7 @@ describe('EnginesListLogic', () => { results, meta: DEFAULT_META, }, + isLoading: false, meta: DEFAULT_META, parameters: { meta: DEFAULT_META, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.test.tsx index 340dd3517746b..3de8546bcf5f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.test.tsx @@ -13,18 +13,34 @@ import { shallow } from 'enzyme'; import { Status } from '../../../../../common/types/api'; +import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; + +import { EmptyEnginesPrompt } from './components/empty_engines_prompt'; import { EnginesListTable } from './components/tables/engines_table'; import { EnginesList } from './engines_list'; import { DEFAULT_META } from './types'; const DEFAULT_VALUES = { data: undefined, - results: [], + isLoading: true, meta: DEFAULT_META, parameters: { meta: DEFAULT_META }, + results: [], status: Status.IDLE, }; -const mockValues = { ...DEFAULT_VALUES }; +const mockValues = { + ...DEFAULT_VALUES, + isLoading: false, + results: [ + { + created: '1999-12-31T23:59:59Z', + indices: ['index-18', 'index-23'], + name: 'engine-name-1', + updated: '1999-12-31T23:59:59Z', + }, + ], + status: Status.SUCCESS, +}; const mockActions = { fetchEngines: jest.fn(), @@ -36,7 +52,23 @@ describe('EnginesList', () => { jest.clearAllMocks(); global.localStorage.clear(); }); - describe('Empty state', () => {}); + it('renders loading when isLoading', () => { + setMockValues(DEFAULT_VALUES); + setMockActions(mockActions); + + const wrapper = shallow(); + const pageTemplate = wrapper.find(EnterpriseSearchContentPageTemplate); + + expect(pageTemplate.prop('isLoading')).toEqual(true); + }); + it('renders empty prompt when no data is available', () => { + setMockValues(DEFAULT_VALUES); + setMockActions(mockActions); + const wrapper = shallow(); + + expect(wrapper.find(EmptyEnginesPrompt)).toHaveLength(1); + expect(wrapper.find(EnginesListTable)).toHaveLength(0); + }); it('renders with Engines data ', async () => { setMockValues(mockValues); @@ -45,5 +77,6 @@ describe('EnginesList', () => { const wrapper = shallow(); expect(wrapper.find(EnginesListTable)).toHaveLength(1); + expect(wrapper.find(EmptyEnginesPrompt)).toHaveLength(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index 1bafef662a135..db125c425c501 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -20,13 +20,30 @@ import { DataPanel } from '../../../shared/data_panel/data_panel'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; +import { EmptyEnginesPrompt } from './components/empty_engines_prompt'; import { EnginesListTable } from './components/tables/engines_table'; import { DeleteEngineModal } from './delete_engine_modal'; import { EnginesListLogic } from './engines_list_logic'; +const CreateButton: React.FC = () => { + return ( + + {i18n.translate('xpack.enterpriseSearch.content.engines.createEngineButtonLabel', { + defaultMessage: 'Create engine', + })} + + ); +}; + export const EnginesList: React.FC = () => { const { fetchEngines, onPaginate, openDeleteEngineModal } = useActions(EnginesListLogic); - const { meta, results } = useValues(EnginesListLogic); + const { meta, results, isLoading } = useValues(EnginesListLogic); const [searchQuery, setSearchValue] = useState(''); const throttledSearchQuery = useThrottle(searchQuery, INPUT_THROTTLE_DELAY_MS); @@ -47,114 +64,113 @@ export const EnginesList: React.FC = () => { }), ]} pageHeader={{ + description: ( + + {' '} + {/* TODO: navigate to documentation url */}{' '} + {i18n.translate('xpack.enterpriseSearch.content.engines.documentation', { + defaultMessage: 'explore our Engines documentation', + })} + + ), + }} + /> + ), pageTitle: i18n.translate('xpack.enterpriseSearch.content.engines.title', { defaultMessage: 'Engines', }), - rightSideItems: [ - - {i18n.translate('xpack.enterpriseSearch.content.engines.createEngineButtonLabel', { - defaultMessage: 'Create engine', - })} - , - ], + rightSideItems: [], }} pageViewTelemetry="Engines" - isLoading={false} + isLoading={isLoading} > - - - {' '} - {/* TODO: navigate to documentation url */}{' '} - {i18n.translate('xpack.enterpriseSearch.content.engines.documentation', { - defaultMessage: 'explore our Engines documentation', - })} - - ), - }} - /> - -
- { - setSearchValue(event.currentTarget.value); - }} - /> -
- - - {i18n.translate('xpack.enterpriseSearch.content.engines.searchPlaceholder.description', { - defaultMessage: 'Locate an engine via name or indices', - })} - + {results.length ? ( + <> +
+ { + setSearchValue(event.currentTarget.value); + }} + /> +
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.engines.searchPlaceholder.description', + { + defaultMessage: 'Locate an engine via name or indices', + } + )} + - - - - - - ), - to: ( - - - - ), - total: , - }} - /> - - - {i18n.translate('xpack.enterpriseSearch.content.engines.title', { - defaultMessage: 'Engines', - })} - - } - > - - + + + + + + ), + to: ( + + + + ), + total: , + }} + /> + + + {i18n.translate('xpack.enterpriseSearch.content.engines.title', { + defaultMessage: 'Engines', + })} + + } + > + + + + ) : ( + + + + )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts index 8f7a012518905..6428217e12b5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts @@ -124,7 +124,7 @@ export const EnginesListLogic = kea [selectors.status], - (status: EngineListValues['status']) => [Status.LOADING].includes(status), + (status: EngineListValues['status']) => [Status.LOADING, Status.IDLE].includes(status), ], results: [() => [selectors.data], (data) => data?.results ?? []], meta: [() => [selectors.parameters], (parameters) => parameters.meta], From e5df1b8bc068094cabf74062212ca07559aff748 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:23:11 -0500 Subject: [PATCH 07/10] [Security Solution] Reduce scope of osquery update fix to only update the package (#149325) ## Summary Reduce the scope of the OSQuery start task to only update the OSQuery integration package to 1.6.0. Originally, we had a check to also rollover indices to fix a bug on a potentially small subset of users who tried OSQuery for the first time in the first few days of release in 8.6.0. However, this check is potentially expensive and risky since it is in the start of Kibana/OSQuery plugin. Here, we reduce the risk by only upgrade the OSQuery integration package automatically which should fix the issue for most users. It is also less risky/expensive as searching over all existing data. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/utils/upgrade_integration.ts | 49 +------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/x-pack/plugins/osquery/server/utils/upgrade_integration.ts b/x-pack/plugins/osquery/server/utils/upgrade_integration.ts index 53e8a7101a0d6..bc68c47418896 100644 --- a/x-pack/plugins/osquery/server/utils/upgrade_integration.ts +++ b/x-pack/plugins/osquery/server/utils/upgrade_integration.ts @@ -9,13 +9,10 @@ import { satisfies } from 'semver'; import { installPackage } from '@kbn/fleet-plugin/server/services/epm/packages'; import { pkgToPkgKey } from '@kbn/fleet-plugin/server/services/epm/registry'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { asyncForEach } from '@kbn/std'; -import { orderBy } from 'lodash'; import type { Installation } from '@kbn/fleet-plugin/common'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import type { Logger } from '@kbn/logging'; -import { OSQUERY_INTEGRATION_NAME } from '../../common'; interface UpgradeIntegrationOptions { packageInfo?: Installation; @@ -31,12 +28,10 @@ export const upgradeIntegration = async ({ esClient, logger, }: UpgradeIntegrationOptions) => { - let updatedPackageResult; - if (packageInfo && satisfies(packageInfo?.version ?? '', '<1.6.0')) { try { logger.info('Updating osquery_manager integration'); - updatedPackageResult = await installPackage({ + await installPackage({ installSource: 'registry', savedObjectsClient: client, pkgkey: pkgToPkgKey({ @@ -53,46 +48,4 @@ export const upgradeIntegration = async ({ logger.error(e); } } - - // Check to see if the package has already been updated to at least 1.6.0 - if ( - satisfies(packageInfo?.version ?? '', '>=1.6.0') || - updatedPackageResult?.status === 'installed' - ) { - try { - // First get all datastreams matching the pattern. - const dataStreams = await esClient.indices.getDataStream({ - name: `logs-${OSQUERY_INTEGRATION_NAME}.result-*`, - }); - - // Then for each of those datastreams, we need to see if they need to rollover. - await asyncForEach(dataStreams.data_streams, async (dataStream) => { - const mapping = await esClient.indices.getMapping({ - index: dataStream.name, - }); - - const valuesToSort = Object.entries(mapping).map(([key, value]) => ({ - index: key, - mapping: value, - })); - - // Sort by index name to get the latest index for detecting if we need to rollover - const dataStreamMapping = orderBy(valuesToSort, ['index'], 'desc'); - - if ( - dataStreamMapping && - // @ts-expect-error 'properties' does not exist on type 'MappingMatchOnlyTextProperty' - dataStreamMapping[0]?.mapping?.mappings?.properties?.data_stream?.properties?.dataset - ?.value === 'generic' - ) { - logger.info('Rolling over index: ' + dataStream.name); - await esClient.indices.rollover({ - alias: dataStream.name, - }); - } - }); - } catch (e) { - logger.error(e); - } - } }; From 105a657faeda7408ee33a39998cc821641f46d8b Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 23 Jan 2023 16:28:11 +0100 Subject: [PATCH 08/10] [Synthetics] Test run details step results details (#149252) Fixes https://github.com/elastic/kibana/issues/145380 --- .../common/components/thershold_indicator.tsx | 86 +++++++ .../browser_steps_list.tsx | 59 ++++- .../monitor_test_result/result_details.tsx | 75 ++++++ .../monitor_summary/last_test_run.tsx | 1 + .../common/network_data/data_formatting.ts | 6 +- .../hooks/use_network_timings.ts | 59 ++--- .../hooks/use_network_timings_prev.ts | 233 ++++++++++++++++++ .../synthetics/hooks/use_redux_es_search.ts | 13 +- .../synthetics/state/elasticsearch/actions.ts | 22 +- .../synthetics/state/elasticsearch/api.ts | 6 +- .../synthetics/state/elasticsearch/effects.ts | 30 ++- 11 files changed, 526 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx new file mode 100644 index 0000000000000..e24b4cd52a2d9 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +export const getDeltaPercent = (current: number, previous: number) => { + if (previous === 0) { + return 0; + } + + return Number((((current - previous) / previous) * 100).toFixed(0)); +}; +export const ThresholdIndicator = ({ + current, + previous, + previousFormatted, + currentFormatted, +}: { + current: number; + previous: number; + previousFormatted: string; + currentFormatted: string; +}) => { + const delta = getDeltaPercent(current, previous); + + const getToolTipContent = () => { + return i18n.translate('xpack.synthetics.stepDetails.palette.tooltip', { + defaultMessage: 'Value is {deltaLabel} compared to previous steps in last 24 hours.', + values: { + deltaLabel: + Math.abs(delta) === 0 + ? i18n.translate('xpack.synthetics.stepDetails.palette.tooltip.noChange', { + defaultMessage: 'same', + }) + : delta > 0 + ? i18n.translate('xpack.synthetics.stepDetails.palette.increased', { + defaultMessage: '{delta}% higher', + values: { delta }, + }) + : i18n.translate('xpack.synthetics.stepDetails.palette.decreased', { + defaultMessage: '{delta}% lower', + values: { delta }, + }), + }, + }); + }; + + const getColor = () => { + if (Math.abs(delta) < 5) { + return 'default'; + } + return delta > 5 ? 'danger' : 'success'; + }; + + const hasDelta = Math.abs(delta) > 0; + + return ( + + + + {currentFormatted} + + + + + {hasDelta ? ( + 0 ? 'sortUp' : 'sortDown'} size="m" color={getColor()} /> + ) : ( + + )} + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx index 3ddf55d7300fc..ec9a1217f8ea4 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx @@ -6,16 +6,23 @@ */ import { i18n } from '@kbn/i18n'; -import React, { CSSProperties } from 'react'; -import { EuiBasicTable, EuiBasicTableColumn, EuiText, useEuiTheme } from '@elastic/eui'; +import React, { CSSProperties, ReactElement, useState } from 'react'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButtonIcon, + EuiText, + useEuiTheme, +} from '@elastic/eui'; import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types'; +import { ResultDetails } from './result_details'; import { JourneyStep } from '../../../../../../common/runtime_types'; import { JourneyStepScreenshotContainer } from '../screenshot/journey_step_screenshot_container'; import { ScreenshotImageSize, THUMBNAIL_SCREENSHOT_SIZE } from '../screenshot/screenshot_size'; import { StepDetailsLinkIcon } from '../links/step_details_link'; -import { StatusBadge, parseBadgeStatus, getTextColorForMonitorStatus } from './status_badge'; +import { parseBadgeStatus, getTextColorForMonitorStatus } from './status_badge'; import { StepDurationText } from './step_duration_text'; interface Props { @@ -25,6 +32,7 @@ interface Props { showStepNumber: boolean; screenshotImageSize?: ScreenshotImageSize; compressed?: boolean; + showExpand?: boolean; } export function isStepEnd(step: JourneyStep) { @@ -38,11 +46,41 @@ export const BrowserStepsList = ({ screenshotImageSize = THUMBNAIL_SCREENSHOT_SIZE, showStepNumber = false, compressed = true, + showExpand = true, }: Props) => { const { euiTheme } = useEuiTheme(); const stepEnds: JourneyStep[] = steps.filter(isStepEnd); + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); + + const toggleDetails = (item: JourneyStep) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item._id]) { + delete itemIdToExpandedRowMapValues[item._id]; + } else { + itemIdToExpandedRowMapValues[item._id] = <>; + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; const columns: Array> = [ + ...(showExpand + ? [ + { + align: 'left' as const, + width: '40px', + isExpander: true, + render: (item: JourneyStep) => ( + toggleDetails(item)} + aria-label={itemIdToExpandedRowMap[item._id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[item._id] ? 'arrowDown' : 'arrowRight'} + /> + ), + }, + ] + : []), ...(showStepNumber ? [ { @@ -100,7 +138,13 @@ export const BrowserStepsList = ({ { field: 'synthetics.step.status', name: RESULT_LABEL, - render: (pingStatus: string) => , + render: (pingStatus: string, item: JourneyStep) => ( + + ), }, { align: 'left', @@ -131,6 +175,12 @@ export const BrowserStepsList = ({ return ( <> ({ + style: { verticalAlign: 'initial' }, + })} + cellProps={() => ({ + style: { verticalAlign: 'initial' }, + })} compressed={compressed} loading={loading} columns={columns} @@ -148,6 +198,7 @@ export const BrowserStepsList = ({ }) } tableLayout={'auto'} + itemId="_id" /> ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx new file mode 100644 index 0000000000000..54f0fe8412551 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList, EuiLoadingContent, EuiSpacer } from '@elastic/eui'; +import { ThresholdIndicator } from '../components/thershold_indicator'; +import { useNetworkTimings } from '../../step_details_page/hooks/use_network_timings'; +import { useNetworkTimingsPrevious24Hours } from '../../step_details_page/hooks/use_network_timings_prev'; +import { formatMillisecond } from '../../step_details_page/common/network_data/data_formatting'; +import { JourneyStep } from '../../../../../../common/runtime_types'; +import { parseBadgeStatus, StatusBadge } from './status_badge'; + +export const ResultDetails = ({ + pingStatus, + isExpanded, + step, +}: { + pingStatus: string; + isExpanded: boolean; + step: JourneyStep; +}) => { + return ( +
+ + {isExpanded && ( + <> + + + + )} +
+ ); +}; +export const TimingDetails = ({ step }: { step: JourneyStep }) => { + const { timingsWithLabels } = useNetworkTimings( + step.monitor.check_group, + step.synthetics.step?.index + ); + + const { timingsWithLabels: prevTimingsWithLabels, loading } = useNetworkTimingsPrevious24Hours( + step.synthetics.step?.index + ); + + const items = timingsWithLabels?.map((item) => { + const prevValueItem = prevTimingsWithLabels?.find((prev) => prev.label === item.label); + const prevValue = prevValueItem?.value ?? 0; + return { + title: item.label, + description: loading ? ( + + ) : ( + + ), + }; + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx index b22039f65ef9a..9c4249517b64f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx @@ -127,6 +127,7 @@ export const LastTestRunComponent = ({ steps={stepsData?.steps ?? []} loading={stepsLoading} showStepNumber={true} + showExpand={false} /> ) : ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts index 10b70d0c86eea..fb12506065cde 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts @@ -491,9 +491,9 @@ export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PA export const formatTooltipHeading = (index: number, fullText: string): string => isNaN(index) ? fullText : `${index}. ${fullText}`; -export const formatMillisecond = (ms: number) => { +export const formatMillisecond = (ms: number, digits?: number) => { if (ms < 1000) { - return `${ms.toFixed(0)} ms`; + return `${ms.toFixed(digits ?? 0)} ms`; } - return `${(ms / 1000).toFixed(1)} s`; + return `${(ms / 1000).toFixed(digits ?? 1)} s`; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts index ed43a38113af9..36c71d1d71b80 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts @@ -7,7 +7,6 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { useEsSearch } from '@kbn/observability-plugin/public'; import { NETWORK_TIMINGS_FIELDS, SYNTHETICS_BLOCKED_TIMINGS, @@ -16,28 +15,33 @@ import { SYNTHETICS_RECEIVE_TIMINGS, SYNTHETICS_SEND_TIMINGS, SYNTHETICS_SSL_TIMINGS, - SYNTHETICS_STEP_DURATION, SYNTHETICS_TOTAL_TIMINGS, SYNTHETICS_WAIT_TIMINGS, } from '@kbn/observability-plugin/common'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; -export const useStepFilters = (prevCheckGroupId?: string) => { - const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); +export const useStepFilters = (checkGroupId: string, stepIndex: number) => { return [ { term: { - 'monitor.check_group': prevCheckGroupId ?? checkGroupId, + 'monitor.check_group': checkGroupId, }, }, { term: { - 'synthetics.step.index': Number(stepIndex), + 'synthetics.step.index': stepIndex, }, }, ]; }; -export const useNetworkTimings = () => { +export const useNetworkTimings = (checkGroupIdArg?: string, stepIndexArg?: number) => { + const params = useParams<{ checkGroupId: string; stepIndex: string; monitorId: string }>(); + + const checkGroupId = checkGroupIdArg ?? params.checkGroupId; + const stepIndex = stepIndexArg ?? Number(params.stepIndex); + const runTimeMappings = NETWORK_TIMINGS_FIELDS.reduce( (acc, field) => ({ ...acc, @@ -48,24 +52,17 @@ export const useNetworkTimings = () => { {} ); - const networkAggs = NETWORK_TIMINGS_FIELDS.reduce( - (acc, field) => ({ - ...acc, - [field]: { - sum: { - field, - }, - }, - }), - {} - ); - - const { data } = useEsSearch( + const { data } = useReduxEsSearch( { - index: 'synthetics-*', + index: SYNTHETICS_INDEX_PATTERN, body: { size: 0, - runtime_mappings: runTimeMappings, + runtime_mappings: { + ...runTimeMappings, + 'synthetics.payload.is_navigation_request': { + type: 'boolean', + }, + }, query: { bool: { filter: [ @@ -74,17 +71,16 @@ export const useNetworkTimings = () => { 'synthetics.type': 'journey/network_info', }, }, - ...useStepFilters(), + { + term: { + 'synthetics.payload.is_navigation_request': true, + }, + }, + ...useStepFilters(checkGroupId, stepIndex), ], }, }, aggs: { - ...networkAggs, - totalDuration: { - sum: { - field: SYNTHETICS_STEP_DURATION, - }, - }, dns: { sum: { field: SYNTHETICS_DNS_TIMINGS, @@ -128,8 +124,8 @@ export const useNetworkTimings = () => { }, }, }, - [], - { name: 'networkTimings' } + [checkGroupId, stepIndex], + { name: `stepNetworkTimingsMetrics/${checkGroupId}/${stepIndex}` } ); const aggs = data?.aggregations; @@ -176,7 +172,6 @@ export const useNetworkTimings = () => { label: SYNTHETICS_WAIT_TIMINGS_LABEL, }, ].sort((a, b) => b.value - a.value), - totalDuration: aggs?.totalDuration.value ?? 0, }; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts new file mode 100644 index 0000000000000..e214d62429d4f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { + NETWORK_TIMINGS_FIELDS, + SYNTHETICS_BLOCKED_TIMINGS, + SYNTHETICS_CONNECT_TIMINGS, + SYNTHETICS_DNS_TIMINGS, + SYNTHETICS_RECEIVE_TIMINGS, + SYNTHETICS_SEND_TIMINGS, + SYNTHETICS_SSL_TIMINGS, + SYNTHETICS_TOTAL_TIMINGS, + SYNTHETICS_WAIT_TIMINGS, +} from '@kbn/observability-plugin/common'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; + +export const useStepFilters = (checkGroupId: string, stepIndex: number) => { + return [ + { + term: { + 'monitor.check_group': checkGroupId, + }, + }, + { + term: { + 'synthetics.step.index': stepIndex, + }, + }, + ]; +}; + +export const useNetworkTimingsPrevious24Hours = (stepIndexArg?: number) => { + const params = useParams<{ checkGroupId: string; stepIndex: string; monitorId: string }>(); + + const configId = params.monitorId; + const checkGroupId = params.checkGroupId; + const stepIndex = stepIndexArg ?? Number(params.stepIndex); + + const runTimeMappings = NETWORK_TIMINGS_FIELDS.reduce( + (acc, field) => ({ + ...acc, + [field]: { + type: 'double', + }, + }), + {} + ); + + const { data, loading } = useReduxEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 0, + runtime_mappings: { + ...runTimeMappings, + 'synthetics.payload.is_navigation_request': { + type: 'boolean', + }, + }, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: 'now', + gte: 'now-24h/h', + }, + }, + }, + { + term: { + 'synthetics.payload.is_navigation_request': true, + }, + }, + { + term: { + 'synthetics.type': 'journey/network_info', + }, + }, + { + term: { + 'synthetics.step.index': stepIndex, + }, + }, + { + term: { + config_id: configId, + }, + }, + ], + must_not: [ + { + term: { + 'monitor.check_group': checkGroupId, + }, + }, + ], + }, + }, + aggs: { + dns: { + percentiles: { + field: SYNTHETICS_DNS_TIMINGS, + percents: [50], + }, + }, + ssl: { + percentiles: { + field: SYNTHETICS_SSL_TIMINGS, + percents: [50], + }, + }, + blocked: { + percentiles: { + field: SYNTHETICS_BLOCKED_TIMINGS, + percents: [50], + }, + }, + connect: { + percentiles: { + field: SYNTHETICS_CONNECT_TIMINGS, + percents: [50], + }, + }, + receive: { + percentiles: { + field: SYNTHETICS_RECEIVE_TIMINGS, + percents: [50], + }, + }, + send: { + percentiles: { + field: SYNTHETICS_SEND_TIMINGS, + percents: [50], + }, + }, + wait: { + percentiles: { + field: SYNTHETICS_WAIT_TIMINGS, + percents: [50], + }, + }, + total: { + percentiles: { + field: SYNTHETICS_TOTAL_TIMINGS, + percents: [50], + }, + }, + }, + }, + }, + [configId, stepIndex, checkGroupId], + { name: `stepNetworkPreviousTimings/${configId}/${stepIndex}` } + ); + + const aggs = data?.aggregations; + + const timings = { + dns: aggs?.dns.values['50.0'] ?? 0, + connect: aggs?.connect.values['50.0'] ?? 0, + receive: aggs?.receive.values['50.0'] ?? 0, + send: aggs?.send.values['50.0'] ?? 0, + wait: aggs?.wait.values['50.0'] ?? 0, + blocked: aggs?.blocked.values['50.0'] ?? 0, + ssl: aggs?.ssl.values['50.0'] ?? 0, + }; + + return { + loading, + timings, + timingsWithLabels: [ + { + value: timings.dns, + label: SYNTHETICS_DNS_TIMINGS_LABEL, + }, + { + value: timings.ssl, + label: SYNTHETICS_SSL_TIMINGS_LABEL, + }, + { + value: timings.blocked, + label: SYNTHETICS_BLOCKED_TIMINGS_LABEL, + }, + { + value: timings.connect, + label: SYNTHETICS_CONNECT_TIMINGS_LABEL, + }, + { + value: timings.receive, + label: SYNTHETICS_RECEIVE_TIMINGS_LABEL, + }, + { + value: timings.send, + label: SYNTHETICS_SEND_TIMINGS_LABEL, + }, + { + value: timings.wait, + label: SYNTHETICS_WAIT_TIMINGS_LABEL, + }, + ].sort((a, b) => b.value - a.value), + }; +}; + +const SYNTHETICS_CONNECT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.connect.label', { + defaultMessage: 'Connect', +}); +const SYNTHETICS_DNS_TIMINGS_LABEL = i18n.translate('xpack.synthetics.dns', { + defaultMessage: 'DNS', +}); +const SYNTHETICS_WAIT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.wait', { + defaultMessage: 'Wait', +}); + +const SYNTHETICS_SSL_TIMINGS_LABEL = i18n.translate('xpack.synthetics.ssl', { + defaultMessage: 'SSL', +}); +const SYNTHETICS_BLOCKED_TIMINGS_LABEL = i18n.translate('xpack.synthetics.blocked', { + defaultMessage: 'Blocked', +}); +const SYNTHETICS_SEND_TIMINGS_LABEL = i18n.translate('xpack.synthetics.send', { + defaultMessage: 'Send', +}); +const SYNTHETICS_RECEIVE_TIMINGS_LABEL = i18n.translate('xpack.synthetics.receive', { + defaultMessage: 'Receive', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts index 72715c596e82a..5afb38431878d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts @@ -8,11 +8,11 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ESSearchResponse } from '@kbn/es-types'; import { IInspectorInfo } from '@kbn/data-plugin/common'; -import { useInspectorContext } from '@kbn/observability-plugin/public'; import { useDispatch, useSelector } from 'react-redux'; import { useEffect, useMemo } from 'react'; import { executeEsQueryAction, + selectEsQueryError, selectEsQueryLoading, selectEsQueryResult, } from '../state/elasticsearch'; @@ -26,27 +26,26 @@ export const useReduxEsSearch = < options: { inspector?: IInspectorInfo; name: string } ) => { const { name } = options ?? {}; - - const { addInspectorRequest } = useInspectorContext(); - const dispatch = useDispatch(); const loadings = useSelector(selectEsQueryLoading); const results = useSelector(selectEsQueryResult); + const errors = useSelector(selectEsQueryError); useEffect(() => { if (params.index) { - dispatch(executeEsQueryAction.get({ params, name, addInspectorRequest })); + dispatch(executeEsQueryAction.get({ params, name })); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [addInspectorRequest, dispatch, name, JSON.stringify(params)]); + }, [dispatch, name, JSON.stringify(params)]); return useMemo(() => { return { data: results[name] as ESSearchResponse, loading: loadings[name], + error: errors[name], }; - }, [loadings, name, results]); + }, [errors, loadings, name, results]); }; export function createEsParams(params: T): T { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts index fbf505ecc950d..93c035bdc8d99 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts @@ -7,14 +7,18 @@ import * as esTypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ESSearchResponse } from '@kbn/es-types'; -import { FetcherResult } from '@kbn/observability-plugin/public/hooks/use_fetcher'; import { createAsyncAction } from '../utils/actions'; -export const executeEsQueryAction = createAsyncAction< - { - params: esTypes.SearchRequest; - name: string; - addInspectorRequest: (result: FetcherResult) => void; - }, - { name: string; result: ESSearchResponse } ->('executeEsQueryAction'); +export interface EsActionPayload { + params: esTypes.SearchRequest; + name: string; +} + +export interface EsActionResponse { + name: string; + result: ESSearchResponse; +} + +export const executeEsQueryAction = createAsyncAction( + 'executeEsQueryAction' +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts index 6f233b89423b2..0df5b7d30cd3e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts @@ -14,20 +14,20 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ESSearchResponse } from '@kbn/es-types'; import { FETCH_STATUS } from '@kbn/observability-plugin/public'; import { getInspectResponse } from '@kbn/observability-plugin/common'; -import type { FetcherResult } from '@kbn/observability-plugin/public/hooks/use_fetcher'; import { kibanaService } from '../../../../utils/kibana_service'; +import { apiService } from '../../../../utils/api_service'; export const executeEsQueryAPI = async ({ params, name, - addInspectorRequest, }: { params: estypes.SearchRequest; name: string; - addInspectorRequest: (result: FetcherResult) => void; }) => { const data = kibanaService.startPlugins.data; + const addInspectorRequest = apiService.addInspectorRequest; + const response = new Promise((resolve, reject) => { const startTime = Date.now(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.ts index d92285e537dc6..868d5a3dd5d6b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.ts @@ -5,15 +5,33 @@ * 2.0. */ -import { takeLeading } from 'redux-saga/effects'; +import { call, put, takeEvery } from 'redux-saga/effects'; -import { fetchEffectFactory } from '../utils/fetch_effect'; -import { executeEsQueryAction } from './actions'; +import { Action } from 'redux-actions'; +import { serializeHttpFetchError } from '../utils/http_error'; +import { EsActionPayload, EsActionResponse, executeEsQueryAction } from './actions'; import { executeEsQueryAPI } from './api'; export function* executeEsQueryEffect() { - yield takeLeading( - executeEsQueryAction.get, - fetchEffectFactory(executeEsQueryAPI, executeEsQueryAction.success, executeEsQueryAction.fail) + const inProgressRequests = new Set(); + + yield takeEvery( + String(executeEsQueryAction.get), + function* (action: Action): Generator { + try { + if (!inProgressRequests.has(action.payload.name)) { + inProgressRequests.add(action.payload.name); + + const response = (yield call(executeEsQueryAPI, action.payload)) as EsActionResponse; + + inProgressRequests.delete(action.payload.name); + + yield put(executeEsQueryAction.success(response)); + } + } catch (e) { + inProgressRequests.delete(action.payload.name); + yield put(executeEsQueryAction.fail(serializeHttpFetchError(e, action.payload))); + } + } ); } From 050cf79cb0fd0e21251daf6f56203e158f518cfa Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 23 Jan 2023 09:06:52 -0700 Subject: [PATCH 09/10] [Maps] fix Do not allow opening multiple tooltips for single feature (#149254) Fixes https://github.com/elastic/kibana/issues/148819 When [multiple tooltips](https://github.com/elastic/kibana/pull/57226) first merged in 7.7.0, TooltipFeature only had `id` and `layerId` property so the original equality check `_.isEqual(features, tooltipState.features)` prevented tooltips from opening that shows identical features. Later releases added new properties to TooltipFeature that broke this deep equals check. This PR resolves the issue by mapping features to just `{id, layerId}` before equality check to ensure those are the only properties that determine equality. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/actions/tooltip_actions.test.ts | 137 ++++++++++++++++++ .../maps/public/actions/tooltip_actions.ts | 9 +- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/maps/public/actions/tooltip_actions.test.ts diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.test.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.test.ts new file mode 100644 index 0000000000000..953f2bca0f56e --- /dev/null +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { TooltipState } from '../../common/descriptor_types'; +import { openOnClickTooltip } from './tooltip_actions'; +import { MapStoreState } from '../reducers/store'; + +describe('openOnClickTooltip', () => { + const newTooltip = { + features: [ + { + id: 'feature1', + layerId: 'layer1', + }, + ], + id: 'tooltip1', + isLocked: true, + location: [0, 0], + } as unknown as TooltipState; + + test('should add tooltip to open tooltips', () => { + const openTooltip = { + features: [ + { + id: 'feature2', + layerId: 'layer1', + }, + ], + id: 'tooltip2', + isLocked: true, + location: [1, 1], + } as unknown as TooltipState; + const action = openOnClickTooltip(newTooltip); + const dispatchMock = jest.fn(); + action(dispatchMock, () => { + return { + map: { + openTooltips: [openTooltip], + }, + } as unknown as MapStoreState; + }); + expect(dispatchMock.mock.calls[0][0]).toEqual({ + openTooltips: [openTooltip, newTooltip], + type: 'SET_OPEN_TOOLTIPS', + }); + }); + + test('should remove existing mouseover tooltips when adding locked tooltips', () => { + const action = openOnClickTooltip(newTooltip); + const dispatchMock = jest.fn(); + action(dispatchMock, () => { + return { + map: { + openTooltips: [ + { + features: [ + { + id: 'feature2', + layerId: 'layer1', + }, + ], + id: 'tooltip2', + isLocked: false, // mouseover tooltip + location: [1, 1], + }, + ], + }, + } as unknown as MapStoreState; + }); + expect(dispatchMock.mock.calls[0][0]).toEqual({ + openTooltips: [newTooltip], + type: 'SET_OPEN_TOOLTIPS', + }); + }); + + test('should remove existing tooltip when adding new tooltip at same location', () => { + const action = openOnClickTooltip(newTooltip); + const dispatchMock = jest.fn(); + action(dispatchMock, () => { + return { + map: { + openTooltips: [ + { + features: [ + { + id: 'feature2', + layerId: 'layer1', + }, + ], + id: 'tooltip2', + isLocked: true, + location: [0, 0], // same location as newTooltip + }, + ], + }, + } as unknown as MapStoreState; + }); + expect(dispatchMock.mock.calls[0][0]).toEqual({ + openTooltips: [newTooltip], + type: 'SET_OPEN_TOOLTIPS', + }); + }); + + test('should remove existing tooltip when adding new tooltip with same features', () => { + const action = openOnClickTooltip(newTooltip); + const dispatchMock = jest.fn(); + action(dispatchMock, () => { + return { + map: { + openTooltips: [ + { + features: [ + { + id: 'feature1', + layerId: 'layer1', + // ensure new props do not break equality check + newProp: 'someValue', + }, + ], + id: 'tooltip2', + isLocked: true, + location: [1, 1], + }, + ], + }, + } as unknown as MapStoreState; + }); + expect(dispatchMock.mock.calls[0][0]).toEqual({ + openTooltips: [newTooltip], + type: 'SET_OPEN_TOOLTIPS', + }); + }); +}); diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index 22deb1fd1e930..780012a9fd6ca 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -29,7 +29,14 @@ export function openOnClickTooltip(tooltipState: TooltipState) { return ( isLocked && !_.isEqual(location, tooltipState.location) && - !_.isEqual(features, tooltipState.features) + !_.isEqual( + features.map(({ id, layerId }) => { + return { id, layerId }; + }), + tooltipState.features.map(({ id, layerId }) => { + return { id, layerId }; + }) + ) ); }); From 164f629fd61c02cadfde9afa78e340ca8a8c38aa Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Mon, 23 Jan 2023 11:09:17 -0500 Subject: [PATCH 10/10] [Response Ops][Alerting] Optimize alerting task runner for persistent (non-lifecycle rule types) (#149043) Resolves https://github.com/elastic/kibana/issues/148573 ## Summary To help prepare for the framework to handle persistent (non-lifecycle rule types) that do not need the auto-recovery functionality performed by the framework, we added a flag for the rule type so they can opt in or out ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### To verify - Create a rule and verify that it is working as expected. - Set `autoRecoverAlerts: true` for a rule type and create a rule using that rule type. Verify that the rule does not recover. --- .../server/alert/create_alert_factory.test.ts | 41 ++++++ .../server/alert/create_alert_factory.ts | 9 ++ .../legacy_alerts_client.test.ts | 2 + .../alerts_client/legacy_alerts_client.ts | 5 + .../server/lib/process_alerts.test.ts | 55 +++++++ .../alerting/server/lib/process_alerts.ts | 13 +- x-pack/plugins/alerting/server/plugin.ts | 1 + .../alerting/server/task_runner/fixtures.ts | 1 + .../server/task_runner/task_runner.test.ts | 74 ++++++++++ .../server/task_runner/task_runner.ts | 13 +- x-pack/plugins/alerting/server/types.ts | 5 + .../plugins/alerts/server/alert_types.ts | 75 ++++++++++ .../builtin_alert_types/auto_recover/index.ts | 15 ++ .../builtin_alert_types/auto_recover/rule.ts | 136 ++++++++++++++++++ .../alerting/builtin_alert_types/index.ts | 1 + 15 files changed, 442 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts index 663a0b7401b2e..c532d1a6640f2 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts @@ -30,6 +30,7 @@ describe('createAlertFactory()', () => { alerts: {}, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` @@ -55,6 +56,7 @@ describe('createAlertFactory()', () => { }, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` @@ -79,6 +81,7 @@ describe('createAlertFactory()', () => { alerts, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); alertFactory.create('1'); expect(alerts).toMatchInlineSnapshot(` @@ -98,6 +101,7 @@ describe('createAlertFactory()', () => { alerts: {}, logger, maxAlerts: 3, + autoRecoverAlerts: true, }); expect(alertFactory.hasReachedAlertLimit()).toBe(false); @@ -117,6 +121,7 @@ describe('createAlertFactory()', () => { alerts: {}, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toEqual({ @@ -158,6 +163,7 @@ describe('createAlertFactory()', () => { logger, canSetRecoveryContext: true, maxAlerts: 1000, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toEqual({ @@ -184,6 +190,7 @@ describe('createAlertFactory()', () => { logger, maxAlerts: 1000, canSetRecoveryContext: true, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toEqual({ @@ -209,6 +216,7 @@ describe('createAlertFactory()', () => { logger, maxAlerts: 1000, canSetRecoveryContext: true, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toEqual({ @@ -233,6 +241,7 @@ describe('createAlertFactory()', () => { logger, maxAlerts: 1000, canSetRecoveryContext: false, + autoRecoverAlerts: true, }); const result = alertFactory.create('1'); expect(result).toEqual({ @@ -259,6 +268,7 @@ describe('createAlertFactory()', () => { alerts: {}, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); const limit = alertFactory.alertLimit.getValue(); @@ -276,6 +286,7 @@ describe('createAlertFactory()', () => { alerts: {}, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); const limit = alertFactory.alertLimit.getValue(); @@ -290,6 +301,7 @@ describe('createAlertFactory()', () => { alerts: {}, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); const limit = alertFactory.alertLimit.getValue(); @@ -298,6 +310,34 @@ describe('createAlertFactory()', () => { alertFactory.alertLimit.setLimitReached(false); alertFactory.alertLimit.checkLimitUsage(); }); + + test('returns empty array if recovered alerts exist but autoRecoverAlerts is false', () => { + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + maxAlerts: 1000, + canSetRecoveryContext: true, + autoRecoverAlerts: false, + }); + const result = alertFactory.create('1'); + expect(result).toEqual({ + meta: { + flappingHistory: [], + }, + state: {}, + context: {}, + scheduledExecutionOptions: undefined, + id: '1', + }); + + const { getRecoveredAlerts: getRecoveredAlertsFn } = alertFactory.done(); + const recoveredAlerts = getRecoveredAlertsFn!(); + expect(Array.isArray(recoveredAlerts)).toBe(true); + expect(recoveredAlerts.length).toEqual(0); + expect(logger.debug).toHaveBeenCalledWith( + `Set autoRecoverAlerts to true on rule type to get access to recovered alerts.` + ); + }); }); describe('getPublicAlertFactory', () => { @@ -306,6 +346,7 @@ describe('getPublicAlertFactory', () => { alerts: {}, logger, maxAlerts: 1000, + autoRecoverAlerts: true, }); expect(alertFactory.create).toBeDefined(); diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts index ff8aacd52f5fe..99f4c3f2b5da6 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -52,6 +52,7 @@ export interface CreateAlertFactoryOpts< alerts: Record>; logger: Logger; maxAlerts: number; + autoRecoverAlerts: boolean; canSetRecoveryContext?: boolean; } @@ -63,6 +64,7 @@ export function createAlertFactory< alerts, logger, maxAlerts, + autoRecoverAlerts, canSetRecoveryContext = false, }: CreateAlertFactoryOpts): AlertFactory { // Keep track of which alerts we started with so we can determine which have recovered @@ -128,6 +130,12 @@ export function createAlertFactory< ); return []; } + if (!autoRecoverAlerts) { + logger.debug( + `Set autoRecoverAlerts to true on rule type to get access to recovered alerts.` + ); + return []; + } const { currentRecoveredAlerts } = processAlerts< State, @@ -140,6 +148,7 @@ export function createAlertFactory< previouslyRecoveredAlerts: {}, hasReachedAlertLimit, alertLimit: maxAlerts, + autoRecoverAlerts, // setFlapping is false, as we only want to use this function to get the recovered alerts setFlapping: false, }); diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts index 25b723e24234d..e7e62ec848ea3 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts @@ -124,6 +124,7 @@ describe('Legacy Alerts Client', () => { logger, maxAlerts: 1000, canSetRecoveryContext: false, + autoRecoverAlerts: true, }); }); @@ -227,6 +228,7 @@ describe('Legacy Alerts Client', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 1000, + autoRecoverAlerts: true, setFlapping: true, }); diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts index 9a8cd78f7fd8f..c4fa1b8565ca3 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts @@ -95,6 +95,7 @@ export class LegacyAlertsClient< alerts: this.alerts, logger: this.options.logger, maxAlerts: this.options.maxAlerts, + autoRecoverAlerts: this.options.ruleType.autoRecoverAlerts ?? true, canSetRecoveryContext: this.options.ruleType.doesSetRecoveryContext ?? false, }); } @@ -121,6 +122,10 @@ export class LegacyAlertsClient< previouslyRecoveredAlerts: this.recoveredAlertsFromPreviousExecution, hasReachedAlertLimit: this.alertFactory!.hasReachedAlertLimit(), alertLimit: this.options.maxAlerts, + autoRecoverAlerts: + this.options.ruleType.autoRecoverAlerts !== undefined + ? this.options.ruleType.autoRecoverAlerts + : true, setFlapping: true, }); diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts index 3b09b072476e6..9dae6c4c033a8 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts @@ -55,6 +55,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -92,6 +93,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -137,6 +139,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -174,6 +177,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -221,6 +225,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -278,6 +283,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -338,6 +344,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -380,6 +387,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -407,6 +415,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -436,6 +445,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -474,6 +484,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -512,11 +523,42 @@ describe('processAlerts', () => { previouslyRecoveredAlerts, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); expect(recoveredAlerts).toEqual(updatedAlerts); }); + + test('should skip recovery calculations if autoRecoverAlerts = false', () => { + const activeAlert = new Alert('1'); + const recoveredAlert1 = new Alert('2'); + const recoveredAlert2 = new Alert('3'); + + const existingAlerts = { + '1': activeAlert, + '2': recoveredAlert1, + '3': recoveredAlert2, + }; + existingAlerts['2'].replaceState({ start: '1969-12-30T00:00:00.000Z', duration: 33000 }); + existingAlerts['3'].replaceState({ start: '1969-12-31T07:34:00.000Z', duration: 23532 }); + + const updatedAlerts = cloneDeep(existingAlerts); + + updatedAlerts['1'].scheduleActions('default' as never, { foo: '1' }); + + const { recoveredAlerts } = processAlerts({ + alerts: updatedAlerts, + existingAlerts, + previouslyRecoveredAlerts: {}, + hasReachedAlertLimit: false, + alertLimit: 10, + autoRecoverAlerts: false, + setFlapping: false, + }); + + expect(recoveredAlerts).toEqual({}); + }); }); describe('when hasReachedAlertLimit is true', () => { @@ -557,6 +599,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 7, + autoRecoverAlerts: true, setFlapping: false, }); @@ -592,6 +635,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 7, + autoRecoverAlerts: true, setFlapping: false, }); @@ -651,6 +695,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: MAX_ALERTS, + autoRecoverAlerts: true, setFlapping: false, }); @@ -684,6 +729,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -734,6 +780,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -770,6 +817,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: { '1': recoveredAlert }, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -825,6 +873,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -858,6 +907,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: alerts, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -899,6 +949,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts, hasReachedAlertLimit: false, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); @@ -965,6 +1016,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -1001,6 +1053,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -1062,6 +1115,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: { '1': activeAlert1 }, hasReachedAlertLimit: true, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: true, }); @@ -1138,6 +1192,7 @@ describe('processAlerts', () => { previouslyRecoveredAlerts: {}, hasReachedAlertLimit: true, alertLimit: 10, + autoRecoverAlerts: true, setFlapping: false, }); diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.ts b/x-pack/plugins/alerting/server/lib/process_alerts.ts index 40c86dc461ab0..6ce363742a3d8 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.ts @@ -20,6 +20,7 @@ interface ProcessAlertsOpts< previouslyRecoveredAlerts: Record>; hasReachedAlertLimit: boolean; alertLimit: number; + autoRecoverAlerts: boolean; // flag used to determine whether or not we want to push the flapping state on to the flappingHistory array setFlapping: boolean; } @@ -47,6 +48,7 @@ export function processAlerts< previouslyRecoveredAlerts, hasReachedAlertLimit, alertLimit, + autoRecoverAlerts, setFlapping, }: ProcessAlertsOpts): ProcessAlertsResult< State, @@ -62,7 +64,13 @@ export function processAlerts< alertLimit, setFlapping ) - : processAlertsHelper(alerts, existingAlerts, previouslyRecoveredAlerts, setFlapping); + : processAlertsHelper( + alerts, + existingAlerts, + previouslyRecoveredAlerts, + autoRecoverAlerts, + setFlapping + ); } function processAlertsHelper< @@ -74,6 +82,7 @@ function processAlertsHelper< alerts: Record>, existingAlerts: Record>, previouslyRecoveredAlerts: Record>, + autoRecoverAlerts: boolean, setFlapping: boolean ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); @@ -123,7 +132,7 @@ function processAlertsHelper< updateAlertFlappingHistory(activeAlerts[id], false); } } - } else if (existingAlertIds.has(id)) { + } else if (existingAlertIds.has(id) && autoRecoverAlerts) { recoveredAlerts[id] = alerts[id]; currentRecoveredAlerts[id] = alerts[id]; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 86d8a7411c16d..b3b94584025c3 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -349,6 +349,7 @@ export class AlertingPlugin { ruleType.cancelAlertsOnRuleTimeout = ruleType.cancelAlertsOnRuleTimeout ?? this.config.cancelAlertsOnRuleTimeout; ruleType.doesSetRecoveryContext = ruleType.doesSetRecoveryContext ?? false; + ruleType.autoRecoverAlerts = ruleType.autoRecoverAlerts ?? true; ruleTypeRegistry.register(ruleType); }, getSecurityHealth: async () => { diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index f841e2e1ff04f..a92b4f6850e4a 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -144,6 +144,7 @@ export const ruleType: jest.Mocked = { producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', + autoRecoverAlerts: true, }; export const mockRunNowResponse = { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 629b9ebea1eff..c0dcd9c74135b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -2876,6 +2876,80 @@ describe('Task Runner', () => { expect(inMemoryMetrics.increment.mock.calls[5][0]).toBe(IN_MEMORY_METRICS.RULE_TIMEOUTS); }); + test('does not persist alertInstances or recoveredAlertInstances passed in from state if autoRecoverAlerts is false', async () => { + ruleType.autoRecoverAlerts = false; + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: RuleExecutorOptions< + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertFactory.create('1').scheduleActions('default'); + return { state: {} }; + } + ); + const date = new Date().toISOString(); + const taskRunner = new TaskRunner( + ruleType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { + bar: false, + start: DATE_1969, + duration: '80000000000', + }, + }, + '2': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: '70000000000', + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams, + inMemoryMetrics + ); + expect(AlertingEventLogger).toHaveBeenCalled(); + + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); + const runnerResult = await taskRunner.run(); + expect(runnerResult.state.alertInstances).toEqual({}); + expect(runnerResult.state.alertRecoveredInstances).toEqual({}); + + testAlertingEventLogCalls({ + activeAlerts: 1, + recoveredAlerts: 0, + triggeredActions: 0, + generatedActions: 1, + status: 'ok', + logAlert: 1, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( + 1, + generateAlertOpts({ + action: EVENT_LOG_ACTIONS.activeInstance, + group: 'default', + state: { bar: false, start: DATE_1969, duration: MOCK_DURATION }, + }) + ); + + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + }); + function testAlertingEventLogCalls({ ruleContext = alertingEventLoggerInitializer, activeAlerts = 0, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 347c330361092..ae9b8fab80426 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -44,6 +44,7 @@ import { RuleTypeParams, RuleTypeState, parseDuration, + RawAlertInstance, } from '../../common'; import { NormalizedRuleType, UntypedNormalizedRuleType } from '../rule_type_registry'; import { getEsErrorMessage } from '../lib/errors'; @@ -455,8 +456,16 @@ export class TaskRunner< } }); - const { alertsToReturn, recoveredAlertsToReturn } = - this.legacyAlertsClient.getAlertsToSerialize(); + let alertsToReturn: Record = {}; + let recoveredAlertsToReturn: Record = {}; + // Only serialize alerts into task state if we're auto-recovering, otherwise + // we don't need to keep this information around. + if (this.ruleType.autoRecoverAlerts) { + const { alertsToReturn: alerts, recoveredAlertsToReturn: recovered } = + this.legacyAlertsClient.getAlertsToSerialize(); + alertsToReturn = alerts; + recoveredAlertsToReturn = recovered; + } return { metrics: ruleRunMetricsStore.getMetrics(), diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index f2a368c062d05..f4b7cc57b27e4 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -204,6 +204,11 @@ export interface RuleType< cancelAlertsOnRuleTimeout?: boolean; doesSetRecoveryContext?: boolean; getSummarizedAlerts?: GetSummarizedAlertsFn; + /** + * Determines whether framework should + * automatically make recovery determination. Defaults to true. + */ + autoRecoverAlerts?: boolean; } export type UntypedRuleType = RuleType< RuleTypeParams, diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts index 9360441c3d5ac..0c8390ca938dc 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts @@ -553,6 +553,80 @@ function getPatternSuccessOrFailureAlertType() { return result; } +function getPatternFiringAutoRecoverFalseAlertType() { + const paramsSchema = schema.object({ + pattern: schema.recordOf( + schema.string(), + schema.arrayOf(schema.oneOf([schema.boolean(), schema.string()])) + ), + reference: schema.maybe(schema.string()), + }); + type ParamsType = TypeOf; + interface State extends RuleTypeState { + patternIndex?: number; + } + const result: RuleType = { + id: 'test.patternFiringAutoRecoverFalse', + name: 'Test: Firing on a Pattern with Auto Recover: false', + actionGroups: [{ id: 'default', name: 'Default' }], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + autoRecoverAlerts: false, + async executor(alertExecutorOptions) { + const { services, state, params } = alertExecutorOptions; + const pattern = params.pattern; + if (typeof pattern !== 'object') throw new Error('pattern is not an object'); + let maxPatternLength = 0; + for (const [instanceId, instancePattern] of Object.entries(pattern)) { + if (!Array.isArray(instancePattern)) { + throw new Error(`pattern for instance ${instanceId} is not an array`); + } + maxPatternLength = Math.max(maxPatternLength, instancePattern.length); + } + + if (params.reference) { + await services.scopedClusterClient.asCurrentUser.index({ + index: ES_TEST_INDEX_NAME, + refresh: 'wait_for', + body: { + reference: params.reference, + source: 'alert:test.patternFiringAutoRecoverFalse', + ...alertExecutorOptions, + }, + }); + } + + // get the pattern index, return if past it + const patternIndex = state.patternIndex ?? 0; + if (patternIndex >= maxPatternLength) { + return { state: { patternIndex } }; + } + + // fire if pattern says to + for (const [instanceId, instancePattern] of Object.entries(pattern)) { + const scheduleByPattern = instancePattern[patternIndex]; + if (scheduleByPattern === true) { + services.alertFactory.create(instanceId).scheduleActions('default', { + ...EscapableStrings, + deep: DeepContextVariables, + }); + } else if (typeof scheduleByPattern === 'string') { + services.alertFactory.create(instanceId).scheduleActions('default', scheduleByPattern); + } + } + + return { + state: { + patternIndex: patternIndex + 1, + }, + }; + }, + }; + return result; +} + function getLongRunningPatternRuleType(cancelAlertsOnRuleTimeout: boolean = true) { let globalPatternIndex = 0; const paramsSchema = schema.object({ @@ -925,4 +999,5 @@ export function defineAlertTypes( alerting.registerType(getPatternSuccessOrFailureAlertType()); alerting.registerType(getExceedsAlertLimitRuleType()); alerting.registerType(getAlwaysFiringAlertAsDataRuleType(logger, { ruleRegistry })); + alerting.registerType(getPatternFiringAutoRecoverFalseAlertType()); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts new file mode 100644 index 0000000000000..619f6e9b4c93e --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('auto_recover_rule', () => { + loadTestFile(require.resolve('./rule')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts new file mode 100644 index 0000000000000..d46c2d2c60958 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { RecoveredActionGroup } from '@kbn/alerting-plugin/common'; + +import { Spaces } from '../../../../scenarios'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, TaskManagerUtils } from '../../../../../common/lib'; +import { createEsDocuments } from '../lib/create_test_data'; + +const RULE_INTERVAL_SECONDS = 6; +const RULE_INTERVALS_TO_WRITE = 5; +const RULE_INTERVAL_MILLIS = RULE_INTERVAL_SECONDS * 1000; +const ES_GROUPS_TO_WRITE = 3; + +// eslint-disable-next-line import/no-default-export +export default function ruleTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + const es = getService('es'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + const taskManagerUtils = new TaskManagerUtils(es, retry); + + describe('rule that sets autoRecoverAlerts to false', () => { + let endDate: string; + const objectRemover = new ObjectRemover(supertest); + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + // write documents in the future, figure out the end date + const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS; + endDate = new Date(endDateMillis).toISOString(); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + }); + + it('runs successfully and does not auto recover', async () => { + const testStart = new Date(); + await createEsDocuments( + es, + esTestIndexTool, + endDate, + RULE_INTERVALS_TO_WRITE, + RULE_INTERVAL_MILLIS, + ES_GROUPS_TO_WRITE + ); + + await createRule({ + name: 'test rule', + pattern: [true, false, true], + }); + + await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + + const actionTestRecord = await esTestIndexTool.waitForDocs('action:test.index-record', '', 2); + + await esTestIndexTool.search('alert:test.patternFiringAutoRecoverFalse', ''); + + expect(actionTestRecord[0]._source.params.message).to.eql('Active message'); + expect(actionTestRecord[1]._source.params.message).to.eql('Active message'); + }); + + interface CreateRuleParams { + name: string; + pattern: boolean[]; + } + + async function createRule(params: CreateRuleParams) { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const { status, body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + name: params.name, + consumer: 'alerts', + enabled: true, + rule_type_id: 'test.patternFiringAutoRecoverFalse', + schedule: { interval: '1s' }, + actions: [ + { + group: 'default', + id: createdAction.id, + params: { + index: ES_TEST_INDEX_NAME, + reference: '', + message: 'Active message', + }, + }, + { + group: RecoveredActionGroup.id, + id: createdAction.id, + params: { + index: ES_TEST_INDEX_NAME, + reference: '', + message: 'Recovered message', + }, + }, + ], + notify_when: 'onThrottleInterval', + params: { + pattern: { + instance: params.pattern, + }, + }, + }); + + expect(status).to.be(200); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts index 51a3cfd04be83..83c77b504833e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts @@ -15,5 +15,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./long_running')); loadTestFile(require.resolve('./cancellable')); loadTestFile(require.resolve('./circuit_breaker')); + loadTestFile(require.resolve('./auto_recover')); }); }