From f378b87cad0d308a29af0659e963adf651aedfbe Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 25 Oct 2023 14:43:20 +0200 Subject: [PATCH 01/24] [Serverless] Improve fleet and integrations serverless breadcrumbs (#169772) ## Summary This fixes deeper context breadcrumbs in serverless navigation for fleet and integration apps. This builds on top of https://github.com/elastic/kibana/pull/169513 where we added merging of navigational project breadcrumbs with deeper context breadcrumbs set by `chrome.setBreadcrumbs`. The merging is based on `deepLinkId`, so we're adding it to base breadcrumbs. The `deepLinkId` is type checked. Example Before/After: Before: ![Screenshot 2023-10-25 at 12 05 33](https://github.com/elastic/kibana/assets/7784120/4a6a0bab-1cef-4b24-8349-246b9612563e) ![Screenshot 2023-10-25 at 12 05 37](https://github.com/elastic/kibana/assets/7784120/63435c3c-1397-4b41-8d46-3d0e9bd32515) After: ![Screenshot 2023-10-25 at 12 06 10](https://github.com/elastic/kibana/assets/7784120/a7519fdd-b21a-40e7-a774-d867bb4e79ec) ![Screenshot 2023-10-25 at 12 06 14](https://github.com/elastic/kibana/assets/7784120/1e99e005-1317-4c62-af1e-c445c9038fc4) --- .../applications/fleet/hooks/use_breadcrumbs.tsx | 2 ++ .../integrations/hooks/use_breadcrumbs.tsx | 1 + .../test_suites/observability/navigation.ts | 15 +++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 8208616ffd0287..49af462dc96f5e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -24,6 +24,7 @@ const BASE_BREADCRUMB: Breadcrumb = { text: i18n.translate('xpack.fleet.breadcrumbs.appTitle', { defaultMessage: 'Fleet', }), + deepLinkId: 'fleet', }; const INTEGRATIONS_BASE_BREADCRUMB: Breadcrumb = { @@ -32,6 +33,7 @@ const INTEGRATIONS_BASE_BREADCRUMB: Breadcrumb = { defaultMessage: 'Integrations', }), useIntegrationsBasePath: true, + deepLinkId: 'integrations', }; const breadcrumbGetters: { diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx index 28f41fcabfcba7..967590c36ce07a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx @@ -19,6 +19,7 @@ const BASE_BREADCRUMB: ChromeBreadcrumb = { text: i18n.translate('xpack.fleet.breadcrumbs.integrationsAppTitle', { defaultMessage: 'Integrations', }), + deepLinkId: 'integrations', }; const breadcrumbGetters: { diff --git a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts index 012c7811c5a80c..c6c6e74edf62b9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts @@ -128,5 +128,20 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Cases', 'Settings']); }); + + it('navigates to integrations', async () => { + await svlCommonNavigation.sidenav.openSection('project_settings_project_nav'); + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'integrations' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts([ + 'Integrations', + 'Browse integrations', + ]); + }); + + it('navigates to fleet', async () => { + await svlCommonNavigation.sidenav.openSection('project_settings_project_nav'); + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'fleet' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Fleet', 'Agents']); + }); }); } From fe22ff0e412a07c8c3f82e909b56986c3ef0d6bc Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:58:15 -0400 Subject: [PATCH 02/24] [Search experience] Fix links is WS gated form (#169699) ## Summary 1. Updates Workplace search gated form links: - Blogs url - https://www.elastic.co/blog/evolution-workplace-search-private-data-elasticsearch - Terms of service - https://www.elastic.co/legal/elastic-cloud-account-terms - Contact you- https://www.elastic.co/legal/privacy-statement#how-we-use-the-information - Privacy statement - https://www.elastic.co/legal/privacy-statement/ 2. Assign respective variable in docs for the urls ## Screen recording https://github.com/elastic/kibana/assets/55930906/941ac635-7caa-4a60-905b-45b3610ed9fa --- packages/kbn-doc-links/src/get_doc_links.ts | 4 ++++ packages/kbn-doc-links/src/types.ts | 4 ++++ .../applications/shared/doc_links/doc_links.ts | 12 ++++++++++++ .../workplace_search/views/overview/gated_form.tsx | 8 +++++--- .../views/overview/gated_form_page.tsx | 3 ++- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index b8d46bad40cc43..16b58be6c6c03d 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -217,6 +217,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { dropbox: `${WORKPLACE_SEARCH_DOCS}workplace-search-dropbox-connector.html`, externalSharePointOnline: `${WORKPLACE_SEARCH_DOCS}sharepoint-online-external.html`, externalIdentities: `${WORKPLACE_SEARCH_DOCS}workplace-search-external-identities-api.html`, + gatedFormBlog: `${ELASTIC_WEBSITE_URL}blog/evolution-workplace-search-private-data-elasticsearch`, gettingStarted: `${WORKPLACE_SEARCH_DOCS}workplace-search-getting-started.html`, gitHub: `${WORKPLACE_SEARCH_DOCS}workplace-search-github-connector.html`, gmail: `${WORKPLACE_SEARCH_DOCS}workplace-search-gmail-connector.html`, @@ -815,6 +816,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { }, legal: { privacyStatement: `${ELASTIC_WEBSITE_URL}legal/product-privacy-statement`, + generalPrivacyStatement: `${ELASTIC_WEBSITE_URL}legal/privacy-statement`, + termsOfService: `${ELASTIC_WEBSITE_URL}legal/elastic-cloud-account-terms`, + dataUse: `${ELASTIC_WEBSITE_URL}legal/privacy-statement#how-we-use-the-information`, }, kibanaUpgradeSavedObjects: { resolveMigrationFailures: `${KIBANA_DOCS}resolve-migrations-failures.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index adc7f13c6c612e..703af63c1027c1 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -198,6 +198,7 @@ export interface DocLinks { readonly dropbox: string; readonly externalSharePointOnline: string; readonly externalIdentities: string; + readonly gatedFormBlog: string; readonly gitHub: string; readonly gettingStarted: string; readonly gmail: string; @@ -572,6 +573,9 @@ export interface DocLinks { }; readonly legal: { readonly privacyStatement: string; + readonly generalPrivacyStatement: string; + readonly termsOfService: string; + readonly dataUse: string; }; readonly kibanaUpgradeSavedObjects: { readonly resolveMigrationFailures: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 6974f956e2bc9c..82a4eedcd9c34b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -151,6 +151,10 @@ class DocLinks { public workplaceSearchDropbox: string; public workplaceSearchExternalIdentities: string; public workplaceSearchExternalSharePointOnline: string; + public workplaceSearchGatedFormBlog: string; + public workplaceSearchGatedFormDataUse: string; + public workplaceSearchGatedFormPrivacyStatement: string; + public workplaceSearchGatedFormTermsOfService: string; public workplaceSearchGettingStarted: string; public workplaceSearchGitHub: string; public workplaceSearchGmail: string; @@ -318,6 +322,10 @@ class DocLinks { this.workplaceSearchDropbox = ''; this.workplaceSearchExternalSharePointOnline = ''; this.workplaceSearchExternalIdentities = ''; + this.workplaceSearchGatedFormBlog = ''; + this.workplaceSearchGatedFormDataUse = ''; + this.workplaceSearchGatedFormPrivacyStatement = ''; + this.workplaceSearchGatedFormTermsOfService = ''; this.workplaceSearchGettingStarted = ''; this.workplaceSearchGitHub = ''; this.workplaceSearchGmail = ''; @@ -475,6 +483,7 @@ class DocLinks { this.syncRules = docLinks.links.enterpriseSearch.syncRules; this.trainedModels = docLinks.links.enterpriseSearch.trainedModels; this.textEmbedding = docLinks.links.enterpriseSearch.textEmbedding; + this.workplaceSearchGatedFormBlog = docLinks.links.workplaceSearch.gatedFormBlog; this.workplaceSearchApiKeys = docLinks.links.workplaceSearch.apiKeys; this.workplaceSearchBox = docLinks.links.workplaceSearch.box; this.workplaceSearchConfluenceCloud = docLinks.links.workplaceSearch.confluenceCloud; @@ -492,6 +501,9 @@ class DocLinks { this.workplaceSearchExternalSharePointOnline = docLinks.links.workplaceSearch.externalSharePointOnline; this.workplaceSearchExternalIdentities = docLinks.links.workplaceSearch.externalIdentities; + this.workplaceSearchGatedFormDataUse = docLinks.links.legal.dataUse; + this.workplaceSearchGatedFormPrivacyStatement = docLinks.links.legal.generalPrivacyStatement; + this.workplaceSearchGatedFormTermsOfService = docLinks.links.legal.termsOfService; this.workplaceSearchGettingStarted = docLinks.links.workplaceSearch.gettingStarted; this.workplaceSearchGitHub = docLinks.links.workplaceSearch.gitHub; this.workplaceSearchGmail = docLinks.links.workplaceSearch.gmail; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/gated_form.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/gated_form.tsx index 70a538d212d774..2ad67febaa3da1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/gated_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/gated_form.tsx @@ -30,6 +30,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../shared/doc_links'; + import { WorkplaceSearchGateLogic } from './gated_form_logic'; const getFeature = (id: string) => { @@ -593,7 +595,7 @@ export const WorkplaceSearchGate: React.FC = () => { details or to opt-out at any time." values={{ contact: ( - + { ), privacyStatementLink: ( - + { ), termsOfService: ( - + = ({ isLoading blogUrl: ( From 4461f5b95ab97eea0bfba3041fde9d171498a96f Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Wed, 25 Oct 2023 14:33:28 +0100 Subject: [PATCH 03/24] [Fleet] Fix flaky output secrets test (#169792) ## Summary Closes #169744 Replaced the search calls with get by ID calls to prevent refresh race condition --- .../fleet_api_integration/apis/outputs/crud.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index 686d29c9cdf221..5f6558df992ca8 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -20,16 +20,6 @@ export default function (providerContext: FtrProviderContext) { let pkgVersion: string; - const getSecrets = async (ids?: string[]) => { - const query = ids ? { terms: { _id: ids } } : { match_all: {} }; - return es.search({ - index: '.fleet-secrets', - body: { - query, - }, - }); - }; - const getSecretById = (id: string) => { return es.get({ index: '.fleet-secrets', @@ -1112,9 +1102,9 @@ export default function (providerContext: FtrProviderContext) { .expect(200); const secretId = res.body.item.secrets.ssl.key.id; - const searchRes = await getSecrets([secretId]); + const secret = await getSecretById(secretId); // @ts-ignore _source unknown type - expect(searchRes.hits.hits[0]._source.value).to.equal('KEY'); + expect(secret._source.value).to.equal('KEY'); }); it('should create ssl.password secret correctly', async function () { @@ -1138,9 +1128,9 @@ export default function (providerContext: FtrProviderContext) { }); const secretId = res.body.item.secrets.password.id; - const searchRes = await getSecrets([secretId]); + const secret = await getSecretById(secretId); // @ts-ignore _source unknown type - expect(searchRes.hits.hits[0]._source.value).to.equal('pass'); + expect(secret._source.value).to.equal('pass'); }); }); From 70dff2ac3eac810febe61cb3a5214472962ffd1d Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Wed, 25 Oct 2023 08:35:19 -0500 Subject: [PATCH 04/24] [Security Solution] remove code related to alert details page (#169172) --- .../common/experimental_features.ts | 5 - .../common/types/timeline/index.ts | 1 - .../common/utils/alert_detail_path.test.ts | 56 - .../common/components/hover_actions/index.tsx | 5 +- .../components/link_to/__mocks__/index.ts | 1 - .../public/common/components/link_to/index.ts | 1 - .../components/link_to/redirect_to_alerts.tsx | 19 - .../breadcrumbs/trailing_breadcrumbs.ts | 3 - .../cases/use_get_related_cases_by_event.ts | 53 - .../public/common/utils/route/types.ts | 7 - .../alert_context_menu.test.tsx | 39 - .../timeline_actions/alert_context_menu.tsx | 16 +- .../use_open_alert_details.tsx | 49 - .../__mocks__/alert_details_response.ts | 2020 ----------------- .../pages/alert_details/__mocks__/index.ts | 8 - .../alert_details/components/error_page.tsx | 33 - .../pages/alert_details/components/header.tsx | 33 - .../alert_details/components/loading_page.tsx | 21 - .../pages/alert_details/index.test.tsx | 144 -- .../detections/pages/alert_details/index.tsx | 96 - .../alert_render_panel.test.tsx | 57 - .../summary/alert_renderer_panel/index.tsx | 56 - .../summary/cases_panel/cases_panel.test.tsx | 223 -- .../cases_panel/cases_panel_actions.tsx | 102 - .../tabs/summary/cases_panel/index.tsx | 196 -- .../tabs/summary/cases_panel/related_case.tsx | 105 - .../summary/host_panel/host_panel.test.tsx | 155 -- .../summary/host_panel/host_panel_actions.tsx | 99 - .../tabs/summary/host_panel/index.tsx | 195 -- .../alert_details/tabs/summary/index.tsx | 93 - .../tabs/summary/rule_panel/index.tsx | 158 -- .../summary/rule_panel/rule_panel.test.tsx | 55 - .../summary/rule_panel/rule_panel_actions.tsx | 78 - .../alert_details/tabs/summary/translation.ts | 239 -- .../tabs/summary/user_panel/index.tsx | 160 -- .../summary/user_panel/user_panel.test.tsx | 112 - .../summary/user_panel/user_panel_actions.tsx | 99 - .../alert_details/tabs/summary/wrappers.tsx | 70 - .../pages/alert_details/translations.ts | 51 - .../detections/pages/alert_details/types.ts | 14 - .../pages/alert_details/utils/breadcrumbs.ts | 53 - .../utils/get_timeline_event_data.ts | 13 - .../pages/alert_details/utils/navigation.ts | 23 - .../event_details/expandable_event.tsx | 26 - .../side_panel/event_details/translations.ts | 14 - .../components/timeline/body/index.test.tsx | 13 - .../translations/translations/fr-FR.json | 40 - .../translations/translations/ja-JP.json | 40 - .../translations/translations/zh-CN.json | 40 - .../test/security_solution_cypress/config.ts | 1 - .../e2e/entity_analytics/enrichments.cy.ts | 12 +- .../explore/cases/attach_alert_to_case.cy.ts | 12 +- .../investigations/alerts/navigation.cy.ts | 70 - .../cypress/screens/alerts.ts | 32 +- .../cypress/screens/alerts_details.ts | 14 - .../cypress/tasks/alerts.ts | 23 - 56 files changed, 18 insertions(+), 5335 deletions(-) delete mode 100644 x-pack/plugins/security_solution/common/utils/alert_detail_path.test.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_alerts.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/containers/cases/use_get_related_cases_by_event.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_open_alert_details.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/alert_details_response.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/index.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/components/error_page.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/components/header.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/components/loading_page.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/alert_render_panel.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel_actions.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/related_case.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel_actions.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel_actions.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/translation.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel_actions.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/wrappers.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/types.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/get_timeline_event_data.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/navigation.ts delete mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/navigation.cy.ts diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 90346011c23e57..e2756d1955e307 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -61,11 +61,6 @@ export const allowedExperimentalValues = Object.freeze({ */ endpointResponseActionsEnabled: true, - /** - * Enables the alert details page currently only accessible via the alert details flyout and alert table context menu - */ - alertDetailsPageEnabled: false, - /** * Enables the `upload` endpoint response action (v8.9) */ diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 0372765db98739..e9d482344a28e0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -48,7 +48,6 @@ export enum TimelineId { active = 'timeline-1', casePage = 'timeline-case', test = 'timeline-test', // Reserved for testing purposes - detectionsAlertDetailsPage = 'detections-alert-details-page', } export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom' | 'eql'; diff --git a/x-pack/plugins/security_solution/common/utils/alert_detail_path.test.ts b/x-pack/plugins/security_solution/common/utils/alert_detail_path.test.ts deleted file mode 100644 index be827e082db143..00000000000000 --- a/x-pack/plugins/security_solution/common/utils/alert_detail_path.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { buildAlertDetailPath, getAlertDetailsUrl } from './alert_detail_path'; - -describe('alert_detail_path', () => { - const defaultArguments = { - alertId: 'testId', - index: 'testIndex', - timestamp: '2023-04-18T00:00:00.000Z', - }; - describe('buildAlertDetailPath', () => { - it('builds the alert detail path as expected', () => { - expect(buildAlertDetailPath(defaultArguments)).toMatchInlineSnapshot( - `"/alerts/redirect/testId?index=testIndex×tamp=2023-04-18T00:00:00.000Z"` - ); - }); - }); - describe('getAlertDetailsUrl', () => { - it('builds the alert detail path without a space id', () => { - expect( - getAlertDetailsUrl({ - ...defaultArguments, - basePath: 'http://somebasepath.com', - }) - ).toMatchInlineSnapshot( - `"http://somebasepath.com/app/security/alerts/redirect/testId?index=testIndex×tamp=2023-04-18T00:00:00.000Z"` - ); - }); - - it('builds the alert detail path with a space id', () => { - expect( - getAlertDetailsUrl({ - ...defaultArguments, - basePath: 'http://somebasepath.com', - spaceId: 'test-space', - }) - ).toMatchInlineSnapshot( - `"http://somebasepath.com/s/test-space/app/security/alerts/redirect/testId?index=testIndex×tamp=2023-04-18T00:00:00.000Z"` - ); - }); - - it('does not build the alert detail path without a basePath', () => { - expect( - getAlertDetailsUrl({ - ...defaultArguments, - spaceId: 'test-space', - }) - ).toBe(undefined); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx index 16ed7f95b87c63..6aab4a0afbe681 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx @@ -217,13 +217,12 @@ export const HoverActions: React.FC = React.memo( const isCaseView = scopeId === TimelineId.casePage; const isTimelineView = scopeId === TimelineId.active; - const isAlertDetailsView = scopeId === TimelineId.detectionsAlertDetailsPage; // TODO Provide a list of disabled/enabled actions as props const isEntityAnalyticsPage = scopeId === SecurityPageName.entityAnalytics; const hideFilters = useMemo( - () => (isAlertDetailsView || isEntityAnalyticsPage) && !isTimelineView, - [isTimelineView, isAlertDetailsView, isEntityAnalyticsPage] + () => isEntityAnalyticsPage && !isTimelineView, + [isTimelineView, isEntityAnalyticsPage] ); const hiddenActionsCount = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts index b7bfb751e4fa82..52ed72dc1a2bd7 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts @@ -12,7 +12,6 @@ export { getAppLandingUrl } from '../redirect_to_landing'; export { getHostDetailsUrl, getHostsUrl } from '../redirect_to_hosts'; export { getNetworkUrl, getNetworkDetailsUrl } from '../redirect_to_network'; export { getTimelineTabsUrl, getTimelineUrl } from '../redirect_to_timelines'; -export { getAlertDetailsUrl, getAlertDetailsTabUrl } from '../redirect_to_alerts'; export { getCaseDetailsUrl, getCaseUrl, diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index 5ef5280df7fa01..abb3e9d7b85fff 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -15,7 +15,6 @@ import { import { useAppUrl } from '../../lib/kibana/hooks'; import type { SecurityPageName } from '../../../app/types'; -export { getAlertDetailsUrl, getAlertDetailsTabUrl } from './redirect_to_alerts'; export { getDetectionEngineUrl, getRuleDetailsUrl } from './redirect_to_detection_engine'; export { getHostDetailsUrl, getTabsOnHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getKubernetesUrl, getKubernetesDetailsUrl } from './redirect_to_kubernetes'; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_alerts.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_alerts.tsx deleted file mode 100644 index d29530f2cdfcaf..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_alerts.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ALERTS_PATH } from '../../../../common/constants'; -import type { AlertDetailRouteType } from '../../../detections/pages/alert_details/types'; -import { appendSearch } from './helpers'; - -export const getAlertDetailsUrl = (alertId: string, search?: string) => - `/${alertId}/summary${appendSearch(search)}`; - -export const getAlertDetailsTabUrl = ( - detailName: string, - tabName: AlertDetailRouteType, - search?: string -) => `${ALERTS_PATH}/${detailName}/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts index 5c45da1bb1ff23..7da3500775e481 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts @@ -15,7 +15,6 @@ import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../e import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs'; import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/breadcrumbs'; import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs'; -import { getTrailingBreadcrumbs as getAlertDetailBreadcrumbs } from '../../../../detections/pages/alert_details/utils/breadcrumbs'; import { getTrailingBreadcrumbs as getDashboardBreadcrumbs } from '../../../../dashboards/pages/breadcrumbs'; export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( @@ -37,8 +36,6 @@ export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( return geExceptionsBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.kubernetes: return getKubernetesBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.alerts: - return getAlertDetailBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.cloudSecurityPostureBenchmarks: return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.dashboards: diff --git a/x-pack/plugins/security_solution/public/common/containers/cases/use_get_related_cases_by_event.ts b/x-pack/plugins/security_solution/public/common/containers/cases/use_get_related_cases_by_event.ts deleted file mode 100644 index fe20d4670f3a79..00000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/cases/use_get_related_cases_by_event.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useState, useEffect } from 'react'; -import type { RelatedCase } from '@kbn/cases-plugin/common'; -import { useKibana, useToasts } from '../../lib/kibana'; -import { CASES_ERROR_TOAST } from '../../components/event_details/insights/translations'; -import { APP_ID } from '../../../../common/constants'; - -export const useGetRelatedCasesByEvent = (eventId: string) => { - const { - services: { cases }, - } = useKibana(); - const toasts = useToasts(); - - const [relatedCases, setRelatedCases] = useState(undefined); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const getRelatedCases = useCallback(async () => { - setLoading(true); - let relatedCasesResponse: RelatedCase[] = []; - try { - if (eventId) { - relatedCasesResponse = - (await cases.api.getRelatedCases(eventId, { - owner: APP_ID, - })) ?? []; - } - } catch (err) { - setError(err); - toasts.addWarning(CASES_ERROR_TOAST(err)); - } finally { - setRelatedCases(relatedCasesResponse); - setLoading(false); - } - }, [eventId, cases.api, toasts]); - - useEffect(() => { - getRelatedCases(); - }, [eventId, getRelatedCases]); - - return { - loading, - error, - relatedCases, - refetchRelatedCases: getRelatedCases, - }; -}; diff --git a/x-pack/plugins/security_solution/public/common/utils/route/types.ts b/x-pack/plugins/security_solution/public/common/utils/route/types.ts index 6e10a6f0d0c860..78d8c9bc39c97d 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/types.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/types.ts @@ -11,7 +11,6 @@ import type React from 'react'; import type { AllRulesTabs } from '../../../detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar'; import type { HostsTableType } from '../../../explore/hosts/store/model'; import type { NetworkRouteType } from '../../../explore/network/pages/navigation/types'; -import type { AlertDetailRouteType } from '../../../detections/pages/alert_details/types'; import type { AdministrationSubTab as AdministrationType } from '../../../management/types'; import type { FlowTarget } from '../../../../common/search_strategy'; import type { UsersTableType } from '../../../explore/users/store/model'; @@ -32,7 +31,6 @@ export type RouteSpyState = | GenericRouteSpyState | GenericRouteSpyState | GenericRouteSpyState - | GenericRouteSpyState | GenericRouteSpyState | GenericRouteSpyState | GenericRouteSpyState @@ -52,15 +50,10 @@ export type RouteSpyState = export type HostRouteSpyState = GenericRouteSpyState; export type UsersRouteSpyState = GenericRouteSpyState; export type NetworkRouteSpyState = GenericRouteSpyState; -export type AlertDetailRouteSpyState = GenericRouteSpyState< - SecurityPageName.alerts, - AlertDetailRouteType ->; export type AdministrationRouteSpyState = GenericRouteSpyState< SecurityPageName.administration, AdministrationType >; -export type DashboardsRouteSpyState = GenericRouteSpyState; export type RouteSpyAction = | { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index b180856da2b290..2b887808696bdb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -99,7 +99,6 @@ const markAsOpenButton = '[data-test-subj="open-alert-status"]'; const markAsAcknowledgedButton = '[data-test-subj="acknowledged-alert-status"]'; const markAsClosedButton = '[data-test-subj="close-alert-status"]'; const addEndpointEventFilterButton = '[data-test-subj="add-event-filter-menu-item"]'; -const openAlertDetailsPageButton = '[data-test-subj="open-alert-details-page-menu-item"]'; const applyAlertTagsButton = '[data-test-subj="alert-tags-context-menu-item"]'; describe('Alert table context menu', () => { @@ -289,44 +288,6 @@ describe('Alert table context menu', () => { }); }); - describe('Open alert details action', () => { - test('it does not render the open alert details page action if kibana.alert.rule.uuid is not set', () => { - const nonAlertProps = { - ...props, - ecsRowData: { - ...ecsRowData, - kibana: { - alert: { - workflow_status: ['open'], - rule: { - parameters: {}, - uuid: [], - }, - }, - }, - }, - }; - - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - wrapper.find(actionMenuButton).simulate('click'); - - expect(wrapper.find(openAlertDetailsPageButton).first().exists()).toEqual(false); - }); - - test('it renders the open alert details action button', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - wrapper.find(actionMenuButton).simulate('click'); - - expect(wrapper.find(openAlertDetailsPageButton).first().exists()).toEqual(true); - }); - }); - describe('Apply alert tags action', () => { test('it renders the apply alert tags action button', () => { const wrapper = mount(, { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index a05c351f3d22d2..a04d8d197da1e4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -20,7 +20,6 @@ import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../common/components/heade import { isActiveTimeline } from '../../../../helpers'; import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; import { OsqueryFlyout } from '../../osquery/osquery_flyout'; -import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { buildGetAlertByIdQuery } from '../../../../detection_engine/rule_exceptions/utils/helpers'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; @@ -44,7 +43,6 @@ import { useEventFilterAction } from './use_event_filter_action'; import { useAddToCaseActions } from './use_add_to_case_actions'; import { isAlertFromEndpointAlert } from '../../../../common/utils/endpoint_alert_check'; import type { Rule } from '../../../../detection_engine/rule_management/logic/types'; -import { useOpenAlertDetailsAction } from './use_open_alert_details'; import type { AlertTableContextMenuItem } from '../types'; import { useAlertTagsActions } from './use_alert_tags_actions'; @@ -73,7 +71,6 @@ const AlertContextMenuComponent: React.FC { const [isPopoverOpen, setPopover] = useState(false); const [isOsqueryFlyoutOpen, setOsqueryFlyoutOpen] = useState(false); - const [routeProps] = useRouteSpy(); const onMenuItemClick = useCallback(() => { setPopover(false); @@ -145,14 +142,11 @@ const AlertContextMenuComponent: React.FC { if (isActiveTimeline(scopeId ?? '')) { refetchQuery([timelineQuery]); - if (routeProps.pageName === 'alerts') { - refetchQuery(globalQuery); - } } else { refetchQuery(globalQuery); if (refetch) refetch(); } - }, [scopeId, globalQuery, timelineQuery, routeProps, refetch]); + }, [scopeId, globalQuery, timelineQuery, refetch]); const ruleIndex = ecsRowData['kibana.alert.rule.parameters']?.index ?? ecsRowData?.signal?.rule?.index; @@ -216,12 +210,6 @@ const AlertContextMenuComponent: React.FC void; - alertId: string | null; -} - -export const ACTION_OPEN_ALERT_DETAILS_PAGE = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.actions.openAlertDetails', - { - defaultMessage: 'Open alert details page', - } -); - -export const useOpenAlertDetailsAction = ({ ruleId, closePopover, alertId }: Props) => { - const isAlertDetailsPageEnabled = useIsExperimentalFeatureEnabled('alertDetailsPageEnabled'); - const alertDetailsActionItems: AlertTableContextMenuItem[] = []; - const { onClick } = useGetSecuritySolutionLinkProps()({ - deepLinkId: SecurityPageName.alerts, - path: alertId ? getAlertDetailsUrl(alertId) : '', - }); - - // We check ruleId to confirm this is an alert, as this page does not support events as of 8.6 - if (ruleId && alertId && isAlertDetailsPageEnabled) { - alertDetailsActionItems.push({ - key: 'open-alert-details-item', - 'data-test-subj': 'open-alert-details-page-menu-item', - onClick, - name: ACTION_OPEN_ALERT_DETAILS_PAGE, - }); - } - - return { - alertDetailsActionItems, - }; -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/alert_details_response.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/alert_details_response.ts deleted file mode 100644 index 5c4e985abf7175..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/alert_details_response.ts +++ /dev/null @@ -1,2020 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; - -// This data was generated using the endpoint test alert generator -export const getMockAlertDetailsFieldsResponse = () => ({ - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325', - _score: 1, - fields: { - 'kibana.alert.severity': ['medium'], - 'process.hash.md5': ['fake md5'], - 'kibana.alert.rule.updated_by': ['elastic'], - 'signal.ancestors.depth': [0], - 'event.category': ['malware'], - 'kibana.alert.rule.rule_name_override': ['message'], - 'Endpoint.capabilities': ['isolation', 'kill_process', 'suspend_process', 'running_processes'], - 'process.parent.pid': [1], - 'process.hash.sha256': ['fake sha256'], - 'host.hostname': ['Host-4cfuh42w7g'], - 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], - 'host.mac': ['f2-32-1b-dc-ec-80'], - 'elastic.agent.id': ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - 'dll.hash.sha256': ['8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2'], - 'kibana.alert.ancestors.depth': [0], - 'signal.rule.enabled': ['true'], - 'signal.rule.max_signals': [10000], - 'host.os.version': ['10.0'], - 'signal.rule.updated_at': ['2022-09-29T19:39:38.137Z'], - 'kibana.alert.risk_score': [47], - 'Endpoint.policy.applied.id': ['C2A9093E-E289-4C0A-AA44-8C32A414FA7A'], - 'kibana.alert.rule.severity_mapping.severity': ['low', 'medium', 'high', 'critical'], - 'event.agent_id_status': ['auth_metadata_missing'], - 'kibana.alert.original_event.id': ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - 'kibana.alert.rule.risk_score_mapping.value': [''], - 'process.Ext.ancestry': ['kj0le842x0', '1r4s9i1br4'], - 'signal.original_event.code': ['memory_signature'], - 'kibana.alert.original_event.module': ['endpoint'], - 'kibana.alert.rule.interval': ['5m'], - 'kibana.alert.rule.type': ['query'], - 'signal.original_event.sequence': [1232], - 'Endpoint.state.isolation': [true], - 'host.architecture': ['x7n6yt4fol'], - 'kibana.alert.rule.immutable': ['true'], - 'kibana.alert.original_event.type': ['info'], - 'event.code': ['memory_signature'], - 'agent.id': ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - 'signal.original_event.module': ['endpoint'], - 'kibana.alert.rule.exceptions_list.list_id': ['endpoint_list'], - 'signal.rule.from': ['now-10m'], - 'kibana.alert.rule.exceptions_list.type': ['endpoint'], - 'process.group_leader.entity_id': ['b74mw1jkrm'], - 'dll.Ext.malware_classification.version': ['3.0.0'], - 'kibana.alert.rule.enabled': ['true'], - 'kibana.alert.rule.version': ['100'], - 'kibana.alert.ancestors.type': ['event'], - 'process.entry_leader.name': ['fake entry'], - 'dll.Ext.compile_time': [1534424710], - 'signal.ancestors.index': ['.ds-logs-endpoint.alerts-default-2022.09.29-000001'], - 'dll.Ext.malware_classification.score': [0], - 'process.entity_id': ['d3v4to81q9'], - 'host.ip': ['10.184.3.36', '10.170.218.86'], - 'agent.type': ['endpoint'], - 'signal.original_event.category': ['malware'], - 'signal.original_event.id': ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - 'process.uptime': [0], - 'Endpoint.policy.applied.name': ['With Eventing'], - 'host.id': ['04794e4e-59cb-4c4a-a8ee-3e6c5b65743c'], - 'process.Ext.code_signature.subject_name': ['bad signer'], - 'process.Ext.token.integrity_level_name': ['high'], - 'signal.original_event.type': ['info'], - 'kibana.alert.rule.max_signals': [10000], - 'signal.rule.author': ['Elastic'], - 'kibana.alert.rule.risk_score': [47], - 'dll.Ext.malware_classification.identifier': ['Whitelisted'], - 'dll.Ext.mapped_address': [5362483200], - 'signal.original_event.dataset': ['endpoint'], - 'kibana.alert.rule.consumer': ['siem'], - 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], - 'kibana.alert.rule.category': ['Custom Query Rule'], - 'host.os.Ext.variant': ['Windows Server'], - 'event.ingested': ['2022-09-29T19:37:00.000Z'], - 'event.action': ['start'], - 'signal.rule.updated_by': ['elastic'], - '@timestamp': ['2022-09-29T19:40:26.051Z'], - 'kibana.alert.original_event.action': ['start'], - 'host.os.platform': ['Windows'], - 'process.session_leader.entity_id': ['b74mw1jkrm'], - 'kibana.alert.rule.severity': ['medium'], - 'kibana.alert.original_event.agent_id_status': ['auth_metadata_missing'], - 'Endpoint.status': ['enrolled'], - 'data_stream.dataset': ['endpoint.alerts'], - 'signal.rule.timestamp_override': ['event.ingested'], - 'kibana.alert.rule.execution.uuid': ['abf39d36-0f1c-4bf9-ae42-1039285380b5'], - 'kibana.alert.uuid': ['f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325'], - 'kibana.version': ['8.6.0'], - 'process.hash.sha1': ['fake sha1'], - 'event.id': ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - 'process.entry_leader.pid': [865], - 'signal.rule.license': ['Elastic License v2'], - 'signal.ancestors.type': ['event'], - 'kibana.alert.rule.rule_id': ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - 'process.session_leader.pid': [745], - 'signal.rule.type': ['query'], - 'Endpoint.policy.applied.version': [5], - 'dll.hash.md5': ['1f2d082566b0fc5f2c238a5180db7451'], - 'kibana.alert.ancestors.id': ['7L3AioMBWJvcpv7vlX2O'], - 'user.name': ['root'], - 'source.ip': ['10.184.3.46'], - 'signal.rule.rule_name_override': ['message'], - 'process.group_leader.name': ['fake leader'], - 'host.os.full': ['Windows Server 2016'], - 'kibana.alert.original_event.code': ['memory_signature'], - 'kibana.alert.rule.risk_score_mapping.field': ['event.risk_score'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.pid': [2], - 'kibana.alert.rule.producer': ['siem'], - 'kibana.alert.rule.to': ['now'], - 'signal.rule.interval': ['5m'], - 'signal.rule.created_by': ['elastic'], - 'kibana.alert.rule.created_by': ['elastic'], - 'kibana.alert.rule.timestamp_override': ['event.ingested'], - 'kibana.alert.original_event.ingested': ['2022-09-29T19:37:00.000Z'], - 'signal.rule.id': ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - 'process.parent.entity_id': ['kj0le842x0'], - 'signal.rule.risk_score': [47], - 'signal.reason': [ - 'malware event with process explorer.exe, on Host-4cfuh42w7g created medium alert Endpoint Security.', - ], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Endpoint Security'], - 'host.name': ['Host-4cfuh42w7g'], - 'signal.status': ['open'], - 'event.kind': ['signal'], - 'kibana.alert.rule.severity_mapping.value': ['21', '47', '73', '99'], - 'signal.rule.tags': ['Elastic', 'Endpoint Security'], - 'signal.rule.created_at': ['2022-09-29T19:39:38.137Z'], - 'kibana.alert.workflow_status': ['open'], - 'Endpoint.policy.applied.status': ['warning'], - 'kibana.alert.rule.uuid': ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - 'kibana.alert.original_event.category': ['malware'], - 'dll.Ext.malware_classification.threshold': [0], - 'kibana.alert.reason': [ - 'malware event with process explorer.exe, on Host-4cfuh42w7g created medium alert Endpoint Security.', - ], - 'dll.pe.architecture': ['x64'], - 'data_stream.type': ['logs'], - 'signal.original_time': ['2022-10-09T07:14:42.194Z'], - 'signal.ancestors.id': ['7L3AioMBWJvcpv7vlX2O'], - 'process.name': ['explorer.exe'], - 'ecs.version': ['1.6.0'], - 'signal.rule.severity': ['medium'], - 'kibana.alert.ancestors.index': ['.ds-logs-endpoint.alerts-default-2022.09.29-000001'], - 'Endpoint.configuration.isolation': [true], - 'Memory_protection.feature': ['signature'], - 'dll.code_signature.trusted': [true], - 'process.Ext.code_signature.trusted': [false], - 'kibana.alert.depth': [1], - 'agent.version': ['8.6.0'], - 'kibana.alert.rule.risk_score_mapping.operator': ['equals'], - 'host.os.family': ['windows'], - 'kibana.alert.rule.from': ['now-10m'], - 'Memory_protection.self_injection': [true], - 'process.start': ['2022-10-09T07:14:42.194Z'], - 'kibana.alert.rule.parameters': [ - { - severity_mapping: [ - { - severity: 'low', - field: 'event.severity', - value: '21', - operator: 'equals', - }, - { - severity: 'medium', - field: 'event.severity', - value: '47', - operator: 'equals', - }, - { - severity: 'high', - field: 'event.severity', - value: '73', - operator: 'equals', - }, - { - severity: 'critical', - field: 'event.severity', - value: '99', - operator: 'equals', - }, - ], - references: [], - description: - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - language: 'kuery', - type: 'query', - rule_name_override: 'message', - exceptions_list: [ - { - list_id: 'endpoint_list', - namespace_type: 'agnostic', - id: 'endpoint_list', - type: 'endpoint', - }, - ], - timestamp_override: 'event.ingested', - from: 'now-10m', - severity: 'medium', - max_signals: 10000, - risk_score: 47, - risk_score_mapping: [ - { - field: 'event.risk_score', - value: '', - operator: 'equals', - }, - ], - author: ['Elastic'], - query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', - index: ['logs-endpoint.alerts-*'], - version: 100, - rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', - license: 'Elastic License v2', - required_fields: [ - { - ecs: true, - name: 'event.kind', - type: 'keyword', - }, - { - ecs: true, - name: 'event.module', - type: 'keyword', - }, - ], - immutable: true, - related_integrations: [], - setup: '', - false_positives: [], - threat: [], - to: 'now', - }, - ], - 'signal.rule.version': ['100'], - 'signal.original_event.kind': ['alert'], - 'kibana.alert.status': ['active'], - 'kibana.alert.rule.severity_mapping.field': [ - 'event.severity', - 'event.severity', - 'event.severity', - 'event.severity', - ], - 'kibana.alert.original_event.dataset': ['endpoint'], - 'signal.depth': [1], - 'signal.rule.immutable': ['true'], - 'process.group_leader.pid': [116], - 'event.sequence': [1232], - 'kibana.alert.rule.rule_type_id': ['siem.queryRule'], - 'process.session_leader.name': ['fake session'], - 'signal.rule.name': ['Endpoint Security'], - 'signal.rule.rule_id': ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - 'event.module': ['endpoint'], - 'dll.hash.sha1': ['ca85243c0af6a6471bdaa560685c51eefd6dbc0d'], - 'kibana.alert.rule.severity_mapping.operator': ['equals', 'equals', 'equals', 'equals'], - 'process.Ext.malware_signature.all_names': ['Windows.Trojan.FakeAgent'], - 'kibana.alert.rule.license': ['Elastic License v2'], - 'kibana.alert.original_event.kind': ['alert'], - 'process.executable': ['C:/fake/explorer.exe'], - 'kibana.alert.rule.updated_at': ['2022-09-29T19:39:38.137Z'], - 'signal.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'dll.Ext.mapped_size': [0], - 'data_stream.namespace': ['default'], - 'kibana.alert.rule.author': ['Elastic'], - 'dll.code_signature.subject_name': ['Cybereason Inc'], - 'Endpoint.policy.applied.endpoint_policy_version': [3], - 'kibana.alert.original_event.sequence': [1232], - 'dll.path': ['C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe'], - 'process.Ext.user': ['SYSTEM'], - 'signal.original_event.action': ['start'], - 'signal.rule.to': ['now'], - 'kibana.alert.rule.created_at': ['2022-09-29T19:39:38.137Z'], - 'process.Ext.malware_signature.identifier': ['diagnostic-malware-signature-v1-fake'], - 'kibana.alert.rule.exceptions_list.namespace_type': ['agnostic'], - 'event.type': ['info'], - 'kibana.space_ids': ['default'], - 'process.entry_leader.entity_id': ['b74mw1jkrm'], - 'kibana.alert.rule.exceptions_list.id': ['endpoint_list'], - 'event.dataset': ['endpoint'], - 'kibana.alert.original_time': ['2022-10-09T07:14:42.194Z'], - }, -}); - -export const getMockAlertDetailsTimelineResponse = () => [ - { - category: 'kibana', - field: 'kibana.alert.severity', - values: ['medium'], - originalValue: ['medium'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.hash.md5', - values: ['fake md5'], - originalValue: ['fake md5'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.updated_by', - values: ['elastic'], - originalValue: ['elastic'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.ancestors.depth', - values: ['0'], - originalValue: ['0'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.category', - values: ['malware'], - originalValue: ['malware'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.rule_name_override', - values: ['message'], - originalValue: ['message'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.capabilities', - values: ['isolation', 'kill_process', 'suspend_process', 'running_processes'], - originalValue: ['isolation', 'kill_process', 'suspend_process', 'running_processes'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.parent.pid', - values: ['1'], - originalValue: ['1'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.hash.sha256', - values: ['fake sha256'], - originalValue: ['fake sha256'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.hostname', - values: ['Host-4cfuh42w7g'], - originalValue: ['Host-4cfuh42w7g'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.tags', - values: ['Elastic', 'Endpoint Security'], - originalValue: ['Elastic', 'Endpoint Security'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.mac', - values: ['f2-32-1b-dc-ec-80'], - originalValue: ['f2-32-1b-dc-ec-80'], - isObjectArray: false, - }, - { - category: 'elastic', - field: 'elastic.agent.id', - values: ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - originalValue: ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.hash.sha256', - values: ['8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2'], - originalValue: ['8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.ancestors.depth', - values: ['0'], - originalValue: ['0'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.enabled', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.max_signals', - values: ['10000'], - originalValue: ['10000'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.os.version', - values: ['10.0'], - originalValue: ['10.0'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.updated_at', - values: ['2022-09-29T19:39:38.137Z'], - originalValue: ['2022-09-29T19:39:38.137Z'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.risk_score', - values: ['47'], - originalValue: ['47'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.policy.applied.id', - values: ['C2A9093E-E289-4C0A-AA44-8C32A414FA7A'], - originalValue: ['C2A9093E-E289-4C0A-AA44-8C32A414FA7A'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.severity_mapping.severity', - values: ['low', 'medium', 'high', 'critical'], - originalValue: ['low', 'medium', 'high', 'critical'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.agent_id_status', - values: ['auth_metadata_missing'], - originalValue: ['auth_metadata_missing'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.id', - values: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - originalValue: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.risk_score_mapping.value', - values: [''], - originalValue: [''], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.ancestry', - values: ['kj0le842x0', '1r4s9i1br4'], - originalValue: ['kj0le842x0', '1r4s9i1br4'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.code', - values: ['memory_signature'], - originalValue: ['memory_signature'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.module', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.interval', - values: ['5m'], - originalValue: ['5m'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.type', - values: ['query'], - originalValue: ['query'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.sequence', - values: ['1232'], - originalValue: ['1232'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.state.isolation', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.architecture', - values: ['x7n6yt4fol'], - originalValue: ['x7n6yt4fol'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.immutable', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.type', - values: ['info'], - originalValue: ['info'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.code', - values: ['memory_signature'], - originalValue: ['memory_signature'], - isObjectArray: false, - }, - { - category: 'agent', - field: 'agent.id', - values: ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - originalValue: ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.module', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.exceptions_list.list_id', - values: ['endpoint_list'], - originalValue: ['endpoint_list'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.from', - values: ['now-10m'], - originalValue: ['now-10m'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.exceptions_list.type', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.group_leader.entity_id', - values: ['b74mw1jkrm'], - originalValue: ['b74mw1jkrm'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.malware_classification.version', - values: ['3.0.0'], - originalValue: ['3.0.0'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.enabled', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.version', - values: ['100'], - originalValue: ['100'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.ancestors.type', - values: ['event'], - originalValue: ['event'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.entry_leader.name', - values: ['fake entry'], - originalValue: ['fake entry'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.compile_time', - values: ['1534424710'], - originalValue: ['1534424710'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.ancestors.index', - values: ['.ds-logs-endpoint.alerts-default-2022.09.29-000001'], - originalValue: ['.ds-logs-endpoint.alerts-default-2022.09.29-000001'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.malware_classification.score', - values: ['0'], - originalValue: ['0'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.entity_id', - values: ['d3v4to81q9'], - originalValue: ['d3v4to81q9'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.ip', - values: ['10.184.3.36', '10.170.218.86'], - originalValue: ['10.184.3.36', '10.170.218.86'], - isObjectArray: false, - }, - { - category: 'agent', - field: 'agent.type', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.category', - values: ['malware'], - originalValue: ['malware'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.id', - values: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - originalValue: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.uptime', - values: ['0'], - originalValue: ['0'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.policy.applied.name', - values: ['With Eventing'], - originalValue: ['With Eventing'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.id', - values: ['04794e4e-59cb-4c4a-a8ee-3e6c5b65743c'], - originalValue: ['04794e4e-59cb-4c4a-a8ee-3e6c5b65743c'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.code_signature.subject_name', - values: ['bad signer'], - originalValue: ['bad signer'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.token.integrity_level_name', - values: ['high'], - originalValue: ['high'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.type', - values: ['info'], - originalValue: ['info'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.max_signals', - values: ['10000'], - originalValue: ['10000'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.author', - values: ['Elastic'], - originalValue: ['Elastic'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.risk_score', - values: ['47'], - originalValue: ['47'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.malware_classification.identifier', - values: ['Whitelisted'], - originalValue: ['Whitelisted'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.mapped_address', - values: ['5362483200'], - originalValue: ['5362483200'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.dataset', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.consumer', - values: ['siem'], - originalValue: ['siem'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.indices', - values: ['logs-endpoint.alerts-*'], - originalValue: ['logs-endpoint.alerts-*'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.category', - values: ['Custom Query Rule'], - originalValue: ['Custom Query Rule'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.os.Ext.variant', - values: ['Windows Server'], - originalValue: ['Windows Server'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.ingested', - values: ['2022-09-29T19:37:00.000Z'], - originalValue: ['2022-09-29T19:37:00.000Z'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.action', - values: ['start'], - originalValue: ['start'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.updated_by', - values: ['elastic'], - originalValue: ['elastic'], - isObjectArray: false, - }, - { - category: 'base', - field: '@timestamp', - values: ['2022-09-29T19:40:26.051Z'], - originalValue: ['2022-09-29T19:40:26.051Z'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.action', - values: ['start'], - originalValue: ['start'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.os.platform', - values: ['Windows'], - originalValue: ['Windows'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.session_leader.entity_id', - values: ['b74mw1jkrm'], - originalValue: ['b74mw1jkrm'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.severity', - values: ['medium'], - originalValue: ['medium'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.agent_id_status', - values: ['auth_metadata_missing'], - originalValue: ['auth_metadata_missing'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.status', - values: ['enrolled'], - originalValue: ['enrolled'], - isObjectArray: false, - }, - { - category: 'data_stream', - field: 'data_stream.dataset', - values: ['endpoint.alerts'], - originalValue: ['endpoint.alerts'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.timestamp_override', - values: ['event.ingested'], - originalValue: ['event.ingested'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.execution.uuid', - values: ['abf39d36-0f1c-4bf9-ae42-1039285380b5'], - originalValue: ['abf39d36-0f1c-4bf9-ae42-1039285380b5'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.uuid', - values: ['f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325'], - originalValue: ['f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.version', - values: ['8.6.0'], - originalValue: ['8.6.0'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.hash.sha1', - values: ['fake sha1'], - originalValue: ['fake sha1'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.id', - values: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - originalValue: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.entry_leader.pid', - values: ['865'], - originalValue: ['865'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.license', - values: ['Elastic License v2'], - originalValue: ['Elastic License v2'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.ancestors.type', - values: ['event'], - originalValue: ['event'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.rule_id', - values: ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - originalValue: ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.session_leader.pid', - values: ['745'], - originalValue: ['745'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.type', - values: ['query'], - originalValue: ['query'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.policy.applied.version', - values: ['5'], - originalValue: ['5'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.hash.md5', - values: ['1f2d082566b0fc5f2c238a5180db7451'], - originalValue: ['1f2d082566b0fc5f2c238a5180db7451'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.ancestors.id', - values: ['7L3AioMBWJvcpv7vlX2O'], - originalValue: ['7L3AioMBWJvcpv7vlX2O'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.rule_name_override', - values: ['message'], - originalValue: ['message'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.group_leader.name', - values: ['fake leader'], - originalValue: ['fake leader'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.os.full', - values: ['Windows Server 2016'], - originalValue: ['Windows Server 2016'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.code', - values: ['memory_signature'], - originalValue: ['memory_signature'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.risk_score_mapping.field', - values: ['event.risk_score'], - originalValue: ['event.risk_score'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - originalValue: [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.pid', - values: ['2'], - originalValue: ['2'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.producer', - values: ['siem'], - originalValue: ['siem'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.to', - values: ['now'], - originalValue: ['now'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.interval', - values: ['5m'], - originalValue: ['5m'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.created_by', - values: ['elastic'], - originalValue: ['elastic'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.created_by', - values: ['elastic'], - originalValue: ['elastic'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.timestamp_override', - values: ['event.ingested'], - originalValue: ['event.ingested'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.ingested', - values: ['2022-09-29T19:37:00.000Z'], - originalValue: ['2022-09-29T19:37:00.000Z'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.id', - values: ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - originalValue: ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.parent.entity_id', - values: ['kj0le842x0'], - originalValue: ['kj0le842x0'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.risk_score', - values: ['47'], - originalValue: ['47'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.reason', - values: [ - 'malware event with process explorer.exe, on Host-4cfuh42w7g created medium alert Endpoint Security.', - ], - originalValue: [ - 'malware event with process explorer.exe, on Host-4cfuh42w7g created medium alert Endpoint Security.', - ], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.os.name', - values: ['Windows'], - originalValue: ['Windows'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.name', - values: ['Endpoint Security'], - originalValue: ['Endpoint Security'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.name', - values: ['Host-4cfuh42w7g'], - originalValue: ['Host-4cfuh42w7g'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.status', - values: ['open'], - originalValue: ['open'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.kind', - values: ['signal'], - originalValue: ['signal'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.severity_mapping.value', - values: ['21', '47', '73', '99'], - originalValue: ['21', '47', '73', '99'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.tags', - values: ['Elastic', 'Endpoint Security'], - originalValue: ['Elastic', 'Endpoint Security'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.created_at', - values: ['2022-09-29T19:39:38.137Z'], - originalValue: ['2022-09-29T19:39:38.137Z'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.workflow_status', - values: ['open'], - originalValue: ['open'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.policy.applied.status', - values: ['warning'], - originalValue: ['warning'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.uuid', - values: ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - originalValue: ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.category', - values: ['malware'], - originalValue: ['malware'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.malware_classification.threshold', - values: ['0'], - originalValue: ['0'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.reason', - values: [ - 'malware event with process explorer.exe, on Host-4cfuh42w7g created medium alert Endpoint Security.', - ], - originalValue: [ - 'malware event with process explorer.exe, on Host-4cfuh42w7g created medium alert Endpoint Security.', - ], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.pe.architecture', - values: ['x64'], - originalValue: ['x64'], - isObjectArray: false, - }, - { - category: 'data_stream', - field: 'data_stream.type', - values: ['logs'], - originalValue: ['logs'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_time', - values: ['2022-10-09T07:14:42.194Z'], - originalValue: ['2022-10-09T07:14:42.194Z'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.ancestors.id', - values: ['7L3AioMBWJvcpv7vlX2O'], - originalValue: ['7L3AioMBWJvcpv7vlX2O'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.name', - values: ['explorer.exe'], - originalValue: ['explorer.exe'], - isObjectArray: false, - }, - { - category: 'ecs', - field: 'ecs.version', - values: ['1.6.0'], - originalValue: ['1.6.0'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.severity', - values: ['medium'], - originalValue: ['medium'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.ancestors.index', - values: ['.ds-logs-endpoint.alerts-default-2022.09.29-000001'], - originalValue: ['.ds-logs-endpoint.alerts-default-2022.09.29-000001'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.configuration.isolation', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'Memory_protection', - field: 'Memory_protection.feature', - values: ['signature'], - originalValue: ['signature'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.code_signature.trusted', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.code_signature.trusted', - values: ['false'], - originalValue: ['false'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.depth', - values: ['1'], - originalValue: ['1'], - isObjectArray: false, - }, - { - category: 'agent', - field: 'agent.version', - values: ['8.6.0'], - originalValue: ['8.6.0'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.risk_score_mapping.operator', - values: ['equals'], - originalValue: ['equals'], - isObjectArray: false, - }, - { - category: 'host', - field: 'host.os.family', - values: ['windows'], - originalValue: ['windows'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.from', - values: ['now-10m'], - originalValue: ['now-10m'], - isObjectArray: false, - }, - { - category: 'Memory_protection', - field: 'Memory_protection.self_injection', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.start', - values: ['2022-10-09T07:14:42.194Z'], - originalValue: ['2022-10-09T07:14:42.194Z'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.severity_mapping.severity', - values: ['low', 'medium', 'high', 'critical'], - originalValue: ['low', 'medium', 'high', 'critical'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.severity_mapping.field', - values: ['event.severity'], - originalValue: ['event.severity'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.severity_mapping.value', - values: ['21', '47', '73', '99'], - originalValue: ['21', '47', '73', '99'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.severity_mapping.operator', - values: ['equals'], - originalValue: ['equals'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.references', - values: [], - originalValue: [], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.description', - values: [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - originalValue: [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.language', - values: ['kuery'], - originalValue: ['kuery'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.type', - values: ['query'], - originalValue: ['query'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.rule_name_override', - values: ['message'], - originalValue: ['message'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.exceptions_list.list_id', - values: ['endpoint_list'], - originalValue: ['endpoint_list'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.exceptions_list.namespace_type', - values: ['agnostic'], - originalValue: ['agnostic'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.exceptions_list.id', - values: ['endpoint_list'], - originalValue: ['endpoint_list'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.exceptions_list.type', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.timestamp_override', - values: ['event.ingested'], - originalValue: ['event.ingested'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.from', - values: ['now-10m'], - originalValue: ['now-10m'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.severity', - values: ['medium'], - originalValue: ['medium'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.max_signals', - values: ['10000'], - originalValue: ['10000'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.risk_score', - values: ['47'], - originalValue: ['47'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.risk_score_mapping.field', - values: ['event.risk_score'], - originalValue: ['event.risk_score'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.risk_score_mapping.value', - values: [''], - originalValue: [''], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.risk_score_mapping.operator', - values: ['equals'], - originalValue: ['equals'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.author', - values: ['Elastic'], - originalValue: ['Elastic'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.query', - values: ['event.kind:alert and event.module:(endpoint and not endgame)\n'], - originalValue: ['event.kind:alert and event.module:(endpoint and not endgame)\n'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.index', - values: ['logs-endpoint.alerts-*'], - originalValue: ['logs-endpoint.alerts-*'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.version', - values: ['100'], - originalValue: ['100'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.rule_id', - values: ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - originalValue: ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.license', - values: ['Elastic License v2'], - originalValue: ['Elastic License v2'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.required_fields.ecs', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.required_fields.name', - values: ['event.kind', 'event.module'], - originalValue: ['event.kind', 'event.module'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.required_fields.type', - values: ['keyword'], - originalValue: ['keyword'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.immutable', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.related_integrations', - values: [], - originalValue: [], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.setup', - values: [''], - originalValue: [''], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.false_positives', - values: [], - originalValue: [], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.threat', - values: [], - originalValue: [], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.to', - values: ['now'], - originalValue: ['now'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.version', - values: ['100'], - originalValue: ['100'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.kind', - values: ['alert'], - originalValue: ['alert'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.status', - values: ['active'], - originalValue: ['active'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.severity_mapping.field', - values: ['event.severity', 'event.severity', 'event.severity', 'event.severity'], - originalValue: ['event.severity', 'event.severity', 'event.severity', 'event.severity'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.dataset', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.depth', - values: ['1'], - originalValue: ['1'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.immutable', - values: ['true'], - originalValue: ['true'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.group_leader.pid', - values: ['116'], - originalValue: ['116'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.sequence', - values: ['1232'], - originalValue: ['1232'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.rule_type_id', - values: ['siem.queryRule'], - originalValue: ['siem.queryRule'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.session_leader.name', - values: ['fake session'], - originalValue: ['fake session'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.name', - values: ['Endpoint Security'], - originalValue: ['Endpoint Security'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.rule_id', - values: ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - originalValue: ['9a1a2dae-0b5f-4c3d-8305-a268d404c306'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.module', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.hash.sha1', - values: ['ca85243c0af6a6471bdaa560685c51eefd6dbc0d'], - originalValue: ['ca85243c0af6a6471bdaa560685c51eefd6dbc0d'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.severity_mapping.operator', - values: ['equals', 'equals', 'equals', 'equals'], - originalValue: ['equals', 'equals', 'equals', 'equals'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.malware_signature.all_names', - values: ['Windows.Trojan.FakeAgent'], - originalValue: ['Windows.Trojan.FakeAgent'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.license', - values: ['Elastic License v2'], - originalValue: ['Elastic License v2'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.kind', - values: ['alert'], - originalValue: ['alert'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.executable', - values: ['C:/fake/explorer.exe'], - originalValue: ['C:/fake/explorer.exe'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.updated_at', - values: ['2022-09-29T19:39:38.137Z'], - originalValue: ['2022-09-29T19:39:38.137Z'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.description', - values: [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - originalValue: [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.Ext.mapped_size', - values: ['0'], - originalValue: ['0'], - isObjectArray: false, - }, - { - category: 'data_stream', - field: 'data_stream.namespace', - values: ['default'], - originalValue: ['default'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.author', - values: ['Elastic'], - originalValue: ['Elastic'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.code_signature.subject_name', - values: ['Cybereason Inc'], - originalValue: ['Cybereason Inc'], - isObjectArray: false, - }, - { - category: 'user', - field: 'user.name', - values: ['root'], - originalValue: ['root'], - isObjectArray: false, - }, - { - category: 'source', - field: 'source.ip', - values: ['10.184.3.46'], - originalValue: ['10.184.3.46'], - isObjectArray: false, - }, - { - category: 'Endpoint', - field: 'Endpoint.policy.applied.endpoint_policy_version', - values: ['3'], - originalValue: ['3'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_event.sequence', - values: ['1232'], - originalValue: ['1232'], - isObjectArray: false, - }, - { - category: 'dll', - field: 'dll.path', - values: ['C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe'], - originalValue: ['C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.user', - values: ['SYSTEM'], - originalValue: ['SYSTEM'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.original_event.action', - values: ['start'], - originalValue: ['start'], - isObjectArray: false, - }, - { - category: 'signal', - field: 'signal.rule.to', - values: ['now'], - originalValue: ['now'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.created_at', - values: ['2022-09-29T19:39:38.137Z'], - originalValue: ['2022-09-29T19:39:38.137Z'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.Ext.malware_signature.identifier', - values: ['diagnostic-malware-signature-v1-fake'], - originalValue: ['diagnostic-malware-signature-v1-fake'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.exceptions_list.namespace_type', - values: ['agnostic'], - originalValue: ['agnostic'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.type', - values: ['info'], - originalValue: ['info'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.space_ids', - values: ['default'], - originalValue: ['default'], - isObjectArray: false, - }, - { - category: 'process', - field: 'process.entry_leader.entity_id', - values: ['b74mw1jkrm'], - originalValue: ['b74mw1jkrm'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.exceptions_list.id', - values: ['endpoint_list'], - originalValue: ['endpoint_list'], - isObjectArray: false, - }, - { - category: 'event', - field: 'event.dataset', - values: ['endpoint'], - originalValue: ['endpoint'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.original_time', - values: ['2022-10-09T07:14:42.194Z'], - originalValue: ['2022-10-09T07:14:42.194Z'], - isObjectArray: false, - }, - { - category: '_index', - field: '_index', - values: ['.internal.alerts-security.alerts-default-000001'], - originalValue: ['.internal.alerts-security.alerts-default-000001'], - isObjectArray: false, - }, - { - category: '_id', - field: '_id', - values: ['f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325'], - originalValue: ['f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325'], - isObjectArray: false, - }, - { - category: '_score', - field: '_score', - values: ['1'], - originalValue: ['1'], - isObjectArray: false, - }, -]; - -export const getMockAlertNestedDetailsTimelineResponse = (): Ecs => ({ - _id: 'f6aa8643ecee466753c45308ea8dc72aba0a44e1faac5f6183fd2ad6666c1325', - timestamp: '2022-09-29T19:40:26.051Z', - _index: '.internal.alerts-security.alerts-default-000001', - kibana: { - alert: { - rule: { - from: ['now-10m'], - name: ['Endpoint Security'], - to: ['now'], - uuid: ['738e91f2-402e-11ed-be15-7be3bb26d7b2'], - type: ['query'], - version: ['100'], - parameters: {}, - }, - workflow_status: ['open'], - original_time: ['2022-10-09T07:14:42.194Z'], - severity: ['medium'], - }, - }, - event: { - code: ['memory_signature'], - module: ['endpoint'], - action: ['start'], - category: ['malware'], - dataset: ['endpoint'], - id: ['7799e1d5-5dc1-4173-9d11-562496cd863b'], - kind: ['signal'], - type: ['info'], - }, - host: { - name: ['Host-4cfuh42w7g'], - os: { - family: ['windows'], - name: ['Windows'], - }, - id: ['04794e4e-59cb-4c4a-a8ee-3e6c5b65743c'], - ip: ['10.184.3.36', '10.170.218.86'], - }, - source: { - ip: ['10.184.3.46'], - }, - agent: { - type: ['endpoint'], - id: ['d08ed3f8-9852-4d0c-a5b1-b48060705369'], - }, - process: { - hash: { - md5: ['fake md5'], - sha1: ['fake sha1'], - sha256: ['fake sha256'], - }, - parent: { - pid: [1], - }, - pid: [2], - name: ['explorer.exe'], - entity_id: ['d3v4to81q9'], - executable: ['C:/fake/explorer.exe'], - entry_leader: { - entity_id: ['b74mw1jkrm'], - name: ['fake entry'], - pid: ['865'], - }, - session_leader: { - entity_id: ['b74mw1jkrm'], - name: ['fake session'], - pid: ['745'], - }, - group_leader: { - entity_id: ['b74mw1jkrm'], - name: ['fake leader'], - pid: ['116'], - }, - }, - user: { - name: ['root'], - }, -}); - -export const mockAlertDetailsFieldsResponse = getMockAlertDetailsFieldsResponse(); - -export const mockAlertDetailsTimelineResponse = getMockAlertDetailsTimelineResponse(); - -export const mockAlertNestedDetailsTimelineResponse = getMockAlertNestedDetailsTimelineResponse(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/index.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/index.ts deleted file mode 100644 index 0771ffa5ccf9f2..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/__mocks__/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './alert_details_response'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/error_page.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/error_page.tsx deleted file mode 100644 index 35386ecf28dc29..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/error_page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiCode, EuiEmptyPrompt } from '@elastic/eui'; -import { ERROR_PAGE_TITLE, ERROR_PAGE_BODY } from '../translations'; - -export const AlertDetailsErrorPage = memo(({ eventId }: { eventId: string }) => { - return ( - {ERROR_PAGE_TITLE}} - body={ -
-

{ERROR_PAGE_BODY}

-

- {`_id: ${eventId}`} -

-
- } - /> - ); -}); - -AlertDetailsErrorPage.displayName = 'AlertDetailsErrorPage'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/header.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/header.tsx deleted file mode 100644 index 67a45a00982661..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; -import { HeaderPage } from '../../../../common/components/header_page'; -import { ALERT_DETAILS_TECHNICAL_PREVIEW } from '../translations'; - -interface AlertDetailsHeaderProps { - loading: boolean; - ruleName?: string; - timestamp?: string; -} - -export const AlertDetailsHeader = React.memo( - ({ loading, ruleName, timestamp }: AlertDetailsHeaderProps) => { - return ( - : ''} - title={ruleName} - /> - ); - } -); - -AlertDetailsHeader.displayName = 'AlertDetailsHeader'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/loading_page.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/loading_page.tsx deleted file mode 100644 index ee24b2e6368744..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/components/loading_page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; -import { LOADING_PAGE_MESSAGE } from '../translations'; - -export const AlertDetailsLoadingPage = memo(({ eventId }: { eventId: string }) => ( - } - body={

{LOADING_PAGE_MESSAGE}

} - /> -)); - -AlertDetailsLoadingPage.displayName = 'AlertDetailsLoadingPage'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/index.test.tsx deleted file mode 100644 index bee3abe3bc1565..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/index.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Router, useParams } from 'react-router-dom'; -import { render } from '@testing-library/react'; -import { AlertDetailsPage } from '.'; -import { TestProviders } from '../../../common/mock'; -import { - mockAlertDetailsFieldsResponse, - mockAlertDetailsTimelineResponse, - mockAlertNestedDetailsTimelineResponse, -} from './__mocks__'; -import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; -import { useTimelineEventsDetails } from '../../../timelines/containers/details'; - -// Node modules mocks -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => mockDispatch, -})); - -(useParams as jest.Mock).mockReturnValue(mockAlertDetailsFieldsResponse._id); - -// Internal Mocks -jest.mock('../../../timelines/containers/details'); -jest.mock('../../../timelines/store/timeline', () => ({ - ...jest.requireActual('../../../timelines/store/timeline'), - timelineActions: { - createTimeline: jest.fn().mockReturnValue('new-timeline'), - }, -})); - -jest.mock('../../../common/containers/sourcerer', () => { - const mockSourcererReturn = { - browserFields: {}, - loading: true, - indexPattern: {}, - selectedPatterns: [], - missingPatterns: [], - }; - return { - useSourcererDataView: jest.fn().mockReturnValue(mockSourcererReturn), - }; -}); - -type Action = 'PUSH' | 'POP' | 'REPLACE'; -const pop: Action = 'POP'; -const getMockHistory = () => ({ - length: 1, - location: { - pathname: `/alerts/${mockAlertDetailsFieldsResponse._id}/summary`, - search: '', - state: '', - hash: '', - }, - action: pop, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - createHref: jest.fn(), - listen: jest.fn(), -}); - -describe('Alert Details Page', () => { - it('should render the loading page', () => { - (useTimelineEventsDetails as jest.Mock).mockReturnValue([true, null, null, null, jest.fn()]); - const { getByTestId } = render( - - - - - - ); - - expect(getByTestId('alert-details-page-loading')).toBeVisible(); - }); - - it('should render the error page', () => { - (useTimelineEventsDetails as jest.Mock).mockReturnValue([false, null, null, null, jest.fn()]); - const { getByTestId } = render( - - - - - - ); - - expect(getByTestId('alert-details-page-error')).toBeVisible(); - }); - - it('should render the header', () => { - (useTimelineEventsDetails as jest.Mock).mockReturnValue([ - false, - mockAlertDetailsTimelineResponse, - mockAlertDetailsFieldsResponse, - mockAlertNestedDetailsTimelineResponse, - jest.fn(), - ]); - const { getByTestId } = render( - - - - - - ); - - expect(getByTestId('header-page-title')).toHaveTextContent( - mockAlertDetailsFieldsResponse.fields[ALERT_RULE_NAME][0] - ); - }); - - it('should create a timeline', () => { - (useTimelineEventsDetails as jest.Mock).mockReturnValue([ - false, - mockAlertDetailsTimelineResponse, - mockAlertDetailsFieldsResponse, - mockAlertNestedDetailsTimelineResponse, - jest.fn(), - ]); - render( - - - - - - ); - - expect(mockDispatch).toHaveBeenCalledWith('new-timeline'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/index.tsx deleted file mode 100644 index 8935ff132f2463..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useEffect, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; -import { Routes, Route } from '@kbn/shared-ux-router'; -import { ALERT_RULE_NAME, TIMESTAMP } from '@kbn/rule-data-utils'; -import { EuiSpacer } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; -import type { RunTimeMappings } from '../../../../common/api/search_strategy'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { TimelineId } from '../../../../common/types/timeline'; -import { useGetFieldsData } from '../../../common/hooks/use_get_fields_data'; -import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { SpyRoute } from '../../../common/utils/route/spy_routes'; -import { getAlertDetailsTabUrl } from '../../../common/components/link_to'; -import { AlertDetailRouteType } from './types'; -import { TabNavigation } from '../../../common/components/navigation/tab_navigation'; -import { getAlertDetailsNavTabs } from './utils/navigation'; -import { SecurityPageName } from '../../../../common/constants'; -import { eventID } from '../../../../common/endpoint/models/event'; -import { useTimelineEventsDetails } from '../../../timelines/containers/details'; -import { AlertDetailsLoadingPage } from './components/loading_page'; -import { AlertDetailsErrorPage } from './components/error_page'; -import { AlertDetailsHeader } from './components/header'; -import { DetailsSummaryTab } from './tabs/summary'; - -// eslint-disable-next-line react/display-name -export const AlertDetailsPage = memo(() => { - const { detailName: eventId } = useParams<{ detailName: string }>(); - const dispatch = useDispatch(); - const sourcererDataView = useSourcererDataView(SourcererScopeName.detections); - const indexName = useMemo( - () => sourcererDataView.selectedPatterns.join(','), - [sourcererDataView.selectedPatterns] - ); - - const [loading, detailsData, searchHit, dataAsNestedObject] = useTimelineEventsDetails({ - indexName, - eventId, - runtimeMappings: sourcererDataView.runtimeMappings as RunTimeMappings, - skip: !eventID, - }); - const dataNotFound = !loading && !detailsData; - const hasData = !loading && detailsData; - - // Example of using useGetFieldsData. Only place it is used currently - const getFieldsData = useGetFieldsData(searchHit?.fields); - const timestamp = getFieldsData(TIMESTAMP) as string | undefined; - const ruleName = getFieldsData(ALERT_RULE_NAME) as string | undefined; - - useEffect(() => { - // TODO: move detail panel to it's own redux state - dispatch( - timelineActions.createTimeline({ - id: TimelineId.detectionsAlertDetailsPage, - columns: [], - dataViewId: null, - indexNames: [], - expandedDetail: {}, - show: false, - }) - ); - }, [dispatch]); - - return ( - <> - {loading && } - {dataNotFound && } - {hasData && ( - <> - - - - - - - - - - )} - - - ); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/alert_render_panel.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/alert_render_panel.test.tsx deleted file mode 100644 index c3952801e5ca41..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/alert_render_panel.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { get } from 'lodash/fp'; -import { render } from '@testing-library/react'; -import { AlertRendererPanel } from '.'; -import { TestProviders } from '../../../../../../common/mock'; -import { mockAlertNestedDetailsTimelineResponse } from '../../../__mocks__'; -import { ALERT_RENDERER_FIELDS } from '../../../../../../timelines/components/timeline/body/renderers/alert_renderer'; - -describe('AlertDetailsPage - SummaryTab - AlertRendererPanel', () => { - it('should render the reason renderer', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('alert-renderer-panel')).toBeVisible(); - }); - - it('should render the render the expected values', () => { - const { getByTestId } = render( - - - - ); - const alertRendererPanelPanel = getByTestId('alert-renderer-panel'); - - ALERT_RENDERER_FIELDS.forEach((rendererField) => { - const fieldValues: string[] | null = get( - rendererField, - mockAlertNestedDetailsTimelineResponse - ); - if (fieldValues && fieldValues.length > 0) { - fieldValues.forEach((value) => { - expect(alertRendererPanelPanel).toHaveTextContent(value); - }); - } - }); - }); - - it('should not render the reason renderer if data is not provided', () => { - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('alert-renderer-panel')).toBeNull(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/index.tsx deleted file mode 100644 index 0dec93eb40b248..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/alert_renderer_panel/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import { defaultRowRenderers } from '../../../../../../timelines/components/timeline/body/renderers'; -import { getRowRenderer } from '../../../../../../timelines/components/timeline/body/renderers/get_row_renderer'; -import { TimelineId } from '../../../../../../../common/types/timeline'; -import { SummaryPanel } from '../wrappers'; -import { ALERT_REASON_PANEL_TITLE } from '../translation'; - -export interface AlertRendererPanelProps { - dataAsNestedObject: Ecs | null; -} - -const RendererContainer = styled.div` - overflow-x: auto; - margin-left: -24px; - - & .euiFlexGroup { - justify-content: flex-start; - } -`; - -export const AlertRendererPanel = React.memo(({ dataAsNestedObject }: AlertRendererPanelProps) => { - const renderer = useMemo( - () => - dataAsNestedObject != null - ? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }) - : null, - [dataAsNestedObject] - ); - - return ( - - {renderer != null && dataAsNestedObject != null && ( -
- - {renderer.renderRow({ - data: dataAsNestedObject, - isDraggable: false, - scopeId: TimelineId.detectionsAlertDetailsPage, - })} - -
- )} -
- ); -}); - -AlertRendererPanel.displayName = 'AlertRendererPanel'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel.test.tsx deleted file mode 100644 index 32905019eb3f8b..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import type { RelatedCase } from '@kbn/cases-plugin/common'; -import { CasesPanel, CASES_PANEL_CASES_COUNT_MAX } from '.'; -import { TestProviders } from '../../../../../../common/mock'; -import { - mockAlertDetailsTimelineResponse, - mockAlertNestedDetailsTimelineResponse, -} from '../../../__mocks__'; -import { ERROR_LOADING_CASES, LOADING_CASES } from '../translation'; -import { useGetRelatedCasesByEvent } from '../../../../../../common/containers/cases/use_get_related_cases_by_event'; -import { useGetUserCasesPermissions } from '../../../../../../common/lib/kibana'; -import { CaseStatuses } from '@kbn/cases-components'; - -jest.mock('../../../../../../common/containers/cases/use_get_related_cases_by_event'); -jest.mock('../../../../../../common/lib/kibana'); - -const defaultPanelProps = { - eventId: mockAlertNestedDetailsTimelineResponse._id, - dataAsNestedObject: mockAlertNestedDetailsTimelineResponse, - detailsData: mockAlertDetailsTimelineResponse, -}; - -describe('AlertDetailsPage - SummaryTab - CasesPanel', () => { - describe('No data', () => { - beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - create: true, - update: true, - }); - }); - it('should render the loading panel', () => { - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: true, - }); - const { getByText } = render( - - - - ); - expect(getByText(LOADING_CASES)).toBeVisible(); - }); - - it('should render the error panel if an error is returned', () => { - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - error: true, - }); - const { getByText } = render( - - - - ); - - expect(getByText(ERROR_LOADING_CASES)).toBeVisible(); - }); - - it('should render the error panel if data is undefined', () => { - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - error: false, - relatedCases: undefined, - }); - const { getByText } = render( - - - - ); - - expect(getByText(ERROR_LOADING_CASES)).toBeVisible(); - }); - - describe('Partial permissions', () => { - it('should only render the add to new case button', () => { - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - relatedCases: [], - }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - create: true, - update: false, - }); - const { getByTestId, queryByTestId } = render( - - - - ); - - expect(getByTestId('add-to-new-case-button')).toBeVisible(); - expect(queryByTestId('add-to-existing-case-button')).toBe(null); - }); - - it('should only render the add to existing case button', () => { - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - relatedCases: [], - }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - create: false, - update: true, - }); - const { getByTestId, queryByTestId } = render( - - - - ); - - expect(getByTestId('add-to-existing-case-button')).toBeVisible(); - expect(queryByTestId('add-to-new-case-button')).toBe(null); - }); - - it('should render both add to new case and add to existing case buttons', () => { - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - relatedCases: [], - }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - create: true, - update: true, - }); - const { getByTestId, queryByTestId } = render( - - - - ); - - expect(getByTestId('add-to-new-case-button')).toBeVisible(); - expect(queryByTestId('add-to-existing-case-button')).toBeVisible(); - }); - }); - }); - describe('has a single related cases', () => { - const mockRelatedCase: RelatedCase = { - createdAt: '2022-11-04T17:22:13.267Z', - title: 'test case', - description: 'Test case description', - status: CaseStatuses.open, - id: 'test-case-id', - totals: { - alerts: 2, - userComments: 4, - }, - }; - - beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - create: true, - update: true, - }); - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - relatedCases: [mockRelatedCase], - }); - }); - - it('should show the related case', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('case-panel')).toHaveTextContent(mockRelatedCase.title); - expect(getByTestId('case-panel')).toHaveTextContent(mockRelatedCase.description); - expect(getByTestId('case-panel')).toHaveTextContent(`${mockRelatedCase.totals.alerts}`); - expect(getByTestId('case-panel')).toHaveTextContent(`${mockRelatedCase.totals.userComments}`); - }); - }); - describe(`has more than ${CASES_PANEL_CASES_COUNT_MAX} related cases`, () => { - const mockRelatedCase: RelatedCase = { - createdAt: '2022-11-04T17:22:13.267Z', - title: 'test case', - description: 'Test case description', - status: CaseStatuses.open, - id: 'test-case-id', - totals: { - alerts: 2, - userComments: 4, - }, - }; - - const mockRelatedCaseList = Array.from(Array(CASES_PANEL_CASES_COUNT_MAX + 3).keys()).map( - (position) => ({ - ...mockRelatedCase, - title: `test case ${position + 1}`, - id: `test-case-id-${position + 1}`, - }) - ); - - beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - create: true, - update: true, - }); - (useGetRelatedCasesByEvent as jest.Mock).mockReturnValue({ - loading: false, - relatedCases: mockRelatedCaseList, - }); - }); - - it('should show the related case', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('case-panel')).toHaveTextContent( - `test case ${CASES_PANEL_CASES_COUNT_MAX}` - ); - expect(getByTestId('case-panel')).not.toHaveTextContent( - `test case ${CASES_PANEL_CASES_COUNT_MAX + 1}` - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel_actions.tsx deleted file mode 100644 index 4ffc16603cb0b9..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/cases_panel_actions.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; -import type { CasesPermissions } from '@kbn/cases-plugin/common'; -import React, { useCallback, useMemo, useState } from 'react'; -import type { CasesPanelProps } from '.'; -import { - ADD_TO_EXISTING_CASE_BUTTON, - ADD_TO_NEW_CASE_BUTTON, - SUMMARY_PANEL_ACTIONS, -} from '../translation'; - -export const CASES_PANEL_ACTIONS_CLASS = 'cases-panel-actions-trigger'; - -export interface CasesPanelActionsProps extends CasesPanelProps { - addToNewCase: () => void; - addToExistingCase: () => void; - className?: string; - userCasesPermissions: CasesPermissions; -} - -export const CasesPanelActions = React.memo( - ({ - addToNewCase, - addToExistingCase, - className, - userCasesPermissions, - }: CasesPanelActionsProps) => { - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = () => { - setPopover(false); - }; - - const items = useMemo(() => { - const options = []; - - if (userCasesPermissions.create) { - options.push( - - {ADD_TO_NEW_CASE_BUTTON} - - ); - } - - if (userCasesPermissions.update) { - options.push( - - {ADD_TO_EXISTING_CASE_BUTTON} - - ); - } - return options; - }, [addToExistingCase, addToNewCase, userCasesPermissions.create, userCasesPermissions.update]); - - const button = useMemo( - () => ( - - ), - [onButtonClick] - ); - - return ( -
- - - -
- ); - } -); - -CasesPanelActions.displayName = 'CasesPanelActions'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/index.tsx deleted file mode 100644 index 72b469780097bd..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/index.tsx +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useMemo } from 'react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, -} from '@elastic/eui'; -import type { Ecs } from '@kbn/cases-plugin/common'; -import { AttachmentType } from '@kbn/cases-plugin/common'; -import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import styled from 'styled-components'; -import type { TimelineEventsDetailsItem } from '../../../../../../../common/search_strategy'; -import { useGetUserCasesPermissions, useKibana } from '../../../../../../common/lib/kibana'; -import { useGetRelatedCasesByEvent } from '../../../../../../common/containers/cases/use_get_related_cases_by_event'; -import { - ADD_TO_EXISTING_CASE_BUTTON, - ADD_TO_NEW_CASE_BUTTON, - CASES_PANEL_SUBTITLE, - CASES_PANEL_TITLE, - CASE_NO_READ_PERMISSIONS, - ERROR_LOADING_CASES, - LOADING_CASES, - NO_RELATED_CASES_FOUND, -} from '../translation'; -import { SummaryPanel } from '../wrappers'; -import { CasesPanelActions, CASES_PANEL_ACTIONS_CLASS } from './cases_panel_actions'; -import { RelatedCasesList } from './related_case'; - -export interface CasesPanelProps { - eventId: string; - dataAsNestedObject: Ecs | null; - detailsData: TimelineEventsDetailsItem[]; -} - -const StyledCasesFlexGroup = styled(EuiFlexGroup)` - max-height: 300px; - overflow-y: auto; -`; - -/** - * There is currently no api limit for the number of cases that can be returned - * To prevent the UI from growing too large, we limit to 25 most recent cases - */ -export const CASES_PANEL_CASES_COUNT_MAX = 25; - -const CasesPanelLoading = () => ( - } - title={

{LOADING_CASES}

} - titleSize="xxs" - /> -); - -const CasesPanelError = () => <>{ERROR_LOADING_CASES}; - -export const CasesPanelNoReadPermissions = () => ; - -export const CasesPanel = React.memo( - ({ eventId, dataAsNestedObject, detailsData }) => { - const { cases: casesUi } = useKibana().services; - const { loading, error, relatedCases, refetchRelatedCases } = - useGetRelatedCasesByEvent(eventId); - const userCasesPermissions = useGetUserCasesPermissions(); - - const caseAttachments: CaseAttachmentsWithoutOwner = useMemo(() => { - return dataAsNestedObject - ? [ - { - alertId: eventId, - index: dataAsNestedObject._index ?? '', - type: AttachmentType.alert, - rule: casesUi.helpers.getRuleIdFromEvent({ - ecs: dataAsNestedObject, - data: detailsData, - }), - }, - ] - : []; - }, [casesUi.helpers, dataAsNestedObject, detailsData, eventId]); - - const createCaseFlyout = casesUi.hooks.useCasesAddToNewCaseFlyout({ - onSuccess: refetchRelatedCases, - }); - - const selectCaseModal = casesUi.hooks.useCasesAddToExistingCaseModal({ - onSuccess: refetchRelatedCases, - }); - - const addToNewCase = useCallback(() => { - if (userCasesPermissions.create) { - createCaseFlyout.open({ attachments: caseAttachments }); - } - }, [userCasesPermissions.create, createCaseFlyout, caseAttachments]); - - const addToExistingCase = useCallback(() => { - if (userCasesPermissions.update) { - selectCaseModal.open({ getAttachments: () => caseAttachments }); - } - }, [caseAttachments, selectCaseModal, userCasesPermissions.update]); - - const renderCasesActions = useCallback( - () => ( - - ), - [ - addToExistingCase, - addToNewCase, - dataAsNestedObject, - detailsData, - eventId, - userCasesPermissions, - ] - ); - - // Sort by most recently created being first - const relatedCasesCount = relatedCases ? relatedCases.length : 0; - const visibleCaseCount = useMemo( - () => Math.min(relatedCasesCount, CASES_PANEL_CASES_COUNT_MAX), - [relatedCasesCount] - ); - const hasRelatedCases = relatedCasesCount > 0; - - if (loading) return ; - - if (error || relatedCases === undefined) return ; - - return ( - - {hasRelatedCases ? ( - - - - ) : ( - - {userCasesPermissions.update && ( - - - {ADD_TO_EXISTING_CASE_BUTTON} - - - )} - {userCasesPermissions.create && ( - - - {ADD_TO_NEW_CASE_BUTTON} - - - )} - - } - /> - )} - - ); - } -); - -CasesPanel.displayName = 'CasesPanel'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/related_case.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/related_case.tsx deleted file mode 100644 index ef55a3d158a837..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/cases_panel/related_case.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiIcon, EuiText } from '@elastic/eui'; -import { Status } from '@kbn/cases-components'; -import type { RelatedCase } from '@kbn/cases-plugin/common'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { CaseDetailsLink } from '../../../../../../common/components/links'; -import { CASES_PANEL_CASE_STATUS } from '../translation'; - -const DescriptionText = styled(EuiText)` - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: normal; -`; - -const ChildFlexGroup = styled(EuiFlexGroup)` - margin: 0; -`; - -const StyledStatusText = styled.span` - margin-right: ${({ theme }) => theme.eui.euiSizeS}; -`; - -const StyledIcon = styled(EuiIcon)` - margin-right: ${({ theme }) => theme.eui.euiSizeS}; -`; - -export const RelatedCasesList = ({ - relatedCases, - maximumVisible, -}: { - relatedCases: RelatedCase[]; - maximumVisible?: number; -}) => { - // Sort related cases, showing the most recently created first. - const sortedRelatedCases = useMemo( - () => - relatedCases - ? relatedCases.sort( - (case1, case2) => - new Date(case2.createdAt).getTime() - new Date(case1.createdAt).getTime() - ) - : [], - [relatedCases] - ); - - // If a maximum visible count is provided, only show cases up to that amount - const visibleCases = useMemo( - () => - maximumVisible && maximumVisible > 0 - ? sortedRelatedCases.slice(0, maximumVisible) - : sortedRelatedCases, - [maximumVisible, sortedRelatedCases] - ); - - return ( - <> - {visibleCases?.map(({ id, title, description, status, totals }) => ( - - - {title} - - - - {description} - - - - - - {`${CASES_PANEL_CASE_STATUS}:`} - - - - - - - - - {totals.userComments} - - - - - - {totals.alerts} - - - - - - - ))} - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx deleted file mode 100644 index 304e03a27de9fd..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel.test.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { find } from 'lodash/fp'; -import { TestProviders } from '../../../../../../common/mock'; -import { - mockAlertDetailsTimelineResponse, - mockAlertNestedDetailsTimelineResponse, -} from '../../../__mocks__'; -import type { HostPanelProps } from '.'; -import { HostPanel } from '.'; -import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; -import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; -import { RiskSeverity } from '../../../../../../../common/search_strategy'; -import { useRiskScore } from '../../../../../../explore/containers/risk_score'; - -jest.mock('../../../../../../management/hooks', () => { - const Generator = jest.requireActual( - '../../../../../../../common/endpoint/data_generators/endpoint_metadata_generator' - ); - - return { - useGetEndpointDetails: jest.fn(() => { - return { - data: new Generator.EndpointMetadataGenerator('seed').generateHostInfo({ - metadata: { - Endpoint: { - state: { - isolation: true, - }, - }, - }, - }), - }; - }), - }; -}); - -jest.mock('../../../../../../explore/containers/risk_score'); -const mockUseRiskScore = useRiskScore as jest.Mock; - -jest.mock('../../../../../containers/detection_engine/alerts/use_host_isolation_status', () => { - return { - useHostIsolationStatus: jest.fn().mockReturnValue({ - loading: false, - isIsolated: false, - agentStatus: 'healthy', - }), - }; -}); - -describe('AlertDetailsPage - SummaryTab - HostPanel', () => { - const defaultRiskReturnValues = { - inspect: null, - refetch: () => {}, - isModuleEnabled: true, - isAuthorized: true, - loading: false, - }; - const HostPanelWithDefaultProps = (propOverrides: Partial) => ( - - - - ); - - beforeEach(() => { - mockUseRiskScore.mockReturnValue({ ...defaultRiskReturnValues }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should render basic host fields', () => { - const { getByTestId } = render(); - const simpleHostFields = ['host.name', 'host.os.name']; - - simpleHostFields.forEach((simpleHostField) => { - expect(getByTestId('host-panel')).toHaveTextContent( - getTimelineEventData(simpleHostField, mockAlertDetailsTimelineResponse) - ); - }); - }); - - describe('Agent status', () => { - it('should show healthy', () => { - const { getByTestId } = render(); - expect(getByTestId('endpointHostAgentStatus').textContent).toEqual('HealthyIsolated'); - }); - }); - - describe('host risk', () => { - it('should not show risk if the license is not valid', () => { - mockUseRiskScore.mockReturnValue({ - ...defaultRiskReturnValues, - isAuthorized: false, - data: null, - }); - const { queryByTestId } = render(); - expect(queryByTestId('host-panel-risk')).toBe(null); - }); - - it('should render risk fields', () => { - const calculatedScoreNorm = 98.9; - const calculatedLevel = RiskSeverity.critical; - - mockUseRiskScore.mockReturnValue({ - ...defaultRiskReturnValues, - isAuthorized: true, - data: [ - { - host: { - name: mockAlertNestedDetailsTimelineResponse.host?.name, - risk: { - calculated_score_norm: calculatedScoreNorm, - calculated_level: calculatedLevel, - }, - }, - }, - ], - }); - const { getByTestId } = render(); - - expect(getByTestId('host-panel-risk')).toHaveTextContent( - `${Math.round(calculatedScoreNorm)}` - ); - expect(getByTestId('host-panel-risk')).toHaveTextContent(calculatedLevel); - }); - }); - - describe('host ip', () => { - it('should render all the ip fields', () => { - const { getByTestId } = render(); - const ipFields = find( - { field: 'host.ip', category: 'host' }, - mockAlertDetailsTimelineResponse - )?.values as string[]; - expect(getByTestId('host-panel-ip')).toHaveTextContent(ipFields[0]); - expect(getByTestId('host-panel-ip')).toHaveTextContent('+1 More'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel_actions.tsx deleted file mode 100644 index d078785bf93f8a..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/host_panel_actions.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import { SecurityPageName } from '../../../../../../app/types'; -import { useGetSecuritySolutionLinkProps } from '../../../../../../common/components/links'; -import { getHostDetailsUrl } from '../../../../../../common/components/link_to'; - -import { OPEN_HOST_DETAILS_PAGE, SUMMARY_PANEL_ACTIONS, VIEW_HOST_SUMMARY } from '../translation'; - -export const HOST_PANEL_ACTIONS_CLASS = 'host-panel-actions-trigger'; - -export const HostPanelActions = React.memo( - ({ - className, - openHostDetailsPanel, - hostName, - }: { - className?: string; - hostName: string; - openHostDetailsPanel: (hostName: string) => void; - }) => { - const [isPopoverOpen, setPopover] = useState(false); - const { href } = useGetSecuritySolutionLinkProps()({ - deepLinkId: SecurityPageName.hosts, - path: getHostDetailsUrl(hostName), - }); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = () => { - setPopover(false); - }; - - const handleOpenHostDetailsPanel = useCallback(() => { - openHostDetailsPanel(hostName); - closePopover(); - }, [hostName, openHostDetailsPanel]); - - const items = useMemo( - () => [ - - {VIEW_HOST_SUMMARY} - , - - {OPEN_HOST_DETAILS_PAGE} - , - ], - [handleOpenHostDetailsPanel, href] - ); - - const button = useMemo( - () => ( - - ), - [onButtonClick] - ); - - return ( -
- - - -
- ); - } -); - -HostPanelActions.displayName = 'HostPanelActions'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx deleted file mode 100644 index 2688dd5cabf3c5..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/host_panel/index.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import React, { useCallback, useMemo } from 'react'; -import { find } from 'lodash/fp'; -import type { EuiFlexItemProps } from '@elastic/eui'; -import { TimelineId } from '../../../../../../../common/types/timeline'; -import { isAlertFromEndpointEvent } from '../../../../../../common/utils/endpoint_alert_check'; -import { SummaryValueCell } from '../../../../../../common/components/event_details/table/summary_value_cell'; -import { useRiskScore } from '../../../../../../explore/containers/risk_score'; -import { RiskScoreEntity } from '../../../../../../../common/search_strategy'; -import { getEmptyTagValue } from '../../../../../../common/components/empty_value'; -import { RiskScoreLevel } from '../../../../../../explore/components/risk_score/severity/common'; -import { - FirstLastSeen, - FirstLastSeenType, -} from '../../../../../../common/components/first_last_seen'; -import { DefaultFieldRenderer } from '../../../../../../timelines/components/field_renderers/field_renderers'; -import { HostDetailsLink, NetworkDetailsLink } from '../../../../../../common/components/links'; -import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model'; -import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; -import { getEnrichedFieldInfo } from '../../../../../../common/components/event_details/helpers'; -import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; -import { - AGENT_STATUS_TITLE, - HOST_NAME_TITLE, - HOST_PANEL_TITLE, - HOST_RISK_LEVEL, - HOST_RISK_SCORE, - IP_ADDRESSES_TITLE, - LAST_SEEN_TITLE, - OPERATING_SYSTEM_TITLE, -} from '../translation'; -import { SummaryPanel } from '../wrappers'; -import { HostPanelActions, HOST_PANEL_ACTIONS_CLASS } from './host_panel_actions'; - -export interface HostPanelProps { - data: TimelineEventsDetailsItem[]; - id: string; - openHostDetailsPanel: (hostName: string, onClose?: (() => void) | undefined) => void; - selectedPatterns: SelectedDataView['selectedPatterns']; - browserFields: SelectedDataView['browserFields']; -} - -const HostPanelSection: React.FC<{ - title?: string | React.ReactElement; - grow?: EuiFlexItemProps['grow']; -}> = ({ grow, title, children }) => - children ? ( - - {title && ( - <> - -
{title}
-
- - - )} - {children} -
- ) : null; - -export const HostPanel = React.memo( - ({ data, id, browserFields, openHostDetailsPanel, selectedPatterns }: HostPanelProps) => { - const hostName = getTimelineEventData('host.name', data); - const hostOs = getTimelineEventData('host.os.name', data); - - const enrichedAgentStatus = useMemo(() => { - const item = find({ field: 'agent.id', category: 'agent' }, data); - if (!data || !isAlertFromEndpointEvent({ data })) return null; - return ( - item && - getEnrichedFieldInfo({ - eventId: id, - contextId: TimelineId.detectionsAlertDetailsPage, - scopeId: TimelineId.detectionsAlertDetailsPage, - browserFields, - item, - field: { id: 'agent.id', overrideField: 'agent.status' }, - linkValueField: undefined, - }) - ); - }, [browserFields, data, id]); - - const { data: hostRisk, isAuthorized: isRiskScoreAuthorized } = useRiskScore({ - riskEntity: RiskScoreEntity.host, - skip: hostName == null, - }); - - const [hostRiskScore, hostRiskLevel] = useMemo(() => { - const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined; - const hostRiskValue = hostRiskData - ? Math.round(hostRiskData.host.risk.calculated_score_norm) - : getEmptyTagValue(); - const hostRiskSeverity = hostRiskData ? ( - - ) : ( - getEmptyTagValue() - ); - - return [hostRiskValue, hostRiskSeverity]; - }, [hostRisk]); - - const hostIpFields = useMemo( - () => find({ field: 'host.ip', category: 'host' }, data)?.values ?? [], - [data] - ); - - const renderHostIp = useCallback( - (ip: string) => (ip != null ? : getEmptyTagValue()), - [] - ); - - const renderHostActions = useCallback( - () => , - [hostName, openHostDetailsPanel] - ); - - return ( - - - - - - - - - - - - - - {hostOs} - {enrichedAgentStatus && ( - - - - )} - - - {isRiskScoreAuthorized && ( - <> - - {hostRiskScore && ( - {hostRiskScore} - )} - {hostRiskLevel && ( - {hostRiskLevel} - )} - - - - )} - - - - - - - - - - - - - - - - ); - } -); - -HostPanel.displayName = 'HostPanel'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/index.tsx deleted file mode 100644 index 2f6d146c8cc9f8..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; -import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import type { SearchHit } from '../../../../../../common/search_strategy'; -import { TimelineId } from '../../../../../../common/types/timeline'; -import { useDetailPanel } from '../../../../../timelines/components/side_panel/hooks/use_detail_panel'; -import { useGetUserCasesPermissions } from '../../../../../common/lib/kibana'; -import type { SelectedDataView } from '../../../../../common/store/sourcerer/model'; -import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; -import { AlertRendererPanel } from './alert_renderer_panel'; -import { RulePanel } from './rule_panel'; -import { CasesPanel, CasesPanelNoReadPermissions } from './cases_panel'; -import { HostPanel } from './host_panel'; -import { UserPanel } from './user_panel'; -import { SummaryColumn, SummaryRow } from './wrappers'; - -export interface DetailsSummaryTabProps { - eventId: string; - dataAsNestedObject: Ecs | null; - detailsData: TimelineEventsDetailsItem[]; - searchHit?: SearchHit; - sourcererDataView: SelectedDataView; -} - -export const DetailsSummaryTab = React.memo( - ({ - dataAsNestedObject, - detailsData, - searchHit, - eventId, - sourcererDataView, - }: DetailsSummaryTabProps) => { - const userCasesPermissions = useGetUserCasesPermissions(); - - const { DetailsPanel, openHostDetailsPanel, openUserDetailsPanel } = useDetailPanel({ - isFlyoutView: true, - sourcererScope: SourcererScopeName.detections, - scopeId: TimelineId.detectionsAlertDetailsPage, - }); - - return ( - <> - - - - - - - - - - - {userCasesPermissions.read ? ( - - ) : ( - - )} - - - {DetailsPanel} - - ); - } -); - -DetailsSummaryTab.displayName = 'DetailsSummaryTab'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/index.tsx deleted file mode 100644 index 4b67dcb87356c6..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/index.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import React, { useCallback, useMemo } from 'react'; -import { css } from '@emotion/react'; -import { find } from 'lodash/fp'; -import type { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item'; -import { - ALERT_RISK_SCORE, - ALERT_RULE_DESCRIPTION, - ALERT_RULE_NAME, - ALERT_RULE_UUID, - ALERT_SEVERITY, - KIBANA_NAMESPACE, -} from '@kbn/rule-data-utils'; -import type { SearchHit } from '../../../../../../../common/search_strategy'; -import { TimelineId } from '../../../../../../../common/types/timeline'; -import { SeverityBadge } from '../../../../../components/rules/severity_badge'; -import { getEnrichedFieldInfo } from '../../../../../../common/components/event_details/helpers'; -import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model'; -import { FormattedFieldValue } from '../../../../../../timelines/components/timeline/body/renderers/formatted_field'; -import { - RISK_SCORE_TITLE, - RULE_DESCRIPTION_TITLE, - RULE_NAME_TITLE, - RULE_PANEL_TITLE, - SEVERITY_TITLE, -} from '../translation'; -import { getMitreComponentParts } from '../../../../../mitre/get_mitre_threat_component'; -import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; -import { SummaryPanel } from '../wrappers'; -import { RulePanelActions, RULE_PANEL_ACTIONS_CLASS } from './rule_panel_actions'; - -export interface RulePanelProps { - data: TimelineEventsDetailsItem[]; - id: string; - browserFields: SelectedDataView['browserFields']; - searchHit?: SearchHit; -} - -const threatTacticContainerStyles = css` - flex-wrap: nowrap; - & .euiFlexGroup { - flex-wrap: nowrap; - } -`; - -interface RuleSectionProps { - ['data-test-subj']?: string; - title: string; - grow?: EuiFlexItemProps['grow']; -} -const RuleSection: React.FC = ({ - grow, - title, - children, - 'data-test-subj': dataTestSubj, -}) => ( - - -
{title}
-
- - {children} -
-); - -export const RulePanel = React.memo(({ data, id, searchHit, browserFields }: RulePanelProps) => { - const ruleUuid = useMemo(() => getTimelineEventData(ALERT_RULE_UUID, data), [data]); - const threatDetails = useMemo(() => getMitreComponentParts(searchHit), [searchHit]); - const alertRiskScore = useMemo(() => getTimelineEventData(ALERT_RISK_SCORE, data), [data]); - const alertSeverity = useMemo( - () => getTimelineEventData(ALERT_SEVERITY, data) as Severity, - [data] - ); - const alertRuleDescription = useMemo( - () => getTimelineEventData(ALERT_RULE_DESCRIPTION, data), - [data] - ); - const shouldShowThreatDetails = !!threatDetails && threatDetails?.length > 0; - - const renderRuleActions = useCallback(() => , [ruleUuid]); - const ruleNameData = useMemo(() => { - const item = find({ field: ALERT_RULE_NAME, category: KIBANA_NAMESPACE }, data); - const linkValueField = find({ field: ALERT_RULE_UUID, category: KIBANA_NAMESPACE }, data); - return ( - item && - getEnrichedFieldInfo({ - eventId: id, - contextId: TimelineId.detectionsAlertDetailsPage, - scopeId: TimelineId.detectionsAlertDetailsPage, - browserFields, - item, - linkValueField, - }) - ); - }, [browserFields, data, id]); - - return ( - - - - - - - - {alertRiskScore} - - - - - - - - {alertRuleDescription} - - - - - {shouldShowThreatDetails && ( - - {threatDetails[0].description} - - )} - - - - - - ); -}); - -RulePanel.displayName = 'RulePanel'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel.test.tsx deleted file mode 100644 index a41659fd2bcf6c..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { ALERT_RISK_SCORE, ALERT_RULE_DESCRIPTION, ALERT_RULE_NAME } from '@kbn/rule-data-utils'; -import { TestProviders } from '../../../../../../common/mock'; -import { - mockAlertDetailsTimelineResponse, - mockAlertNestedDetailsTimelineResponse, -} from '../../../__mocks__'; -import type { RulePanelProps } from '.'; -import { RulePanel } from '.'; -import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; -import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; - -describe('AlertDetailsPage - SummaryTab - RulePanel', () => { - const RulePanelWithDefaultProps = (propOverrides: Partial) => ( - - - - ); - it('should render basic rule fields', () => { - const { getByTestId } = render(); - const simpleRuleFields = [ALERT_RISK_SCORE, ALERT_RULE_DESCRIPTION]; - - simpleRuleFields.forEach((simpleRuleField) => { - expect(getByTestId('rule-panel')).toHaveTextContent( - getTimelineEventData(simpleRuleField, mockAlertDetailsTimelineResponse) - ); - }); - }); - - it('should render the expected severity', () => { - const { getByTestId } = render(); - expect(getByTestId('rule-panel-severity')).toHaveTextContent('Medium'); - }); - - describe('Rule name link', () => { - it('should render the rule name as a link button', () => { - const { getByTestId } = render(); - const ruleName = getTimelineEventData(ALERT_RULE_NAME, mockAlertDetailsTimelineResponse); - expect(getByTestId('ruleName')).toHaveTextContent(ruleName); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel_actions.tsx deleted file mode 100644 index a2eec20864a2bd..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/rule_panel/rule_panel_actions.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import { getRuleDetailsUrl } from '../../../../../../common/components/link_to'; -import { SecurityPageName } from '../../../../../../app/types'; -import { useGetSecuritySolutionLinkProps } from '../../../../../../common/components/links'; - -import { SUMMARY_PANEL_ACTIONS, OPEN_RULE_DETAILS_PAGE } from '../translation'; - -export const RULE_PANEL_ACTIONS_CLASS = 'rule-panel-actions-trigger'; - -export const RulePanelActions = React.memo( - ({ className, ruleUuid }: { className?: string; ruleUuid: string }) => { - const [isPopoverOpen, setPopover] = useState(false); - const { href } = useGetSecuritySolutionLinkProps()({ - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(ruleUuid), - }); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = () => { - setPopover(false); - }; - - const items = useMemo( - () => [ - - {OPEN_RULE_DETAILS_PAGE} - , - ], - [href] - ); - - const button = useMemo( - () => ( - - ), - [onButtonClick] - ); - - return ( -
- - - -
- ); - } -); - -RulePanelActions.displayName = 'RulePanelActions'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/translation.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/translation.ts deleted file mode 100644 index 6a509ff958735d..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/translation.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const CASES_PANEL_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.cases.title', - { - defaultMessage: 'Cases', - } -); - -export const CASES_PANEL_SUBTITLE = (caseCount: number) => - i18n.translate('xpack.securitySolution.alerts.alertDetails.summary.cases.subTitle', { - values: { caseCount }, - defaultMessage: 'Showing the {caseCount} most recently created cases containing this alert', - }); - -export const CASES_PANEL_CASE_STATUS = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.cases.status', - { - defaultMessage: 'Status', - } -); - -export const ALERT_REASON_PANEL_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.alertReason.title', - { - defaultMessage: 'Alert reason', - } -); - -export const RULE_PANEL_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.rule.title', - { - defaultMessage: 'Rule', - } -); - -export const HOST_PANEL_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.title', - { - defaultMessage: 'Host', - } -); - -export const USER_PANEL_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.user.title', - { - defaultMessage: 'User', - } -); - -export const RULE_NAME_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.rule.name', - { - defaultMessage: 'Rule name', - } -); - -export const RISK_SCORE_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.rule.riskScore', - { - defaultMessage: 'Risk score', - } -); - -export const SEVERITY_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.rule.severity', - { - defaultMessage: 'Severity', - } -); - -export const RULE_DESCRIPTION_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.rule.description', - { - defaultMessage: 'Rule description', - } -); - -export const OPEN_RULE_DETAILS_PAGE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.rule.action.openRuleDetailsPage', - { - defaultMessage: 'Open rule details page', - } -); - -export const NO_RELATED_CASES_FOUND = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.case.noCasesFound', - { - defaultMessage: 'Related cases were not found for this alert', - } -); - -export const LOADING_CASES = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.case.loading', - { - defaultMessage: 'Loading related cases...', - } -); - -export const ERROR_LOADING_CASES = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.case.error', - { - defaultMessage: 'Error loading related cases', - } -); - -export const CASE_NO_READ_PERMISSIONS = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.case.noRead', - { - defaultMessage: - 'You do not have the required permissions to view related cases. If you need to view cases, contact your Kibana administrator', - } -); - -export const ADD_TO_EXISTING_CASE_BUTTON = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.case.addToExistingCase', - { - defaultMessage: 'Add to existing case', - } -); - -export const ADD_TO_NEW_CASE_BUTTON = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.case.addToNewCase', - { - defaultMessage: 'Add to new case', - } -); - -export const HOST_NAME_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.hostName.title', - { - defaultMessage: 'Host name', - } -); - -export const OPERATING_SYSTEM_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.osName.title', - { - defaultMessage: 'Operating system', - } -); - -export const AGENT_STATUS_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.agentStatus.title', - { - defaultMessage: 'Agent status', - } -); - -export const IP_ADDRESSES_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.ipAddresses.title', - { - defaultMessage: 'IP addresses', - } -); - -export const LAST_SEEN_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.lastSeen.title', - { - defaultMessage: 'Last seen', - } -); - -export const VIEW_HOST_SUMMARY = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.action.viewHostSummary', - { - defaultMessage: 'View host summary', - } -); - -export const OPEN_HOST_DETAILS_PAGE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.action.openHostDetailsPage', - { - defaultMessage: 'Open host details page', - } -); - -export const HOST_RISK_SCORE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.riskScore', - { - defaultMessage: 'Host risk score', - } -); - -export const HOST_RISK_LEVEL = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.host.riskLevel', - { - defaultMessage: 'Host risk level', - } -); - -export const USER_NAME_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.user.userName.title', - { - defaultMessage: 'User name', - } -); - -export const USER_RISK_SCORE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.user.riskScore', - { - defaultMessage: 'User risk score', - } -); - -export const USER_RISK_LEVEL = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.user.riskLevel', - { - defaultMessage: 'User risk level', - } -); - -export const VIEW_USER_SUMMARY = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.user.action.viewUserSummary', - { - defaultMessage: 'View user summary', - } -); - -export const OPEN_USER_DETAILS_PAGE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.user.action.openUserDetailsPage', - { - defaultMessage: 'Open user details page', - } -); - -export const SUMMARY_PANEL_ACTIONS = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.summary.panelMoreActions', - { - defaultMessage: 'More actions', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx deleted file mode 100644 index 3fca60579b1da6..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import React, { useCallback, useMemo } from 'react'; -import { find } from 'lodash/fp'; -import type { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item'; -import { useRiskScore } from '../../../../../../explore/containers/risk_score'; -import { RiskScoreEntity } from '../../../../../../../common/search_strategy'; -import { getEmptyTagValue } from '../../../../../../common/components/empty_value'; -import { RiskScoreLevel } from '../../../../../../explore/components/risk_score/severity/common'; -import { - FirstLastSeen, - FirstLastSeenType, -} from '../../../../../../common/components/first_last_seen'; -import { DefaultFieldRenderer } from '../../../../../../timelines/components/field_renderers/field_renderers'; -import { NetworkDetailsLink, UserDetailsLink } from '../../../../../../common/components/links'; -import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model'; -import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; -import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; -import { - IP_ADDRESSES_TITLE, - LAST_SEEN_TITLE, - USER_NAME_TITLE, - USER_PANEL_TITLE, - USER_RISK_LEVEL, - USER_RISK_SCORE, -} from '../translation'; -import { SummaryPanel } from '../wrappers'; -import { UserPanelActions, USER_PANEL_ACTIONS_CLASS } from './user_panel_actions'; - -export interface UserPanelProps { - data: TimelineEventsDetailsItem[] | null; - selectedPatterns: SelectedDataView['selectedPatterns']; - openUserDetailsPanel: (userName: string, onClose?: (() => void) | undefined) => void; -} - -const UserPanelSection: React.FC<{ - title?: string | React.ReactElement; - grow?: EuiFlexItemProps['grow']; -}> = ({ grow, title, children }) => - children ? ( - - {title && ( - <> - -
{title}
-
- - - )} - {children} -
- ) : null; - -export const UserPanel = React.memo( - ({ data, selectedPatterns, openUserDetailsPanel }: UserPanelProps) => { - const userName = useMemo(() => getTimelineEventData('user.name', data), [data]); - - const { data: userRisk, isAuthorized: isRiskScoreAuthorized } = useRiskScore({ - riskEntity: RiskScoreEntity.user, - skip: userName == null, - }); - - const renderUserActions = useCallback( - () => , - [openUserDetailsPanel, userName] - ); - - const [userRiskScore, userRiskLevel] = useMemo(() => { - const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined; - const userRiskValue = userRiskData - ? Math.round(userRiskData.user.risk.calculated_score_norm) - : getEmptyTagValue(); - const userRiskSeverity = userRiskData ? ( - - ) : ( - getEmptyTagValue() - ); - - return [userRiskValue, userRiskSeverity]; - }, [userRisk]); - - const sourceIpFields = useMemo( - () => find({ field: 'source.ip', category: 'source' }, data)?.values ?? [], - [data] - ); - - const renderSourceIp = useCallback( - (ip: string) => (ip != null ? : getEmptyTagValue()), - [] - ); - - return ( - - - - - - - - - {userName ? : getEmptyTagValue()} - - - - {isRiskScoreAuthorized && ( - <> - - {userRiskScore && ( - {userRiskScore} - )} - {userRiskLevel && ( - {userRiskLevel} - )} - - - - )} - - - - - - - - - - - - - - - - ); - } -); - -UserPanel.displayName = 'UserPanel'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel.test.tsx deleted file mode 100644 index a2d5978d05e96f..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { TestProviders } from '../../../../../../common/mock'; -import { - mockAlertDetailsTimelineResponse, - mockAlertNestedDetailsTimelineResponse, -} from '../../../__mocks__'; -import type { UserPanelProps } from '.'; -import { UserPanel } from '.'; -import { getTimelineEventData } from '../../../utils/get_timeline_event_data'; -import { RiskSeverity } from '../../../../../../../common/search_strategy'; -import { useRiskScore } from '../../../../../../explore/containers/risk_score'; -import { find } from 'lodash/fp'; - -jest.mock('../../../../../../explore/containers/risk_score'); -const mockUseRiskScore = useRiskScore as jest.Mock; - -describe('AlertDetailsPage - SummaryTab - UserPanel', () => { - const defaultRiskReturnValues = { - inspect: null, - refetch: () => {}, - isModuleEnabled: true, - isAuthorized: true, - loading: false, - }; - const UserPanelWithDefaultProps = (propOverrides: Partial) => ( - - - - ); - - beforeEach(() => { - mockUseRiskScore.mockReturnValue({ ...defaultRiskReturnValues }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should render basic user fields', () => { - const { getByTestId } = render(); - const simpleUserFields = ['user.name']; - - simpleUserFields.forEach((simpleUserField) => { - expect(getByTestId('user-panel')).toHaveTextContent( - getTimelineEventData(simpleUserField, mockAlertDetailsTimelineResponse) - ); - }); - }); - - describe('user risk', () => { - it('should not show risk if the license is not valid', () => { - mockUseRiskScore.mockReturnValue({ - ...defaultRiskReturnValues, - isAuthorized: false, - data: null, - }); - const { queryByTestId } = render(); - expect(queryByTestId('user-panel-risk')).toBe(null); - }); - - it('should render risk fields', () => { - const calculatedScoreNorm = 98.9; - const calculatedLevel = RiskSeverity.critical; - - mockUseRiskScore.mockReturnValue({ - ...defaultRiskReturnValues, - isAuthorized: true, - data: [ - { - user: { - name: mockAlertNestedDetailsTimelineResponse.user?.name, - risk: { - calculated_score_norm: calculatedScoreNorm, - calculated_level: calculatedLevel, - }, - }, - }, - ], - }); - const { getByTestId } = render(); - - expect(getByTestId('user-panel-risk')).toHaveTextContent( - `${Math.round(calculatedScoreNorm)}` - ); - expect(getByTestId('user-panel-risk')).toHaveTextContent(calculatedLevel); - }); - }); - - describe('source ip', () => { - it('should render all the ip fields', () => { - const { getByTestId } = render(); - const ipFields = find( - { field: 'source.ip', category: 'source' }, - mockAlertDetailsTimelineResponse - )?.values as string[]; - expect(getByTestId('user-panel-ip')).toHaveTextContent(ipFields[0]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel_actions.tsx deleted file mode 100644 index 575673a494a2f5..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/user_panel/user_panel_actions.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import { getUsersDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_users'; -import { SecurityPageName } from '../../../../../../app/types'; -import { useGetSecuritySolutionLinkProps } from '../../../../../../common/components/links'; - -import { OPEN_USER_DETAILS_PAGE, SUMMARY_PANEL_ACTIONS, VIEW_USER_SUMMARY } from '../translation'; - -export const USER_PANEL_ACTIONS_CLASS = 'user-panel-actions-trigger'; - -export const UserPanelActions = React.memo( - ({ - className, - openUserDetailsPanel, - userName, - }: { - className?: string; - userName: string; - openUserDetailsPanel: (userName: string) => void; - }) => { - const [isPopoverOpen, setPopover] = useState(false); - const { href } = useGetSecuritySolutionLinkProps()({ - deepLinkId: SecurityPageName.users, - path: getUsersDetailsUrl(userName), - }); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = () => { - setPopover(false); - }; - - const handleopenUserDetailsPanel = useCallback(() => { - openUserDetailsPanel(userName); - closePopover(); - }, [userName, openUserDetailsPanel]); - - const items = useMemo( - () => [ - - {VIEW_USER_SUMMARY} - , - - {OPEN_USER_DETAILS_PAGE} - , - ], - [handleopenUserDetailsPanel, href] - ); - - const button = useMemo( - () => ( - - ), - [onButtonClick] - ); - - return ( -
- - - -
- ); - } -); - -UserPanelActions.displayName = 'UserPanelActions'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/wrappers.tsx b/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/wrappers.tsx deleted file mode 100644 index fd9cf26c7280eb..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/tabs/summary/wrappers.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import type { EuiFlexItemProps } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { HoverVisibilityContainer } from '../../../../../common/components/hover_visibility_container'; - -export const SummaryColumn: React.FC<{ grow?: EuiFlexItemProps['grow'] }> = ({ - children, - grow, -}) => ( - - - {children} - - -); - -export const SummaryRow: React.FC<{ grow?: EuiFlexItemProps['grow'] }> = ({ children, grow }) => ( - - - {children} - - -); - -export const SummaryPanel: React.FC<{ - grow?: EuiFlexItemProps['grow']; - title: string; - description?: string; - actionsClassName?: string; - renderActionsPopover?: () => JSX.Element; -}> = ({ actionsClassName, children, description, grow = false, renderActionsPopover, title }) => ( - - - - - - -

{title}

-
- - {description && ( - -

{description}

-
- )} -
- {actionsClassName && renderActionsPopover ? ( - {renderActionsPopover()} - ) : null} -
-
- - {children} -
-
-); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/translations.ts deleted file mode 100644 index 0a6ac1d91401f7..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/translations.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ALERT_DETAILS_TECHNICAL_PREVIEW = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.header.technicalPreview', - { - defaultMessage: 'Technical Preview', - } -); - -export const SUMMARY_PAGE_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.navigation.summary', - { - defaultMessage: 'Summary', - } -); - -export const BACK_TO_ALERTS_LINK = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.header.backToAlerts', - { - defaultMessage: 'Back to alerts', - } -); - -export const LOADING_PAGE_MESSAGE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.loadingPage.message', - { - defaultMessage: 'Loading details page...', - } -); - -export const ERROR_PAGE_TITLE = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.errorPage.title', - { - defaultMessage: 'Unable to load the details page', - } -); - -export const ERROR_PAGE_BODY = i18n.translate( - 'xpack.securitySolution.alerts.alertDetails.errorPage.message', - { - defaultMessage: - 'There was an error loading the details page. Please confirm the following id points to a valid document', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/types.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/types.ts deleted file mode 100644 index 3b4138a9d3d7d3..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { NavTab } from '../../../common/components/navigation/types'; - -export enum AlertDetailRouteType { - summary = 'summary', -} - -export type AlertDetailNavTabs = Record<`${AlertDetailRouteType}`, NavTab>; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts deleted file mode 100644 index 2b6dca72bf078e..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ChromeBreadcrumb } from '@kbn/core/public'; -import type { GetTrailingBreadcrumbs } from '../../../../common/components/navigation/breadcrumbs/types'; -import { getAlertDetailsUrl } from '../../../../common/components/link_to'; -import { SecurityPageName } from '../../../../../common/constants'; -import type { AlertDetailRouteSpyState } from '../../../../common/utils/route/types'; -import { AlertDetailRouteType } from '../types'; -import * as i18n from '../translations'; - -const TabNameMappedToI18nKey: Record = { - [AlertDetailRouteType.summary]: i18n.SUMMARY_PAGE_TITLE, -}; - -/** - * This module should only export this function. - * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. - * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. - */ -export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( - params, - getSecuritySolutionUrl -) => { - let breadcrumb: ChromeBreadcrumb[] = []; - - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state?.ruleName ?? params.detailName, - href: getSecuritySolutionUrl({ - path: getAlertDetailsUrl(params.detailName, ''), - deepLinkId: SecurityPageName.alerts, - }), - }, - ]; - } - if (params.tabName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[params.tabName], - href: '', - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/get_timeline_event_data.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/get_timeline_event_data.ts deleted file mode 100644 index 7d5dbc5440087c..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/get_timeline_event_data.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; - -export const getTimelineEventData = (field: string, data: TimelineEventsDetailsItem[] | null) => { - const valueArray = data?.find((datum) => datum.field === field)?.values; - return valueArray && valueArray.length > 0 ? valueArray[0] : ''; -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/navigation.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/navigation.ts deleted file mode 100644 index 540cd99ad9bde7..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/navigation.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AlertDetailNavTabs } from '../types'; -import { ALERTS_PATH } from '../../../../../common/constants'; -import { AlertDetailRouteType } from '../types'; -import * as i18n from '../translations'; - -export const getAlertDetailsTabUrl = (alertId: string, tabName: AlertDetailRouteType) => - `${ALERTS_PATH}/${alertId}/${tabName}`; - -export const getAlertDetailsNavTabs = (alertId: string): AlertDetailNavTabs => ({ - [AlertDetailRouteType.summary]: { - id: AlertDetailRouteType.summary, - name: i18n.SUMMARY_PAGE_TITLE, - href: getAlertDetailsTabUrl(alertId, AlertDetailRouteType.summary), - disabled: false, - }, -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index 2f530424f93848..82d41e0eafb33b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -23,12 +23,6 @@ import React from 'react'; import styled from 'styled-components'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { getAlertDetailsUrl } from '../../../../common/components/link_to'; -import { - SecuritySolutionLinkAnchor, - useGetSecuritySolutionLinkProps, -} from '../../../../common/components/links'; import type { TimelineTabs } from '../../../../../common/types/timeline'; import type { BrowserFields } from '../../../../common/containers/source'; import { EventDetails } from '../../../../common/components/event_details/event_details'; @@ -39,7 +33,6 @@ import { EVENT_SUMMARY_CONVERSATION_ID, } from '../../../../common/components/event_details/translations'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; -import { SecurityPageName } from '../../../../../common/constants'; import { useGetAlertDetailsFlyoutLink } from './use_get_alert_details_flyout_link'; export type HandleOnEventClosed = () => void; @@ -98,12 +91,6 @@ export const ExpandableEventTitle = React.memo( timestamp, }) => { const { hasAssistantPrivilege } = useAssistantAvailability(); - const isAlertDetailsPageEnabled = useIsExperimentalFeatureEnabled('alertDetailsPageEnabled'); - const { onClick } = useGetSecuritySolutionLinkProps()({ - deepLinkId: SecurityPageName.alerts, - path: eventId && isAlert ? getAlertDetailsUrl(eventId) : '', - }); - const alertDetailsLink = useGetAlertDetailsFlyoutLink({ _id: eventId, _index: eventIndex, @@ -124,19 +111,6 @@ export const ExpandableEventTitle = React.memo( )} - {isAlert && eventId && isAlertDetailsPageEnabled && ( - <> - - - {i18n.OPEN_ALERT_DETAILS_PAGE} - - - - )} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/translations.ts index a40a9095ea1112..3416636f71e177 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/translations.ts @@ -7,20 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const MESSAGE = i18n.translate( - 'xpack.securitySolution.timeline.expandableEvent.messageTitle', - { - defaultMessage: 'Message', - } -); - -export const OPEN_ALERT_DETAILS_PAGE = i18n.translate( - 'xpack.securitySolution.timeline.expandableEvent.openAlertDetails', - { - defaultMessage: 'Open alert details page', - } -); - export const CLOSE = i18n.translate( 'xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index cd0b49c515bb6f..4ab99faba09c4b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -131,19 +131,6 @@ jest.mock('../../../../common/components/links', () => { }; }); -jest.mock( - '../../../../detections/components/alerts_table/timeline_actions/use_open_alert_details', - () => { - return { - useOpenAlertDetailsAction: () => { - return { - alertDetailsActionItems: [], - }; - }, - }; - } -); - // Prevent Resolver from rendering jest.mock('../../graph_overlay'); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 25530be35e9a7a..ba03ddd298d5f1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29158,7 +29158,6 @@ "xpack.securitySolution.alertDetails.overview.insights.relatedCasesFailure": "Impossible de charger les cas connexes : \"{error}\"", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCount": "{count} {count, plural, =1 {alerte} one {alertes} many {alertes} other {alertes}} supprimée(s)", "xpack.securitySolution.alertDetails.overview.riskDataTooltipContent": "La classification des risques n'est affichée que lorsqu'elle est disponible pour une {riskEntity}. Vérifiez que {riskScoreDocumentationLink} est activé dans votre environnement.", - "xpack.securitySolution.alerts.alertDetails.summary.cases.subTitle": "Affichage des {caseCount} cas les plus récemment créés contenant cette alerte", "xpack.securitySolution.alertSummaryView.alertSummaryViewContextDescription": "Alerte (à partir de {view})", "xpack.securitySolution.alertSummaryView.eventSummaryViewContextDescription": "Événement (à partir de {view})", "xpack.securitySolution.anomaliesTable.table.unit": "{totalCount, plural, =1 {anomalie} one {anomalies} many {anomalies} other {anomalies}}", @@ -29735,42 +29734,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "Lire moins", "xpack.securitySolution.alertDetails.summary.readMore": "En savoir plus", "xpack.securitySolution.alertDetails.threatIntel": "Threat Intelligence", - "xpack.securitySolution.alerts.alertDetails.errorPage.message": "Une erreur s'est produite lors du chargement de la page de détails. Veuillez confirmer que l'ID suivant pointe vers un document valide", - "xpack.securitySolution.alerts.alertDetails.errorPage.title": "Impossible de charger la page de détails", - "xpack.securitySolution.alerts.alertDetails.header.backToAlerts": "Retour aux alertes", - "xpack.securitySolution.alerts.alertDetails.header.technicalPreview": "Version d'évaluation technique", - "xpack.securitySolution.alerts.alertDetails.loadingPage.message": "Chargement de la page de détails...", - "xpack.securitySolution.alerts.alertDetails.navigation.summary": "Résumé", - "xpack.securitySolution.alerts.alertDetails.summary.alertReason.title": "Raison d'alerte", - "xpack.securitySolution.alerts.alertDetails.summary.case.addToExistingCase": "Ajouter à un cas existant", - "xpack.securitySolution.alerts.alertDetails.summary.case.addToNewCase": "Ajouter au nouveau cas", - "xpack.securitySolution.alerts.alertDetails.summary.case.error": "Erreur lors du chargement des cas connexes", - "xpack.securitySolution.alerts.alertDetails.summary.case.loading": "Chargement des cas connexes...", - "xpack.securitySolution.alerts.alertDetails.summary.case.noCasesFound": "Impossible de trouver les cas connexes pour cette alerte", - "xpack.securitySolution.alerts.alertDetails.summary.case.noRead": "Vous ne disposez pas des autorisations requises pour afficher les cas connexes. Si vous avez besoin d'afficher les cas, contactez votre administrateur Kibana", - "xpack.securitySolution.alerts.alertDetails.summary.cases.status": "Statut", - "xpack.securitySolution.alerts.alertDetails.summary.cases.title": "Cas", - "xpack.securitySolution.alerts.alertDetails.summary.host.action.openHostDetailsPage": "Ouvrir la page de détails de l'hôte", - "xpack.securitySolution.alerts.alertDetails.summary.host.action.viewHostSummary": "Afficher le résumé de l'hôte", - "xpack.securitySolution.alerts.alertDetails.summary.host.agentStatus.title": "Statut de l'agent", - "xpack.securitySolution.alerts.alertDetails.summary.host.hostName.title": "Nom d'hôte", - "xpack.securitySolution.alerts.alertDetails.summary.host.osName.title": "Système d'exploitation", - "xpack.securitySolution.alerts.alertDetails.summary.host.riskScore": "Score de risque de l'hôte", - "xpack.securitySolution.alerts.alertDetails.summary.host.title": "Hôte", - "xpack.securitySolution.alerts.alertDetails.summary.ipAddresses.title": "Adresses IP", - "xpack.securitySolution.alerts.alertDetails.summary.lastSeen.title": "Vu en dernier", - "xpack.securitySolution.alerts.alertDetails.summary.panelMoreActions": "Plus d'actions", - "xpack.securitySolution.alerts.alertDetails.summary.rule.action.openRuleDetailsPage": "Ouvrir la page de détails de la règle", - "xpack.securitySolution.alerts.alertDetails.summary.rule.description": "Description de la règle", - "xpack.securitySolution.alerts.alertDetails.summary.rule.name": "Nom de règle", - "xpack.securitySolution.alerts.alertDetails.summary.rule.riskScore": "Score de risque", - "xpack.securitySolution.alerts.alertDetails.summary.rule.severity": "Sévérité", - "xpack.securitySolution.alerts.alertDetails.summary.rule.title": "Règle", - "xpack.securitySolution.alerts.alertDetails.summary.user.action.openUserDetailsPage": "Ouvrir la page de détails de l'utilisateur", - "xpack.securitySolution.alerts.alertDetails.summary.user.action.viewUserSummary": "Afficher le résumé de l'utilisateur", - "xpack.securitySolution.alerts.alertDetails.summary.user.riskScore": "Score de risque de l'utilisateur", - "xpack.securitySolution.alerts.alertDetails.summary.user.title": "Utilisateur", - "xpack.securitySolution.alerts.alertDetails.summary.user.userName.title": "Nom d'utilisateur", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "Impossible de mettre à jour les alertes", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "Sélectionnez un score de risque pour toutes les alertes générées par cette règle.", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "Score de risque par défaut", @@ -30364,7 +30327,6 @@ "xpack.securitySolution.detectionEngine.alerts.actions.addToNewCase": "Ajouter au nouveau cas", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel": "Envoyer une alerte à la chronologie", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "Investiguer dans la chronologie", - "xpack.securitySolution.detectionEngine.alerts.actions.openAlertDetails": "Ouvrir la page de détails de l'alerte", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.chartTitle": "Alertes les plus fréquentes par", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.destinationLabel": "destination", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.hostNameLabel": "hôte", @@ -33543,8 +33505,6 @@ "xpack.securitySolution.timeline.expandableEvent.alertTitleLabel": "Détails de l'alerte", "xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel": "fermer", "xpack.securitySolution.timeline.expandableEvent.eventTitleLabel": "Détails de l'événement", - "xpack.securitySolution.timeline.expandableEvent.messageTitle": "Message", - "xpack.securitySolution.timeline.expandableEvent.openAlertDetails": "Ouvrir la page de détails de l'alerte", "xpack.securitySolution.timeline.expandableEvent.placeholder": "Sélectionner un événement pour afficher ses détails", "xpack.securitySolution.timeline.expandableEvent.shareAlert": "Partager l'alerte", "xpack.securitySolution.timeline.failDescription": "Une erreur s'est produite", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3fa06a6d8c35f1..b2a00bc71bd435 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29157,7 +29157,6 @@ "xpack.securitySolution.alertDetails.overview.insights.relatedCasesFailure": "関連するケースを読み込めません:\"{error}\"", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCount": "{count}件の抑制された{count, plural, =1 {アラート} other {アラート}}", "xpack.securitySolution.alertDetails.overview.riskDataTooltipContent": "リスク分類は、{riskEntity}で使用可能なときにのみ表示されます。{riskScoreDocumentationLink}が環境内で有効であることを確認します。", - "xpack.securitySolution.alerts.alertDetails.summary.cases.subTitle": "このアラートを含む直近に作成された{caseCount}件のケースを表示しています", "xpack.securitySolution.alertSummaryView.alertSummaryViewContextDescription": "アラート({view}から)", "xpack.securitySolution.alertSummaryView.eventSummaryViewContextDescription": "イベント({view}から)", "xpack.securitySolution.anomaliesTable.table.unit": "{totalCount, plural, =1 {異常} other {異常}}", @@ -29734,42 +29733,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alertDetails.threatIntel": "Threat Intel", - "xpack.securitySolution.alerts.alertDetails.errorPage.message": "詳細ページの読み込みエラーが発生しました。次のIDが有効なドキュメントを参照していることを確認してください", - "xpack.securitySolution.alerts.alertDetails.errorPage.title": "詳細ページを読み込めません", - "xpack.securitySolution.alerts.alertDetails.header.backToAlerts": "アラートに戻る", - "xpack.securitySolution.alerts.alertDetails.header.technicalPreview": "テクニカルプレビュー", - "xpack.securitySolution.alerts.alertDetails.loadingPage.message": "詳細ページを読み込んでいます...", - "xpack.securitySolution.alerts.alertDetails.navigation.summary": "まとめ", - "xpack.securitySolution.alerts.alertDetails.summary.alertReason.title": "アラートの理由", - "xpack.securitySolution.alerts.alertDetails.summary.case.addToExistingCase": "既存のケースに追加", - "xpack.securitySolution.alerts.alertDetails.summary.case.addToNewCase": "新しいケースに追加", - "xpack.securitySolution.alerts.alertDetails.summary.case.error": "関連するケースの読み込みエラー", - "xpack.securitySolution.alerts.alertDetails.summary.case.loading": "関連するケースを読み込んでいます...", - "xpack.securitySolution.alerts.alertDetails.summary.case.noCasesFound": "このアラートの関連ケースが見つかりませんでした", - "xpack.securitySolution.alerts.alertDetails.summary.case.noRead": "関連ケースを表示する権限がありません。ケースを表示する必要がある場合は、Kibana管理者に連絡してください。", - "xpack.securitySolution.alerts.alertDetails.summary.cases.status": "ステータス", - "xpack.securitySolution.alerts.alertDetails.summary.cases.title": "ケース", - "xpack.securitySolution.alerts.alertDetails.summary.host.action.openHostDetailsPage": "ホスト詳細ページを開く", - "xpack.securitySolution.alerts.alertDetails.summary.host.action.viewHostSummary": "ホスト概要を表示", - "xpack.securitySolution.alerts.alertDetails.summary.host.agentStatus.title": "エージェントステータス", - "xpack.securitySolution.alerts.alertDetails.summary.host.hostName.title": "ホスト名", - "xpack.securitySolution.alerts.alertDetails.summary.host.osName.title": "オペレーティングシステム", - "xpack.securitySolution.alerts.alertDetails.summary.host.riskScore": "ホストリスクスコア", - "xpack.securitySolution.alerts.alertDetails.summary.host.title": "ホスト", - "xpack.securitySolution.alerts.alertDetails.summary.ipAddresses.title": "IP アドレス", - "xpack.securitySolution.alerts.alertDetails.summary.lastSeen.title": "前回の認識", - "xpack.securitySolution.alerts.alertDetails.summary.panelMoreActions": "さらにアクションを表示", - "xpack.securitySolution.alerts.alertDetails.summary.rule.action.openRuleDetailsPage": "ルール詳細ページを開く", - "xpack.securitySolution.alerts.alertDetails.summary.rule.description": "ルールの説明", - "xpack.securitySolution.alerts.alertDetails.summary.rule.name": "ルール名", - "xpack.securitySolution.alerts.alertDetails.summary.rule.riskScore": "リスクスコア", - "xpack.securitySolution.alerts.alertDetails.summary.rule.severity": "深刻度", - "xpack.securitySolution.alerts.alertDetails.summary.rule.title": "ルール", - "xpack.securitySolution.alerts.alertDetails.summary.user.action.openUserDetailsPage": "ユーザー詳細ページを開く", - "xpack.securitySolution.alerts.alertDetails.summary.user.action.viewUserSummary": "ユーザー概要を表示", - "xpack.securitySolution.alerts.alertDetails.summary.user.riskScore": "ユーザーリスクスコア", - "xpack.securitySolution.alerts.alertDetails.summary.user.title": "ユーザー", - "xpack.securitySolution.alerts.alertDetails.summary.user.userName.title": "ユーザー名", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", @@ -30363,7 +30326,6 @@ "xpack.securitySolution.detectionEngine.alerts.actions.addToNewCase": "新しいケースに追加", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel": "アラートをタイムラインに送信", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "タイムラインで調査", - "xpack.securitySolution.detectionEngine.alerts.actions.openAlertDetails": "アラート詳細ページを開く", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.chartTitle": "上位のアラート", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.destinationLabel": "デスティネーション", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.hostNameLabel": "ホスト", @@ -33542,8 +33504,6 @@ "xpack.securitySolution.timeline.expandableEvent.alertTitleLabel": "アラートの詳細", "xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel": "閉じる", "xpack.securitySolution.timeline.expandableEvent.eventTitleLabel": "イベントの詳細", - "xpack.securitySolution.timeline.expandableEvent.messageTitle": "メッセージ", - "xpack.securitySolution.timeline.expandableEvent.openAlertDetails": "アラート詳細ページを開く", "xpack.securitySolution.timeline.expandableEvent.placeholder": "イベント詳細を表示するには、イベントを選択します", "xpack.securitySolution.timeline.expandableEvent.shareAlert": "アラートを共有", "xpack.securitySolution.timeline.failDescription": "エラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2c57adbd57df1e..5c97128b8c8f16 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29153,7 +29153,6 @@ "xpack.securitySolution.alertDetails.overview.insights.relatedCasesFailure": "无法加载相关案例:“{error}”", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCount": "{count} 个已阻止{count, plural, =1 {告警} other {告警}}", "xpack.securitySolution.alertDetails.overview.riskDataTooltipContent": "仅在其对 {riskEntity} 可用时才会显示风险分类。确保在您的环境中启用了 {riskScoreDocumentationLink}。", - "xpack.securitySolution.alerts.alertDetails.summary.cases.subTitle": "正在显示 {caseCount} 个包含此告警的最新创建的案例", "xpack.securitySolution.alertSummaryView.alertSummaryViewContextDescription": "告警(来自 {view})", "xpack.securitySolution.alertSummaryView.eventSummaryViewContextDescription": "事件(来自 {view})", "xpack.securitySolution.anomaliesTable.table.unit": "{totalCount, plural, =1 {异常} other {异常}}", @@ -29730,42 +29729,6 @@ "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alertDetails.threatIntel": "威胁情报", - "xpack.securitySolution.alerts.alertDetails.errorPage.message": "加载详情页面时出错。请确认以下 ID 是否指向有效文档", - "xpack.securitySolution.alerts.alertDetails.errorPage.title": "无法加载详情页面", - "xpack.securitySolution.alerts.alertDetails.header.backToAlerts": "返回到告警", - "xpack.securitySolution.alerts.alertDetails.header.technicalPreview": "技术预览", - "xpack.securitySolution.alerts.alertDetails.loadingPage.message": "正在加载详情页面......", - "xpack.securitySolution.alerts.alertDetails.navigation.summary": "摘要", - "xpack.securitySolution.alerts.alertDetails.summary.alertReason.title": "告警原因", - "xpack.securitySolution.alerts.alertDetails.summary.case.addToExistingCase": "添加到现有案例", - "xpack.securitySolution.alerts.alertDetails.summary.case.addToNewCase": "添加到新案例", - "xpack.securitySolution.alerts.alertDetails.summary.case.error": "加载相关案例时出错", - "xpack.securitySolution.alerts.alertDetails.summary.case.loading": "正在加载相关案例......", - "xpack.securitySolution.alerts.alertDetails.summary.case.noCasesFound": "找不到此告警的相关案例", - "xpack.securitySolution.alerts.alertDetails.summary.case.noRead": "您没有查看相关案例所需的权限。如果需要查看案例,请联系您的 Kibana 管理员", - "xpack.securitySolution.alerts.alertDetails.summary.cases.status": "状态", - "xpack.securitySolution.alerts.alertDetails.summary.cases.title": "案例", - "xpack.securitySolution.alerts.alertDetails.summary.host.action.openHostDetailsPage": "打开主机详情页面", - "xpack.securitySolution.alerts.alertDetails.summary.host.action.viewHostSummary": "查看主机摘要", - "xpack.securitySolution.alerts.alertDetails.summary.host.agentStatus.title": "代理状态", - "xpack.securitySolution.alerts.alertDetails.summary.host.hostName.title": "主机名", - "xpack.securitySolution.alerts.alertDetails.summary.host.osName.title": "操作系统", - "xpack.securitySolution.alerts.alertDetails.summary.host.riskScore": "主机风险分数", - "xpack.securitySolution.alerts.alertDetails.summary.host.title": "主机", - "xpack.securitySolution.alerts.alertDetails.summary.ipAddresses.title": "IP 地址", - "xpack.securitySolution.alerts.alertDetails.summary.lastSeen.title": "最后看到时间", - "xpack.securitySolution.alerts.alertDetails.summary.panelMoreActions": "更多操作", - "xpack.securitySolution.alerts.alertDetails.summary.rule.action.openRuleDetailsPage": "打开规则详情页面", - "xpack.securitySolution.alerts.alertDetails.summary.rule.description": "规则描述", - "xpack.securitySolution.alerts.alertDetails.summary.rule.name": "规则名称", - "xpack.securitySolution.alerts.alertDetails.summary.rule.riskScore": "风险分数", - "xpack.securitySolution.alerts.alertDetails.summary.rule.severity": "严重性", - "xpack.securitySolution.alerts.alertDetails.summary.rule.title": "规则", - "xpack.securitySolution.alerts.alertDetails.summary.user.action.openUserDetailsPage": "打开用户详情页面", - "xpack.securitySolution.alerts.alertDetails.summary.user.action.viewUserSummary": "查看用户摘要", - "xpack.securitySolution.alerts.alertDetails.summary.user.riskScore": "用户风险分数", - "xpack.securitySolution.alerts.alertDetails.summary.user.title": "用户", - "xpack.securitySolution.alerts.alertDetails.summary.user.userName.title": "用户名", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数", @@ -30359,7 +30322,6 @@ "xpack.securitySolution.detectionEngine.alerts.actions.addToNewCase": "添加到新案例", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel": "将告警发送到时间线", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "在时间线中调查", - "xpack.securitySolution.detectionEngine.alerts.actions.openAlertDetails": "打开告警详情页面", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.chartTitle": "排名靠前规则排列依据", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.destinationLabel": "目标", "xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.hostNameLabel": "主机", @@ -33538,8 +33500,6 @@ "xpack.securitySolution.timeline.expandableEvent.alertTitleLabel": "告警详情", "xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel": "关闭", "xpack.securitySolution.timeline.expandableEvent.eventTitleLabel": "事件详情", - "xpack.securitySolution.timeline.expandableEvent.messageTitle": "消息", - "xpack.securitySolution.timeline.expandableEvent.openAlertDetails": "打开告警详情页面", "xpack.securitySolution.timeline.expandableEvent.placeholder": "选择事件以显示事件详情", "xpack.securitySolution.timeline.expandableEvent.shareAlert": "共享告警", "xpack.securitySolution.timeline.failDescription": "发生错误", diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 99569e7b3084fd..fb34362f7fb9b5 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -45,7 +45,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - 'alertDetailsPageEnabled', 'chartEmbeddablesEnabled', ])}`, // mock cloud to enable the guided onboarding tour in e2e tests diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts index f5716f33ff288f..99cbf31012d75b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts @@ -7,8 +7,8 @@ import { getNewRule } from '../../objects/rule'; import { - HOST_RISK_HEADER_COLIMN, - USER_RISK_HEADER_COLIMN, + HOST_RISK_HEADER_COLUMN, + USER_RISK_HEADER_COLUMN, HOST_RISK_COLUMN, USER_RISK_COLUMN, ACTION_COLUMN, @@ -71,8 +71,8 @@ describe('Enrichment', { tags: ['@ess', '@serverless'] }, () => { cy.get(ALERTS_COUNT) .invoke('text') .should('match', /^[1-9].+$/); // Any number of alerts - cy.get(HOST_RISK_HEADER_COLIMN).contains('host.risk.calculated_level'); - cy.get(USER_RISK_HEADER_COLIMN).contains('user.risk.calculated_level'); + cy.get(HOST_RISK_HEADER_COLUMN).contains('host.risk.calculated_level'); + cy.get(USER_RISK_HEADER_COLUMN).contains('user.risk.calculated_level'); scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); cy.get(HOST_RISK_COLUMN).contains('Low'); scrollAlertTableColumnIntoView(USER_RISK_COLUMN); @@ -115,8 +115,8 @@ describe('Enrichment', { tags: ['@ess', '@serverless'] }, () => { cy.get(ALERTS_COUNT) .invoke('text') .should('match', /^[1-9].+$/); // Any number of alerts - cy.get(HOST_RISK_HEADER_COLIMN).contains('host.risk.calculated_level'); - cy.get(USER_RISK_HEADER_COLIMN).contains('user.risk.calculated_level'); + cy.get(HOST_RISK_HEADER_COLUMN).contains('host.risk.calculated_level'); + cy.get(USER_RISK_HEADER_COLUMN).contains('user.risk.calculated_level'); scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); cy.get(HOST_RISK_COLUMN).contains('Critical'); scrollAlertTableColumnIntoView(USER_RISK_COLUMN); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts index caa560f13aeadd..f10681a516146c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_alert_to_case.cy.ts @@ -16,7 +16,7 @@ import { login } from '../../../tasks/login'; import { visit } from '../../../tasks/navigation'; import { ALERTS_URL } from '../../../urls/navigation'; -import { ATTACH_ALERT_TO_CASE_BUTTON, ATTACH_TO_NEW_CASE_BUTTON } from '../../../screens/alerts'; +import { ATTACH_ALERT_TO_CASE_BUTTON, TIMELINE_CONTEXT_MENU_BTN } from '../../../screens/alerts'; import { LOADING_INDICATOR } from '../../../screens/security_header'; const loadDetectionsPage = (role: ROLES) => { @@ -41,15 +41,13 @@ describe('Alerts timeline', { tags: ['@ess'] }, () => { }); it('should not allow user with read only privileges to attach alerts to existing cases', () => { - // Disabled actions for read only users are hidden, so only open alert details button should show - expandFirstAlertActions(); - cy.get(ATTACH_ALERT_TO_CASE_BUTTON).should('not.exist'); + // Disabled actions for read only users are hidden, so the ... icon is not even shown + cy.get(TIMELINE_CONTEXT_MENU_BTN).should('not.exist'); }); it('should not allow user with read only privileges to attach alerts to a new case', () => { - // Disabled actions for read only users are hidden, so only open alert details button should show - expandFirstAlertActions(); - cy.get(ATTACH_TO_NEW_CASE_BUTTON).should('not.exist'); + // Disabled actions for read only users are hidden, so the ... icon is not even shown + cy.get(TIMELINE_CONTEXT_MENU_BTN).should('not.exist'); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/navigation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/navigation.cy.ts deleted file mode 100644 index d61ba89fa90b25..00000000000000 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/navigation.cy.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { expandFirstAlert, waitForAlerts } from '../../../tasks/alerts'; -import { createRule } from '../../../tasks/api_calls/rules'; -import { cleanKibana } from '../../../tasks/common'; -import { login } from '../../../tasks/login'; -import { visit, visitWithTimeRange } from '../../../tasks/navigation'; - -import { getNewRule } from '../../../objects/rule'; - -import { ALERTS_URL } from '../../../urls/navigation'; -import { - OPEN_ALERT_DETAILS_PAGE_CONTEXT_MENU_BTN, - TIMELINE_CONTEXT_MENU_BTN, - ALERTS_REFRESH_BTN, -} from '../../../screens/alerts'; -import { PAGE_TITLE } from '../../../screens/common/page'; -import { OPEN_ALERT_DETAILS_PAGE } from '../../../screens/alerts_details'; - -// This is skipped as the details page POC will be removed in favor of the expanded alert flyout -// https://github.com/elastic/kibana/issues/154477 -describe.skip('Alert Details Page Navigation', { tags: ['@ess', '@serverless'] }, () => { - describe('navigating to alert details page', () => { - const rule = getNewRule(); - before(() => { - cleanKibana(); - login(); - createRule({ ...rule, rule_id: 'rule1' }); - }); - - describe('context menu', () => { - beforeEach(() => { - visit(ALERTS_URL); - waitForAlerts(); - }); - - it('should navigate to the details page from the alert context menu', () => { - // Sometimes the alerts are not loaded yet, so we need to refresh the page - cy.get(TIMELINE_CONTEXT_MENU_BTN).then(($btns) => { - if ($btns.length === 0) { - cy.get(ALERTS_REFRESH_BTN).click(); - } - }); - cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); - cy.get(OPEN_ALERT_DETAILS_PAGE_CONTEXT_MENU_BTN).click({ force: true }); - cy.get(PAGE_TITLE).should('contain.text', rule.name); - cy.url().should('include', '/summary'); - }); - }); - - describe('flyout', () => { - beforeEach(() => { - visitWithTimeRange(ALERTS_URL); - waitForAlerts(); - }); - - it('should navigate to the details page from the alert flyout', () => { - expandFirstAlert(); - cy.get(OPEN_ALERT_DETAILS_PAGE).click({ force: true }); - cy.get(PAGE_TITLE).should('contain.text', rule.name); - cy.url().should('include', '/summary'); - }); - }); - }); -}); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts index 86a1703959ea95..d0681a4348e064 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts.ts @@ -33,8 +33,6 @@ export const ALERT_SEVERITY = '[data-test-subj="formatted-field-kibana.alert.sev export const ALERT_DATA_GRID = '[data-test-subj="euiDataGridBody"]'; -export const ALERTS = '[data-test-subj="events-viewer-panel"][data-test-subj="event"]'; - export const ALERTS_COUNT = '[data-test-subj="toolbar-alerts-count"]'; export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; @@ -53,12 +51,6 @@ export const TAKE_ACTION_MENU = '[data-test-subj="takeActionPanelMenu"]'; export const CLOSE_FLYOUT = '[data-test-subj="euiFlyoutCloseButton"]'; -export const GROUP_BY_TOP_INPUT = '[data-test-subj="groupByTop"] [data-test-subj="comboBoxInput"]'; - -export const HOST_NAME = '[data-test-subj^=formatted-field][data-test-subj$=host\\.name]'; - -export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; - export const MARK_ALERT_ACKNOWLEDGED_BTN = '[data-test-subj="acknowledged-alert-status"]'; export const ALERTS_REFRESH_BTN = `${GLOBAL_FILTERS_CONTAINER} [data-test-subj="querySubmitButton"]`; @@ -69,19 +61,11 @@ export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]'; export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]'; -export const OPEN_ALERT_DETAILS_PAGE_CONTEXT_MENU_BTN = - '[data-test-subj="open-alert-details-page-menu-item"]'; - export const COLUMN_HEADER = '[data-test-subj="dataGridHeader"]'; -export const TIMESTAMP_COLUMN = '[data-test-subj="dataGridHeaderCell-@timestamp"]'; -export const MESSAGE = '[data-test-subj="formatted-field-message"]'; -export const REASON = - '[data-test-subj="dataGridRowCell"][data-gridcell-column-id="kibana.alert.reason"]'; - -export const RISK_SCORE = '[data-test-subj^=formatted-field][data-test-subj$=risk_score]'; +export const TIMESTAMP_COLUMN = '[data-test-subj="dataGridHeaderCell-@timestamp"]'; -export const RULE_NAME = '[data-test-subj^=formatted-field][data-test-subj$=rule\\.name]'; +export const MESSAGE = '[data-test-subj="formatted-field-message"]'; export const SELECTED_ALERTS = '[data-test-subj="selectedShowBulkActionsButton"]'; @@ -93,28 +77,22 @@ export const OPEN_ANALYZER_BTN = '[data-test-subj="view-in-analyzer"]'; export const ANALYZER_NODE = '[data-test-subj="resolver:node"'; -export const SEVERITY = '[data-test-subj^=formatted-field][data-test-subj$=severity]'; - -export const SOURCE_IP = '[data-test-subj^=formatted-field][data-test-subj$=source\\.ip]'; - export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActionsButton"]'; export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; -export const USER_NAME = '[data-test-subj^=formatted-field][data-test-subj$=user\\.name]'; - export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="add-to-existing-case-action"]'; export const ATTACH_TO_NEW_CASE_BUTTON = '[data-test-subj="add-to-new-case-action"]'; export const USER_COLUMN = '[data-gridcell-column-id="user.name"]'; -export const HOST_RISK_HEADER_COLIMN = +export const HOST_RISK_HEADER_COLUMN = '[data-test-subj="dataGridHeaderCell-host.risk.calculated_level"]'; export const HOST_RISK_COLUMN = '[data-gridcell-column-id="host.risk.calculated_level"]'; -export const USER_RISK_HEADER_COLIMN = +export const USER_RISK_HEADER_COLUMN = '[data-test-subj="dataGridHeaderCell-user.risk.calculated_level"]'; export const USER_RISK_COLUMN = '[data-gridcell-column-id="user.risk.calculated_level"]'; @@ -152,8 +130,6 @@ export const ACTIONS_EXPAND_BUTTON = '[data-test-subj="euiDataGridCellExpandButt export const SHOW_TOP_N_HEADER = '[data-test-subj="topN-container"] [data-test-subj="header-section-title"]'; -export const SHOW_TOP_N_CLOSE_BUTTON = '[data-test-subj="close"]'; - export const ALERTS_HISTOGRAM_LEGEND = '[data-test-subj="alerts-histogram-panel"] .echLegendItem__action'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts_details.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts_details.ts index 9d57a2b502f33b..70f43b35d1211c 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts_details.ts @@ -17,10 +17,6 @@ export const ENRICHMENT_QUERY_START_INPUT = '.start-picker'; export const ENRICHMENT_QUERY_END_INPUT = '.end-picker'; -export const FIELD = (value: string) => { - return `[data-test-subj="event-field-${value}"]`; -}; - export const FILTER_INPUT = '[data-test-subj="eventDetails"] .euiFieldSearch'; export const INDICATOR_MATCH_ENRICHMENT_SECTION = '[data-test-subj="threat-match-detected"]'; @@ -32,14 +28,8 @@ export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]'; export const JSON_TEXT = '[data-test-subj="jsonView"]'; -export const OVERVIEW_RISK_SCORE = '[data-test-subj="eventDetails"] [data-test-subj="riskScore"]'; - export const OVERVIEW_RULE = '[data-test-subj="eventDetails"] [data-test-subj="ruleName"]'; -export const OVERVIEW_RULE_TYPE = '[data-test-subj="event-field-kibana.alert.rule.type"]'; - -export const OVERVIEW_SEVERITY = '[data-test-subj="eventDetails"] [data-test-subj="severity"]'; - export const OVERVIEW_STATUS = '[data-test-subj="eventDetails"] [data-test-subj="alertStatus"]'; export const EVENT_DETAILS_ALERT_STATUS_POPOVER = @@ -69,8 +59,6 @@ export const THREAT_INTEL_TAB = '[data-test-subj="threatIntelTab"]'; export const UPDATE_ENRICHMENT_RANGE_BUTTON = '[data-test-subj="enrichment-button"]'; -export const OVERVIEW_TAB = '[data-test-subj="overviewTab"]'; - export const SUMMARY_VIEW_INVESTIGATE_IN_TIMELINE_BUTTON = `${SUMMARY_VIEW} [aria-label='Investigate in timeline']`; export const INSIGHTS_RELATED_ALERTS_BY_SESSION = `[data-test-subj='related-alerts-by-session']`; @@ -83,6 +71,4 @@ export const INSIGHTS_INVESTIGATE_ANCESTRY_ALERTS_IN_TIMELINE_BUTTON = `[data-te export const ENRICHED_DATA_ROW = `[data-test-subj='EnrichedDataRow']`; -export const OPEN_ALERT_DETAILS_PAGE = `[data-test-subj="open-alert-details-page"]`; - export const COPY_ALERT_FLYOUT_LINK = `[data-test-subj="copy-alert-flyout-link"]`; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts index 4950f2c65fab2b..a81cc24ae9653d 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts @@ -9,15 +9,12 @@ import { encode } from '@kbn/rison'; import { recurse } from 'cypress-recurse'; import { formatPageFilterSearchParam } from '@kbn/security-solution-plugin/common/utils/format_page_filter_search_param'; import type { FilterItemObj } from '@kbn/security-solution-plugin/public/common/components/filter_group/types'; -import { TOP_N_CONTAINER } from '../screens/network/flows'; import { ADD_EXCEPTION_BTN, ALERT_CHECKBOX, CLOSE_ALERT_BTN, CLOSE_SELECTED_ALERTS_BTN, EXPAND_ALERT_BTN, - GROUP_BY_TOP_INPUT, - MANAGE_ALERT_DETECTION_RULES_BTN, MARK_ALERT_ACKNOWLEDGED_BTN, OPEN_ALERT_BTN, SEND_ALERT_TO_TIMELINE_BTN, @@ -43,7 +40,6 @@ import { ALERT_COUNT_TABLE_COLUMN, SELECT_HISTOGRAM, CELL_FILTER_OUT_BUTTON, - SHOW_TOP_N_CLOSE_BUTTON, ALERTS_HISTOGRAM_LEGEND, LEGEND_ACTIONS, SESSION_VIEWER_BUTTON, @@ -239,10 +235,6 @@ export const goToClosedAlerts = () => { cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; -export const goToManageAlertsDetectionRules = () => { - cy.get(MANAGE_ALERT_DETECTION_RULES_BTN).should('exist').click(); -}; - export const goToOpenedAlertsOnRuleDetailsPage = () => { cy.get(OPENED_ALERTS_FILTER_BTN).click({ force: true }); cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); @@ -283,11 +275,6 @@ export const selectAlertsHistogram = () => { cy.get(SELECT_HISTOGRAM).click({ force: true }); }; -export const clearGroupByTopInput = () => { - cy.get(GROUP_BY_TOP_INPUT).focus(); - cy.get(GROUP_BY_TOP_INPUT).type('{backspace}'); -}; - export const goToAcknowledgedAlerts = () => { /* * below line commented because alertPageFiltersEnabled feature flag @@ -372,11 +359,6 @@ export const showTopNAlertProperty = (propertySelector: string, rowIndex: number clickExpandActions(propertySelector, rowIndex); cy.get(CELL_SHOW_TOP_FIELD_BUTTON).first().click({ force: true }); }; -export const closeTopNAlertProperty = () => { - cy.get(TOP_N_CONTAINER).then(() => { - cy.get(SHOW_TOP_N_CLOSE_BUTTON).click(); - }); -}; export const waitForAlerts = () => { /* @@ -477,11 +459,6 @@ export const openSessionViewerFromAlertTable = (rowIndex: number = 0) => { cy.get(SESSION_VIEWER_BUTTON).eq(rowIndex).click(); }; -export const openAlertTaggingContextMenu = () => { - cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click(); - cy.get(ALERT_TAGGING_CONTEXT_MENU_ITEM).click(); -}; - export const openAlertTaggingBulkActionMenu = () => { cy.get(TAKE_ACTION_POPOVER_BTN).click(); cy.get(ALERT_TAGGING_CONTEXT_MENU_ITEM).click(); From 0993ce4db9f94cd0c561945a510a7fc022266a40 Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:50:27 +0100 Subject: [PATCH 05/24] [DOCS] 8.11.0 release notes targeting elastic:main (#169819) ## Summary Adds the release notes for 8.11.0, and incorporates the feedback from the original draft [PR](https://github.com/elastic/kibana/pull/168593). The merged in [PR](https://github.com/elastic/kibana/pull/168710) targeted the wrong branch, so no release notes are visible for 8.11.0. --- docs/CHANGELOG.asciidoc | 230 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 8c66afcd0ef294..2b24ca538caa90 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,7 @@ Review important information about the {kib} 8.x releases. +* <> * <> * <> * <> @@ -52,6 +53,235 @@ Review important information about the {kib} 8.x releases. * <> -- +[[release-notes-8.11.0]] +== {kib} 8.11.0 + + +For information about the {kib} 8.11.0 release, review the following information. + + +[float] +[[breaking-changes-8.11.0]] +=== Breaking changes + + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 8.11.0, review the breaking changes, then mitigate the impact to your application. + + +[discrete] +[[breaking-167085]] +.Improve config output validation for default output. +[%collapsible] +==== +*Details* + +Improve config output validation to not allow to defining multiple default outputs in {kib} configuration. For more information, refer to ({kibana-pull}167085[#167085]). +==== +[discrete] +[[breaking-161806]] +.Convert filterQuery to KQL. +[%collapsible] +==== +*Details* + +Converts `filterQuery` to a KQL query string. For more information, refer to ({kibana-pull}161806[#161806]). +==== +[float] +[[deprecations-8.11.0]] +=== Deprecations + + +The following functionality is deprecated in 8.11.0, and will be removed in 9.0.0. +Deprecated functionality does not have an immediate impact on your application, but we strongly recommend +you make the necessary updates after you upgrade to 8.11.0. + + +[discrete] +[[deprecation-164651]] +.Updates to move from doc_root.vulnerability.package -> doc_root.package (ECS). +[%collapsible] +==== +*Details* + +This updates all instances of vulnerability.package to the ECS standard package fieldset. For more information, refer to ({kibana-pull}164651[#164651]). +==== +[float] +[[features-8.11.0]] +=== Features +{kib} 8.11.0 adds the following new and notable features. + + +Alerting:: +* Adds support for the new ES|QL language for {es} query rules ({kibana-pull}165973[#165973]). +* Elasticsearch query rule can select multiple group-by terms ({kibana-pull}166146[#166146]). +* Adds a Log tab to the Observability Rules page ({kibana-pull}165115[#165115]). +APM:: +* Adds bulk action to untrack selected alerts ({kibana-pull}167579[#167579]). +* Introduce custom dashboards tab in service overview ({kibana-pull}166789[#166789]). +* Adds service profiling Top 10 Functions ({kibana-pull}166226[#166226]). +* Adds service profiling flamegraph ({kibana-pull}165360[#165360]). +Cases:: +* Adds custom fields in Cases ({kibana-pull}167016[#167016]). +Dashboard:: +* Copy panel refactor ({kibana-pull}166991[#166991]). +* Make links panel available under technical preview ({kibana-pull}166896[#166896]). +* Store view mode in local storage ({kibana-pull}166523[#166523]). +* Adds a read only state for Managed Dashboards ({kibana-pull}166204[#166204]). +Discover:: +* Adds resize support to the Discover field list sidebar ({kibana-pull}167066[#167066]). +Elastic Security:: +For the Elastic Security 8.11.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Enterprise Search:: +For the Elastic Enterprise Search 8.11.0 release information, refer to {enterprise-search-ref}/changelog.html[_Elastic Enterprise Search Documentation Release notes_]. +Fleet:: +* Set env variable `ELASTIC_NETINFO:false` in {kib} ({kibana-pull}166156[#166156]). +* Added restart upgrade action ({kibana-pull}166154[#166154]). +* Adds ability to set a proxy for agent binary source ({kibana-pull}164168[#164168]). +* Adds ability to set a proxy for agent download source ({kibana-pull}164078[#164078]). +Lens & Visualizations:: +* Adds color mapping for categorical dimensions in *Lens* available under technical preview ({kibana-pull}162389[#162389]). +* Inline editing of **Lens** panels on a dashboard or canvas ({kibana-pull}166169[#166169]). +* Individual annotation editing from library ({kibana-pull}163346[#163346]). +Logs:: +* Convert log explorer profile into standalone app available under technical preview ({kibana-pull}164493[#164493]). +Machine Learning:: +* Adds support for the ELSER v2 download in the Trained Models UI ({kibana-pull}167407[#167407]). +* Adds data drift detection workflow from Trained Models to Data comparison view ({kibana-pull}162853[#162853]). +Management:: +* Supports for viewing and editing data retention per data stream in Index Management is available under technical preview ({kibana-pull}167006[#167006]). +* Supports for viewing and editing data retention per data stream in Index Management is available under technical preview ({kibana-pull}167006[#167006]). +* Index details can now be viewed on a new index details page in Index Management ({kibana-pull}165705[#165705]). +* Supports for managing, executing, and deleting enrich policies in Index Management ({kibana-pull}164080[#164080]). +Platform:: +* ES|QL, a new query language, is available under technical preview in Discover and Dashboards ({kibana-pull}146971[#146971]). +Querying & Filtering:: +* Saved queries can now be shared between multiple spaces ({kibana-pull}163436[#163436]). +Uptime:: +* Adds a document viewer to the summary pings table ({kibana-pull}163926[#163926]). + + +For more information about the features introduced in 8.11.0, refer to <>. + + +[[enhancements-and-bug-fixes-v8.11.0]] +=== Enhancements and bug fixes + + +For detailed information about the 8.11.0 release, review the enhancements and bug fixes. + + +[float] +[[enhancement-v8.11.0]] +=== Enhancements +APM:: +* Changed mobile badge from 'technical preview' to 'beta' ({kibana-pull}167543[#167543]). +* New Profiling ES Flamegraph API ({kibana-pull}167477[#167477]). +* Adds Universal Profiling to O11y overview and Setup guide ({kibana-pull}165092[#165092]). +* Mark disabled alerts as Untracked in both Stack Management and o11y ({kibana-pull}164788[#164788]). +* Adds time range to event metadata API ({kibana-pull}167132[#167132]). +* New settings to control CO2 calculation ({kibana-pull}166637[#166637]). +* Adds permissions for "input-only" package ({kibana-pull}166234[#166234]). +* Adds selecting the consumer based on the authorized consumers when a user is creating an ES Query threshold rule ({kibana-pull}166032[#166032]). +* Migrate Ace based `EuiCodeEditor` to Monaco based code editor ({kibana-pull}165951[#165951]). +* Mobile UI crash widget added ({kibana-pull}163527[#163527]). +Cases:: +* Show a warning message to inform user that navigating after the 10Kth case is not possible ({kibana-pull}164323[#164323]). +Dashboard:: +* Focus on a single panel while disabling all other panels ({kibana-pull}165417[#165417]). +* Adds filter details to panel settings ({kibana-pull}162913[#162913]). +* Adds support for date fields in the options list controls ({kibana-pull}164362[#164362]). +Discover:: +* Redesign for the grid, panels and sidebar ({kibana-pull}165866[#165866]). +* Set data table row height to auto-fit by default ({kibana-pull}164218[#164218]). +* Allow fetching more documents on Discover page ({kibana-pull}163784[#163784]). +Elastic Security:: +For the Elastic Security 8.11.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Enterprise Search:: +For the Elastic Enterprise Search 8.11.0 release information, refer to {enterprise-search-ref}/changelog.html[_Elastic Enterprise Search Documentation Release notes_]. +Fleet:: +* Adds sidebar navigation showing headings extracted from the readme ({kibana-pull}167216[#167216]). +Inspector:: +* Clusters tab added under Inspector ({kibana-pull}166025[#166025]). +* Open incomplete response warning in Inspector ({kibana-pull}167205[#167205]). +Lens & Visualizations:: +* Other bucket defaults to false for top values greater than equal 1000 in *Lens* ({kibana-pull}167141[#167141]). +* Adds support for decimals in percentiles in *Lens* ({kibana-pull}165703[#165703]). +Machine Learning:: +* Updates ELSER version for Elastic Assistant ({kibana-pull}167522[#167522]). +* Retains `created_by` setting when exporting anomaly detection jobs ({kibana-pull}167319[#167319]). +* Improves the wording of awaiting ML nodes messages ({kibana-pull}167306[#167306]). +* Adds `created_by` job property for the advanced wizard ({kibana-pull}167021[#167021]). +* Trained model testing: only show indices with supported fields ({kibana-pull}166490[#166490]). +* Alerts as data integration for Anomaly Detection rule type ({kibana-pull}166349[#166349]). +* Data Frame Analytics Trained models: adds the ability to reindex after pipeline creation ({kibana-pull}166312[#166312]). +* Adds Create a data view button to index or saved search selector in ML pages and Transforms management ({kibana-pull}166668[#166668]). +* Improvements to UX of adding ML embeddables to a dashboard ({kibana-pull}165714[#165714]). +* AIOps: Supports text fields in log rate analysis ({kibana-pull}165124[#165124]). +* Data Frame Analytics creation wizard: adds ability to add custom URLs to jobs ({kibana-pull}164520[#164520]). +Management:: +* Adds Create a data view button to index or saved search selector in ML pages and Transforms management ({kibana-pull}166668[#166668]). +* Improve loading behavior of Transforms list if stats request is slow or is not available ({kibana-pull}166320[#166320]). +* Adds support for PATCH requests in Console ({kibana-pull}165634[#165634]). +* Improves autocomplete to suggest knn in search query ({kibana-pull}165531[#165531]). +* Improves display for long descriptions in Transforms ({kibana-pull}165149[#165149]). +* Improve transform list reloading behavior ({kibana-pull}164296[#164296]). +Maps:: +* Allow by value styling for EMS boundary fields ({kibana-pull}166306[#166306]). +* Adds support for `geo_shape` fields as the entity geospatial field when creating tracking containment alerts ({kibana-pull}164100[#164100]). +Observability:: +* ES|QL query generation ({kibana-pull}166041[#166041]). +Querying & Filtering:: +* New "Saved Query Management" privilege to allow saving queries across Kibana ({kibana-pull}166937[#166937]). +* Improvements to the filter builder inputs for long fields ({kibana-pull}166024[#166024]). +Uptime:: +* Added ability to hide public locations ({kibana-pull}164863[#164863]). + + +[float] +[[fixes-v8.11.0]] +=== Bug Fixes +Alerting:: +* Improve error handling in ES Index action response ({kibana-pull}164841[#164841]). +* Bring back toggle column on alert table ({kibana-pull}168158[#168158]). +* Fixes Errors rules link on observability alert page ({kibana-pull}167027[#167027]). +* Enable read-only users to access rules ({kibana-pull}167003[#167003]). +* Fixes rule snooze toast copy ({kibana-pull}166030[#166030]). +APM:: +* Ensure APM data view is available across all spaces ({kibana-pull}167704[#167704]). +* Adds an environment param to the service metadata details endpoint ({kibana-pull}167173[#167173]). +* Fixes set up process ({kibana-pull}167067[#167067]). +Dashboard:: +* Generate new panel IDs on Dashboard clone ({kibana-pull}166299[#166299]). +Elastic Security:: +For the Elastic Security 8.11.0 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Enterprise Search:: +For the Elastic Enterprise Search 8.11.0 release information, refer to {enterprise-search-ref}/changelog.html[_Elastic Enterprise Search Documentation Release notes_]. +Fleet:: +* Vastly improve performance of Fleet final pipeline's date formatting logic for `event.ingested` ({kibana-pull}167318[#167318]). +Lens & Visualizations:: +* Fixes heatmap color assignment on single value scenario in *Lens* ({kibana-pull}167995[#167995]). +* Fixes mosaic with 2 axis coloring in *Lens* ({kibana-pull}167035[#167035]). +* Show icons/titles instead of previews in suggestions panel in *Lens* ({kibana-pull}166808[#166808]). +* Consider root level filters buckets correctly when building other terms bucket ({kibana-pull}165656[#165656]). +* Prevent user to use decimals for custom Percentile rank function in Top values in *Lens* ({kibana-pull}165616[#165616]). +* Fixes the Graph application settings tab when in dark mode ({kibana-pull}165614[#165614]). +* Fixes Visualize List search and CRUD operations via content management ({kibana-pull}165485[#165485]). +Logs:: +* Use correct ML API to query blocking tasks ({kibana-pull}167779[#167779]). +Machine Learning:: +* AIOps: Fixes log pattern analysis sparklines and chart ({kibana-pull}168337[#168337]). +* AIOps: Fixes Data View runtime fields support in the Change point detection UI ({kibana-pull}168249[#168249]). +* Fixes anomaly charts when partition field contains an empty string ({kibana-pull}168102[#168102]). +* Data Frame analytics outlier detection results: ensure scatterplot matrix adheres to bounding box ({kibana-pull}167941[#167941]). +* Fixes Anomaly charts embeddable fails to load if partition value is empty string ({kibana-pull}167827[#167827]). +Management:: +* Fixes `isErrorResponse` when cluster details are provided ({kibana-pull}166667[#166667]). +* Fixes autocomplete not to be prompted between triple quotes ({kibana-pull}165535[#165535]). +* Fixes autocomplete on only 1 letter typed in Console's request editor ({kibana-pull}164707[#164707]). +* Fixing duration field formatter showing 0 seconds instead of "few seconds" ({kibana-pull}164659[#164659]). +* Fixes a bug that autocomplete does not work right after a comma ({kibana-pull}164608[#164608]). +* Fixes unnecessary autocompletes on HTTP methods ({kibana-pull}163233[#163233]). +Presentation:: +* Fixes ES query rule boundary field changed when editing the rule ({kibana-pull}165155[#165155]). + [[release-notes-8.10.4]] == {kib} 8.10.4 From 13d17925440a87a46db8490414e3c6acd6edccf7 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Wed, 25 Oct 2023 16:01:46 +0200 Subject: [PATCH 06/24] [ftr] read username from config (#169755) ## Summary Similar to #169639 The tests fail on MKI because username is hardcoded to `elastic_serverless`. Reading value from FTR config should fix it. --- .../functional/test_suites/common/reporting/management.ts | 6 ++++-- .../functional/test_suites/observability/cases/view_case.ts | 4 ++-- .../functional/test_suites/security/ftr/cases/view_case.ts | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts b/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts index 70f0037cd17c32..c36000889d4812 100644 --- a/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts +++ b/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts @@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'svlCommonPage', 'header']); const reportingAPI = getService('svlReportingApi'); + const config = getService('config'); const navigateToReportingManagement = async () => { log.debug(`navigating to reporting management app`); @@ -47,8 +48,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ], }; - const TEST_USERNAME = 'elastic_serverless'; - const TEST_PASSWORD = 'changeme'; + // Kibana CI and MKI use different users + const TEST_USERNAME = config.get('servers.kibana.username'); + const TEST_PASSWORD = config.get('servers.kibana.password'); before('initialize saved object archive', async () => { // add test saved search object diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts index c60b7a8ed103c7..0e60fa0125234c 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts @@ -28,7 +28,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const cases = getService('cases'); const svlCases = getService('svlCases'); const find = getService('find'); - + const config = getService('config'); const retry = getService('retry'); const comboBox = getService('comboBox'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); @@ -453,7 +453,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const reporterText = await reporter.getVisibleText(); - expect(reporterText).to.be('elastic_serverless'); + expect(reporterText).to.be(config.get('servers.kibana.username')); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts index c3d82858576347..d9531a4529ee5c 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts @@ -28,7 +28,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const cases = getService('cases'); const svlCases = getService('svlCases'); const find = getService('find'); - + const config = getService('config'); const retry = getService('retry'); const comboBox = getService('comboBox'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); @@ -452,7 +452,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const reporterText = await reporter.getVisibleText(); - expect(reporterText).to.be('elastic_serverless'); + expect(reporterText).to.be(config.get('servers.kibana.username')); }); }); From 7e97dd90e78199f2b90725b075d60df427fab876 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 25 Oct 2023 08:34:06 -0600 Subject: [PATCH 07/24] [ML][AIOps] Telemetry: Track change point detection runs (#169158) ## Summary This PR adds UI tracking for Change Point Detection for AIOps. - tracks type of analysis and source (where the analysis is being run from) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Quynh Nguyen (Quinn) <43350163+qn895@users.noreply.github.com> --- x-pack/plugins/aiops/common/constants.ts | 2 ++ .../change_point_detection_root.tsx | 3 ++ .../change_point_detection/constants.ts | 6 ++++ .../use_change_point_agg_request.ts | 29 +++++++++++++++++++ .../embeddable_change_point_chart.tsx | 11 +++++-- ...mbeddable_change_point_chart_component.tsx | 1 - .../embeddable_change_point_chart_factory.ts | 7 +++-- .../public/hooks/use_aiops_app_context.ts | 7 +++++ x-pack/plugins/aiops/public/types.ts | 2 ++ x-pack/plugins/aiops/tsconfig.json | 1 + .../aiops/change_point_detection.tsx | 1 + 11 files changed, 65 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/aiops/common/constants.ts b/x-pack/plugins/aiops/common/constants.ts index 47bdee0d5e6c6a..5916464e909804 100644 --- a/x-pack/plugins/aiops/common/constants.ts +++ b/x-pack/plugins/aiops/common/constants.ts @@ -30,3 +30,5 @@ export const AIOPS_TELEMETRY_ID = { AIOPS_DEFAULT_SOURCE: 'ml_aiops_labs', AIOPS_ANALYSIS_RUN_ORIGIN: 'aiops-analysis-run-origin', } as const; + +export const EMBEDDABLE_ORIGIN = 'embeddable'; diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx index 414e214fd1fe74..b63e17e2e0d867 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx @@ -37,6 +37,7 @@ import { } from './change_point_detection_context'; import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check'; import { ReloadContextProvider } from '../../hooks/use_reload'; +import { AIOPS_TELEMETRY_ID } from '../../../common/constants'; const localStorage = new Storage(window.localStorage); @@ -76,6 +77,8 @@ export const ChangePointDetectionAppState: FC return <>{warning}; } + appDependencies.embeddingOrigin = AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE; + const PresentationContextProvider = appDependencies.presentationUtil?.ContextProvider ?? React.Fragment; diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/constants.ts b/x-pack/plugins/aiops/public/components/change_point_detection/constants.ts index 0219b3ac87fc00..6e49f9fe0fa707 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/constants.ts +++ b/x-pack/plugins/aiops/public/components/change_point_detection/constants.ts @@ -38,3 +38,9 @@ export const EXCLUDED_CHANGE_POINT_TYPES = new Set([ ]); export const MAX_CHANGE_POINT_CONFIGS = 6; + +export const CHANGE_POINT_DETECTION_EVENT = { + RUN: 'ran_aiops_change_point_detection', + SUCCESS: 'aiops_change_point_detection_success', + ERROR: 'aiops_change_point_detection_error', +} as const; diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/use_change_point_agg_request.ts b/x-pack/plugins/aiops/public/components/change_point_detection/use_change_point_agg_request.ts index a574ae7abd09ba..0393ab5e5a6fca 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/use_change_point_agg_request.ts +++ b/x-pack/plugins/aiops/public/components/change_point_detection/use_change_point_agg_request.ts @@ -13,6 +13,7 @@ import type { MappingRuntimeFields, SearchRequest, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { METRIC_TYPE } from '@kbn/analytics'; import { useReload } from '../../hooks/use_reload'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { @@ -28,6 +29,7 @@ import { COMPOSITE_AGG_SIZE, EXCLUDED_CHANGE_POINT_TYPES, SPLIT_FIELD_CARDINALITY_LIMIT, + CHANGE_POINT_DETECTION_EVENT, } from './constants'; interface RequestOptions { @@ -122,6 +124,8 @@ export function useChangePointResults( ) { const { notifications: { toasts }, + usageCollection, + embeddingOrigin, } = useAiopsAppContext(); const { dataView } = useDataSource(); @@ -187,11 +191,27 @@ export function useChangePointResults( runtimeMappings ); + if (usageCollection?.reportUiCounter && embeddingOrigin) { + usageCollection.reportUiCounter( + embeddingOrigin, + METRIC_TYPE.COUNT, + CHANGE_POINT_DETECTION_EVENT.RUN + ); + } + const result = await runRequest< { params: SearchRequest }, { rawResponse: ChangePointAggResponse } >({ params: requestPayload }); + if (usageCollection?.reportUiCounter && embeddingOrigin) { + usageCollection.reportUiCounter( + embeddingOrigin, + METRIC_TYPE.COUNT, + CHANGE_POINT_DETECTION_EVENT.SUCCESS + ); + } + if (result === null) { setProgress(null); return; @@ -257,6 +277,13 @@ export function useChangePointResults( ); } } catch (e) { + if (usageCollection?.reportUiCounter && embeddingOrigin) { + usageCollection.reportUiCounter( + embeddingOrigin, + METRIC_TYPE.COUNT, + CHANGE_POINT_DETECTION_EVENT.ERROR + ); + } toasts.addError(e, { title: i18n.translate('xpack.aiops.changePointDetection.fetchErrorTitle', { defaultMessage: 'Failed to fetch change points', @@ -265,6 +292,7 @@ export function useChangePointResults( } }, [ + embeddingOrigin, isSingleMetric, totalAggPages, dataView, @@ -278,6 +306,7 @@ export function useChangePointResults( splitFieldsOptions, runRequest, toasts, + usageCollection, ] ); diff --git a/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart.tsx b/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart.tsx index af5942024ec99d..f9d2109d88df6b 100644 --- a/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart.tsx +++ b/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart.tsx @@ -21,9 +21,10 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker'; import { pick } from 'lodash'; import { LensPublicStart } from '@kbn/lens-plugin/public'; import { Subject } from 'rxjs'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { EmbeddableInputTracker } from './embeddable_chart_component_wrapper'; -import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants'; +import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE, EMBEDDABLE_ORIGIN } from '../../common/constants'; import { AiopsAppContext, type AiopsAppDependencies } from '../hooks/use_aiops_app_context'; import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component'; @@ -40,6 +41,7 @@ export interface EmbeddableChangePointChartDeps { notifications: CoreStart['notifications']; i18n: CoreStart['i18n']; lens: LensPublicStart; + usageCollection: UsageCollectionSetup; } export type IEmbeddableChangePointChart = typeof EmbeddableChangePointChart; @@ -121,10 +123,15 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable< const input = this.getInput(); const input$ = this.getInput$(); + const aiopsAppContextValue = { + ...this.deps, + embeddingOrigin: this.parent?.type ?? EMBEDDABLE_ORIGIN, + } as unknown as AiopsAppDependencies; + ReactDOM.render( - + ( diff --git a/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart_factory.ts b/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart_factory.ts index 9bf35b0ca2b1a8..ef7c3a431cc180 100644 --- a/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart_factory.ts +++ b/x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart_factory.ts @@ -71,8 +71,10 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin async create(input: EmbeddableChangePointChartInput, parent?: IContainer) { try { - const [{ i18n: i18nService, theme, http, uiSettings, notifications }, { lens, data }] = - await this.getStartServices(); + const [ + { i18n: i18nService, theme, http, uiSettings, notifications }, + { lens, data, usageCollection }, + ] = await this.getStartServices(); return new EmbeddableChangePointChart( { @@ -83,6 +85,7 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin data, notifications, lens, + usageCollection, }, input, parent diff --git a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts index aa364a416a0468..5714ae5283fb29 100644 --- a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts +++ b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts @@ -32,6 +32,7 @@ import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { CasesUiStart } from '@kbn/cases-plugin/public'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; /** * AIOps App Dependencies to be provided via React context. @@ -84,6 +85,10 @@ export interface AiopsAppDependencies { * Unified search. */ unifiedSearch: UnifiedSearchPublicPluginStart; + /** + * Usage collection. + */ + usageCollection?: UsageCollectionSetup; /** * Used to create deep links to other plugins. */ @@ -115,6 +120,8 @@ export interface AiopsAppDependencies { embeddable?: EmbeddableStart; cases?: CasesUiStart; isServerless?: boolean; + /** Identifier to indicate the plugin utilizing the component */ + embeddingOrigin?: string; } /** diff --git a/x-pack/plugins/aiops/public/types.ts b/x-pack/plugins/aiops/public/types.ts index e0f86c68864bd3..8b40d4c257434c 100755 --- a/x-pack/plugins/aiops/public/types.ts +++ b/x-pack/plugins/aiops/public/types.ts @@ -18,6 +18,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { CasesUiSetup } from '@kbn/cases-plugin/public'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { EmbeddableChangePointChartInput } from './embeddable/embeddable_change_point_chart'; export interface AiopsPluginSetupDeps { @@ -40,6 +41,7 @@ export interface AiopsPluginStartDeps { licensing: LicensingPluginStart; executionContext: ExecutionContextStart; embeddable: EmbeddableStart; + usageCollection: UsageCollectionSetup; } export type AiopsPluginSetup = void; diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index 1c0095046c7359..67e8908f5c4213 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -65,6 +65,7 @@ "@kbn/react-kibana-mount", "@kbn/ml-chi2test", "@kbn/usage-collection-plugin", + "@kbn/analytics", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx index 774c3ac6f9d2d6..551e19a2644634 100644 --- a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx +++ b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx @@ -67,6 +67,7 @@ export const ChangePointDetectionPage: FC = () => { 'embeddable', 'cases', 'i18n', + 'usageCollection', ]), fieldStats: { useFieldStatsTrigger, FieldStatsFlyoutProvider }, }} From 551e4f05527030090cba8adc9e4366fd00949989 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:12:32 -0700 Subject: [PATCH 08/24] [osquery] Remove unnecessary `ghost` colors from `EuiBottomBar` (#169309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary 👋 Hey y'all - EUI will shortly be deprecating the `ghost` color in all button components (see https://eui.elastic.co/v89.0.0/#/navigation/button#ghost-vs-dark-mode). In this PR, all components using `color="ghost"` are being used within an `EuiBottomBar` and as such already automatically inherit dark mode coloring. I'm opening this PR ahead of time for your team so you can test this migration and ensure no UI regressions have occurred as a result. ### Checklist - [x] Tested in light and dark mode --- x-pack/plugins/osquery/public/packs/form/index.tsx | 2 +- .../plugins/osquery/public/routes/saved_queries/edit/form.tsx | 2 +- x-pack/plugins/osquery/public/routes/saved_queries/new/form.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/osquery/public/packs/form/index.tsx b/x-pack/plugins/osquery/public/packs/form/index.tsx index 1a5b9152b3ac52..259e6d0d7b9eee 100644 --- a/x-pack/plugins/osquery/public/packs/form/index.tsx +++ b/x-pack/plugins/osquery/public/packs/form/index.tsx @@ -300,7 +300,7 @@ const PackFormComponent: React.FC = ({ - + = ({ - + = ({ - + Date: Wed, 25 Oct 2023 17:19:24 +0200 Subject: [PATCH 09/24] Add smart logic to log information about plugin status changes (#168207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary New attempt at fixing https://github.com/elastic/kibana/issues/116718 Inspired on https://github.com/elastic/kibana/pull/126320 Here's what the newly logged `[status]` information looks like on a fresh startup: image The first 2 entries are logs from Core services 🆕 . The next 5 entries are emitted due to `taskManager` plugin emitting a degraded status right at startup. I have created an issue to tackle that one: https://github.com/elastic/kibana/issues/168237 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/cached_plugins_status.ts | 15 +- .../src/get_summary_status.test.ts | 229 +++++++--------- .../src/get_summary_status.ts | 127 +++++---- .../src/log_core_services_status.test.ts | 255 ++++++++++++++++++ .../src/log_core_services_status.ts | 164 +++++++++++ .../src/log_overall_status.test.ts | 187 +++++++------ .../src/log_overall_status.ts | 66 +++-- .../src/log_plugins_status.test.ts | 216 +++++++++++++++ .../src/log_plugins_status.ts | 154 +++++++++++ .../src/plugins_status.test.ts | 131 +++++---- .../src/plugins_status.ts | 186 +++++++------ .../src/status_service.test.mocks.ts | 22 ++ .../src/status_service.test.ts | 145 +++++++--- .../src/status_service.ts | 53 ++-- .../core-status-server-internal/src/types.ts | 24 ++ .../core-status-server-internal/tsconfig.json | 2 + .../licensing/server/plugin_status.test.ts | 31 +-- .../plugins/licensing/server/plugin_status.ts | 9 +- x-pack/plugins/task_manager/server/plugin.ts | 2 +- x-pack/test/scalability/apis/api.status.json | 2 +- .../scalability/apis/api.status.no_auth.json | 2 +- 21 files changed, 1527 insertions(+), 495 deletions(-) create mode 100644 packages/core/status/core-status-server-internal/src/log_core_services_status.test.ts create mode 100644 packages/core/status/core-status-server-internal/src/log_core_services_status.ts create mode 100644 packages/core/status/core-status-server-internal/src/log_plugins_status.test.ts create mode 100644 packages/core/status/core-status-server-internal/src/log_plugins_status.ts create mode 100644 packages/core/status/core-status-server-internal/src/status_service.test.mocks.ts diff --git a/packages/core/status/core-status-server-internal/src/cached_plugins_status.ts b/packages/core/status/core-status-server-internal/src/cached_plugins_status.ts index 20cc30d83176e6..b7d45d3682971e 100644 --- a/packages/core/status/core-status-server-internal/src/cached_plugins_status.ts +++ b/packages/core/status/core-status-server-internal/src/cached_plugins_status.ts @@ -8,14 +8,13 @@ import type { Observable } from 'rxjs'; import type { PluginName } from '@kbn/core-base-common'; -import type { ServiceStatus } from '@kbn/core-status-common'; - import { type Deps, PluginsStatusService as BasePluginsStatusService } from './plugins_status'; +import type { PluginStatus } from './types'; export class PluginsStatusService extends BasePluginsStatusService { - private all$?: Observable>; - private dependenciesStatuses$: Record>>; - private derivedStatuses$: Record>; + private all$?: Observable>; + private dependenciesStatuses$: Record>>; + private derivedStatuses$: Record>; constructor(deps: Deps) { super(deps); @@ -23,7 +22,7 @@ export class PluginsStatusService extends BasePluginsStatusService { this.derivedStatuses$ = {}; } - public getAll$(): Observable> { + public getAll$(): Observable> { if (!this.all$) { this.all$ = super.getAll$(); } @@ -31,7 +30,7 @@ export class PluginsStatusService extends BasePluginsStatusService { return this.all$; } - public getDependenciesStatus$(plugin: PluginName): Observable> { + public getDependenciesStatus$(plugin: PluginName): Observable> { if (!this.dependenciesStatuses$[plugin]) { this.dependenciesStatuses$[plugin] = super.getDependenciesStatus$(plugin); } @@ -39,7 +38,7 @@ export class PluginsStatusService extends BasePluginsStatusService { return this.dependenciesStatuses$[plugin]; } - public getDerivedStatus$(plugin: PluginName): Observable { + public getDerivedStatus$(plugin: PluginName): Observable { if (!this.derivedStatuses$[plugin]) { this.derivedStatuses$[plugin] = super.getDerivedStatus$(plugin); } diff --git a/packages/core/status/core-status-server-internal/src/get_summary_status.test.ts b/packages/core/status/core-status-server-internal/src/get_summary_status.test.ts index 2bb692bfc311be..4807b33a8e4103 100644 --- a/packages/core/status/core-status-server-internal/src/get_summary_status.test.ts +++ b/packages/core/status/core-status-server-internal/src/get_summary_status.test.ts @@ -8,160 +8,127 @@ import { ServiceStatus, ServiceStatusLevels } from '@kbn/core-status-common'; import { getSummaryStatus } from './get_summary_status'; +import { PluginStatus } from './types'; describe('getSummaryStatus', () => { - const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available' }; - const degraded: ServiceStatus = { + const availableService: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'Available', + }; + const degradedService: ServiceStatus = { level: ServiceStatusLevels.degraded, summary: 'This is degraded!', }; - const unavailable: ServiceStatus = { - level: ServiceStatusLevels.unavailable, - summary: 'This is unavailable!', - }; - const critical: ServiceStatus = { + const criticalService: ServiceStatus = { level: ServiceStatusLevels.critical, summary: 'This is critical!', }; + const availablePluginA: PluginStatus = { + level: ServiceStatusLevels.available, + summary: 'A is available', + reported: true, + }; + const unavailablePluginA: PluginStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'A is unavailable!', + reported: true, + }; + const availablePluginB: PluginStatus = { + level: ServiceStatusLevels.available, + summary: 'B is available', + reported: true, + }; + const unavailablePluginB: PluginStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'B is unavailable!', + reported: true, + }; + const unavailablePluginC: PluginStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'C is unavailable!', + // Note that C has an inferred status + }; it('returns available when all status are available', () => { expect( - getSummaryStatus( - Object.entries({ - s1: available, - s2: available, - s3: available, - }) - ) - ).toMatchObject({ - level: ServiceStatusLevels.available, - }); + getSummaryStatus({ + serviceStatuses: { elasticsearch: availableService, savedObjects: availableService }, + pluginStatuses: { a: availablePluginA, b: availablePluginB }, + }) + ).toMatchInlineSnapshot(` + Object { + "level": "available", + "summary": "All services and plugins are available", + } + `); }); it('returns degraded when the worst status is degraded', () => { expect( - getSummaryStatus( - Object.entries({ - s1: available, - s2: degraded, - s3: available, - }) - ) - ).toMatchObject({ - level: ServiceStatusLevels.degraded, - }); + getSummaryStatus({ + serviceStatuses: { elasticsearch: degradedService, savedObjects: availableService }, + pluginStatuses: { a: availablePluginA, b: availablePluginB }, + }) + ).toMatchInlineSnapshot(` + Object { + "detail": "See the status page for more information", + "level": "degraded", + "meta": Object { + "affectedPlugins": Array [], + "failingPlugins": Array [], + "failingServices": Array [ + "elasticsearch", + ], + }, + "summary": "1 service(s) and 0 plugin(s) are degraded: elasticsearch", + } + `); }); it('returns unavailable when the worst status is unavailable', () => { expect( - getSummaryStatus( - Object.entries({ - s1: available, - s2: degraded, - s3: unavailable, - }) - ) - ).toMatchObject({ - level: ServiceStatusLevels.unavailable, - }); + getSummaryStatus({ + serviceStatuses: { elasticsearch: degradedService, savedObjects: availableService }, + pluginStatuses: { a: unavailablePluginA, b: unavailablePluginB, c: unavailablePluginC }, + }) + ).toMatchInlineSnapshot(` + Object { + "detail": "See the status page for more information", + "level": "unavailable", + "meta": Object { + "affectedPlugins": Array [ + "c", + ], + "failingPlugins": Array [ + "a", + "b", + ], + "failingServices": Array [], + }, + "summary": "0 service(s) and 2 plugin(s) are unavailable: a, b", + } + `); }); it('returns critical when the worst status is critical', () => { expect( - getSummaryStatus( - Object.entries({ - s1: critical, - s2: degraded, - s3: unavailable, - }) - ) - ).toMatchObject({ - level: ServiceStatusLevels.critical, - }); - }); - - describe('summary', () => { - it('returns correct summary when a single service is affected', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, - }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '1 service is unavailable: s2', - detail: 'See the status page for more information', - meta: { - affectedServices: ['s2'], - }, - }); - }); - - it('returns correct summary when multiple services are affected', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, - }, - s3: { - level: ServiceStatusLevels.unavailable, - summary: 'Proin mattis', - detail: 'Nunc quis nulla at mi lobortis pretium.', - documentationUrl: 'http://helpmenow.com/problem2', - meta: { - other: { data: 'over there' }, - }, - }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '2 services are unavailable: s2, s3', - detail: 'See the status page for more information', - meta: { - affectedServices: ['s2', 's3'], - }, - }); - }); - - it('returns correct summary more than `maxServices` services are affected', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: available, - s3: degraded, - s4: degraded, - s5: degraded, - s6: available, - s7: degraded, - }), - { maxServices: 3 } - ) - ).toEqual({ - level: ServiceStatusLevels.degraded, - summary: '5 services are degraded: s1, s3, s4 and 2 other(s)', - detail: 'See the status page for more information', - meta: { - affectedServices: ['s1', 's3', 's4', 's5', 's7'], + getSummaryStatus({ + serviceStatuses: { elasticsearch: degradedService, savedObjects: criticalService }, + pluginStatuses: { a: availablePluginA, b: unavailablePluginB }, + }) + ).toMatchInlineSnapshot(` + Object { + "detail": "See the status page for more information", + "level": "critical", + "meta": Object { + "affectedPlugins": Array [], + "failingPlugins": Array [], + "failingServices": Array [ + "savedObjects", + ], }, - }); - }); + "summary": "1 service(s) and 0 plugin(s) are critical: savedObjects", + } + `); }); }); diff --git a/packages/core/status/core-status-server-internal/src/get_summary_status.ts b/packages/core/status/core-status-server-internal/src/get_summary_status.ts index 083364fad2fd2d..39d8bec4696d2a 100644 --- a/packages/core/status/core-status-server-internal/src/get_summary_status.ts +++ b/packages/core/status/core-status-server-internal/src/get_summary_status.ts @@ -6,80 +6,117 @@ * Side Public License, v 1. */ +import { PluginName } from '@kbn/core-base-common'; import { + type CoreStatus, ServiceStatusLevels, type ServiceStatus, type ServiceStatusLevel, } from '@kbn/core-status-common'; +import type { NamedPluginStatus, NamedServiceStatus, PluginStatus } from './types'; + +interface GetSummaryStatusParams { + serviceStatuses?: CoreStatus; + pluginStatuses?: Record; +} /** * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. */ -export const getSummaryStatus = ( - statuses: Array<[string, ServiceStatus]>, - { - allAvailableSummary = `All services are available`, - maxServices = 3, - }: { allAvailableSummary?: string; maxServices?: number } = {} -): ServiceStatus => { - const { highestLevel, highestStatuses } = highestLevelSummary(statuses); +export const getSummaryStatus = ({ + serviceStatuses, + pluginStatuses, +}: GetSummaryStatusParams): ServiceStatus => { + const { highestLevel, highestLevelServices, highestLevelPlugins } = highestLevelSummary({ + serviceStatuses, + pluginStatuses, + }); if (highestLevel === ServiceStatusLevels.available) { return { level: ServiceStatusLevels.available, - summary: allAvailableSummary, + summary: + serviceStatuses && pluginStatuses + ? 'All services and plugins are available' + : serviceStatuses + ? 'All services are available' + : 'All plugins are available', }; } else { - const affectedServices = highestStatuses.map(([serviceName]) => serviceName); + const failingPlugins = highestLevelPlugins?.filter(({ reported }) => reported); + const affectedPlugins = highestLevelPlugins?.filter(({ reported }) => !reported); + const failingServicesNames = highestLevelServices?.map(({ name }) => name); + const failingPluginsNames = failingPlugins?.map(({ name }) => name); + const affectedPluginsNames = affectedPlugins?.map(({ name }) => name); return { level: highestLevel, - summary: getSummaryContent(affectedServices, highestLevel, maxServices), + summary: getSummaryContent({ + level: highestLevel, + services: failingServicesNames, + plugins: failingPluginsNames, + }), // TODO: include URL to status page detail: `See the status page for more information`, meta: { - affectedServices, + failingServices: failingServicesNames, + failingPlugins: failingPluginsNames, + affectedPlugins: affectedPluginsNames, }, }; } }; -const getSummaryContent = ( - affectedServices: string[], - statusLevel: ServiceStatusLevel, - maxServices: number -): string => { - const serviceCount = affectedServices.length; - if (serviceCount === 1) { - return `1 service is ${statusLevel.toString()}: ${affectedServices[0]}`; - } else if (serviceCount > maxServices) { - const exceedingCount = serviceCount - maxServices; - return `${serviceCount} services are ${statusLevel.toString()}: ${affectedServices - .slice(0, maxServices) - .join(', ')} and ${exceedingCount} other(s)`; - } else { - return `${serviceCount} services are ${statusLevel.toString()}: ${affectedServices.join(', ')}`; - } -}; +interface GetSummaryContentParams { + level: ServiceStatusLevel; + services: string[]; + plugins: string[]; +} -type StatusPair = [string, ServiceStatus]; +const getSummaryContent = ({ level, services, plugins }: GetSummaryContentParams): string => { + const list = [...services, ...plugins].join(', '); + return `${services.length} service(s) and ${ + plugins.length + } plugin(s) are ${level.toString()}: ${list}`; +}; -const highestLevelSummary = ( - statuses: StatusPair[] -): { highestLevel: ServiceStatusLevel; highestStatuses: StatusPair[] } => { - let highestLevel: ServiceStatusLevel = ServiceStatusLevels.available; - let highestStatuses: StatusPair[] = []; +const highestLevelSummary = ({ serviceStatuses, pluginStatuses }: GetSummaryStatusParams) => { + let highestServiceLevel: ServiceStatusLevel = ServiceStatusLevels.available; + let highestPluginLevel: ServiceStatusLevel = ServiceStatusLevels.available; + let highestLevelServices: NamedServiceStatus[] = []; + let highestLevelPlugins: NamedPluginStatus[] = []; - for (const pair of statuses) { - if (pair[1].level === highestLevel) { - highestStatuses.push(pair); - } else if (pair[1].level > highestLevel) { - highestLevel = pair[1].level; - highestStatuses = [pair]; + if (serviceStatuses) { + let name: keyof CoreStatus; + for (name in serviceStatuses) { + if (Object.hasOwn(serviceStatuses, name)) { + const namedStatus: NamedServiceStatus = { ...serviceStatuses[name], name }; + if (serviceStatuses[name].level === highestServiceLevel) { + highestLevelServices.push(namedStatus); + } else if (serviceStatuses[name].level > highestServiceLevel) { + highestLevelServices = [namedStatus]; + highestServiceLevel = serviceStatuses[name].level; + } + } } } - return { - highestLevel, - highestStatuses, - }; + if (pluginStatuses) { + Object.entries(pluginStatuses).forEach(([name, pluginStatus]) => { + const namedStatus: NamedPluginStatus = { ...pluginStatus, name }; + if (pluginStatus.level === highestPluginLevel) { + highestLevelPlugins.push(namedStatus); + } else if (pluginStatus.level > highestPluginLevel) { + highestLevelPlugins = [namedStatus]; + highestPluginLevel = pluginStatus.level; + } + }); + } + + if (highestServiceLevel === highestPluginLevel) { + return { highestLevel: highestServiceLevel, highestLevelServices, highestLevelPlugins }; + } else if (highestServiceLevel > highestPluginLevel) { + return { highestLevel: highestServiceLevel, highestLevelServices, highestLevelPlugins: [] }; + } else { + return { highestLevel: highestPluginLevel, highestLevelServices: [], highestLevelPlugins }; + } }; diff --git a/packages/core/status/core-status-server-internal/src/log_core_services_status.test.ts b/packages/core/status/core-status-server-internal/src/log_core_services_status.test.ts new file mode 100644 index 00000000000000..125243aec84b46 --- /dev/null +++ b/packages/core/status/core-status-server-internal/src/log_core_services_status.test.ts @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Subject } from 'rxjs'; +import type { Logger } from '@kbn/logging'; +import type { ILoggingSystem } from '@kbn/core-logging-server-internal'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { type CoreStatus, ServiceStatusLevels, ServiceStatus } from '@kbn/core-status-common'; +import { logCoreStatusChanges } from './log_core_services_status'; + +const delay = async (millis: number = 10) => + await new Promise((resolve) => setTimeout(resolve, millis)); + +describe('logCoreStatusChanges', () => { + const serviceUnavailable: ServiceStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'Unavail!', + }; + const serviceAvailable: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'Avail!', + }; + + let core$: Subject; + let stop$: Subject; + let loggerFactory: jest.Mocked; + let l: Logger; // using short name for clarity + + beforeEach(() => { + core$ = new Subject(); + stop$ = new Subject(); + loggerFactory = loggingSystemMock.create(); + l = loggerFactory.get('status', 'plugins'); + }); + + afterEach(() => { + stop$.next(); + stop$.complete(); + loggingSystemMock.clear(loggerFactory); + }); + + it("logs core services' status changes", async () => { + logCoreStatusChanges({ + logger: l, + core$, + stop$, + }); + + core$.next({ elasticsearch: serviceAvailable, savedObjects: serviceUnavailable }); + core$.next({ elasticsearch: serviceAvailable, savedObjects: serviceAvailable }); + core$.next({ elasticsearch: serviceAvailable, savedObjects: serviceAvailable }); + + await delay(); + + expect(l.get).toBeCalledTimes(3); + expect(l.get).nthCalledWith(1, 'elasticsearch'); + expect(l.get).nthCalledWith(2, 'savedObjects'); + expect(l.get).nthCalledWith(3, 'savedObjects'); + expect(l.warn).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.info).toBeCalledTimes(2); + expect(l.info).nthCalledWith(1, 'elasticsearch service is now available: Avail!'); + expect(l.error).nthCalledWith(1, 'savedObjects service is now unavailable: Unavail!'); + expect(l.info).nthCalledWith(2, 'savedObjects service is now available: Avail!'); + }); + + it('stops logging when the stop$ observable has emitted', async () => { + logCoreStatusChanges({ + logger: l, + core$, + stop$, + }); + + core$.next({ elasticsearch: serviceAvailable, savedObjects: serviceUnavailable }); + stop$.next(); + core$.next({ elasticsearch: serviceAvailable, savedObjects: serviceAvailable }); + core$.next({ elasticsearch: serviceAvailable, savedObjects: serviceAvailable }); + + await delay(); + + expect(l.get).toBeCalledTimes(2); + expect(l.get).nthCalledWith(1, 'elasticsearch'); + expect(l.get).nthCalledWith(2, 'savedObjects'); + expect(l.warn).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.info).toBeCalledTimes(1); + expect(l.info).nthCalledWith(1, 'elasticsearch service is now available: Avail!'); + expect(l.error).nthCalledWith(1, 'savedObjects service is now unavailable: Unavail!'); + }); + + it('throttles and aggregates messages of plugins that emit too often', async () => { + logCoreStatusChanges({ + logger: l, + core$, + stop$, + throttleIntervalMillis: 10, + }); + + // savedObjects remains unavailable, elasticsearch is switching repeatedly + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceUnavailable, elasticsearch: serviceUnavailable }); + + // savedObjects becomes available, elasticsearch keeps switching + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceAvailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceUnavailable }); + core$.next({ savedObjects: serviceAvailable, elasticsearch: serviceAvailable }); + + // give the 'bufferTime' operator enough time to emit and log + await delay(20); + + expect(l.get).toBeCalledWith('elasticsearch'); + expect(l.get).toBeCalledWith('savedObjects'); + expect(l.warn).not.toHaveBeenCalled(); + expect(l.info).toHaveBeenCalledTimes(4); + expect(l.error).toHaveBeenCalledTimes(3); + expect(l.error).nthCalledWith(1, 'savedObjects service is now unavailable: Unavail!'); + expect(l.info).nthCalledWith(1, 'elasticsearch service is now available: Avail!'); + expect(l.error).nthCalledWith(2, 'elasticsearch service is now unavailable: Unavail!'); + expect(l.info).nthCalledWith(2, 'elasticsearch service is now available: Avail!'); + expect(l.info).nthCalledWith(3, 'savedObjects service is now available: Avail!'); + expect(l.error).nthCalledWith( + 3, + 'elasticsearch service is now unavailable: Unavail! (repeated 10 times)' + ); + expect(l.info).nthCalledWith( + 4, + 'elasticsearch service is now available: Avail! (repeated 10 times)' + ); + }); + + it('discards messages when a plugin emits too many different ones', async () => { + logCoreStatusChanges({ + logger: l, + core$, + stop$, + throttleIntervalMillis: 10, + maxThrottledMessages: 4, + }); + + // elasticsearch service keeps changing status, with different messages each time + let attempt = 0; + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceUnavailable, summary: `attempt #${++attempt}` }, + }); + + // give the 'bufferTime' operator enough time to emit and log + await delay(20); + + // emit a last message (some time after) + core$.next({ + savedObjects: serviceUnavailable, + elasticsearch: { ...serviceAvailable, summary: `attempt #${++attempt}` }, + }); + + expect(l.get).toBeCalledWith('elasticsearch'); + expect(l.get).toBeCalledWith('savedObjects'); + expect(l.info).toHaveBeenCalledTimes(5); + expect(l.error).toHaveBeenCalledTimes(4); + expect(l.warn).toHaveBeenCalledTimes(1); + // the first 3 messages are the max allowed per interval + expect(l.info).nthCalledWith(1, 'elasticsearch service is now available: attempt #1'); + expect(l.error).nthCalledWith(1, 'savedObjects service is now unavailable: Unavail!'); + expect(l.error).nthCalledWith(2, 'elasticsearch service is now unavailable: attempt #2'); + expect(l.info).nthCalledWith(2, 'elasticsearch service is now available: attempt #3'); + // the next 4 messages are throttled (emitted after 10ms) + expect(l.error).nthCalledWith(3, 'elasticsearch service is now unavailable: attempt #4'); + expect(l.info).nthCalledWith(3, 'elasticsearch service is now available: attempt #5'); + expect(l.error).nthCalledWith(4, 'elasticsearch service is now unavailable: attempt #6'); + expect(l.info).nthCalledWith(4, 'elasticsearch service is now available: attempt #7'); + + // these messages exceed the maxThrottledMessages quota, truncated + warning + expect(l.warn).nthCalledWith( + 1, + '7 other status updates from [elasticsearch] have been truncated to avoid flooding the logs' + ); + // and the last message, after the buffered / truncated ones + expect(l.info).nthCalledWith(5, 'elasticsearch service is now available: attempt #15'); + }); +}); diff --git a/packages/core/status/core-status-server-internal/src/log_core_services_status.ts b/packages/core/status/core-status-server-internal/src/log_core_services_status.ts new file mode 100644 index 00000000000000..e258bdd75a3aa1 --- /dev/null +++ b/packages/core/status/core-status-server-internal/src/log_core_services_status.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uniq } from 'lodash'; +import { merge, type Observable, Subject, type Subscription } from 'rxjs'; +import { pairwise, takeUntil, map, startWith, bufferTime, concatAll, filter } from 'rxjs/operators'; +import type { Logger } from '@kbn/logging'; +import { type CoreStatus, ServiceStatusLevels } from '@kbn/core-status-common'; +import type { LoggableServiceStatus } from './types'; + +// let services log up to 3 status changes every 30s (extra messages will be throttled / aggregated) +const MAX_MESSAGES_PER_SERVICE_PER_INTERVAL = 3; +const THROTTLE_INTERVAL_MILLIS = 30000; +const MAX_THROTTLED_MESSAGES = 10; + +interface LogCoreStatusChangesParams { + logger: Logger; + core$: Observable; + stop$: Observable; + maxMessagesPerServicePerInterval?: number; + throttleIntervalMillis?: number; + maxThrottledMessages?: number; +} + +export const logCoreStatusChanges = ({ + logger, + core$, + stop$, + maxMessagesPerServicePerInterval = MAX_MESSAGES_PER_SERVICE_PER_INTERVAL, + throttleIntervalMillis = THROTTLE_INTERVAL_MILLIS, + maxThrottledMessages = MAX_THROTTLED_MESSAGES, +}: LogCoreStatusChangesParams): Subscription => { + const buffer = new Subject(); + const throttled$: Observable = buffer.asObservable().pipe( + takeUntil(stop$), + bufferTime(maxMessagesPerServicePerInterval), + map((statuses) => { + const aggregated = // aggregate repeated messages, and count nbr. of repetitions + statuses.filter((candidateStatus, index) => { + const firstMessageIndex = statuses.findIndex( + (status) => + candidateStatus.name === status.name && + candidateStatus.level === status.level && + candidateStatus.summary === status.summary + ); + if (index !== firstMessageIndex) { + // this is not the first time this message is logged, increase 'repeats' counter for the first occurrence + statuses[firstMessageIndex].repeats = (statuses[firstMessageIndex].repeats ?? 1) + 1; + return false; + } else { + // this is the first time this message is logged, let it through + return true; + } + }); + + if (aggregated.length > maxThrottledMessages) { + const list: string = uniq( + aggregated.slice(maxThrottledMessages).map(({ name }) => name) + ).join(', '); + + return [ + ...aggregated.slice(0, maxThrottledMessages), + `${ + aggregated.length - maxThrottledMessages + } other status updates from [${list}] have been truncated to avoid flooding the logs`, + ]; + } else { + return aggregated; + } + }), + concatAll() + ); + + const lastMessagesTimestamps: Record = {}; + + const direct$: Observable = core$.pipe( + startWith(undefined), // consider all services unavailable by default + takeUntil(stop$), + pairwise(), + map(([previous, current]) => getServiceUpdates({ previous, current: current! })), + concatAll(), + filter((serviceStatus: LoggableServiceStatus) => { + const now = Date.now(); + const pluginQuota = lastMessagesTimestamps[serviceStatus.name] || []; + lastMessagesTimestamps[serviceStatus.name] = pluginQuota; + + // remove timestamps of messages older than the threshold + while (pluginQuota.length > 0 && pluginQuota[0] < now - throttleIntervalMillis) { + pluginQuota.shift(); + } + + if (pluginQuota.length >= maxMessagesPerServicePerInterval) { + // we're still over quota, throttle the message + buffer.next(serviceStatus); + return false; + } else { + // let the message pass through + pluginQuota.push(now); + return true; + } + }) + ); + + return merge(direct$, throttled$).subscribe((event) => { + if (typeof event === 'string') { + logger.warn(event); + } else { + const serviceStatus: LoggableServiceStatus = event; + const { name } = serviceStatus; + const serviceLogger = logger.get(name); + const message = getServiceStatusMessage(serviceStatus); + + switch (serviceStatus.level) { + case ServiceStatusLevels.available: + serviceLogger.info(message); + break; + case ServiceStatusLevels.degraded: + serviceLogger.warn(message); + break; + default: + serviceLogger.error(message); + } + } + }); +}; + +const getServiceUpdates = ({ + current, + previous, +}: { + current: CoreStatus; + previous?: CoreStatus; +}): LoggableServiceStatus[] => { + let name: keyof CoreStatus; + const updated: LoggableServiceStatus[] = []; + + for (name in current) { + if (Object.hasOwn(current, name)) { + const currentLevel = current[name].level; + const previousLevel = previous?.[name].level; + + if (currentLevel !== previousLevel) { + updated.push({ ...current[name], name }); + } + } + } + return updated; +}; + +const getServiceStatusMessage = ({ + name, + level, + summary, + detail, + repeats = 0, +}: LoggableServiceStatus): string => + `${name} service is now ${level?.toString()}: ${summary}${detail ? ` | ${detail}` : ''}${ + repeats > 1 ? ` (repeated ${repeats} times)` : '' + }`; diff --git a/packages/core/status/core-status-server-internal/src/log_overall_status.test.ts b/packages/core/status/core-status-server-internal/src/log_overall_status.test.ts index f74fb472d08f6f..18b3953da7e05c 100644 --- a/packages/core/status/core-status-server-internal/src/log_overall_status.test.ts +++ b/packages/core/status/core-status-server-internal/src/log_overall_status.test.ts @@ -6,98 +6,127 @@ * Side Public License, v 1. */ -import { TestScheduler } from 'rxjs/testing'; -import { ServiceStatus, ServiceStatusLevels } from '@kbn/core-status-common'; -import { getOverallStatusChanges } from './log_overall_status'; +import { Subject } from 'rxjs'; +import type { Logger } from '@kbn/logging'; +import type { ILoggingSystem } from '@kbn/core-logging-server-internal'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { ServiceStatusLevels, ServiceStatus } from '@kbn/core-status-common'; +import { logOverallStatusChanges } from './log_overall_status'; -const getTestScheduler = () => - new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); +const delay = async (millis: number = 10) => + await new Promise((resolve) => setTimeout(resolve, millis)); + +describe('logOverallStatusChanges', () => { + let overall$: Subject; + let stop$: Subject; + let loggerFactory: jest.Mocked; + let l: Logger; // using short name for clarity + + beforeEach(() => { + overall$ = new Subject(); + stop$ = new Subject(); + loggerFactory = loggingSystemMock.create(); + l = loggerFactory.get('status', 'plugins'); }); -const createStatus = (parts: Partial = {}): ServiceStatus => ({ - level: ServiceStatusLevels.available, - summary: 'summary', - ...parts, -}); + afterEach(() => { + stop$.next(); + stop$.complete(); + loggingSystemMock.clear(loggerFactory); + }); -describe('getOverallStatusChanges', () => { - it('emits an initial message after first overall$ emission', () => { - getTestScheduler().run(({ expectObservable, hot }) => { - const overall$ = hot('--a', { - a: createStatus(), - }); - const stop$ = hot(''); - const expected = '--a'; - - expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { - a: 'Kibana is now available', - }); + it('emits an initial message after first overall$ emission', async () => { + logOverallStatusChanges({ + logger: l, + overall$, + stop$, }); + + overall$.next({ level: ServiceStatusLevels.unavailable, summary: 'Initializing . . .' }); + + await delay(); + + expect(l.get).not.toBeCalled(); + expect(l.info).not.toBeCalled(); + expect(l.warn).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.error).nthCalledWith(1, 'Kibana is now unavailable: Initializing . . .'); }); - it('emits a new message every time the status level changes', () => { - getTestScheduler().run(({ expectObservable, hot }) => { - const overall$ = hot('--a--b', { - a: createStatus({ - level: ServiceStatusLevels.degraded, - }), - b: createStatus({ - level: ServiceStatusLevels.available, - }), - }); - const stop$ = hot(''); - const expected = '--a--b'; - - expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { - a: 'Kibana is now degraded', - b: 'Kibana is now available (was degraded)', - }); + it('emits a new message every time the status level changes', async () => { + logOverallStatusChanges({ + logger: l, + overall$, + stop$, }); + + overall$.next({ level: ServiceStatusLevels.unavailable, summary: 'Initializing . . .' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting for ES indices' }); + overall$.next({ level: ServiceStatusLevels.available, summary: 'Ready!' }); + + await delay(); + + expect(l.get).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.error).nthCalledWith(1, 'Kibana is now unavailable: Initializing . . .'); + expect(l.warn).toBeCalledTimes(1); + expect(l.warn).nthCalledWith( + 1, + 'Kibana is now degraded (was unavailable): Waiting for ES indices' + ); + expect(l.info).toBeCalledTimes(1); + expect(l.info).nthCalledWith(1, 'Kibana is now available (was degraded)'); }); - it('does not emit when the status stays the same', () => { - getTestScheduler().run(({ expectObservable, hot }) => { - const overall$ = hot('--a--b--c', { - a: createStatus({ - level: ServiceStatusLevels.degraded, - summary: 'summary 1', - }), - b: createStatus({ - level: ServiceStatusLevels.degraded, - summary: 'summary 2', - }), - c: createStatus({ - level: ServiceStatusLevels.available, - summary: 'summary 2', - }), - }); - const stop$ = hot(''); - const expected = '--a-----b'; - - expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { - a: 'Kibana is now degraded', - b: 'Kibana is now available (was degraded)', - }); + it('does not emit when the status stays the same', async () => { + logOverallStatusChanges({ + logger: l, + overall$, + stop$, }); + + overall$.next({ level: ServiceStatusLevels.unavailable, summary: 'Initializing . . .' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting for ES indices' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting (attempt #2)' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting (attempt #3)' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting (attempt #4)' }); + overall$.next({ level: ServiceStatusLevels.available, summary: 'Ready!' }); + + await delay(); + + expect(l.get).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.error).nthCalledWith(1, 'Kibana is now unavailable: Initializing . . .'); + expect(l.warn).toBeCalledTimes(1); + expect(l.warn).nthCalledWith( + 1, + 'Kibana is now degraded (was unavailable): Waiting for ES indices' + ); + expect(l.info).toBeCalledTimes(1); + expect(l.info).nthCalledWith(1, 'Kibana is now available (was degraded)'); }); - it('stops emitting once `stop$` emits', () => { - getTestScheduler().run(({ expectObservable, hot }) => { - const overall$ = hot('--a--b', { - a: createStatus({ - level: ServiceStatusLevels.degraded, - }), - b: createStatus({ - level: ServiceStatusLevels.available, - }), - }); - const stop$ = hot('----(s|)'); - const expected = '--a-|'; - - expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { - a: 'Kibana is now degraded', - }); + it('stops emitting once `stop$` emits', async () => { + logOverallStatusChanges({ + logger: l, + overall$, + stop$, }); + + overall$.next({ level: ServiceStatusLevels.unavailable, summary: 'Initializing . . .' }); + stop$.next(); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting for ES indices' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting (attempt #2)' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting (attempt #3)' }); + overall$.next({ level: ServiceStatusLevels.degraded, summary: 'Waiting (attempt #4)' }); + overall$.next({ level: ServiceStatusLevels.available, summary: 'Ready!' }); + + await delay(); + + expect(l.get).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.error).nthCalledWith(1, 'Kibana is now unavailable: Initializing . . .'); + expect(l.warn).not.toBeCalled(); + expect(l.info).not.toBeCalled(); }); }); diff --git a/packages/core/status/core-status-server-internal/src/log_overall_status.ts b/packages/core/status/core-status-server-internal/src/log_overall_status.ts index e69fad24c12e04..cac40b5ec8fa03 100644 --- a/packages/core/status/core-status-server-internal/src/log_overall_status.ts +++ b/packages/core/status/core-status-server-internal/src/log_overall_status.ts @@ -6,26 +6,52 @@ * Side Public License, v 1. */ -import { Observable } from 'rxjs'; -import { distinctUntilChanged, pairwise, startWith, takeUntil, map } from 'rxjs/operators'; -import type { ServiceStatus } from '@kbn/core-status-common'; +import type { Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged, pairwise, takeUntil, map, startWith } from 'rxjs/operators'; +import { type ServiceStatus, ServiceStatusLevels } from '@kbn/core-status-common'; +import type { Logger } from '@kbn/logging'; -export const getOverallStatusChanges = ( - overall$: Observable, - stop$: Observable -) => { - return overall$.pipe( - takeUntil(stop$), - distinctUntilChanged((previous, next) => { - return previous.level.toString() === next.level.toString(); - }), - startWith(undefined), - pairwise(), - map(([oldStatus, newStatus]) => { - if (oldStatus) { - return `Kibana is now ${newStatus!.level.toString()} (was ${oldStatus!.level.toString()})`; +interface LogOverallStatusChangesParams { + logger: Logger; + overall$: Observable; + stop$: Observable; +} + +export const logOverallStatusChanges = ({ + logger, + overall$, + stop$, +}: LogOverallStatusChangesParams): Subscription => { + return overall$ + .pipe( + takeUntil(stop$), + distinctUntilChanged((previous, next) => { + return previous.level.toString() === next.level.toString(); + }), + startWith(undefined), + pairwise(), + map(([oldStatus, newStatus]) => { + const oldStatusMessage = oldStatus ? ` (was ${oldStatus!.level.toString()})` : ''; + const reason = + newStatus?.level !== ServiceStatusLevels.available && newStatus?.summary + ? `: ${newStatus?.summary}` + : ''; + return { + message: `Kibana is now ${newStatus!.level.toString()}${oldStatusMessage}${reason}`, + level: newStatus?.level, + }; + }) + ) + .subscribe(({ message, level }) => { + switch (level) { + case ServiceStatusLevels.available: + logger.info(message); + break; + case ServiceStatusLevels.degraded: + logger.warn(message); + break; + default: + logger.error(message); } - return `Kibana is now ${newStatus!.level.toString()}`; - }) - ); + }); }; diff --git a/packages/core/status/core-status-server-internal/src/log_plugins_status.test.ts b/packages/core/status/core-status-server-internal/src/log_plugins_status.test.ts new file mode 100644 index 00000000000000..aa7250ef52230c --- /dev/null +++ b/packages/core/status/core-status-server-internal/src/log_plugins_status.test.ts @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Subject } from 'rxjs'; +import type { Logger } from '@kbn/logging'; +import type { ILoggingSystem } from '@kbn/core-logging-server-internal'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { ServiceStatusLevels } from '@kbn/core-status-common'; +import { logPluginsStatusChanges } from './log_plugins_status'; +import type { PluginStatus } from './types'; + +const delay = async (millis: number = 10) => + await new Promise((resolve) => setTimeout(resolve, millis)); + +describe('logPluginsStatusChanges', () => { + const reportedUnavailable: PluginStatus = { + reported: true, + level: ServiceStatusLevels.unavailable, + summary: 'Unavail!', + }; + const reportedAvailable: PluginStatus = { + reported: true, + level: ServiceStatusLevels.available, + summary: 'Avail!', + }; + const inferredUnavailable: PluginStatus = { + reported: false, + level: ServiceStatusLevels.unavailable, + summary: 'Unavail!', + }; + const inferredAvailable: PluginStatus = { + reported: false, + level: ServiceStatusLevels.available, + summary: 'Avail!', + }; + + let plugins$: Subject>; + let stop$: Subject; + let loggerFactory: jest.Mocked; + let l: Logger; // using short name for clarity + + beforeEach(() => { + plugins$ = new Subject>(); + stop$ = new Subject(); + loggerFactory = loggingSystemMock.create(); + l = loggerFactory.get('status', 'plugins'); + }); + + afterEach(() => { + stop$.next(); + stop$.complete(); + loggingSystemMock.clear(loggerFactory); + }); + + it("logs plugins' status changes", async () => { + logPluginsStatusChanges({ + logger: l, + plugins$, + stop$, + }); + + plugins$.next({ A: reportedAvailable, B: reportedUnavailable, C: inferredUnavailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable, C: inferredAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable, C: inferredAvailable }); + + await delay(); + expect(l.get).toBeCalledTimes(3); + expect(l.get).nthCalledWith(1, 'A'); + expect(l.get).nthCalledWith(2, 'B'); + expect(l.get).nthCalledWith(3, 'B'); + expect(l.warn).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.info).toBeCalledTimes(2); + expect(l.error).nthCalledWith(1, 'B plugin is now unavailable: Unavail!'); + expect(l.info).nthCalledWith(1, 'A plugin is now available: Avail!'); + expect(l.info).nthCalledWith(2, 'B plugin is now available: Avail!'); + }); + + it('stops logging when the stop$ observable has emitted', async () => { + logPluginsStatusChanges({ + logger: l, + plugins$, + stop$, + }); + + plugins$.next({ A: reportedAvailable, B: reportedUnavailable, C: inferredUnavailable }); + stop$.next(); + plugins$.next({ A: reportedAvailable, B: reportedAvailable, C: inferredAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable, C: inferredAvailable }); + + await delay(); + + expect(l.get).toBeCalledTimes(2); + expect(l.get).nthCalledWith(1, 'A'); + expect(l.get).nthCalledWith(2, 'B'); + expect(l.warn).not.toBeCalled(); + expect(l.error).toBeCalledTimes(1); + expect(l.info).toBeCalledTimes(1); + expect(l.info).nthCalledWith(1, 'A plugin is now available: Avail!'); + expect(l.error).nthCalledWith(1, 'B plugin is now unavailable: Unavail!'); + }); + + it('throttles and aggregates messages of plugins that emit too often', async () => { + logPluginsStatusChanges({ + logger: l, + plugins$, + stop$, + throttleIntervalMillis: 10, + }); + + // A remains unavailable, B is switching repeatedly + plugins$.next({ A: reportedUnavailable, B: reportedAvailable }); + plugins$.next({ A: reportedUnavailable, B: reportedUnavailable }); + plugins$.next({ A: reportedUnavailable, B: reportedAvailable }); + plugins$.next({ A: reportedUnavailable, B: reportedUnavailable }); + plugins$.next({ A: reportedUnavailable, B: reportedAvailable }); + plugins$.next({ A: reportedUnavailable, B: reportedUnavailable }); + plugins$.next({ A: reportedUnavailable, B: reportedAvailable }); + plugins$.next({ A: reportedUnavailable, B: reportedUnavailable }); + plugins$.next({ A: reportedUnavailable, B: reportedAvailable }); + plugins$.next({ A: reportedUnavailable, B: reportedUnavailable }); + plugins$.next({ A: reportedUnavailable, B: reportedAvailable }); + plugins$.next({ A: reportedUnavailable, B: reportedUnavailable }); + + // A becomes available, B keeps switching + plugins$.next({ A: reportedAvailable, B: reportedAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedUnavailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedUnavailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedUnavailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedUnavailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable }); + plugins$.next({ A: reportedAvailable, B: reportedUnavailable }); + plugins$.next({ A: reportedAvailable, B: reportedAvailable }); + + // give the 'bufferTime' operator enough time to emit and log + await delay(20); + + expect(l.get).toBeCalledWith('A'); + expect(l.get).toBeCalledWith('B'); + expect(l.get).not.toBeCalledWith('C'); + expect(l.warn).not.toHaveBeenCalled(); + expect(l.info).toHaveBeenCalledTimes(4); + expect(l.error).toHaveBeenCalledTimes(3); + expect(l.error).nthCalledWith(1, 'A plugin is now unavailable: Unavail!'); + expect(l.info).nthCalledWith(1, 'B plugin is now available: Avail!'); + expect(l.error).nthCalledWith(2, 'B plugin is now unavailable: Unavail!'); + expect(l.info).nthCalledWith(2, 'B plugin is now available: Avail!'); + expect(l.info).nthCalledWith(3, 'A plugin is now available: Avail!'); + expect(l.error).nthCalledWith(3, 'B plugin is now unavailable: Unavail! (repeated 10 times)'); + expect(l.info).nthCalledWith(4, 'B plugin is now available: Avail! (repeated 10 times)'); + }); + + it('discards messages when a plugin emits too many different ones', async () => { + logPluginsStatusChanges({ + logger: l, + plugins$, + stop$, + throttleIntervalMillis: 10, + maxThrottledMessages: 4, + }); + + // A plugin keeps changing status, with different messages each time + let attempt = 0; + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + plugins$.next({ A: { ...reportedUnavailable, summary: `attempt #${++attempt}` } }); + + // give the 'bufferTime' operator enough time to emit and log + await delay(20); + + // emit a last message (some time after) + plugins$.next({ A: { ...reportedAvailable, summary: `attempt #${++attempt}` } }); + + expect(l.get).toBeCalledWith('A'); + expect(l.get).not.toBeCalledWith('B'); + expect(l.info).toHaveBeenCalledTimes(5); + expect(l.error).toHaveBeenCalledTimes(3); + expect(l.warn).toHaveBeenCalledTimes(1); + // the first 3 messages are the max allowed per interval + expect(l.info).nthCalledWith(1, 'A plugin is now available: attempt #1'); + expect(l.error).nthCalledWith(1, 'A plugin is now unavailable: attempt #2'); + expect(l.info).nthCalledWith(2, 'A plugin is now available: attempt #3'); + // the next 4 messages are throttled (emitted after 10ms) + expect(l.error).nthCalledWith(2, 'A plugin is now unavailable: attempt #4'); + expect(l.info).nthCalledWith(3, 'A plugin is now available: attempt #5'); + expect(l.error).nthCalledWith(3, 'A plugin is now unavailable: attempt #6'); + expect(l.info).nthCalledWith(4, 'A plugin is now available: attempt #7'); + + // these messages exceed the maxThrottledMessages quota, truncated + warning + expect(l.warn).nthCalledWith( + 1, + '7 other status updates from [A] have been truncated to avoid flooding the logs' + ); + // and the last message, after the buffered / truncated ones + expect(l.info).nthCalledWith(5, 'A plugin is now available: attempt #15'); + }); +}); diff --git a/packages/core/status/core-status-server-internal/src/log_plugins_status.ts b/packages/core/status/core-status-server-internal/src/log_plugins_status.ts new file mode 100644 index 00000000000000..3352bf35963b94 --- /dev/null +++ b/packages/core/status/core-status-server-internal/src/log_plugins_status.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uniq } from 'lodash'; +import { merge, type Observable, Subject, type Subscription } from 'rxjs'; +import { pairwise, takeUntil, map, startWith, bufferTime, filter, concatAll } from 'rxjs/operators'; +import { Logger } from '@kbn/logging'; +import type { PluginName } from '@kbn/core-base-common'; +import { ServiceStatusLevels } from '@kbn/core-status-common'; +import type { LoggablePluginStatus, PluginStatus } from './types'; + +// let plugins log up to 3 status changes every 30s (extra messages will be throttled / aggregated) +const MAX_MESSAGES_PER_PLUGIN_PER_INTERVAL = 3; +const THROTTLE_INTERVAL_MILLIS = 30000; +const MAX_THROTTLED_MESSAGES = 10; + +interface LogPluginsStatusChangesParams { + logger: Logger; + plugins$: Observable>; + stop$: Observable; + maxMessagesPerPluginPerInterval?: number; + throttleIntervalMillis?: number; + maxThrottledMessages?: number; +} + +export const logPluginsStatusChanges = ({ + logger, + plugins$, + stop$, + maxMessagesPerPluginPerInterval = MAX_MESSAGES_PER_PLUGIN_PER_INTERVAL, + throttleIntervalMillis = THROTTLE_INTERVAL_MILLIS, + maxThrottledMessages = MAX_THROTTLED_MESSAGES, +}: LogPluginsStatusChangesParams): Subscription => { + const buffer = new Subject(); + const throttled$: Observable = buffer.asObservable().pipe( + takeUntil(stop$), + bufferTime(maxMessagesPerPluginPerInterval), + map((statuses) => { + const aggregated = // aggregate repeated messages, and count nbr. of repetitions + statuses.filter((candidateStatus, index) => { + const firstMessageIndex = statuses.findIndex( + (status) => + candidateStatus.name === status.name && + candidateStatus.level === status.level && + candidateStatus.summary === status.summary + ); + if (index !== firstMessageIndex) { + // this is not the first time this message is logged, increase 'repeats' counter for the first occurrence + statuses[firstMessageIndex].repeats = (statuses[firstMessageIndex].repeats ?? 1) + 1; + return false; + } else { + // this is the first time this message is logged, let it through + return true; + } + }); + + if (aggregated.length > maxThrottledMessages) { + const list: string = uniq( + aggregated.slice(maxThrottledMessages).map(({ name }) => name) + ).join(', '); + + return [ + ...aggregated.slice(0, maxThrottledMessages), + `${ + aggregated.length - maxThrottledMessages + } other status updates from [${list}] have been truncated to avoid flooding the logs`, + ]; + } else { + return aggregated; + } + }), + concatAll() + ); + + const lastMessagesTimestamps: Record = {}; + + const direct$: Observable = plugins$.pipe( + startWith({}), // consider all plugins unavailable by default + takeUntil(stop$), + pairwise(), + map(([oldStatus, newStatus]) => getPluginUpdates(oldStatus, newStatus)), + concatAll(), + filter((pluginStatus: LoggablePluginStatus) => { + const now = Date.now(); + const pluginQuota = lastMessagesTimestamps[pluginStatus.name] || []; + lastMessagesTimestamps[pluginStatus.name] = pluginQuota; + + // remove timestamps of messages older than the threshold + while (pluginQuota.length > 0 && pluginQuota[0] < now - throttleIntervalMillis) { + pluginQuota.shift(); + } + + if (pluginQuota.length >= maxMessagesPerPluginPerInterval) { + // we're still over quota, throttle the message + buffer.next(pluginStatus); + return false; + } else { + // let the message pass through + pluginQuota.push(now); + return true; + } + }) + ); + + return merge(direct$, throttled$).subscribe((event) => { + if (typeof event === 'string') { + logger.warn(event); + } else { + const pluginStatus: LoggablePluginStatus = event; + const { name } = pluginStatus; + const pluginLogger = logger.get(name); + const message = getPluginStatusMessage(pluginStatus); + + switch (pluginStatus.level) { + case ServiceStatusLevels.available: + pluginLogger.info(message); + break; + case ServiceStatusLevels.degraded: + pluginLogger.warn(message); + break; + default: + pluginLogger.error(message); + } + } + }); +}; + +const getPluginUpdates = ( + previous: Record, + next: Record +): LoggablePluginStatus[] => + Object.entries(next) + .filter(([name, pluginStatus]) => { + const currentLevel = pluginStatus.level; + const previousLevel = previous[name]?.level; + return pluginStatus.reported && currentLevel !== previousLevel; + }) + .map(([name, pluginStatus]) => ({ ...pluginStatus, name })); + +const getPluginStatusMessage = ({ + name, + level, + summary, + detail, + repeats = 0, +}: LoggablePluginStatus): string => + `${name} plugin is now ${level?.toString()}: ${summary}${detail ? ` | ${detail}` : ''}${ + repeats > 1 ? ` (repeated ${repeats} times)` : '' + }`; diff --git a/packages/core/status/core-status-server-internal/src/plugins_status.test.ts b/packages/core/status/core-status-server-internal/src/plugins_status.test.ts index b84151fb9da83e..d1d35ade56159c 100644 --- a/packages/core/status/core-status-server-internal/src/plugins_status.test.ts +++ b/packages/core/status/core-status-server-internal/src/plugins_status.test.ts @@ -8,7 +8,7 @@ import type { PluginName } from '@kbn/core-base-common'; import { PluginsStatusService } from './plugins_status'; -import { of, Observable, BehaviorSubject, ReplaySubject } from 'rxjs'; +import { of, Observable, BehaviorSubject, ReplaySubject, firstValueFrom } from 'rxjs'; import { ServiceStatusLevels, CoreStatus, ServiceStatus } from '@kbn/core-status-common'; import { first, skip } from 'rxjs/operators'; import { ServiceStatusLevelSnapshotSerializer } from './test_helpers'; @@ -64,7 +64,7 @@ describe('PluginStatusService', () => { }); expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.available, - summary: 'All dependencies are available', + summary: 'All services are available', }); const serviceDegraded = new PluginsStatusService({ @@ -73,7 +73,7 @@ describe('PluginStatusService', () => { }); expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.degraded, - summary: '1 service is degraded: savedObjects', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -84,7 +84,7 @@ describe('PluginStatusService', () => { }); expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.critical, - summary: '1 service is critical: elasticsearch', + summary: '1 service(s) and 0 plugin(s) are critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -95,7 +95,7 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: savedObjects, a', + summary: '1 service(s) and 1 plugin(s) are degraded: savedObjects, a', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -106,7 +106,7 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.unavailable, - summary: '1 service is unavailable: a', + summary: '0 service(s) and 1 plugin(s) are unavailable: a', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -120,7 +120,7 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.critical, - summary: '1 service is critical: elasticsearch', + summary: '1 service(s) and 0 plugin(s) are critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -132,7 +132,7 @@ describe('PluginStatusService', () => { service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.unavailable, - summary: '1 service is unavailable: b', + summary: '0 service(s) and 1 plugin(s) are unavailable: b', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -154,9 +154,9 @@ describe('PluginStatusService', () => { pluginDependencies, }); expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + a: { level: ServiceStatusLevels.available, summary: 'All services are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All services are available' }, + c: { level: ServiceStatusLevels.available, summary: 'All services are available' }, }); const serviceDegraded = new PluginsStatusService({ @@ -166,19 +166,19 @@ describe('PluginStatusService', () => { expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ a: { level: ServiceStatusLevels.degraded, - summary: '1 service is degraded: savedObjects', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, b: { level: ServiceStatusLevels.degraded, - summary: '1 service is degraded: savedObjects', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.degraded, - summary: '1 service is degraded: savedObjects', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -191,19 +191,19 @@ describe('PluginStatusService', () => { expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ a: { level: ServiceStatusLevels.critical, - summary: '1 service is critical: elasticsearch', + summary: '1 service(s) and 0 plugin(s) are critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }, b: { level: ServiceStatusLevels.critical, - summary: '1 service is critical: elasticsearch', + summary: '1 service(s) and 0 plugin(s) are critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.critical, - summary: '1 service is critical: elasticsearch', + summary: '1 service(s) and 0 plugin(s) are critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -215,16 +215,16 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); expect(await service.getAll$().pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available despite savedObjects being degraded + a: { level: ServiceStatusLevels.available, summary: 'a status', reported: true }, // a is available despite savedObjects being degraded b: { level: ServiceStatusLevels.degraded, - summary: '1 service is degraded: savedObjects', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: savedObjects, b', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -251,9 +251,23 @@ describe('PluginStatusService', () => { subscription.unsubscribe(); expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, - { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, - { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, + { + a: { level: ServiceStatusLevels.degraded, summary: 'a degraded', reported: true }, + }, + { + a: { + level: ServiceStatusLevels.unavailable, + summary: 'a unavailable', + reported: true, + }, + }, + { + a: { + level: ServiceStatusLevels.available, + summary: 'a available', + reported: true, + }, + }, ]); }); @@ -279,9 +293,23 @@ describe('PluginStatusService', () => { subscription.unsubscribe(); expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, - { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, - { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, + { + a: { level: ServiceStatusLevels.degraded, summary: 'a degraded', reported: true }, + }, + { + a: { + level: ServiceStatusLevels.unavailable, + summary: 'a unavailable', + reported: true, + }, + }, + { + a: { + level: ServiceStatusLevels.available, + summary: 'a available', + reported: true, + }, + }, ]); }); @@ -306,8 +334,20 @@ describe('PluginStatusService', () => { subscription.unsubscribe(); expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.available, summary: 'summary initial' } }, - { a: { level: ServiceStatusLevels.available, summary: 'summary updated' } }, + { + a: { + level: ServiceStatusLevels.available, + summary: 'summary initial', + reported: true, + }, + }, + { + a: { + level: ServiceStatusLevels.available, + summary: 'summary updated', + reported: true, + }, + }, ]); }); @@ -326,26 +366,25 @@ describe('PluginStatusService', () => { const pluginA$ = new ReplaySubject(1); service.set('a', pluginA$); // the first emission happens right after core$ services emit - const firstEmission = service.getAll$().pipe(skip(1), first()).toPromise(); + const firstEmission = firstValueFrom(service.getAll$().pipe(skip(1))); expect(await firstEmission).toEqual({ - a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' }, + a: { + level: ServiceStatusLevels.unavailable, + summary: 'Status check timed out after 10ms', + reported: true, + }, b: { level: ServiceStatusLevels.unavailable, - summary: '1 service is unavailable: a', + summary: '0 service(s) and 1 plugin(s) are unavailable: a', detail: 'See the status page for more information', meta: { - affectedServices: ['a'], + affectedPlugins: [], + failingServices: [], + failingPlugins: ['a'], }, }, }); - - pluginA$.next({ level: ServiceStatusLevels.available, summary: 'a available' }); - const secondEmission = service.getAll$().pipe(first()).toPromise(); - expect(await secondEmission).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); }); }); @@ -359,11 +398,11 @@ describe('PluginStatusService', () => { }); expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({}); expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + a: { level: ServiceStatusLevels.available, summary: 'All services are available' }, }); expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + a: { level: ServiceStatusLevels.available, summary: 'All services are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All services are available' }, }); }); @@ -372,10 +411,10 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + a: { level: ServiceStatusLevels.available, summary: 'a status', reported: true }, // a is available depsite savedObjects being degraded b: { level: ServiceStatusLevels.degraded, - summary: '1 service is degraded: savedObjects', + summary: '1 service(s) and 0 plugin(s) are degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -413,7 +452,7 @@ describe('PluginStatusService', () => { await delay(25); subscription.unsubscribe(); - expect(statusUpdates).toStrictEqual([{ a: available }]); + expect(statusUpdates).toStrictEqual([{ a: { ...available, reported: true } }]); }); it('debounces events in quick succession', async () => { @@ -455,12 +494,14 @@ describe('PluginStatusService', () => { Object { "a": Object { "level": degraded, + "reported": true, "summary": "a degraded", }, }, Object { "a": Object { "level": available, + "reported": true, "summary": "a available", }, }, diff --git a/packages/core/status/core-status-server-internal/src/plugins_status.ts b/packages/core/status/core-status-server-internal/src/plugins_status.ts index 58272636bfe854..8d20eff18927c2 100644 --- a/packages/core/status/core-status-server-internal/src/plugins_status.ts +++ b/packages/core/status/core-status-server-internal/src/plugins_status.ts @@ -6,26 +6,28 @@ * Side Public License, v 1. */ -import { BehaviorSubject, Observable, ReplaySubject, Subscription } from 'rxjs'; +import { BehaviorSubject, type Observable, ReplaySubject, type Subscription } from 'rxjs'; import { map, distinctUntilChanged, filter, - debounceTime, - timeoutWith, + timeout, startWith, + tap, + debounceTime, } from 'rxjs/operators'; import { sortBy } from 'lodash'; import { isDeepStrictEqual } from 'util'; import type { PluginName } from '@kbn/core-base-common'; import { ServiceStatusLevels, type CoreStatus, type ServiceStatus } from '@kbn/core-status-common'; import { getSummaryStatus } from './get_summary_status'; +import type { PluginStatus } from './types'; const STATUS_TIMEOUT_MS = 30 * 1000; // 30 seconds const defaultStatus: ServiceStatus = { level: ServiceStatusLevels.unavailable, - summary: `Status check timed out after ${STATUS_TIMEOUT_MS / 1000}s`, + summary: 'Status not yet available', }; export interface Deps { @@ -39,13 +41,13 @@ interface PluginData { depth: number; // depth of this plugin in the dependency tree (root plugins will have depth = 1) dependencies: PluginName[]; reverseDependencies: PluginName[]; - reportedStatus?: ServiceStatus; - derivedStatus: ServiceStatus; + reportedStatus?: PluginStatus; + derivedStatus: PluginStatus; }; } -interface PluginStatus { - [name: PluginName]: ServiceStatus; +interface PluginsStatus { + [name: PluginName]: PluginStatus; } interface ReportedStatusSubscriptions { @@ -53,15 +55,18 @@ interface ReportedStatusSubscriptions { } export class PluginsStatusService { - private coreStatus: CoreStatus = { elasticsearch: defaultStatus, savedObjects: defaultStatus }; + private coreStatus: CoreStatus = { + elasticsearch: { ...defaultStatus }, + savedObjects: { ...defaultStatus }, + }; private pluginData: PluginData; private rootPlugins: PluginName[]; // root plugins are those that do not have any dependencies private orderedPluginNames: PluginName[]; private pluginData$ = new ReplaySubject(1); - private pluginStatus: PluginStatus = {}; - private pluginStatus$ = new BehaviorSubject(this.pluginStatus); + private pluginStatus: PluginsStatus = {}; + private pluginStatus$ = new BehaviorSubject(this.pluginStatus); private reportedStatusSubscriptions: ReportedStatusSubscriptions = {}; - private isReportingStatus: Record = {}; + private reportingStatus: Record = {}; private newRegistrationsAllowed = true; private coreSubscription: Subscription; @@ -71,10 +76,15 @@ export class PluginsStatusService { this.orderedPluginNames = this.getOrderedPluginNames(); this.coreSubscription = deps.core$ - .pipe(debounceTime(10)) - .subscribe((coreStatus: CoreStatus) => { - this.coreStatus = coreStatus; - this.updateRootPluginsStatuses(); + .pipe( + debounceTime(10), + tap((coreStatus) => (this.coreStatus = coreStatus)), + map((serviceStatuses) => getSummaryStatus({ serviceStatuses })), + // no need to recalculate plugins statuses if core status hasn't changed + distinctUntilChanged((previous, current) => previous.level === current.level) + ) + .subscribe((derivedCoreStatus: ServiceStatus) => { + this.updateRootPluginsStatuses(derivedCoreStatus); this.updateDependantStatuses(this.rootPlugins); this.emitCurrentStatus(); }); @@ -93,27 +103,43 @@ export class PluginsStatusService { ); } - this.isReportingStatus[plugin] = true; + this.reportingStatus[plugin] = true; // unsubscribe from any previous subscriptions. Ideally plugins should register a status Observable only once this.reportedStatusSubscriptions[plugin]?.unsubscribe(); // delete any derived statuses calculated before the custom status Observable was registered delete this.pluginStatus[plugin]; - this.reportedStatusSubscriptions[plugin] = status$ - // Set a timeout for externally-defined status Observables + const statusChanged$ = status$.pipe(distinctUntilChanged()); + + this.reportedStatusSubscriptions[plugin] = statusChanged$ .pipe( - timeoutWith(this.statusTimeoutMs, status$.pipe(startWith(defaultStatus))), - distinctUntilChanged() + // Set a timeout for externally-defined status Observables + timeout({ + first: this.statusTimeoutMs, + with: () => + statusChanged$.pipe( + startWith({ + level: ServiceStatusLevels.unavailable, + summary: `Status check timed out after ${ + this.statusTimeoutMs < 1000 + ? `${this.statusTimeoutMs}ms` + : `${this.statusTimeoutMs / 1000}s` + }`, + }) + ), + }) ) .subscribe((status) => { - const levelChanged = this.updatePluginReportedStatus(plugin, status); + const { levelChanged, summaryChanged } = this.updatePluginReportedStatus(plugin, status); if (levelChanged) { this.updateDependantStatuses([plugin]); } - this.emitCurrentStatus(); + if (levelChanged || summaryChanged) { + this.emitCurrentStatus(); + } }); } @@ -126,27 +152,27 @@ export class PluginsStatusService { /** * Obtain an Observable of the status of all the plugins - * @returns {Observable>} An Observable that will yield the current status of all plugins + * @returns {Observable>} An Observable that will yield the current status of all plugins */ - public getAll$(): Observable> { + public getAll$(): Observable> { return this.pluginStatus$.asObservable().pipe( // do not emit until we have a status for all plugins filter((all) => Object.keys(all).length === this.orderedPluginNames.length), - distinctUntilChanged>(isDeepStrictEqual) + distinctUntilChanged>(isDeepStrictEqual) ); } /** * Obtain an Observable of the status of the dependencies of the given plugin * @param {PluginName} plugin the name of the plugin whose dependencies' status must be retreived - * @returns {Observable>} An Observable that will yield the current status of the plugin's dependencies + * @returns {Observable>} An Observable that will yield the current status of the plugin's dependencies */ - public getDependenciesStatus$(plugin: PluginName): Observable> { + public getDependenciesStatus$(plugin: PluginName): Observable> { const directDependencies = this.pluginData[plugin].dependencies; return this.getAll$().pipe( map((allStatus) => { - const dependenciesStatus: Record = {}; + const dependenciesStatus: Record = {}; directDependencies.forEach((dep) => (dependenciesStatus[dep] = allStatus[dep])); return dependenciesStatus; }), @@ -157,13 +183,13 @@ export class PluginsStatusService { /** * Obtain an Observable of the derived status of the given plugin * @param {PluginName} plugin the name of the plugin whose derived status must be retrieved - * @returns {Observable} An Observable that will yield the derived status of the plugin + * @returns {Observable} An Observable that will yield the derived status of the plugin */ - public getDerivedStatus$(plugin: PluginName): Observable { + public getDerivedStatus$(plugin: PluginName): Observable { return this.pluginData$.asObservable().pipe( map((pluginData) => pluginData[plugin]?.derivedStatus), - filter((status: ServiceStatus | undefined): status is ServiceStatus => !!status), - distinctUntilChanged(isDeepStrictEqual) + filter((status: PluginStatus | undefined): status is PluginStatus => !!status), + distinctUntilChanged(isDeepStrictEqual) ); } @@ -252,17 +278,13 @@ export class PluginsStatusService { /** * Updates the root plugins statuses according to the current core services status */ - private updateRootPluginsStatuses(): void { - const derivedStatus = getSummaryStatus(Object.entries(this.coreStatus), { - allAvailableSummary: `All dependencies are available`, - }); - + private updateRootPluginsStatuses(derivedCoreStatus: ServiceStatus): void { // note that the derived status is the same for all root plugins - this.rootPlugins.forEach((plugin) => { - this.pluginData[plugin].derivedStatus = derivedStatus; - if (!this.isReportingStatus[plugin]) { + this.rootPlugins.forEach((pluginName) => { + this.pluginData[pluginName].derivedStatus = derivedCoreStatus; + if (!this.reportingStatus[pluginName]) { // this root plugin has NOT registered any status Observable. Thus, its status is derived from core - this.pluginStatus[plugin] = derivedStatus; + this.pluginStatus[pluginName] = derivedCoreStatus; } }); } @@ -285,7 +307,7 @@ export class PluginsStatusService { const current = this.orderedPluginNames[i]; if (toCheck.has(current)) { // update the current plugin status - this.updatePluginStatus(current); + this.updatePluginsStatus(current); // flag all its reverse dependencies to be checked // TODO flag them only IF the status of this plugin has changed, seems to break some tests this.pluginData[current].reverseDependencies.forEach((revDep) => toCheck.add(revDep)); @@ -298,11 +320,11 @@ export class PluginsStatusService { * Optionally, if the plugin has not registered a custom status Observable, update its "current" status as well * @param {PluginName} plugin The name of the plugin to be updated */ - private updatePluginStatus(plugin: PluginName): void { - const newStatus = this.determinePluginStatus(plugin); + private updatePluginsStatus(plugin: PluginName): void { + const newStatus = this.determineDerivedStatus(plugin); this.pluginData[plugin].derivedStatus = newStatus; - if (!this.isReportingStatus[plugin]) { + if (!this.reportingStatus[plugin]) { // this plugin has NOT registered any status Observable. // Thus, its status is derived from its dependencies + core this.pluginStatus[plugin] = newStatus; @@ -310,45 +332,53 @@ export class PluginsStatusService { } /** - * Deterime the current plugin status, taking into account its reported status, its derived status - * and the status of the core services - * @param {PluginName} plugin the name of the plugin whose status must be determined - * @returns {ServiceStatus} The status of the plugin + * Determine the plugin's derived status (taking into account dependencies and core services) + * @param {PluginName} pluginName the name of the plugin whose status must be determined + * @returns {PluginStatus} The status of the plugin */ - private determinePluginStatus(plugin: PluginName): ServiceStatus { - const coreStatus: Array<[PluginName, ServiceStatus]> = Object.entries(this.coreStatus); - const newLocal = this.pluginData[plugin]; - - let depsStatus: Array<[PluginName, ServiceStatus]> = []; - - if (Object.keys(this.isReportingStatus).length) { + private determineDerivedStatus(pluginName: PluginName): PluginStatus { + if (Object.keys(this.reportingStatus).length) { // if at least one plugin has registered a status Observable... take into account plugin dependencies - depsStatus = newLocal.dependencies.map((dependency) => [ - dependency, - this.pluginData[dependency].reportedStatus || this.pluginData[dependency].derivedStatus, - ]); - } + const pluginData = this.pluginData[pluginName]; - const newStatus = getSummaryStatus([...coreStatus, ...depsStatus], { - allAvailableSummary: `All dependencies are available`, - }); - - return newStatus; + const dependenciesStatuses = Object.fromEntries( + pluginData.dependencies.map((dependency) => [ + dependency, + this.pluginData[dependency].reportedStatus ?? this.pluginData[dependency].derivedStatus, + ]) + ); + return getSummaryStatus({ + serviceStatuses: this.coreStatus, + pluginStatuses: dependenciesStatuses, + }); + } else { + // no plugins have registered a status Observable... infer status from Core services only + return getSummaryStatus({ + serviceStatuses: this.coreStatus, + }); + } } /** - * Updates the reported status for the given plugin, along with the status of its dependencies tree. - * @param {PluginName} plugin The name of the plugin whose reported status must be updated - * @param {ServiceStatus} reportedStatus The newly reported status for that plugin - * @return {boolean} true if the level of the reported status changed + * Updates the reported status for the given plugin. + * @param {PluginName} pluginName The name of the plugin whose reported status must be updated + * @param {ServiceStatus} status The newly reported status for that plugin + * @return {Object} indicating whether the level and/or the summary have changed */ - private updatePluginReportedStatus(plugin: PluginName, reportedStatus: ServiceStatus): boolean { - const previousReportedStatus = this.pluginData[plugin].reportedStatus; - - this.pluginData[plugin].reportedStatus = reportedStatus; - this.pluginStatus[plugin] = reportedStatus; - - return previousReportedStatus?.level !== reportedStatus.level; + private updatePluginReportedStatus(pluginName: PluginName, status: ServiceStatus) { + const previousReportedStatus = this.pluginData[pluginName].reportedStatus; + + const reportedStatus: PluginStatus = { + ...status, + reported: true, + }; + this.pluginData[pluginName].reportedStatus = reportedStatus; + this.pluginStatus[pluginName] = reportedStatus; + + return { + levelChanged: previousReportedStatus?.level !== reportedStatus.level, + summaryChanged: previousReportedStatus?.summary !== reportedStatus.summary, + }; } /** diff --git a/packages/core/status/core-status-server-internal/src/status_service.test.mocks.ts b/packages/core/status/core-status-server-internal/src/status_service.test.mocks.ts new file mode 100644 index 00000000000000..0b7bc2e3fe0b69 --- /dev/null +++ b/packages/core/status/core-status-server-internal/src/status_service.test.mocks.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const logOverallStatusChangesMock = jest.fn(); +jest.doMock('./log_overall_status', () => ({ + logOverallStatusChanges: logOverallStatusChangesMock, +})); + +export const logCoreStatusChangesMock = jest.fn(); +jest.doMock('./log_core_services_status', () => ({ + logCoreStatusChanges: logCoreStatusChangesMock, +})); + +export const logPluginsStatusChangesMock = jest.fn(); +jest.doMock('./log_plugins_status', () => ({ + logPluginsStatusChanges: logPluginsStatusChangesMock, +})); diff --git a/packages/core/status/core-status-server-internal/src/status_service.test.ts b/packages/core/status/core-status-server-internal/src/status_service.test.ts index 9bc25b66167fe5..c4ae2732af15c1 100644 --- a/packages/core/status/core-status-server-internal/src/status_service.test.ts +++ b/packages/core/status/core-status-server-internal/src/status_service.test.ts @@ -6,30 +6,47 @@ * Side Public License, v 1. */ -import { of, BehaviorSubject, firstValueFrom } from 'rxjs'; +import { of, BehaviorSubject, firstValueFrom, Observable } from 'rxjs'; -import { ServiceStatus, ServiceStatusLevels, CoreStatus } from '@kbn/core-status-common'; -import { InternalStatusServiceSetup } from './types'; -import { StatusService, StatusServiceSetupDeps } from './status_service'; +import { type ServiceStatus, ServiceStatusLevels, type CoreStatus } from '@kbn/core-status-common'; +import type { ILoggingSystem } from '@kbn/core-logging-server-internal'; import { first, take, toArray } from 'rxjs/operators'; import { mockCoreContext } from '@kbn/core-base-server-mocks'; import { environmentServiceMock } from '@kbn/core-environment-server-mocks'; import { mockRouter, RouterMock } from '@kbn/core-http-router-server-mocks'; import { httpServiceMock } from '@kbn/core-http-server-mocks'; -import { ServiceStatusLevelSnapshotSerializer } from './test_helpers'; import { metricsServiceMock } from '@kbn/core-metrics-server-mocks'; import { configServiceMock } from '@kbn/config-mocks'; import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks'; import { analyticsServiceMock } from '@kbn/core-analytics-server-mocks'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; +import { + logCoreStatusChangesMock, + logPluginsStatusChangesMock, + logOverallStatusChangesMock, +} from './status_service.test.mocks'; +import { StatusService, type StatusServiceSetupDeps } from './status_service'; +import { ServiceStatusLevelSnapshotSerializer } from './test_helpers'; +import type { InternalStatusServiceSetup } from './types'; + expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); describe('StatusService', () => { let service: StatusService; + let logger: jest.Mocked; beforeEach(() => { - service = new StatusService(mockCoreContext.create()); + logger = loggingSystemMock.create(); + service = new StatusService(mockCoreContext.create({ logger })); + }); + + afterEach(() => { + loggingSystemMock.clear(logger); + logCoreStatusChangesMock.mockReset(); + logPluginsStatusChangesMock.mockReset(); + logOverallStatusChangesMock.mockReset(); }); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -47,7 +64,7 @@ describe('StatusService', () => { summary: 'This is critical!', }; - const setupDeps = (overrides: Partial): StatusServiceSetupDeps => { + const setupDeps = (overrides: Partial = {}): StatusServiceSetupDeps => { return { analytics: analyticsServiceMock.createAnalyticsServiceSetup(), elasticsearch: { @@ -216,7 +233,7 @@ describe('StatusService', () => { ); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); }); @@ -236,15 +253,15 @@ describe('StatusService', () => { const subResult3 = await setup.overall$.pipe(first()).toPromise(); expect(subResult1).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); expect(subResult2).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); expect(subResult3).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); }); @@ -289,15 +306,17 @@ describe('StatusService', () => { "detail": "See the status page for more information", "level": degraded, "meta": Object { - "affectedServices": Array [ + "affectedPlugins": Array [], + "failingPlugins": Array [], + "failingServices": Array [ "savedObjects", ], }, - "summary": "1 service is degraded: savedObjects", + "summary": "1 service(s) and 0 plugin(s) are degraded: savedObjects", }, Object { "level": available, - "summary": "All services are available", + "summary": "All services and plugins are available", }, ] `); @@ -339,15 +358,17 @@ describe('StatusService', () => { "detail": "See the status page for more information", "level": degraded, "meta": Object { - "affectedServices": Array [ + "affectedPlugins": Array [], + "failingPlugins": Array [], + "failingServices": Array [ "savedObjects", ], }, - "summary": "1 service is degraded: savedObjects", + "summary": "1 service(s) and 0 plugin(s) are degraded: savedObjects", }, Object { "level": available, - "summary": "All services are available", + "summary": "All services and plugins are available", }, ] `); @@ -368,7 +389,7 @@ describe('StatusService', () => { ); expect(await setup.coreOverall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); }); @@ -385,7 +406,7 @@ describe('StatusService', () => { ); expect(await setup.coreOverall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.critical, - summary: '1 service is critical: savedObjects', + summary: '1 service(s) and 0 plugin(s) are critical: savedObjects', }); }); @@ -407,15 +428,15 @@ describe('StatusService', () => { expect(subResult1).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); expect(subResult2).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); expect(subResult3).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '2 services are degraded: elasticsearch, savedObjects', + summary: '2 service(s) and 0 plugin(s) are degraded: elasticsearch, savedObjects', }); }); @@ -460,11 +481,13 @@ describe('StatusService', () => { "detail": "See the status page for more information", "level": degraded, "meta": Object { - "affectedServices": Array [ + "affectedPlugins": Array [], + "failingPlugins": Array [], + "failingServices": Array [ "savedObjects", ], }, - "summary": "1 service is degraded: savedObjects", + "summary": "1 service(s) and 0 plugin(s) are degraded: savedObjects", }, Object { "level": available, @@ -510,11 +533,13 @@ describe('StatusService', () => { "detail": "See the status page for more information", "level": degraded, "meta": Object { - "affectedServices": Array [ + "affectedPlugins": Array [], + "failingPlugins": Array [], + "failingServices": Array [ "savedObjects", ], }, - "summary": "1 service is degraded: savedObjects", + "summary": "1 service(s) and 0 plugin(s) are degraded: savedObjects", }, Object { "level": available, @@ -539,17 +564,17 @@ describe('StatusService', () => { const { context$ } = analyticsMock.registerContextProvider.mock.calls[0][0]; await expect(firstValueFrom(context$.pipe(take(2), toArray()))).resolves .toMatchInlineSnapshot(` - Array [ - Object { - "overall_status_level": "initializing", - "overall_status_summary": "Kibana is starting up", - }, - Object { - "overall_status_level": "available", - "overall_status_summary": "All services are available", - }, - ] - `); + Array [ + Object { + "overall_status_level": "initializing", + "overall_status_summary": "Kibana is starting up", + }, + Object { + "overall_status_level": "available", + "overall_status_summary": "All services and plugins are available", + }, + ] + `); }); test('registers and reports an event', async () => { @@ -563,11 +588,55 @@ describe('StatusService', () => { "core-overall_status_changed", Object { "overall_status_level": "available", - "overall_status_summary": "All services are available", + "overall_status_summary": "All services and plugins are available", }, ] `); }); }); }); + + describe('#start', () => { + it('calls logCoreStatusChangesMock with the right params', async () => { + await service.setup(setupDeps()); + await service.start(); + + expect(logCoreStatusChangesMock).toHaveBeenCalledTimes(1); + expect(logCoreStatusChangesMock).toHaveBeenCalledWith( + expect.objectContaining({ + logger: expect.any(Object), + core$: expect.any(Observable), + stop$: expect.any(Observable), + }) + ); + }); + + it('calls logPluginsStatusChangesMock with the right params', async () => { + await service.setup(setupDeps()); + await service.start(); + + expect(logPluginsStatusChangesMock).toHaveBeenCalledTimes(1); + expect(logPluginsStatusChangesMock).toHaveBeenCalledWith( + expect.objectContaining({ + logger: expect.any(Object), + plugins$: expect.any(Observable), + stop$: expect.any(Observable), + }) + ); + }); + + it('calls logOverallStatusChangesMock with the right params', async () => { + await service.setup(setupDeps()); + await service.start(); + + expect(logOverallStatusChangesMock).toHaveBeenCalledTimes(1); + expect(logOverallStatusChangesMock).toHaveBeenCalledWith( + expect.objectContaining({ + logger: expect.any(Object), + overall$: expect.any(Observable), + stop$: expect.any(Observable), + }) + ); + }); + }); }); diff --git a/packages/core/status/core-status-server-internal/src/status_service.ts b/packages/core/status/core-status-server-internal/src/status_service.ts index be75573b8d3db5..0d1a53615da5fd 100644 --- a/packages/core/status/core-status-server-internal/src/status_service.ts +++ b/packages/core/status/core-status-server-internal/src/status_service.ts @@ -7,15 +7,15 @@ */ import { - Observable, + type Observable, combineLatest, - Subscription, + type Subscription, Subject, firstValueFrom, tap, BehaviorSubject, } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, debounceTime, takeUntil } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, takeUntil, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import type { RootSchema } from '@kbn/analytics-client'; @@ -32,14 +32,16 @@ import type { InternalElasticsearchServiceSetup } from '@kbn/core-elasticsearch- import type { InternalMetricsServiceSetup } from '@kbn/core-metrics-server-internal'; import type { InternalSavedObjectsServiceSetup } from '@kbn/core-saved-objects-server-internal'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; -import type { ServiceStatus, CoreStatus } from '@kbn/core-status-common'; +import { type ServiceStatus, type CoreStatus } from '@kbn/core-status-common'; import { registerStatusRoute, registerPrebootStatusRoute } from './routes'; -import { statusConfig as config, StatusConfigType } from './status_config'; +import { statusConfig as config, type StatusConfigType } from './status_config'; import type { InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; import { PluginsStatusService } from './cached_plugins_status'; -import { getOverallStatusChanges } from './log_overall_status'; +import { logCoreStatusChanges } from './log_core_services_status'; +import { logPluginsStatusChanges } from './log_plugins_status'; +import { logOverallStatusChanges } from './log_overall_status'; interface StatusLogMeta extends LogMeta { kibana: { status: ServiceStatus }; @@ -70,6 +72,7 @@ export class StatusService implements CoreService { private readonly config$: Observable; private readonly stop$ = new Subject(); + private core$?: Observable; private overall$?: Observable; private pluginsStatus?: PluginsStatusService; private subscriptions: Subscription[] = []; @@ -96,17 +99,14 @@ export class StatusService implements CoreService { coreUsageData, }: StatusServiceSetupDeps) { const statusConfig = await firstValueFrom(this.config$); - const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); + const core$ = (this.core$ = this.setupCoreStatus({ elasticsearch, savedObjects })); this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); this.overall$ = combineLatest([core$, this.pluginsStatus.getAll$()]).pipe( // Prevent many emissions at once from dependency status resolution from making this too noisy debounceTime(80), - map(([coreStatus, pluginsStatus]) => { - const summary = getSummaryStatus([ - ...Object.entries(coreStatus), - ...Object.entries(pluginsStatus), - ]); + map(([serviceStatuses, pluginStatuses]) => { + const summary = getSummaryStatus({ serviceStatuses, pluginStatuses }); this.logger.debug(`Recalculated overall status`, { kibana: { status: summary, @@ -123,8 +123,8 @@ export class StatusService implements CoreService { const coreOverall$ = core$.pipe( // Prevent many emissions at once from dependency status resolution from making this too noisy debounceTime(25), - map((coreStatus) => { - const coreOverall = getSummaryStatus([...Object.entries(coreStatus)]); + map((serviceStatuses) => { + const coreOverall = getSummaryStatus({ serviceStatuses }); this.logger.debug(`Recalculated core overall status`, { kibana: { status: coreOverall, @@ -180,10 +180,7 @@ export class StatusService implements CoreService { throw new Error(`StatusService#setup must be called before #start`); } this.pluginsStatus.blockNewRegistrations(); - - getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => { - this.logger.info(message); - }); + this.logStatusChanges(); } public stop() { @@ -247,4 +244,24 @@ export class StatusService implements CoreService { tap((statusPayload) => analytics.reportEvent(overallStatusChangedEventName, statusPayload)) ).subscribe(context$); } + + private logStatusChanges() { + logCoreStatusChanges({ + logger: this.logger.get('core'), + core$: this.core$!, + stop$: this.stop$, + }); + + logPluginsStatusChanges({ + logger: this.logger.get('plugins'), + plugins$: this.pluginsStatus!.getAll$(), + stop$: this.stop$, + }); + + logOverallStatusChanges({ + logger: this.logger, + overall$: this.overall$!, + stop$: this.stop$, + }); + } } diff --git a/packages/core/status/core-status-server-internal/src/types.ts b/packages/core/status/core-status-server-internal/src/types.ts index 520a313bc97863..bdcd9f58bf7bfa 100644 --- a/packages/core/status/core-status-server-internal/src/types.ts +++ b/packages/core/status/core-status-server-internal/src/types.ts @@ -26,3 +26,27 @@ export interface InternalStatusServiceSetup getDerivedStatus$(plugin: PluginName): Observable; }; } + +/** @internal */ +export interface NamedStatus extends ServiceStatus { + name: string; // the name of the service / plugin +} + +/** @internal */ +export interface NamedServiceStatus extends ServiceStatus, NamedStatus {} + +/** @internal */ +export interface LoggableServiceStatus extends NamedServiceStatus { + repeats?: number; // whether this status has been reported repeatedly recently (and how many times) +} + +/** @internal */ +export interface PluginStatus extends ServiceStatus { + reported?: boolean; // whether this status is reported (true) or inferred (false) +} + +/** @internal */ +export interface NamedPluginStatus extends PluginStatus, NamedStatus {} + +/** @internal */ +export interface LoggablePluginStatus extends PluginStatus, LoggableServiceStatus {} diff --git a/packages/core/status/core-status-server-internal/tsconfig.json b/packages/core/status/core-status-server-internal/tsconfig.json index ec555e676e1e8a..0cc9f82b847945 100644 --- a/packages/core/status/core-status-server-internal/tsconfig.json +++ b/packages/core/status/core-status-server-internal/tsconfig.json @@ -40,6 +40,8 @@ "@kbn/config-mocks", "@kbn/core-usage-data-server-mocks", "@kbn/core-analytics-server-mocks", + "@kbn/core-logging-server-internal", + "@kbn/core-logging-server-mocks", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/licensing/server/plugin_status.test.ts b/x-pack/plugins/licensing/server/plugin_status.test.ts index 713392901adaf2..a9daebef1e78fe 100644 --- a/x-pack/plugins/licensing/server/plugin_status.test.ts +++ b/x-pack/plugins/licensing/server/plugin_status.test.ts @@ -9,49 +9,33 @@ import { TestScheduler } from 'rxjs/testing'; import { ServiceStatusLevels } from '@kbn/core/server'; import { licenseMock } from '../common/licensing.mock'; import { getPluginStatus$ } from './plugin_status'; -import { ILicense } from '../common/types'; +import type { ILicense } from '../common/types'; const getTestScheduler = () => new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); -const degradedStatus = { - level: ServiceStatusLevels.degraded, - summary: expect.any(String), -}; const availableStatus = { level: ServiceStatusLevels.available, summary: expect.any(String), }; + const unavailableStatus = { level: ServiceStatusLevels.unavailable, summary: expect.any(String), }; describe('getPluginStatus$', () => { - it('emits an initial `degraded` status', () => { - getTestScheduler().run(({ expectObservable, hot }) => { - const license$ = hot('|'); - const stop$ = hot(''); - const expected = '(a|)'; - - expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { - a: degradedStatus, - }); - }); - }); - it('emits an `available` status once the license emits', () => { getTestScheduler().run(({ expectObservable, hot }) => { const license$ = hot('--a', { a: licenseMock.createLicenseMock(), }); const stop$ = hot(''); - const expected = 'a-b'; + const expected = '--b'; expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { - a: degradedStatus, b: availableStatus, }); }); @@ -66,10 +50,9 @@ describe('getPluginStatus$', () => { a: errorLicense, }); const stop$ = hot(''); - const expected = 'a-b'; + const expected = '--b'; expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { - a: degradedStatus, b: unavailableStatus, }); }); @@ -86,10 +69,9 @@ describe('getPluginStatus$', () => { b: validLicense, }); const stop$ = hot(''); - const expected = 'a-b--c'; + const expected = '--b--c'; expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { - a: degradedStatus, b: unavailableStatus, c: availableStatus, }); @@ -103,10 +85,9 @@ describe('getPluginStatus$', () => { b: licenseMock.createLicenseMock(), }); const stop$ = hot('----a', { a: undefined }); - const expected = 'a-b-|'; + const expected = '--b-|'; expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { - a: degradedStatus, b: availableStatus, }); }); diff --git a/x-pack/plugins/licensing/server/plugin_status.ts b/x-pack/plugins/licensing/server/plugin_status.ts index b1afd9ea21af2e..a65b51d13eb894 100644 --- a/x-pack/plugins/licensing/server/plugin_status.ts +++ b/x-pack/plugins/licensing/server/plugin_status.ts @@ -5,17 +5,16 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { takeUntil, startWith, map } from 'rxjs/operators'; -import { ServiceStatus, ServiceStatusLevels } from '@kbn/core/server'; -import { ILicense } from '../common/types'; +import type { Observable } from 'rxjs'; +import { takeUntil, map } from 'rxjs/operators'; +import { type ServiceStatus, ServiceStatusLevels } from '@kbn/core/server'; +import type { ILicense } from '../common/types'; export const getPluginStatus$ = ( license$: Observable, stop$: Observable ): Observable => { return license$.pipe( - startWith(undefined), takeUntil(stop$), map((license) => { if (license) { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 4c9879f25b591f..2fbabc1fe4d67d 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -179,7 +179,7 @@ export class TaskManagerPlugin core.status.set( combineLatest([core.status.derivedStatus$, serviceStatus$]).pipe( map(([derivedStatus, serviceStatus]) => - serviceStatus.level > derivedStatus.level ? serviceStatus : derivedStatus + serviceStatus.level >= derivedStatus.level ? serviceStatus : derivedStatus ) ) ); diff --git a/x-pack/test/scalability/apis/api.status.json b/x-pack/test/scalability/apis/api.status.json index 396d9d0645fc39..a8379af4552826 100644 --- a/x-pack/test/scalability/apis/api.status.json +++ b/x-pack/test/scalability/apis/api.status.json @@ -1,5 +1,5 @@ { - "journeyName": "POST /api/status", + "journeyName": "GET /api/status", "scalabilitySetup": { "responseTimeThreshold": { "threshold1": 1000, diff --git a/x-pack/test/scalability/apis/api.status.no_auth.json b/x-pack/test/scalability/apis/api.status.no_auth.json index 2fe293f072a257..b0cc1a243edd05 100644 --- a/x-pack/test/scalability/apis/api.status.no_auth.json +++ b/x-pack/test/scalability/apis/api.status.no_auth.json @@ -1,5 +1,5 @@ { - "journeyName": "POST /api/status", + "journeyName": "GET /api/status no-auth", "scalabilitySetup": { "responseTimeThreshold": { "threshold1": 1000, From 8938a5778ab6a3b5b638e4da5fe926d5dc0fd827 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 25 Oct 2023 18:04:02 +0200 Subject: [PATCH 10/24] Handle array values in i18nrc (#169637) --- packages/kbn-eslint-plugin-i18n/README.mdx | 3 +- ...get_i18n_identifier_from_file_path.test.ts | 4 + .../get_i18n_identifier_from_file_path.ts | 4 +- .../helpers/get_intent_from_node.ts | 30 +++-- ..._translated_with_formatted_message.test.ts | 31 +++++ ...ld_be_translated_with_formatted_message.ts | 60 ++++++++- ...ngs_should_be_translated_with_i18n.test.ts | 117 +++++++++++------- .../strings_should_be_translated_with_i18n.ts | 64 +++++++++- 8 files changed, 255 insertions(+), 58 deletions(-) diff --git a/packages/kbn-eslint-plugin-i18n/README.mdx b/packages/kbn-eslint-plugin-i18n/README.mdx index f72f01c06a632d..100f83d167b6ea 100644 --- a/packages/kbn-eslint-plugin-i18n/README.mdx +++ b/packages/kbn-eslint-plugin-i18n/README.mdx @@ -9,13 +9,14 @@ tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'i18n'] `@kbn/eslint-plugin-i18n` is an ESLint plugin providing custom rules for validating JSXCode in the Kibana repo to make sure they are translated. Note: At the moment these rules only work for apps that are inside `/x-pack/plugins`. - If you want to enable this rule on code that is outside of this path, adjust `/helpers/get_i18n_identifier_from_file_path.ts`. ## `@kbn/i18n/strings_should_be_translated_with_i18n` This rule warns engineers to translate their strings by using i18n.translate from the '@kbn/i18n' package. It provides an autofix that takes into account the context of the translatable string in the JSX tree to generate a translation ID. +It kicks in on JSXText elements and specific JSXAttributes (`label` and `aria-label`) which expect a translated value. ## `@kbn/i18n/strings_should_be_translated_with_formatted_message` This rule warns engineers to translate their strings by using `` from the '@kbn/i18n-react' package. It provides an autofix that takes into account the context of the translatable string in the JSX tree and to generate a translation ID. +It kicks in on JSXText elements and specific JSXAttributes (`label` and `aria-label`) which expect a translated value. diff --git a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts index d1157b7b16f10f..6e01b89b235659 100644 --- a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts +++ b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts @@ -14,6 +14,10 @@ const testMap = [ ['x-pack/plugins/observability/foo/bar/baz/header_actions.tsx', 'xpack.observability'], ['x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx', 'xpack.apm'], ['x-pack/plugins/cases/public/components/foo.tsx', 'xpack.cases'], + [ + 'x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx', + 'xpack.synthetics', + ], [ 'packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx', 'app_not_found_in_i18nrc', diff --git a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts index 288e2692bd76af..d23a42f4ebcfb7 100644 --- a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts +++ b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts @@ -24,6 +24,8 @@ export function getI18nIdentifierFromFilePath(fileName: string, cwd: string) { const i18nrc = JSON.parse(i18nrcFile); return i18nrc && i18nrc.paths - ? findKey(i18nrc.paths, (v) => v === path) ?? 'app_not_found_in_i18nrc' + ? findKey(i18nrc.paths, (v) => + Array.isArray(v) ? v.find((e) => e === path) : typeof v === 'string' && v === path + ) ?? 'app_not_found_in_i18nrc' : 'could_not_find_i18nrc'; } diff --git a/packages/kbn-eslint-plugin-i18n/helpers/get_intent_from_node.ts b/packages/kbn-eslint-plugin-i18n/helpers/get_intent_from_node.ts index 4cbd6bb9e330d5..687bfd31cfba2f 100644 --- a/packages/kbn-eslint-plugin-i18n/helpers/get_intent_from_node.ts +++ b/packages/kbn-eslint-plugin-i18n/helpers/get_intent_from_node.ts @@ -5,12 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { TSESTree } from '@typescript-eslint/typescript-estree'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree'; import { lowerCaseFirstLetter, upperCaseFirstLetter } from './utils'; -export function getIntentFromNode(originalNode: TSESTree.JSXText): string { - const value = lowerCaseFirstLetter( - originalNode.value +export function getIntentFromNode(value: string, parent: TSESTree.Node | undefined): string { + const processedValue = lowerCaseFirstLetter( + value .replace(/[?!@#$%^&*()_+\][{}|/<>,'"]/g, '') .trim() .split(' ') @@ -19,8 +19,6 @@ export function getIntentFromNode(originalNode: TSESTree.JSXText): string { .join('') ); - const { parent } = originalNode; - if ( parent && 'openingElement' in parent && @@ -30,11 +28,25 @@ export function getIntentFromNode(originalNode: TSESTree.JSXText): string { const parentTagName = String(parent.openingElement.name.name); if (parentTagName.includes('Eui')) { - return `${value}${parentTagName.replace('Eui', '')}Label`; + return `${processedValue}${parentTagName.replace('Eui', '')}Label`; } - return `${lowerCaseFirstLetter(parentTagName)}.${value}Label`; + return `${lowerCaseFirstLetter(parentTagName)}.${processedValue}Label`; + } + + if ( + parent && + 'parent' in parent && + parent.parent && + 'name' in parent.parent && + typeof parent.parent.name !== 'string' && + 'type' in parent.parent.name && + parent.parent.name.type === AST_NODE_TYPES.JSXIdentifier + ) { + const parentTagName = String(parent.parent.name.name); + + return `${lowerCaseFirstLetter(parentTagName)}.${processedValue}Label`; } - return `${value}Label`; + return `${processedValue}Label`; } diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts index aa545c9bb0ee06..10bdbda351892d 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts @@ -95,6 +95,18 @@ function YetAnotherComponent() { ) }`, }, + { + filename: 'x-pack/plugins/observability/public/test_component.tsx', + code: ` +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +function TestComponent() { + return ( + } /> + ) + }`, + }, ]; const invalid = [ @@ -160,6 +172,25 @@ function YetAnotherComponent() { ], output: valid[2].code, }, + { + filename: valid[3].filename, + code: ` +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +function TestComponent() { + return ( + + ) + }`, + errors: [ + { + line: 7, + message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + }, + ], + output: valid[3].code, + }, ]; for (const [name, tester] of [tsTester, babelTester]) { diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts index 251fb3b3752fce..67e2aaec256d76 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts @@ -37,7 +37,7 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { const i18nAppId = getI18nIdentifierFromFilePath(filename, cwd); const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration; const functionName = getFunctionName(functionDeclaration); - const intent = getIntentFromNode(node); + const intent = getIntentFromNode(value, node.parent); const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' @@ -72,6 +72,64 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { }, }); }, + JSXAttribute: (node: TSESTree.JSXAttribute) => { + if (node.name.name !== 'aria-label' && node.name.name !== 'label') return; + + let val: string = ''; + + // label={'foo'} + if ( + node.value && + 'expression' in node.value && + 'value' in node.value.expression && + typeof node.value.expression.value === 'string' + ) { + val = node.value.expression.value; + } + + // label="foo" + if (node.value && 'value' in node.value && typeof node.value.value === 'string') { + val = node.value.value; + } + + if (!val) return; + + // Start building the translation ID suggestion + const i18nAppId = getI18nIdentifierFromFilePath(filename, cwd); + const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration; + const functionName = getFunctionName(functionDeclaration); + const intent = getIntentFromNode(val, node); + + const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' + + // Check if i18n has already been imported into the file. + const { + hasI18nImportLine, + i18nPackageImportLine: i18nImportLine, + rangeToAddI18nImportLine, + } = getI18nImportFixer({ + sourceCode, + mode: 'FormattedMessage', + }); + + // Show warning to developer and offer autofix suggestion + report({ + node: node as any, + message: + 'Strings should be translated with . Use the autofix suggestion or add your own.', + fix(fixer) { + return [ + fixer.replaceTextRange( + node.value!.range, + `{}` + ), + !hasI18nImportLine + ? fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) + : null, + ].filter(isTruthy); + }, + }); + }, } as Rule.RuleListener; }, }; diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts index 4c5916831b6cbc..dc938cd6effd32 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts @@ -46,10 +46,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; function TestComponent() { - return ( -
{i18n.translate('app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel', { defaultMessage: "This is a test"})}
- ) -}`, + return ( +
{i18n.translate('app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test'})}
+ ) + }`, }, { filename: 'x-pack/plugins/observability/public/another_component.tsx', @@ -58,30 +58,42 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; function AnotherComponent() { - return ( - - - - {i18n.translate('app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel', { defaultMessage: "This is a test"})} - - - - ) -}`, + return ( + + + + {i18n.translate('app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test'})} + + + + ) + }`, }, { filename: 'x-pack/plugins/observability/public/yet_another_component.tsx', code: ` + import React from 'react'; +import { i18n } from '@kbn/i18n'; + + function YetAnotherComponent() { + return ( +
+ {i18n.translate('app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me'})} +
+ ) + }`, + }, + { + filename: 'x-pack/plugins/observability/public/test_component.tsx', + code: ` import React from 'react'; import { i18n } from '@kbn/i18n'; -function YetAnotherComponent() { - return ( -
- {i18n.translate('app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: "Select me"})} -
- ) -}`, +function TestComponent() { + return ( + + ) + }`, }, ]; @@ -92,10 +104,10 @@ const invalid = [ import React from 'react'; function TestComponent() { - return ( -
This is a test
- ) -}`, + return ( +
This is a test
+ ) + }`, errors: [ { line: 6, @@ -110,16 +122,16 @@ function TestComponent() { import React from 'react'; function AnotherComponent() { - return ( - - - - This is a test - - - - ) -}`, + return ( + + + + This is a test + + + + ) + }`, errors: [ { line: 9, @@ -131,15 +143,15 @@ function AnotherComponent() { { filename: valid[2].filename, code: ` -import React from 'react'; + import React from 'react'; -function YetAnotherComponent() { - return ( -
- Select me -
- ) -}`, + function YetAnotherComponent() { + return ( +
+ Select me +
+ ) + }`, errors: [ { line: 7, @@ -148,6 +160,25 @@ function YetAnotherComponent() { ], output: valid[2].code, }, + { + filename: valid[3].filename, + code: ` +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +function TestComponent() { + return ( + + ) + }`, + errors: [ + { + line: 7, + message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + }, + ], + output: valid[3].code, + }, ]; for (const [name, tester] of [tsTester, babelTester]) { diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts index a1e0da36921b6d..ba31f6109075a8 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts @@ -37,11 +37,11 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { const i18nAppId = getI18nIdentifierFromFilePath(filename, cwd); const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration; const functionName = getFunctionName(functionDeclaration); - const intent = getIntentFromNode(node); + const intent = getIntentFromNode(value, node.parent); const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' - // Check if i18n has already been imported into the file. + // Check if i18n has already been imported into the file const { hasI18nImportLine, i18nPackageImportLine: i18nImportLine, @@ -60,7 +60,65 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { return [ fixer.replaceText( node, - `${whiteSpaces}{i18n.translate('${translationIdSuggestion}', { defaultMessage: "${value}"})}` + `${whiteSpaces}{i18n.translate('${translationIdSuggestion}', { defaultMessage: '${value}'})}` + ), + !hasI18nImportLine + ? fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) + : null, + ].filter(isTruthy); + }, + }); + }, + JSXAttribute: (node: TSESTree.JSXAttribute) => { + if (node.name.name !== 'aria-label' && node.name.name !== 'label') return; + + let val: string = ''; + + // label={'foo'} + if ( + node.value && + 'expression' in node.value && + 'value' in node.value.expression && + typeof node.value.expression.value === 'string' + ) { + val = node.value.expression.value; + } + + // label="foo" + if (node.value && 'value' in node.value && typeof node.value.value === 'string') { + val = node.value.value; + } + + if (!val) return; + + // Start building the translation ID suggestion + const i18nAppId = getI18nIdentifierFromFilePath(filename, cwd); + const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration; + const functionName = getFunctionName(functionDeclaration); + const intent = getIntentFromNode(val, node); + + const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' + + // Check if i18n has already been imported into the file. + const { + hasI18nImportLine, + i18nPackageImportLine: i18nImportLine, + rangeToAddI18nImportLine, + } = getI18nImportFixer({ + sourceCode, + mode: 'i18n.translate', + }); + + // Show warning to developer and offer autofix suggestion + report({ + node: node as any, + message: + 'Strings should be translated with i18n. Use the autofix suggestion or add your own.', + fix(fixer) { + return [ + fixer.replaceTextRange( + node.value!.range, + `{i18n.translate('${translationIdSuggestion}', { defaultMessage: '${val}'})}` ), !hasI18nImportLine ? fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) From 5c578a06b3d6e23ea1b78de983522fd660faa6e3 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 25 Oct 2023 18:09:08 +0200 Subject: [PATCH 11/24] [ML] AIOps: Improve `flushFix` for Log Rate Analysis (#165069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves the `flushFix` behaviour for Log Rate Analysis. Previously the setting would add a 4KB size additional dummy payload to each object returned as ndjson. For the dataset used for testing this, this would result in an overall response payload of ˜900Kbytes. For comparison, without `flushFix` the response size would be ˜40Kbytes in this case. This PR changes the behaviour to only send a dummy payload every 500ms if the real data sent in the last 500ms wasn't bigger than 4Kbytes. Depending on the speed of the response, this can bring down the overall response payload to ˜300Kbytes (Cloud uncached), ˜150Kbytes (Cloud cached) or even ˜70Kbytes (local cluster) for the same dataset. --- .../api/reducer_stream/request_body_schema.ts | 4 ++- .../app/pages/page_reducer_stream/index.tsx | 10 +++++- .../server/routes/reducer_stream.ts | 3 +- .../server/stream_factory.test.ts | 4 +++ .../response_stream/server/stream_factory.ts | 34 +++++++++++++++---- .../log_rate_analysis_results.tsx | 2 +- .../aiops/log_rate_analysis_full_analysis.ts | 11 ++++-- .../aiops/log_rate_analysis_groups_only.ts | 12 ++++--- 8 files changed, 62 insertions(+), 18 deletions(-) diff --git a/examples/response_stream/common/api/reducer_stream/request_body_schema.ts b/examples/response_stream/common/api/reducer_stream/request_body_schema.ts index 8318a411ab86a0..ebd08e55cb863b 100644 --- a/examples/response_stream/common/api/reducer_stream/request_body_schema.ts +++ b/examples/response_stream/common/api/reducer_stream/request_body_schema.ts @@ -9,11 +9,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const reducerStreamRequestBodySchema = schema.object({ - /** Boolean flag to enable/disabling simulation of response errors. */ + /** Boolean flag to enable/disable simulation of response errors. */ simulateErrors: schema.maybe(schema.boolean()), /** Maximum timeout between streaming messages. */ timeout: schema.maybe(schema.number()), /** Setting to override headers derived compression */ compressResponse: schema.maybe(schema.boolean()), + /** Boolean flag to enable/disable 4KB payload flush fix. */ + flushFix: schema.maybe(schema.boolean()), }); export type ReducerStreamRequestBodySchema = TypeOf; diff --git a/examples/response_stream/public/containers/app/pages/page_reducer_stream/index.tsx b/examples/response_stream/public/containers/app/pages/page_reducer_stream/index.tsx index 466b6ddec75a0f..a55f25292cf5de 100644 --- a/examples/response_stream/public/containers/app/pages/page_reducer_stream/index.tsx +++ b/examples/response_stream/public/containers/app/pages/page_reducer_stream/index.tsx @@ -43,12 +43,13 @@ export const PageReducerStream: FC = () => { const [simulateErrors, setSimulateErrors] = useState(false); const [compressResponse, setCompressResponse] = useState(true); + const [flushFix, setFlushFix] = useState(false); const { dispatch, start, cancel, data, errors, isCancelled, isRunning } = useFetchStream( http, RESPONSE_STREAM_API_ENDPOINT.REDUCER_STREAM, '1', - { compressResponse, simulateErrors }, + { compressResponse, flushFix, simulateErrors }, { reducer: reducerStreamReducer, initialState } ); @@ -149,6 +150,13 @@ export const PageReducerStream: FC = () => { onChange={(e) => setCompressResponse(!compressResponse)} compressed /> + setFlushFix(!flushFix)} + compressed + /> ); diff --git a/examples/response_stream/server/routes/reducer_stream.ts b/examples/response_stream/server/routes/reducer_stream.ts index 81ba44205d31b4..5e03cd0732e745 100644 --- a/examples/response_stream/server/routes/reducer_stream.ts +++ b/examples/response_stream/server/routes/reducer_stream.ts @@ -60,7 +60,8 @@ export const defineReducerStreamRoute = (router: IRouter, logger: Logger) => { const { end, push, responseWithHeaders } = streamFactory( request.headers, logger, - request.body.compressResponse + request.body.compressResponse, + request.body.flushFix ); const entities = [ diff --git a/x-pack/packages/ml/response_stream/server/stream_factory.test.ts b/x-pack/packages/ml/response_stream/server/stream_factory.test.ts index 27751b7dc3fd13..4b75cf4e0826a0 100644 --- a/x-pack/packages/ml/response_stream/server/stream_factory.test.ts +++ b/x-pack/packages/ml/response_stream/server/stream_factory.test.ts @@ -49,6 +49,7 @@ describe('streamFactory', () => { Connection: 'keep-alive', 'Transfer-Encoding': 'chunked', 'X-Accel-Buffering': 'no', + 'X-Content-Type-Options': 'nosniff', }); expect(streamResult).toBe('push1push2'); }); @@ -75,6 +76,7 @@ describe('streamFactory', () => { Connection: 'keep-alive', 'Transfer-Encoding': 'chunked', 'X-Accel-Buffering': 'no', + 'X-Content-Type-Options': 'nosniff', }); expect(parsedItems).toHaveLength(2); expect(parsedItems[0]).toStrictEqual(mockItem1); @@ -121,6 +123,7 @@ describe('streamFactory', () => { 'content-encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Accel-Buffering': 'no', + 'X-Content-Type-Options': 'nosniff', }); expect(streamResult).toBe('push1push2'); @@ -165,6 +168,7 @@ describe('streamFactory', () => { 'content-encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Accel-Buffering': 'no', + 'X-Content-Type-Options': 'nosniff', }); expect(parsedItems).toHaveLength(2); expect(parsedItems[0]).toStrictEqual(mockItem1); diff --git a/x-pack/packages/ml/response_stream/server/stream_factory.ts b/x-pack/packages/ml/response_stream/server/stream_factory.ts index ab676e0104b78d..8836c241e55d8b 100644 --- a/x-pack/packages/ml/response_stream/server/stream_factory.ts +++ b/x-pack/packages/ml/response_stream/server/stream_factory.ts @@ -19,6 +19,7 @@ function isCompressedSream(arg: unknown): arg is zlib.Gzip { return typeof arg === 'object' && arg !== null && typeof (arg as zlib.Gzip).flush === 'function'; } +const FLUSH_KEEP_ALIVE_INTERVAL_MS = 500; const FLUSH_PAYLOAD_SIZE = 4 * 1024; class UncompressedResponseStream extends Stream.PassThrough {} @@ -76,6 +77,7 @@ export function streamFactory( const flushPayload = flushFix ? crypto.randomBytes(FLUSH_PAYLOAD_SIZE).toString('hex') : undefined; + let responseSizeSinceLastKeepAlive = 0; const stream = isCompressed ? zlib.createGzip() : new UncompressedResponseStream(); @@ -132,6 +134,25 @@ export function streamFactory( // otherwise check the integrity of the data to be pushed. if (streamType === undefined) { streamType = typeof d === 'string' ? 'string' : 'ndjson'; + + // This is a fix for ndjson streaming with proxy configurations + // that buffer responses up to 4KB in size. We keep track of the + // size of the response sent so far and if it's still smaller than + // FLUSH_PAYLOAD_SIZE then we'll push an additional keep-alive object + // that contains the flush fix payload. + if (flushFix && streamType === 'ndjson') { + function repeat() { + if (!tryToEnd) { + if (responseSizeSinceLastKeepAlive < FLUSH_PAYLOAD_SIZE) { + push({ flushPayload } as unknown as T); + } + responseSizeSinceLastKeepAlive = 0; + setTimeout(repeat, FLUSH_KEEP_ALIVE_INTERVAL_MS); + } + } + + repeat(); + } } else if (streamType === 'string' && typeof d !== 'string') { logger.error('Must not push non-string chunks to a string based stream.'); return; @@ -148,13 +169,11 @@ export function streamFactory( try { const line = - streamType === 'ndjson' - ? `${JSON.stringify({ - ...d, - // This is a temporary fix for response streaming with proxy configurations that buffer responses up to 4KB in size. - ...(flushFix ? { flushPayload } : {}), - })}${DELIMITER}` - : d; + streamType === 'ndjson' ? `${JSON.stringify(d)}${DELIMITER}` : (d as unknown as string); + + if (streamType === 'ndjson') { + responseSizeSinceLastKeepAlive += new Blob([line]).size; + } waitForCallbacks.push(1); const writeOk = stream.write(line, () => { @@ -211,6 +230,7 @@ export function streamFactory( // This disables response buffering on proxy servers (Nginx, uwsgi, fastcgi, etc.) // Otherwise, those proxies buffer responses up to 4/8 KiB. 'X-Accel-Buffering': 'no', + 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'Transfer-Encoding': 'chunked', diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx index 40ee98f3234dcc..97d7201f0140da 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx @@ -176,7 +176,7 @@ export const LogRateAnalysisResults: FC = ({ data, isRunning, errors: streamErrors, - } = useFetchStream( + } = useFetchStream( http, '/internal/aiops/log_rate_analysis', '1', diff --git a/x-pack/test/api_integration/apis/aiops/log_rate_analysis_full_analysis.ts b/x-pack/test/api_integration/apis/aiops/log_rate_analysis_full_analysis.ts index 5ac7474324c502..c9fe22a472f4fc 100644 --- a/x-pack/test/api_integration/apis/aiops/log_rate_analysis_full_analysis.ts +++ b/x-pack/test/api_integration/apis/aiops/log_rate_analysis_full_analysis.ts @@ -191,8 +191,15 @@ export default ({ getService }: FtrProviderContext) => { data.push(action); } - // If streaming works correctly we should receive more than one chunk. - expect(chunkCounter).to.be.greaterThan(1); + // Originally we assumed that we can assert streaming in contrast + // to non-streaming if there is more than one chunk. However, + // this turned out to be flaky since a stream could finish fast + // enough to contain only one chunk. So now we are checking if + // there's just one chunk or more. + expect(chunkCounter).to.be.greaterThan( + 0, + `Expected 'chunkCounter' to be greater than 0, got ${chunkCounter}.` + ); await assertAnalysisResult(data); } diff --git a/x-pack/test/api_integration/apis/aiops/log_rate_analysis_groups_only.ts b/x-pack/test/api_integration/apis/aiops/log_rate_analysis_groups_only.ts index cfd812e4f435ce..8aeccc6af9a97f 100644 --- a/x-pack/test/api_integration/apis/aiops/log_rate_analysis_groups_only.ts +++ b/x-pack/test/api_integration/apis/aiops/log_rate_analysis_groups_only.ts @@ -194,12 +194,14 @@ export default ({ getService }: FtrProviderContext) => { data.push(action); } - // If streaming works correctly we should receive more than one chunk. + // Originally we assumed that we can assert streaming in contrast + // to non-streaming if there is more than one chunk. However, + // this turned out to be flaky since a stream could finish fast + // enough to contain only one chunk. So now we are checking if + // there's just one chunk or more. expect(chunkCounter).to.be.greaterThan( - 1, - `Expected 'chunkCounter' to be greater than 1, got ${chunkCounter} with the following data: ${JSON.stringify( - data - )}.` + 0, + `Expected 'chunkCounter' to be greater than 0, got ${chunkCounter}.` ); await assertAnalysisResult(data); From c40c2e6f59dfd9aeafb7de6182121b4e5cfdbeea Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 25 Oct 2023 18:20:04 +0200 Subject: [PATCH 12/24] [Security Solution] Enable shared-ux nav (#169499) ## Summary Makes the shared-ux navigation the default side navigation component for Security projects. The old Security navigation component and the experimental flag will be removed altogether in a separate PR. There is no visual difference from the previous navigation: ![snapshot](https://github.com/elastic/kibana/assets/17747913/2896e8de-45eb-412f-b319-e919e65a0ae7) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/navigation_item_open_panel.tsx | 13 +- .../ui/components/panel/navigation_panel.tsx | 4 +- .../common/experimental_features.ts | 2 +- .../navigation_tree/navigation_tree.ts | 3 + .../alerts/building_block_alerts.cy.ts | 6 +- .../screens/serverless_security_header.ts | 122 ++++++++++++++++++ .../cypress/tasks/serverless/navigation.ts | 6 +- .../page_objects/svl_sec_landing_page.ts | 2 +- .../services/ml/security_navigation.ts | 2 +- .../ftr/cases/attachment_framework.ts | 9 +- .../security/ftr/cases/configure.ts | 9 +- .../security/ftr/cases/list_view.ts | 10 +- .../security/ftr/cases/view_case.ts | 3 +- .../test_suites/security/ftr/navigation.ts | 26 +++- .../shared/lib/cases/helpers.ts | 7 +- 15 files changed, 188 insertions(+), 36 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item_open_panel.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item_open_panel.tsx index 99ed44565dec46..5c116e26a2ff76 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item_open_panel.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item_open_panel.tsx @@ -63,6 +63,15 @@ export const NavigationItemOpenPanel: FC = ({ item, navigateToUrl }: Prop getStyles(euiTheme) ); + const dataTestSubj = classNames(`nav-item`, `nav-item-${id}`, { + [`nav-item-deepLinkId-${deepLink?.id}`]: !!deepLink, + [`nav-item-id-${id}`]: id, + [`nav-item-isActive`]: isActive, + }); + const buttonDataTestSubj = classNames(`panelOpener`, `panelOpener-${id}`, { + [`panelOpener-deepLinkId-${deepLink?.id}`]: !!deepLink, + }); + const onLinkClick = useCallback( (e: React.MouseEvent) => { if (!href) { @@ -95,7 +104,7 @@ export const NavigationItemOpenPanel: FC = ({ item, navigateToUrl }: Prop className={itemClassNames} color="text" size="s" - data-test-subj={`sideNavItemLink-${id}`} + data-test-subj={dataTestSubj} />
@@ -111,7 +120,7 @@ export const NavigationItemOpenPanel: FC = ({ item, navigateToUrl }: Prop aria-label={i18n.translate('sharedUXPackages.chrome.sideNavigation.togglePanel', { defaultMessage: 'Toggle panel navigation', })} - data-test-subj={`panelOpener-${id}`} + data-test-subj={buttonDataTestSubj} />
)} diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx index 616d1aca201cf7..ca118249d123b2 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx @@ -37,7 +37,9 @@ export const NavigationPanel: FC = () => { const onOutsideClick = useCallback( ({ target }: Event) => { // Only close if we are not clicking on the currently selected nav node - if ((target as HTMLButtonElement).dataset.testSubj !== `panelOpener-${selectedNode?.id}`) { + if ( + !(target as HTMLButtonElement).dataset.testSubj?.includes(`panelOpener-${selectedNode?.id}`) + ) { close(); } }, diff --git a/x-pack/plugins/security_solution_serverless/common/experimental_features.ts b/x-pack/plugins/security_solution_serverless/common/experimental_features.ts index 500bad2a0483dc..76b57064da160c 100644 --- a/x-pack/plugins/security_solution_serverless/common/experimental_features.ts +++ b/x-pack/plugins/security_solution_serverless/common/experimental_features.ts @@ -19,7 +19,7 @@ export const allowedExperimentalValues = Object.freeze({ /** * Enables the use of the of the product navigation from shared-ux package in the Security Solution app */ - platformNavEnabled: false, + platformNavEnabled: true, }); type ServerlessExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts index 2d9ad8c1bb2d51..bbab7fb56f0570 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree/navigation_tree.ts @@ -52,6 +52,9 @@ export const formatNavigationTree = ( breadcrumbStatus: 'hidden', defaultIsCollapsed: false, children: bodyChildren, + accordionProps: { + arrowProps: { css: { display: 'none' } }, + }, }, ], footer: formatFooterNodesFromLinks(footerNavItems, footerCategories), diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts index 5efdfb7d94c8f9..2b5a85c28f22c2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/building_block_alerts.cy.ts @@ -8,13 +8,13 @@ import { getBuildingBlockRule } from '../../../objects/rule'; import { OVERVIEW_ALERTS_HISTOGRAM_EMPTY } from '../../../screens/overview'; import { HIGHLIGHTED_ROWS_IN_TABLE } from '../../../screens/rule_details'; -import { OVERVIEW } from '../../../screens/security_header'; import { createRule } from '../../../tasks/api_calls/rules'; import { cleanKibana } from '../../../tasks/common'; import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; import { login } from '../../../tasks/login'; +import { visit } from '../../../tasks/navigation'; import { visitRuleDetailsPage, waitForTheRuleToBeExecuted } from '../../../tasks/rule_details'; -import { navigateFromHeaderTo } from '../../../tasks/security_header'; +import { OVERVIEW_URL } from '../../../urls/navigation'; const EXPECTED_NUMBER_OF_ALERTS = 5; @@ -42,7 +42,7 @@ describe('Alerts generated by building block rules', { tags: ['@ess', '@serverle // Make sure rows are highlighted cy.get(HIGHLIGHTED_ROWS_IN_TABLE).should('exist'); - navigateFromHeaderTo(OVERVIEW); + visit(OVERVIEW_URL); // Check that generated events are hidden on the Overview page cy.get(OVERVIEW_ALERTS_HISTOGRAM_EMPTY).should('contain.text', 'No results found'); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts new file mode 100644 index 00000000000000..11885714a0dda7 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts @@ -0,0 +1,122 @@ +/* + * 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. + */ + +// main panels links +export const DASHBOARDS = '[data-test-subj$="nav-item-deepLinkId-securitySolutionUI:dashboards"]'; +export const DASHBOARDS_PANEL_BTN = + '[data-test-subj*="panelOpener-deepLinkId-securitySolutionUI:dashboards"]'; + +export const INVESTIGATIONS = + '[data-test-subj$="nav-item-deepLinkId-securitySolutionUI:investigations"]'; +export const INVESTIGATIONS_PANEL_BTN = + '[data-test-subj*="panelOpener-deepLinkId-securitySolutionUI:investigations"]'; + +export const EXPLORE = '[data-test-subj$="nav-item-deepLinkId-securitySolutionUI:explore"]'; +export const EXPLORE_PANEL_BTN = + '[data-test-subj*="panelOpener-deepLinkId-securitySolutionUI:explore"]'; + +export const RULES_LANDING = + '[data-test-subj$="nav-item-deepLinkId-securitySolutionUI:rules-landing"]'; +export const RULES_PANEL_BTN = + '[data-test-subj*="panelOpener-deepLinkId-securitySolutionUI:rules-landing"]'; + +export const ASSETS = '[data-test-subj$="nav-item-deepLinkId-securitySolutionUI:assets"]'; +export const ASSETS_PANEL_BTN = + '[data-test-subj*="panelOpener-deepLinkId-securitySolutionUI:assets"]'; + +// main direct links +export const DISCOVER = '[data-test-subj*="nav-item-deepLinkId-discover"]'; + +export const ALERTS = '[data-test-subj*="nav-item-deepLinkId-securitySolutionUI:alerts"]'; + +export const CSP_FINDINGS = + '[data-test-subj*="nav-item-deepLinkId-securitySolutionUI:cloud_security_posture-findings"]'; + +export const CASES = '[data-test-subj*="nav-item-deepLinkId-securitySolutionUI:cases"]'; + +// nested links + +export const OVERVIEW = '[data-test-subj="solutionSideNavPanelLink-overview"]'; + +export const DETECTION_RESPONSE = '[data-test-subj="solutionSideNavPanelLink-detection_response"]'; + +export const ENTITY_ANALYTICS = '[data-test-subj="solutionSideNavPanelLink-entity_analytics"]'; + +export const TIMELINES = '[data-test-subj="solutionSideNavPanelLink-timelines"]'; + +export const KUBERNETES = '[data-test-subj="solutionSideNavPanelLink-kubernetes"]'; + +export const CSP_DASHBOARD = + '[data-test-subj="solutionSideNavPanelLink-cloud_security_posture-dashboard"]'; + +export const HOSTS = '[data-test-subj="solutionSideNavPanelLink-hosts"]'; + +export const ENDPOINTS = '[data-test-subj="solutionSideNavPanelLink-endpoints"]'; + +export const POLICIES = '[data-test-subj="solutionSideNavPanelLink-policy"]'; + +export const TRUSTED_APPS = '[data-test-subj="solutionSideNavPanelLink-trusted_apps"]'; + +export const EVENT_FILTERS = '[data-test-subj="solutionSideNavPanelLink-event_filters"]'; + +export const BLOCKLIST = '[data-test-subj="solutionSideNavPanelLink-blocklist"]'; + +export const CSP_BENCHMARKS = + '[data-test-subj="solutionSideNavPanelLink-cloud_security_posture-benchmarks"]'; + +export const NETWORK = '[data-test-subj="solutionSideNavPanelLink-network"]'; + +export const USERS = '[data-test-subj="solutionSideNavPanelLink-users"]'; + +export const INDICATORS = '[data-test-subj="solutionSideNavItemLink-threat_intelligence"]'; + +export const RULES = '[data-test-subj="solutionSideNavPanelLink-rules"]'; + +export const EXCEPTIONS = '[data-test-subj="solutionSideNavPanelLink-exceptions"]'; + +// opens the navigation panel for a given nested link +export const openNavigationPanelFor = (page: string) => { + let panel; + switch (page) { + case OVERVIEW: + case DETECTION_RESPONSE: + case KUBERNETES: + case ENTITY_ANALYTICS: + case CSP_DASHBOARD: { + panel = DASHBOARDS_PANEL_BTN; + break; + } + case HOSTS: + case NETWORK: + case USERS: { + panel = EXPLORE_PANEL_BTN; + break; + } + case RULES: + case EXCEPTIONS: + case CSP_BENCHMARKS: { + panel = RULES_PANEL_BTN; + break; + } + case ENDPOINTS: + case TRUSTED_APPS: + case EVENT_FILTERS: + case POLICIES: + case BLOCKLIST: { + panel = ASSETS_PANEL_BTN; + break; + } + } + if (panel) { + openNavigationPanel(panel); + } +}; + +// opens the navigation panel of a main link +export const openNavigationPanel = (page: string) => { + cy.get(page).click(); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/serverless/navigation.ts b/x-pack/test/security_solution_cypress/cypress/tasks/serverless/navigation.ts index 3dd31a0ec981c7..90c380e8f88b4b 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/serverless/navigation.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/serverless/navigation.ts @@ -5,14 +5,12 @@ * 2.0. */ -const serverlessLocator = { - alerts: '[data-test-subj="solutionSideNavItemLink-alerts"]', -}; +import { ALERTS } from '../../screens/serverless_security_header'; const navigateTo = (page: string) => { cy.get(page).click(); }; export const navigateToAlertsPageInServerless = () => { - navigateTo(serverlessLocator.alerts); + navigateTo(ALERTS); }; diff --git a/x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts b/x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts index dc87ebd5db9d30..7ff15c0d382156 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts @@ -12,7 +12,7 @@ export function SvlSecLandingPageProvider({ getService }: FtrProviderContext) { return { async assertSvlSecSideNavExists() { - await testSubjects.existOrFail('securitySolutionNavHeading'); + await testSubjects.existOrFail('securitySolutionSideNav'); }, }; } diff --git a/x-pack/test_serverless/functional/services/ml/security_navigation.ts b/x-pack/test_serverless/functional/services/ml/security_navigation.ts index 2d80ebd9d413e5..17ebe65b451943 100644 --- a/x-pack/test_serverless/functional/services/ml/security_navigation.ts +++ b/x-pack/test_serverless/functional/services/ml/security_navigation.ts @@ -11,7 +11,7 @@ export function MachineLearningNavigationProviderSecurity({ getService }: FtrPro const testSubjects = getService('testSubjects'); async function navigateToArea(id: string) { - await testSubjects.click('~solutionSideNavItemButton-machine_learning-landing'); + await testSubjects.click('~panelOpener-deepLinkId-securitySolutionUI:machine_learning-landing'); await testSubjects.existOrFail(`~solutionSideNavPanelLink-ml:${id}`, { timeout: 60 * 1000, }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts index 24da4464e9fb3d..c20c2da7235cb8 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts @@ -9,9 +9,9 @@ import { expect } from 'expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getPageObject, getService }: FtrProviderContext) => { + const common = getPageObject('common'); const dashboard = getPageObject('dashboard'); const lens = getPageObject('lens'); - const svlSecNavigation = getService('svlSecNavigation'); const svlCommonPage = getPageObject('svlCommonPage'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); @@ -25,9 +25,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('lens visualization', () => { before(async () => { await svlCommonPage.login(); - await svlSecNavigation.navigateToLandingPage(); - - await testSubjects.click('solutionSideNavItemLink-dashboards'); + await common.navigateToApp('security', { path: 'dashboards' }); await header.waitUntilLoadingHasFinished(); await retry.waitFor('createDashboardButton', async () => { @@ -88,7 +86,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { owner: 'securitySolution', }); - await testSubjects.click('solutionSideNavItemLink-dashboards'); + await common.navigateToApp('security', { path: 'dashboards' }); + await header.waitUntilLoadingHasFinished(); if (await testSubjects.exists('edit-unsaved-New-Dashboard')) { await testSubjects.click('edit-unsaved-New-Dashboard'); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts index b19e1746847ed2..373782e69bb679 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts @@ -6,13 +6,16 @@ */ import expect from '@kbn/expect'; +import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; +import { navigateToCasesApp } from '../../../../../shared/lib/cases/helpers'; import { FtrProviderContext } from '../../../../ftr_provider_context'; +const owner = SECURITY_SOLUTION_OWNER; + export default ({ getPageObject, getService }: FtrProviderContext) => { const common = getPageObject('common'); const header = getPageObject('header'); const svlCommonPage = getPageObject('svlCommonPage'); - const svlSecNavigation = getService('svlSecNavigation'); const testSubjects = getService('testSubjects'); const cases = getService('cases'); const svlCases = getService('svlCases'); @@ -23,9 +26,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('Configure Case', function () { before(async () => { await svlCommonPage.login(); - await svlSecNavigation.navigateToLandingPage(); - await testSubjects.click('solutionSideNavItemLink-cases'); - await header.waitUntilLoadingHasFinished(); + await navigateToCasesApp(getPageObject, getService, owner); await retry.waitFor('configure-case-button exist', async () => { return await testSubjects.exists('configure-case-button'); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts index 8a753e5d4829ab..e672f99780fa8d 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/list_view.ts @@ -6,10 +6,14 @@ */ import expect from '@kbn/expect'; +import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; import { CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; import { SeverityAll } from '@kbn/cases-plugin/common/ui'; +import { navigateToCasesApp } from '../../../../../shared/lib/cases/helpers'; import { FtrProviderContext } from '../../../../ftr_provider_context'; +const owner = SECURITY_SOLUTION_OWNER; + export default ({ getPageObject, getService }: FtrProviderContext) => { const header = getPageObject('header'); const testSubjects = getService('testSubjects'); @@ -24,7 +28,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await svlSecNavigation.navigateToLandingPage(); - await testSubjects.click('solutionSideNavItemLink-cases'); + await navigateToCasesApp(getPageObject, getService, owner); }); after(async () => { @@ -151,7 +155,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('severity filtering', () => { // Error: retry.tryForTime timeout: Error: expected 10 to equal 5 before(async () => { - await testSubjects.click('solutionSideNavItemLink-cases'); + await navigateToCasesApp(getPageObject, getService, owner); await cases.api.createCase({ severity: CaseSeverity.LOW, owner: 'securitySolution' }); await cases.api.createCase({ severity: CaseSeverity.LOW, owner: 'securitySolution' }); @@ -167,7 +171,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { * There is no easy way to clear the filtering. * Refreshing the page seems to be easier. */ - await testSubjects.click('solutionSideNavItemLink-cases'); + await navigateToCasesApp(getPageObject, getService, owner); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts index d9531a4529ee5c..d9429f93bc9af6 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts @@ -18,6 +18,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; import { createOneCaseBeforeDeleteAllAfter, createAndNavigateToCase, + navigateToCasesApp, } from '../../../../../shared/lib/cases/helpers'; const owner = SECURITY_SOLUTION_OWNER; @@ -473,7 +474,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { ]; before(async () => { - await testSubjects.click('solutionSideNavItemLink-cases'); + await navigateToCasesApp(getPageObject, getService, owner); await cases.api.createConfigWithCustomFields({ customFields, owner }); await cases.api.createCase({ customFields: [ diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts index 998ad9a2096c95..ef33a898de4fb0 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts @@ -6,21 +6,27 @@ */ import expect from '@kbn/expect'; +import { AppDeepLinkId } from '@kbn/core-chrome-browser'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObject, getService }: FtrProviderContext) { + const svlCommonPage = getPageObject('svlCommonPage'); const svlSecLandingPage = getPageObject('svlSecLandingPage'); const svlSecNavigation = getService('svlSecNavigation'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - // FLAKY: https://github.com/elastic/kibana/issues/165629 - describe.skip('navigation', function () { + describe('navigation', function () { before(async () => { + await svlCommonPage.login(); await svlSecNavigation.navigateToLandingPage(); }); + after(async () => { + await svlCommonPage.forceLogout(); + }); + it('has security serverless side nav', async () => { await svlSecLandingPage.assertSvlSecSideNavExists(); await svlCommonNavigation.expectExists(); @@ -30,7 +36,9 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await svlCommonNavigation.breadcrumbs.expectExists(); // TODO: use `deepLinkId` instead of `text`, once security deep links are available in @kbn/core-chrome-browser await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Get started' }); - await testSubjects.click('solutionSideNavItemLink-alerts'); + await svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'securitySolutionUI:alerts' as AppDeepLinkId, + }); await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Alerts' }); await svlCommonNavigation.breadcrumbs.clickHome(); await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Get started' }); @@ -38,8 +46,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { it('navigate using search', async () => { await svlCommonNavigation.search.showSearch(); - await svlCommonNavigation.search.searchFor('dashboards'); - await svlCommonNavigation.search.clickOnOption(1); + await svlCommonNavigation.search.searchFor('security dashboards'); + await svlCommonNavigation.search.clickOnOption(0); await svlCommonNavigation.search.hideSearch(); await expect(await browser.getCurrentUrl()).contain('app/security/dashboards'); @@ -49,11 +57,15 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await svlSecLandingPage.assertSvlSecSideNavExists(); await svlCommonNavigation.expectExists(); - expect(await testSubjects.existOrFail('solutionSideNavItemLink-cases')); + await svlCommonNavigation.sidenav.expectLinkExists({ + deepLinkId: 'securitySolutionUI:cases' as AppDeepLinkId, + }); }); it('navigates to cases app', async () => { - await testSubjects.click('solutionSideNavItemLink-cases'); + await svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'securitySolutionUI:cases' as AppDeepLinkId, + }); expect(await browser.getCurrentUrl()).contain('/app/security/cases'); await testSubjects.existOrFail('cases-all-title'); diff --git a/x-pack/test_serverless/shared/lib/cases/helpers.ts b/x-pack/test_serverless/shared/lib/cases/helpers.ts index 5cc4aa637ec43e..71df12e44d78ab 100644 --- a/x-pack/test_serverless/shared/lib/cases/helpers.ts +++ b/x-pack/test_serverless/shared/lib/cases/helpers.ts @@ -6,6 +6,7 @@ */ import { SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common'; +import { AppDeepLinkId } from '@kbn/core-chrome-browser'; import { FtrProviderContext } from '../../../functional/ftr_provider_context'; export const createOneCaseBeforeDeleteAllAfter = ( @@ -64,15 +65,15 @@ export const navigateToCasesApp = async ( getService: FtrProviderContext['getService'], owner: string ) => { - const testSubjects = getService('testSubjects'); - const common = getPageObject('common'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); await common.navigateToApp('landingPage'); if (owner === SECURITY_SOLUTION_OWNER) { - await testSubjects.click('solutionSideNavItemLink-cases'); + await svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'securitySolutionUI:cases' as AppDeepLinkId, + }); } else { await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-overview:cases' }); } From abcab9476202ba941c1dd8a00a7ee68397fde3fa Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 25 Oct 2023 18:28:17 +0200 Subject: [PATCH 13/24] [EDR Workflows] Unskip CY tests (#168457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unskipped tests: 1. `endpoint_alerts.cy.ts` 2. `response_console_mocked_data.cy.ts` - https://github.com/elastic/security-team/issues/7763 3. `no_license.cy.ts` - https://github.com/elastic/security-team/issues/7763 4. `endpoints.cy.ts` Changes: 1. Introduced interval for `cy.waitUntill` calls, I've noticed locally that running these request without throttling can cause API issues 2. Increased timeout for CI `burn` jobs - with this PR as an example, when burning 3 test suites one hour might not be enough at this point. We should think about splitting these. --------- Co-authored-by: Patryk Kopyciński --- .../no_license.cy.ts | 10 +++--- .../cypress/e2e/endpoint_alerts.cy.ts | 15 +++------ .../response_console_mocked_data.cy.ts | 31 +++++++++---------- .../management/cypress/screens/alerts.ts | 2 +- .../public/management/cypress/tasks/alerts.ts | 4 +-- .../cypress/tasks/response_actions.ts | 2 +- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts index 0869f10c73ef0b..d1449672bed26e 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { navigateToAlertsList } from '../../screens/alerts'; import { disableExpandableFlyoutAdvancedSettings } from '../../tasks/common'; -import { APP_ALERTS_PATH } from '../../../../../common/constants'; import { closeAllToasts } from '../../tasks/toasts'; import { fillUpNewRule } from '../../tasks/response_actions'; import { login, ROLE } from '../../tasks/login'; @@ -25,7 +25,6 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } } it('response actions are disabled', () => { fillUpNewRule(ruleName, ruleDescription); - // addEndpointResponseAction(); cy.getByTestSubj('response-actions-wrapper').within(() => { cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( 'be.disabled' @@ -38,7 +37,7 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } } let endpointData: ReturnTypeFromChainable | undefined; let alertData: ReturnTypeFromChainable | undefined; const [endpointAgentId, endpointHostname] = generateRandomStringName(2); - before(() => { + beforeEach(() => { login(); disableExpandableFlyoutAdvancedSettings(); indexEndpointRuleAlerts({ @@ -58,7 +57,7 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } } }); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); endpointData = undefined; @@ -69,8 +68,9 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } } alertData = undefined; } }); + it('show the permission denied callout', () => { - cy.visit(APP_ALERTS_PATH); + navigateToAlertsList(`query=(language:kuery,query:'agent.id: "${endpointAgentId}" ')`); closeAllToasts(); cy.getByTestSubj('expand-event').first().click(); cy.getByTestSubj('response-actions-notification').should('not.have.text', '0'); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts index bf6dc8c57a478b..06b33141bad1b8 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts @@ -15,17 +15,17 @@ import type { IndexedFleetEndpointPolicyResponse } from '../../../../common/endp import { enableAllPolicyProtections } from '../tasks/endpoint_policy'; import type { PolicyData, ResponseActionApiResponse } from '../../../../common/endpoint/types'; import type { CreateAndEnrollEndpointHostResponse } from '../../../../scripts/endpoint/common/endpoint_host_services'; -import { login } from '../tasks/login'; +import { login, ROLE } from '../tasks/login'; import { EXECUTE_ROUTE } from '../../../../common/endpoint/constants'; import { waitForActionToComplete } from '../tasks/response_actions'; -// FIXME: Flaky. Needs fixing (security team issue #7763) -describe.skip('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () => { +describe('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; - before(() => { + beforeEach(() => { + login(ROLE.soc_manager); getEndpointIntegrationVersion().then((version) => { createAgentPolicyTask(version, 'alerts test').then((data) => { indexedPolicy = data; @@ -41,7 +41,7 @@ describe.skip('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () }); }); - after(() => { + afterEach(() => { if (createdHost) { cy.task('destroyEndpointHost', createdHost); } @@ -55,10 +55,6 @@ describe.skip('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () } }); - beforeEach(() => { - login(); - }); - it('should create a Detection Engine alert from an endpoint alert', () => { // Triggers a Malicious Behaviour alert on Linux system (`grep *` was added only to identify this specific alert) const executeMaliciousCommand = `bash -c cat /dev/tcp/foo | grep ${Math.random() @@ -89,7 +85,6 @@ describe.skip('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () `query=(language:kuery,query:'agent.id: "${createdHost.agentId}" ')` ); }); - getAlertsTableRows().should('have.length.greaterThan', 0); }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_mocked_data.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_mocked_data.cy.ts index f80e3b17e6017e..7ae7aa49f2e947 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_mocked_data.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console_mocked_data.cy.ts @@ -34,7 +34,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles let endpointHostname: string; let isolateRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; @@ -43,7 +43,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles ); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined @@ -76,14 +76,14 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles let endpointHostname: string; let releaseRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: true }).then((indexEndpoints) => { endpointData = indexEndpoints; endpointHostname = endpointData.data.hosts[0].host.name; }); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined @@ -115,7 +115,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles let endpointHostname: string; let processesRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; @@ -124,7 +124,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles ); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined @@ -155,7 +155,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles let endpointHostname: string; let killProcessRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; @@ -164,7 +164,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles ); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined @@ -194,7 +194,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles let endpointHostname: string; let suspendProcessRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; @@ -203,7 +203,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles ); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined @@ -228,13 +228,12 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles }); }); - // Broken until this is fixed: https://github.com/elastic/kibana/issues/162760 - describe.skip('`get-file` command', () => { + describe('`get-file` command', () => { let endpointData: ReturnTypeFromChainable; let endpointHostname: string; let getFileRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; @@ -243,7 +242,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles ); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined @@ -283,7 +282,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles let endpointHostname: string; let executeRequestResponse: ActionDetails; - before(() => { + beforeEach(() => { indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; @@ -292,7 +291,7 @@ describe('Response console', { tags: ['@ess', '@serverless', '@brokenInServerles ); }); - after(() => { + afterEach(() => { if (endpointData) { endpointData.cleanup(); // @ts-expect-error ignore setting to undefined diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts index b434458c4dc1d2..7ad73ad142eeb1 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts @@ -37,7 +37,7 @@ export const getAlertsTableRows = (timeout?: number): Cypress.Chainable $rows); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts index 30679364bd2f87..ac4d2e80ebe442 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts @@ -48,7 +48,7 @@ export const waitForEndpointAlerts = ( return (streamedAlerts.hits.total as estypes.SearchTotalHits).value > 0; }); }, - { timeout } + { timeout, interval: 2000 } ) .then(() => { // Stop/start Endpoint rule so that it can pickup and create Detection alerts @@ -143,7 +143,7 @@ export const waitForDetectionAlerts = ( return Boolean((alertsResponse.hits.total as estypes.SearchTotalHits)?.value ?? 0); }); }, - { timeout } + { timeout, interval: 2000 } ); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 8f4f1e797910b3..80f1aaba567a09 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -105,7 +105,7 @@ export const waitForActionToComplete = ( return false; }); }, - { timeout } + { timeout, interval: 2000 } ) .then(() => { if (!action) { From 850060039330f4d8dd0c26e02ecee522d575e919 Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:48:36 -0500 Subject: [PATCH 14/24] [Security Solution] Add version header to alert table actions (#169731) ## Summary Some add to timeline actions are missing version header when sending requests. This is preventing user from adding alerts (of the rule types below) to timeline - clicking `investigate in timeline` will throw a `failed to create ... timeline` error. This PR adds version header to api calls related to: 1. alert suppression 2. threshold rule 3. new term rule ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../public/detections/components/alerts_table/actions.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index a52446d389d7a6..364b521c1ec303 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -451,6 +451,7 @@ const createThresholdTimeline = async ( const alertResponse = await KibanaServices.get().http.fetch< estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + version: '2023-10-31', method: 'POST', body: JSON.stringify(buildAlertsQuery([ecsData._id])), }); @@ -608,6 +609,7 @@ const createNewTermsTimeline = async ( const alertResponse = await KibanaServices.get().http.fetch< estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + version: '2023-10-31', method: 'POST', body: JSON.stringify(buildAlertsQuery([ecsData._id])), }); @@ -773,6 +775,7 @@ const createSuppressedTimeline = async ( const alertResponse = await KibanaServices.get().http.fetch< estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + version: '2023-10-31', method: 'POST', body: JSON.stringify(buildAlertsQuery([ecsData._id])), }); From 3a5d6cc92b157cf61a49c97b0c9f537285cc2299 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 25 Oct 2023 12:12:06 -0500 Subject: [PATCH 15/24] [RAM] Reset rule settings modal on cancel (#169720) ## Summary Fixes #169296 - Resets the rule settings modal when the user clicks Cancel, but caches the initial pull from the server so that a second request isn't necessary on reopen - Updates this cache on save so that the reset on modal close remains accurate ### 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 --- .../rules_settings_modal.test.tsx | 30 +++++++++ .../rules_setting/rules_settings_modal.tsx | 66 ++++++++++++++----- 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx index 7552b3fac7e26c..f33178ad4f6b06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx @@ -200,6 +200,36 @@ describe('rules_settings_modal', () => { expect(modalProps.onSave).toHaveBeenCalledTimes(1); }); + test('reset flapping settings to initial state on cancel without triggering another server reload', async () => { + const result = render(); + expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1); + await waitForModalLoad(); + + const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput'); + const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput'); + + fireEvent.change(lookBackWindowInput, { target: { value: 15 } }); + fireEvent.change(statusChangeThresholdInput, { target: { value: 3 } }); + + expect(lookBackWindowInput.getAttribute('value')).toBe('15'); + expect(statusChangeThresholdInput.getAttribute('value')).toBe('3'); + + // Try cancelling + userEvent.click(result.getByTestId('rulesSettingsModalCancelButton')); + + expect(modalProps.onClose).toHaveBeenCalledTimes(1); + expect(updateFlappingSettingsMock).not.toHaveBeenCalled(); + expect(modalProps.onSave).not.toHaveBeenCalled(); + + expect(screen.queryByTestId('centerJustifiedSpinner')).toBe(null); + expect(lookBackWindowInput.getAttribute('value')).toBe('10'); + expect(statusChangeThresholdInput.getAttribute('value')).toBe('10'); + + expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1); + }); + test('should prevent statusChangeThreshold from being greater than lookBackWindow', async () => { const result = render(); await waitForModalLoad(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx index c15286325495ad..a75a7139c11895 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useState } from 'react'; +import React, { memo, useCallback, useState, useRef } from 'react'; import { RulesSettingsFlappingProperties, RulesSettingsProperties, @@ -60,6 +60,26 @@ export const RulesSettingsErrorPrompt = memo(() => { ); }); +const useResettableState: ( + initialValue?: T +) => [T | undefined, boolean, (next: T, shouldUpdateInitialValue?: boolean) => void, () => void] = ( + initalValue +) => { + const initialValueRef = useRef(initalValue); + const [value, setValue] = useState(initalValue); + const [hasChanged, setHasChanged] = useState(false); + const reset = () => { + setValue(initialValueRef.current); + setHasChanged(false); + }; + const updateValue = (next: typeof value, shouldUpdateInitialValue = false) => { + setValue(next); + setHasChanged(true); + if (shouldUpdateInitialValue) initialValueRef.current = next; + }; + return [value, hasChanged, updateValue, reset]; +}; + export interface RulesSettingsModalProps { isVisible: boolean; setUpdatingRulesSettings?: (isUpdating: boolean) => void; @@ -84,21 +104,24 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { }, } = capabilities; - const [flappingSettings, setFlappingSettings] = useState(); - const [hasFlappingChanged, setHasFlappingChanged] = useState(false); + const [flappingSettings, hasFlappingChanged, setFlappingSettings, resetFlappingSettings] = + useResettableState(); - const [queryDelaySettings, setQueryDelaySettings] = useState(); - const [hasQueryDelayChanged, setHasQueryDelayChanged] = useState(false); + const [queryDelaySettings, hasQueryDelayChanged, setQueryDelaySettings, resetQueryDelaySettings] = + useResettableState(); const { isLoading: isFlappingLoading, isError: hasFlappingError } = useGetFlappingSettings({ enabled: isVisible, onSuccess: (fetchedSettings) => { if (!flappingSettings) { - setFlappingSettings({ - enabled: fetchedSettings.enabled, - lookBackWindow: fetchedSettings.lookBackWindow, - statusChangeThreshold: fetchedSettings.statusChangeThreshold, - }); + setFlappingSettings( + { + enabled: fetchedSettings.enabled, + lookBackWindow: fetchedSettings.lookBackWindow, + statusChangeThreshold: fetchedSettings.statusChangeThreshold, + }, + true // Update the initial value so we don't need to fetch it from the server again + ); } }, }); @@ -107,13 +130,22 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { enabled: isVisible, onSuccess: (fetchedSettings) => { if (!queryDelaySettings) { - setQueryDelaySettings({ - delay: fetchedSettings.delay, - }); + setQueryDelaySettings( + { + delay: fetchedSettings.delay, + }, + true + ); } }, }); + const onCloseModal = useCallback(() => { + resetFlappingSettings(); + resetQueryDelaySettings(); + onClose(); + }, [onClose, resetFlappingSettings, resetQueryDelaySettings]); + const { mutate } = useUpdateRuleSettings({ onSave, onClose, @@ -148,7 +180,6 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { newSettings.statusChangeThreshold ), }); - setHasFlappingChanged(true); } if (setting === 'queryDelay') { @@ -160,7 +191,6 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { [key]: value, }; setQueryDelaySettings(newSettings); - setHasQueryDelayChanged(true); } }; @@ -168,9 +198,11 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { const updatedSettings: RulesSettingsProperties = {}; if (canWriteFlappingSettings && hasFlappingChanged) { updatedSettings.flapping = flappingSettings; + setFlappingSettings(flappingSettings!, true); } if (canWriteQueryDelaySettings && hasQueryDelayChanged) { updatedSettings.queryDelay = queryDelaySettings; + setQueryDelaySettings(queryDelaySettings!, true); } mutate(updatedSettings); }; @@ -214,7 +246,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { }; return ( - + { - + Date: Wed, 25 Oct 2023 19:28:11 +0200 Subject: [PATCH 16/24] [infra UI] Fix: Processes tab is showing error when cpu is null (#169272) Closes #168196 ## Summary This PR handles the case when `process_list` API returns `null` value for `cpu`. ## Investigation I checked when that happened and my first assumption was that the API would return null in case the process data is not fully available for all processes (in case of restarting metricbeat for example) but I saw some hosts also on `edge lite` that have null values (only for some processes - not all of them). I tried the same query on the edge lite and then I saw documents with the cpu value `null` and some processes that have the cpu value available for the same timeframe: ![image](https://github.com/elastic/kibana/assets/14139027/175e2b98-c95c-46f4-a619-a4ef680de58a) I don't think it's related to certain processes or indices as it is reproducible using different ones. This could be considered an edge case but as mentioned in [the comment](https://github.com/elastic/kibana/issues/168196#issuecomment-1752967009) having the null values is likely to happen when using the auto-refresh option. ## The fix To fix the issue we agreed on handling the case when `null` cpu value is present by showing a `N/A` with an explanation and a CPU chart placeholder with the same explanation in the process list table instead of throwing an error | Before | After | | ------ | ------ | | ![image](https://github.com/elastic/kibana/assets/14139027/fb70a69b-62fe-466a-93ae-3313c3b7ba5b) | ![image](https://github.com/elastic/kibana/assets/14139027/4fb6cf39-8840-4a78-886b-9293aba7b521) ![image](https://github.com/elastic/kibana/assets/14139027/6a804fe8-a564-48e8-a00b-60d83457600d) | ## Testing - Go to infra -> Hosts -> open the hosts flyout and select the processes tab: - It can be tricky to reproduce the issue locally some options are: - Remote cluster: In case egde **lite** cluster is used the host who I find to have this issue is [gke-edge-lite-oblt-edge-lite-oblt-poo-c1d12345-sbnt](https://edge-lite-oblt.kb.us-west2.gcp.elastic-cloud.com/app/metrics/hosts?waffleTime=(currentTime:1697469375748,isAutoReloading:!f)&_a=(dateRange:(from:now-2m,to:now-1m),filters:!(),limit:50,panelFilters:!(),query:(language:kuery,query:%27%27))&controlPanels=(cloud.provider:(explicitInput:(fieldName:cloud.provider,id:cloud.provider,title:%27Cloud%20Provider%27),grow:!f,order:1,type:optionsListControl,width:medium),host.os.name:(explicitInput:(fieldName:host.os.name,id:host.os.name,title:%27Operating%20System%27),grow:!f,order:0,type:optionsListControl,width:medium))&tableProperties=(detailsItemId:gke-edge-lite-oblt-edge-lite-oblt-poo-c1d12345-sbnt-Ubuntu,pagination:(pageIndex:0,pageSize:20),sorting:(direction:asc,field:name))&assetDetails=(dateRange:(from:%272023-10-17T09:18:11.097Z%27,to:%272023-10-17T09:19:11.097Z%27),name:gke-edge-lite-oblt-edge-lite-oblt-poo-c1d12345-sbnt,tabId:processes)&waffleFilter=(expression:%27%27,kind:kuery)&waffleOptions=(accountId:%27%27,autoBounds:!t,boundsOverride:(max:1,min:0),customMetrics:!(),customOptions:!(),groupBy:!(),legend:(palette:cool,reverseColors:!f,steps:10),metric:(type:cpu),nodeType:host,region:%27%27,sort:(by:name,direction:desc),source:url,timelineOpen:!f,view:map)) - Metricbeat: It can be reproduced by stoping metricbeat for around a minute then stating it again and refreshing the interval to `now` until data with `null` CPU values are displayed. - Look at the processes and find a process with `N/A` value in the CPU column (A tooltip should appear after clicking on the question mark icon next to `N/A`) and extend the process: ![image](https://github.com/elastic/kibana/assets/14139027/4fb6cf39-8840-4a78-886b-9293aba7b521) - The same tooltip should appear after clicking on the question mark icon next to the "No results found" placeholder: ![image](https://github.com/elastic/kibana/assets/14139027/6a804fe8-a564-48e8-a00b-60d83457600d) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../http_api/host_details/process_list.ts | 4 +- .../metric_not_available_explanation.tsx | 56 +++++++++++++++++ .../tabs/processes/process_row.tsx | 6 +- .../tabs/processes/process_row_charts.tsx | 63 +++++++++++++++---- .../tabs/processes/processes_table.tsx | 40 ++++++++++-- .../asset_details/tabs/processes/types.ts | 4 +- x-pack/plugins/infra/tsconfig.json | 1 + 7 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/asset_details/components/metric_not_available_explanation.tsx diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 34b7defc289fc6..4203742cc2fbc0 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -84,8 +84,8 @@ const summaryPropertyRT = rt.union([rt.number, rt.string]); export const ProcessListAPIResponseRT = rt.type({ processList: rt.array( rt.type({ - cpu: rt.number, - memory: rt.number, + cpu: rt.union([rt.null, rt.number]), + memory: rt.union([rt.null, rt.number]), startTime: rt.number, pid: rt.number, state: rt.string, diff --git a/x-pack/plugins/infra/public/components/asset_details/components/metric_not_available_explanation.tsx b/x-pack/plugins/infra/public/components/asset_details/components/metric_not_available_explanation.tsx new file mode 100644 index 00000000000000..a5a6996e4debb1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/components/metric_not_available_explanation.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; +import { Popover } from '../tabs/common/popover'; +import { useDateRangeProviderContext } from '../hooks/use_date_range'; + +export const MetricNotAvailableExplanationTooltip = ({ metricName }: { metricName: string }) => { + const { getDateRangeInTimestamp } = useDateRangeProviderContext(); + const dateFromRange = new Date(getDateRangeInTimestamp().to); + + return ( + + +

+ + ), + time: ( + + ), + metric: metricName, + }} + /> +

+

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row.tsx index 1ec925d9ce148d..a874e071e6c236 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row.tsx @@ -188,7 +188,11 @@ export const ProcessRow = ({ cells, item, supportAIAssistant = false }: Props) = {item.user}
- + {supportAIAssistant && } diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row_charts.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row_charts.tsx index 8cd6d923690766..47c4a6102798ed 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row_charts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/process_row_charts.tsx @@ -13,12 +13,16 @@ import { EuiFlexItem, EuiLoadingChart, EuiText, + EuiFlexGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { first, last } from 'lodash'; import moment from 'moment'; import React, { useMemo } from 'react'; +import { IconChartLine } from '@kbn/chart-icons'; +import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; +import { css } from '@emotion/react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { calculateDomain } from '../../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; import { useProcessListRowChart } from '../../hooks/use_process_list_row_chart'; import { useTimelineChartTheme } from '../../../../utils/use_timeline_chart_theme'; @@ -28,12 +32,39 @@ import { createFormatter } from '../../../../../common/formatters'; import { MetricsExplorerAggregation } from '../../../../../common/http_api'; import { Process } from './types'; import { MetricsExplorerChartType } from '../../../../../common/metrics_explorer_views/types'; +import { MetricNotAvailableExplanationTooltip } from '../../components/metric_not_available_explanation'; interface Props { command: string; + hasCpuData: boolean; + hasMemoryData: boolean; } -export const ProcessRowCharts = ({ command }: Props) => { +const EmptyChartPlaceholder = ({ metricName }: { metricName: string }) => ( + + + + + + + +
+ } + /> +); + +export const ProcessRowCharts = ({ command, hasCpuData, hasMemoryData }: Props) => { const { loading, error, response } = useProcessListRowChart(command); const isLoading = loading || !response; @@ -42,15 +73,19 @@ export const ProcessRowCharts = ({ command }: Props) => { {failedToLoadChart}} /> ) : isLoading ? ( - ) : ( + ) : hasCpuData ? ( + ) : ( + ); const memoryChart = error ? ( {failedToLoadChart}} /> ) : isLoading ? ( - ) : ( + ) : hasMemoryData ? ( + ) : ( + ); return ( @@ -103,7 +138,14 @@ const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => { : { max: 0, min: 0 }; return ( - +
{ locale={i18n.getLocale()} /> - +
); }; -const ChartContainer = euiStyled.div` - width: 100%; - height: 140px; -`; - const cpuMetricLabel = i18n.translate( 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU', { @@ -156,6 +193,10 @@ const memoryMetricLabel = i18n.translate( } ); +const memory = i18n.translate('xpack.infra.metrics.nodeDetails.processes.expandedRowMemory', { + defaultMessage: 'memory', +}); + const failedToLoadChart = i18n.translate( 'xpack.infra.metrics.nodeDetails.processes.failedToLoadChart', { diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx index ed84b84288db0d..1205382832eb79 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes_table.tsx @@ -24,6 +24,8 @@ import { LEFT_ALIGNMENT, RIGHT_ALIGNMENT, EuiCode, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { css } from '@emotion/react'; import { EuiTableRow } from '@elastic/eui'; @@ -36,6 +38,8 @@ import { ProcessRow } from './process_row'; import { StateBadge } from './state_badge'; import { STATE_ORDER } from './states'; import type { ProcessListAPIResponse } from '../../../../../common/http_api'; +import { MetricNotAvailableExplanationTooltip } from '../../components/metric_not_available_explanation'; +import { NOT_AVAILABLE_LABEL } from '../../translations'; interface TableProps { processList: ProcessListAPIResponse['processList']; @@ -276,6 +280,10 @@ const RuntimeCell = ({ startTime, currentTime }: { startTime: number; currentTim return <>{`${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}`}; }; +const columnLabelCPU = i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { + defaultMessage: 'CPU', +}); + const columns: Array<{ field: keyof Process; name: string; @@ -317,11 +325,19 @@ const columns: Array<{ }, { field: 'cpu', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { - defaultMessage: 'CPU', - }), + name: columnLabelCPU, sortable: true, - render: (value: number) => FORMATTERS.percent(value), + render: (value: number | null) => + value === null ? ( + + {NOT_AVAILABLE_LABEL} + + + + + ) : ( + FORMATTERS.percent(value) + ), }, { field: 'memory', @@ -329,7 +345,21 @@ const columns: Array<{ defaultMessage: 'Mem.', }), sortable: true, - render: (value: number) => FORMATTERS.percent(value), + render: (value: number | null) => + value === null ? ( + + {NOT_AVAILABLE_LABEL} + + + + + ) : ( + FORMATTERS.percent(value) + ), }, ]; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/types.ts b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/types.ts index 024ffe9e5cdf5e..61366d2fcf7a53 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/types.ts +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/types.ts @@ -10,8 +10,8 @@ import { STATE_NAMES } from './states'; export interface Process { command: string; - cpu: number; - memory: number; + cpu: number | null; + memory: number | null; startTime: number; state: keyof typeof STATE_NAMES; pid: number; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 5ba552e1245e74..e3b71afa57001d 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -72,6 +72,7 @@ "@kbn/lens-embeddable-utils", "@kbn/metrics-data-access-plugin", "@kbn/expressions-plugin", + "@kbn/chart-icons", "@kbn/advanced-settings-plugin", "@kbn/cloud-plugin" ], From e0a120b80b763a3d25f9e2492e4b2070aac5f5bc Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 18:33:11 +0100 Subject: [PATCH 17/24] skip flaky suite (#169820) --- x-pack/test/profiling_api_integration/tests/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/profiling_api_integration/tests/index.ts b/x-pack/test/profiling_api_integration/tests/index.ts index 55edc2e8406b09..4ee62b760db04b 100644 --- a/x-pack/test/profiling_api_integration/tests/index.ts +++ b/x-pack/test/profiling_api_integration/tests/index.ts @@ -27,7 +27,8 @@ export default function profilingApiIntegrationTests({ }: FtrProviderContext) { const registry = getService('registry'); - describe('Profiling API tests', function () { + // FLAKY: https://github.com/elastic/kibana/issues/169820 + describe.skip('Profiling API tests', function () { const filePattern = getGlobPattern(); const tests = globby.sync(filePattern, { cwd }); From 820c7b9af6604ad40d23ec29068a4bf3c562d070 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 18:44:05 +0100 Subject: [PATCH 18/24] skip flaky suite (#162545) --- x-pack/test/fleet_api_integration/apis/agents/reassign.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 8838a5e0bbcdfe..141372b9aae933 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -88,7 +88,8 @@ export default function (providerContext: FtrProviderContext) { }); }); - describe('bulk reassign agents', () => { + // FLAKY: https://github.com/elastic/kibana/issues/162545 + describe.skip('bulk reassign agents', () => { it('should allow to reassign multiple agents by id', async () => { await supertest .post(`/api/fleet/agents/bulk_reassign`) From d610e88a99a137e5881a2617b057678e639472ea Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 18:45:17 +0100 Subject: [PATCH 19/24] skip flaky suite (#169458) --- test/functional/apps/discover/group4/_chart_hidden.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/group4/_chart_hidden.ts b/test/functional/apps/discover/group4/_chart_hidden.ts index 6bee290df896d5..05af7445d755e4 100644 --- a/test/functional/apps/discover/group4/_chart_hidden.ts +++ b/test/functional/apps/discover/group4/_chart_hidden.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover show/hide chart test', function () { + // FLAKY: https://github.com/elastic/kibana/issues/169458 + describe.skip('discover show/hide chart test', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); log.debug('load kibana index with default index pattern'); From a0cf2b6e78e4d2e1cea883565d7d2c16830d37a8 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 18:51:51 +0100 Subject: [PATCH 20/24] skip flaky suite (#169459) --- test/functional/apps/discover/group4/_chart_hidden.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/apps/discover/group4/_chart_hidden.ts b/test/functional/apps/discover/group4/_chart_hidden.ts index 05af7445d755e4..7a1e1408056fb5 100644 --- a/test/functional/apps/discover/group4/_chart_hidden.ts +++ b/test/functional/apps/discover/group4/_chart_hidden.ts @@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; // FLAKY: https://github.com/elastic/kibana/issues/169458 + // FLAKY: https://github.com/elastic/kibana/issues/169459 describe.skip('discover show/hide chart test', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); From 389ff736143beb0251c5eb4b1ec27c7acf6f86c4 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 18:53:00 +0100 Subject: [PATCH 21/24] skip flaky suite (#169828) --- .../automated_response_actions.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts index b94f389958f9f8..1948434b39c9f5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/automated_response_actions.cy.ts @@ -75,7 +75,8 @@ describe( disableExpandableFlyoutAdvancedSettings(); }); - describe('From alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/169828 + describe.skip('From alerts', () => { let ruleId: string; let ruleName: string; From 76bd0ef2fda6216650713e5765d5c7aed6bdaf2b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 18:54:18 +0100 Subject: [PATCH 22/24] skip flaky suite (#169747) --- x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts index 4ba7ab5befabf4..72d4adcbe26691 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts @@ -14,7 +14,8 @@ import { navigateTo } from '../../tasks/navigation'; import { loadLiveQuery, loadCase, cleanupCase } from '../../tasks/api_fixtures'; import { ServerlessRoleName } from '../../support/roles'; -describe('Add to Cases', () => { +// FLAKY: https://github.com/elastic/kibana/issues/169747 +describe.skip('Add to Cases', () => { let liveQueryId: string; let liveQueryQuery: string; before(() => { From c68ecf8a28dd03d28fff720c5fb080352d2b5294 Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Wed, 25 Oct 2023 13:02:33 -0500 Subject: [PATCH 23/24] [Security Solution]Expandable flyout - Replace rule sections with new components (#169029) This PR updates rule preview panel in the document expandable flyout: - Replaced rule details sections with the simplified components from https://github.com/elastic/kibana/pull/166158 - Added `itemRenderer` to allow custom render of the description list - Removed `isPanelView` props from the rule detail read only components. It was added to accommodate the preview styling (https://github.com/elastic/kibana/pull/163027) **No UI change from this PR** **How to test** - Go to alerts page and generate some alerts - Expand a row in the table, a flyout should appear - Click `Show rule summary` to expand the rule preview panel --- .../rule_details/rule_about_section.tsx | 14 +- .../rule_details/rule_definition_section.tsx | 9 +- .../rule_details/rule_schedule_section.tsx | 12 +- .../rules/description_step/index.tsx | 20 --- .../rules/step_about_rule/index.tsx | 9 +- .../rules/step_define_rule/index.tsx | 3 - .../rules/step_schedule_rule/index.tsx | 9 +- .../preview/components/rule_preview.tsx | 155 ++++++++---------- 8 files changed, 91 insertions(+), 140 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx index 7c1ada1c6e1bcf..f223214c3c7680 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx @@ -367,24 +367,30 @@ const prepareAboutSectionListItems = ( return aboutSectionListItems; }; -export interface RuleAboutSectionProps { +export interface RuleAboutSectionProps extends React.ComponentProps { rule: Partial; hideName?: boolean; hideDescription?: boolean; } -export const RuleAboutSection = ({ rule, hideName, hideDescription }: RuleAboutSectionProps) => { +export const RuleAboutSection = ({ + rule, + hideName, + hideDescription, + ...descriptionListProps +}: RuleAboutSectionProps) => { const aboutSectionListItems = prepareAboutSectionListItems(rule, hideName, hideDescription); return (
); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx index 1ff2eb43b744c5..52d8ad920c9681 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx @@ -579,7 +579,8 @@ const prepareDefinitionSectionListItems = ( return definitionSectionListItems; }; -export interface RuleDefinitionSectionProps { +export interface RuleDefinitionSectionProps + extends React.ComponentProps { rule: Partial; isInteractive?: boolean; dataTestSubj?: string; @@ -589,6 +590,7 @@ export const RuleDefinitionSection = ({ rule, isInteractive = false, dataTestSubj, + ...descriptionListProps }: RuleDefinitionSectionProps) => { const { savedQuery } = useGetSavedQuery({ savedQueryId: rule.type === 'saved_query' ? rule.saved_id : '', @@ -604,11 +606,12 @@ export const RuleDefinitionSection = ({ return (
); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx index b805b0a0a878ec..556bd119c52473 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx @@ -27,11 +27,14 @@ const From = ({ from, interval }: FromProps) => ( {getHumanizedDuration(from, interval)} ); -export interface RuleScheduleSectionProps { +export interface RuleScheduleSectionProps extends React.ComponentProps { rule: Partial; } -export const RuleScheduleSection = ({ rule }: RuleScheduleSectionProps) => { +export const RuleScheduleSection = ({ + rule, + ...descriptionListProps +}: RuleScheduleSectionProps) => { if (!rule.interval || !rule.from) { return null; } @@ -52,10 +55,11 @@ export const RuleScheduleSection = ({ rule }: RuleScheduleSectionProps) => { return (
); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index a55d229bb1b975..4fe7b5378b1ce1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -9,7 +9,6 @@ import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; -import { css } from '@emotion/css'; import type { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { DataViewBase, Filter } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; @@ -65,13 +64,6 @@ const DescriptionListContainer = styled(EuiDescriptionList)` } `; -const panelViewStyle = css` - dt { - font-size: 90% !important; - } - text-overflow: ellipsis; -`; - const DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%']; interface StepRuleDescriptionProps { @@ -79,7 +71,6 @@ interface StepRuleDescriptionProps { data: unknown; indexPatterns?: DataViewBase; schema: FormSchema; - isInPanelView?: boolean; // Option to show description list in smaller font } export const StepRuleDescriptionComponent = ({ @@ -87,7 +78,6 @@ export const StepRuleDescriptionComponent = ({ columns = 'multi', indexPatterns, schema, - isInPanelView, }: StepRuleDescriptionProps) => { const kibana = useKibana(); const license = useLicense(); @@ -134,16 +124,6 @@ export const StepRuleDescriptionComponent = ({ ); } - if (isInPanelView) { - return ( - - - - - - ); - } - return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 1dc19e69dd8d7d..cbbf27668ce8ea 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -60,7 +60,6 @@ interface StepAboutRuleReadOnlyProps { addPadding: boolean; descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: AboutStepRule; - isInPanelView?: boolean; // Option to show description list in smaller font } const ThreeQuartersContainer = styled.div` @@ -399,16 +398,10 @@ const StepAboutRuleReadOnlyComponent: FC = ({ addPadding, defaultValues: data, descriptionColumns, - isInPanelView = false, }) => { return ( - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index e1d9234cddc38e..90ba699131625e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -118,7 +118,6 @@ interface StepDefineRuleReadOnlyProps { descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: DefineStepRule; indexPattern: DataViewBase; - isInPanelView?: boolean; // Option to show description list in smaller font } export const MyLabelButton = styled(EuiButtonEmpty)` @@ -994,7 +993,6 @@ const StepDefineRuleReadOnlyComponent: FC = ({ defaultValues: data, descriptionColumns, indexPattern, - isInPanelView = false, }) => { const dataForDescription: Partial = getStepDataDataSource(data); @@ -1005,7 +1003,6 @@ const StepDefineRuleReadOnlyComponent: FC = ({ schema={filterRuleFieldsForType(schema, data.ruleType)} data={filterRuleFieldsForType(dataForDescription, data.ruleType)} indexPatterns={indexPattern} - isInPanelView={isInPanelView} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index 30699d60912cb4..a4971a66972e77 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -27,7 +27,6 @@ interface StepScheduleRuleReadOnlyProps { addPadding: boolean; descriptionColumns: 'multi' | 'single' | 'singleSplit'; defaultValues: ScheduleStepRule; - isInPanelView?: boolean; // Option to show description list in smaller font } const StepScheduleRuleComponent: FC = ({ @@ -70,16 +69,10 @@ const StepScheduleRuleReadOnlyComponent: FC = ({ addPadding, defaultValues: data, descriptionColumns, - isInPanelView = false, }) => { return ( - + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/preview/components/rule_preview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/preview/components/rule_preview.tsx index 45873684880500..c5e862b3f7a0f7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/preview/components/rule_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/preview/components/rule_preview.tsx @@ -6,19 +6,19 @@ */ import React, { memo, useState, useEffect } from 'react'; import { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '../../../../common/lib/kibana'; -import { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query'; import type { Rule } from '../../../../detection_engine/rule_management/logic'; import { usePreviewPanelContext } from '../context'; import { ExpandableSection } from '../../right/components/expandable_section'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { getStepsData } from '../../../../detections/pages/detection_engine/rules/helpers'; import { RulePreviewTitle } from './rule_preview_title'; -import { StepAboutRuleReadOnly } from '../../../../detections/components/rules/step_about_rule'; -import { StepDefineRuleReadOnly } from '../../../../detections/components/rules/step_define_rule'; -import { StepScheduleRuleReadOnly } from '../../../../detections/components/rules/step_schedule_rule'; +import { RuleAboutSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_about_section'; +import { RuleScheduleSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_schedule_section'; +import { RuleDefinitionSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_definition_section'; import { StepRuleActionsReadOnly } from '../../../../detections/components/rules/step_rule_actions'; +import { castRuleAsRuleResponse } from '../../../../detection_engine/rule_details_ui/pages/rule_details/cast_rule_as_rule_response'; import { FlyoutLoading } from '../../../shared/components/flyout_loading'; import { FlyoutError } from '../../../shared/components/flyout_error'; import { @@ -30,18 +30,24 @@ import { RULE_PREVIEW_LOADING_TEST_ID, } from './test_ids'; +const panelViewStyle = css` + dt { + font-size: 90% !important; + } + text-overflow: ellipsis; +`; + /** * Rule summary on a preview panel on top of the right section of expandable flyout */ export const RulePreview: React.FC = memo(() => { - const { ruleId, indexPattern } = usePreviewPanelContext(); + const { ruleId } = usePreviewPanelContext(); const [rule, setRule] = useState(null); const { rule: maybeRule, loading: ruleLoading, isExistingRule, } = useRuleWithFallback(ruleId ?? ''); - const { data } = useKibana().services; // persist rule until refresh is complete useEffect(() => { @@ -50,32 +56,8 @@ export const RulePreview: React.FC = memo(() => { } }, [maybeRule]); - const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = - rule != null - ? getStepsData({ rule, detailsView: true }) - : { - aboutRuleData: null, - defineRuleData: null, - scheduleRuleData: null, - ruleActionsData: null, - }; - - const [dataViewTitle, setDataViewTitle] = useState(); - - useEffect(() => { - const fetchDataViewTitle = async () => { - if (defineRuleData?.dataViewId != null && defineRuleData?.dataViewId !== '') { - const dataView = await data.dataViews.get(defineRuleData?.dataViewId); - setDataViewTitle(dataView.title); - } - }; - fetchDataViewTitle(); - }, [data.dataViews, defineRuleData?.dataViewId]); - - const { isSavedQueryLoading, savedQueryBar } = useGetSavedQuery({ - savedQueryId: rule?.saved_id, - ruleType: rule?.type, - }); + const { ruleActionsData } = + rule != null ? getStepsData({ rule, detailsView: true }) : { ruleActionsData: null }; const hasNotificationActions = Boolean(ruleActionsData?.actions?.length); const hasResponseActions = Boolean(ruleActionsData?.responseActions?.length); @@ -84,9 +66,15 @@ export const RulePreview: React.FC = memo(() => { return ruleLoading ? ( ) : rule ? ( - + + { > {rule.description} - {aboutRuleData && ( - + + + + } + expanded={false} + data-test-subj={RULE_PREVIEW_DEFINITION_TEST_ID} + > + + + + - )} + } + expanded={false} + data-test-subj={RULE_PREVIEW_SCHEDULE_TEST_ID} + > + - {defineRuleData && !isSavedQueryLoading && ( - <> - - } - expanded={false} - data-test-subj={RULE_PREVIEW_DEFINITION_TEST_ID} - > - - - - - )} - {scheduleRuleData && ( - <> - - } - expanded={false} - data-test-subj={RULE_PREVIEW_SCHEDULE_TEST_ID} - > - - - - - )} {hasActions && ( Date: Wed, 25 Oct 2023 19:20:44 +0100 Subject: [PATCH 24/24] skip flaky suite (#142496) --- x-pack/plugins/fleet/server/integration_tests/ha_setup.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/integration_tests/ha_setup.test.ts b/x-pack/plugins/fleet/server/integration_tests/ha_setup.test.ts index 25ab8fd65ae1fb..cd20cd62fef795 100644 --- a/x-pack/plugins/fleet/server/integration_tests/ha_setup.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/ha_setup.test.ts @@ -141,7 +141,8 @@ describe('Fleet setup preconfiguration with multiple instances Kibana', () => { await stopServers(); }); - describe('preconfiguration setup', () => { + // FLAKY: https://github.com/elastic/kibana/issues/142496 + describe.skip('preconfiguration setup', () => { it('sets up Fleet correctly with single Kibana instance', async () => { await addRoots(1); const [root1Start] = await startRoots();