From d06b98b88049dab43da12fb1a6b329e10b4d1833 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 25 Oct 2023 11:59:43 +0200 Subject: [PATCH 001/119] [Security Solution] Management landing cards added (#169625) ## Summary issue: https://github.com/elastic/kibana/issues/167453 Adds the Security-specific project cards to the Management cards landing page: - Entity Risk Score - Maps - Visualize library ### Screenshot management_cards --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../cards_navigation/src/cards_navigation.tsx | 2 +- .../security-solution/navigation/index.ts | 7 ++- .../navigation/src/navigation.ts | 15 ++++++ .../public/common/icons/entity_analytics.tsx | 3 +- .../public/management/links.ts | 2 +- .../public/navigation/index.ts | 3 +- .../links/sections/project_settings_links.ts | 21 ++++++-- .../sections/project_settings_translations.ts | 17 ++++++- .../public/navigation/links/util.ts | 1 - .../public/navigation/management_cards.ts | 50 +++++++++++++++++++ .../tsconfig.json | 3 +- 11 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/security_solution_serverless/public/navigation/management_cards.ts diff --git a/packages/kbn-management/cards_navigation/src/cards_navigation.tsx b/packages/kbn-management/cards_navigation/src/cards_navigation.tsx index f9d5624a05447..303742dd25fdf 100644 --- a/packages/kbn-management/cards_navigation/src/cards_navigation.tsx +++ b/packages/kbn-management/cards_navigation/src/cards_navigation.tsx @@ -184,7 +184,7 @@ export const CardsNavigation = ({ } + icon={} titleSize="xs" title={app.title} description={app.description} diff --git a/x-pack/packages/security-solution/navigation/index.ts b/x-pack/packages/security-solution/navigation/index.ts index 6088006869153..d14d119612c04 100644 --- a/x-pack/packages/security-solution/navigation/index.ts +++ b/x-pack/packages/security-solution/navigation/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -export { useGetAppUrl, useNavigateTo, useNavigation } from './src/navigation'; +export { + useGetAppUrl, + useNavigateTo, + useNavigation, + getNavigationPropsFromId, +} from './src/navigation'; export type { GetAppUrl, NavigateTo } from './src/navigation'; export { NavigationProvider } from './src/context'; export { SecurityPageName, LinkCategoryType } from './src/constants'; diff --git a/x-pack/packages/security-solution/navigation/src/navigation.ts b/x-pack/packages/security-solution/navigation/src/navigation.ts index 7474baf1fd3ba..6c18d55d5745f 100644 --- a/x-pack/packages/security-solution/navigation/src/navigation.ts +++ b/x-pack/packages/security-solution/navigation/src/navigation.ts @@ -9,6 +9,7 @@ import type { NavigateToAppOptions } from '@kbn/core/public'; import { useCallback } from 'react'; import { SECURITY_UI_APP_ID } from './constants'; import { useNavigationContext } from './context'; +import { getAppIdsFromId } from './links'; export type GetAppUrl = (param: { appId?: string; @@ -82,3 +83,17 @@ export const useNavigation = () => { const { getAppUrl } = useGetAppUrl(); return { navigateTo, getAppUrl }; }; + +/** + * Returns the appId, deepLinkId, and path from a given navigation id + */ +export const getNavigationPropsFromId = ( + id: string +): { + appId: string; + deepLinkId?: string; + path?: string; +} => { + const { appId = SECURITY_UI_APP_ID, ...options } = getAppIdsFromId(id); + return { appId, ...options }; +}; diff --git a/x-pack/plugins/security_solution/public/common/icons/entity_analytics.tsx b/x-pack/plugins/security_solution/public/common/icons/entity_analytics.tsx index e282f07c4dcdb..a9e4123fe4d41 100644 --- a/x-pack/plugins/security_solution/public/common/icons/entity_analytics.tsx +++ b/x-pack/plugins/security_solution/public/common/icons/entity_analytics.tsx @@ -20,13 +20,12 @@ export const IconEntityAnalytics: React.FC> = ({ ...prop fillRule="evenodd" clipRule="evenodd" d="M25.332 7C25.332 8.10457 26.2275 9 27.332 9C28.4366 9 29.332 8.10457 29.332 7C29.332 5.89543 28.4366 5 27.332 5C26.2275 5 25.332 5.89543 25.332 7ZM23.332 7C23.332 7.37644 23.384 7.74073 23.4812 8.08609L17.6976 11.1707C15.9888 8.65367 13.1035 7 9.83203 7C4.58533 7 0.332031 11.2533 0.332031 16.5C0.332031 21.7467 4.58533 26 9.83203 26C12.6903 26 15.2537 24.7377 16.9952 22.7403L23.387 26.3356C23.3508 26.5517 23.332 26.7737 23.332 27C23.332 29.2091 25.1229 31 27.332 31C29.5412 31 31.332 29.2091 31.332 27C31.332 24.7909 29.5412 23 27.332 23C26.0677 23 24.9404 23.5866 24.2074 24.5024L18.1491 21.0946C18.672 20.15 19.0387 19.1068 19.2143 18H24.4581C24.9021 19.7252 26.4682 21 28.332 21C30.5412 21 32.332 19.2091 32.332 17C32.332 14.7909 30.5412 13 28.332 13C26.4682 13 24.9021 14.2748 24.458 16H19.3191C19.2631 14.9207 19.027 13.8891 18.6403 12.9346L24.49 9.81475C25.2149 10.5466 26.2205 11 27.332 11C29.5412 11 31.332 9.20914 31.332 7C31.332 4.79086 29.5412 3 27.332 3C25.1229 3 23.332 4.79086 23.332 7ZM28.332 19C27.2275 19 26.332 18.1046 26.332 17C26.332 15.8954 27.2275 15 28.332 15C29.4366 15 30.332 15.8954 30.332 17C30.332 18.1046 29.4366 19 28.332 19ZM25.332 27C25.332 28.1046 26.2275 29 27.332 29C28.4366 29 29.332 28.1046 29.332 27C29.332 25.8954 28.4366 25 27.332 25C26.2275 25 25.332 25.8954 25.332 27ZM9.83203 24C5.68989 24 2.33203 20.6421 2.33203 16.5C2.33203 12.3579 5.6899 9 9.83203 9C13.9742 9 17.332 12.3579 17.332 16.5C17.332 20.6421 13.9742 24 9.83203 24Z" - fill="#343741" + className="euiIcon__fillSecondary" /> diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 60c4c93d43fa6..919291c482996 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -170,7 +170,7 @@ export const links: LinkItem = { id: SecurityPageName.entityAnalyticsManagement, title: ENTITY_ANALYTICS_RISK_SCORE, description: i18n.translate('xpack.securitySolution.appLinks.entityRiskScoringDescription', { - defaultMessage: 'Manage entity risk scoring and detect insider threats.', + defaultMessage: 'Monitor user and host risk scores, and track anomalies.', }), landingIcon: IconEntityAnalytics, path: ENTITY_ANALYTICS_MANAGEMENT_PATH, diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/index.ts b/x-pack/plugins/security_solution_serverless/public/navigation/index.ts index 0dd14ebacd544..e88261f165413 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/index.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/index.ts @@ -16,6 +16,7 @@ import { getSecuritySideNavComponent } from './side_navigation'; import { SecuritySideNavComponent } from './project_navigation'; import { projectAppLinksSwitcher } from './links/app_links'; import { formatProjectDeepLinks } from './links/deep_links'; +import { enableManagementCardsLanding } from './management_cards'; export const setupNavigation = ( _core: CoreSetup, @@ -29,7 +30,7 @@ export const startNavigation = (services: Services) => { const { serverless, management } = services; serverless.setProjectHome(APP_PATH); - management.setupCardsNavigation({ enabled: true }); + enableManagementCardsLanding(services); const projectNavigationTree = new ProjectNavigationTree(services); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts index f2deadd1fdf25..5e94100e3fee1 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_links.ts @@ -15,7 +15,14 @@ export const createProjectSettingsLinksFromManage = (manageLink: LinkItem): Link const entityAnalyticsLink = manageLink.links?.find( ({ id }) => id === SecurityPageName.entityAnalyticsManagement ); - return entityAnalyticsLink ? [{ ...entityAnalyticsLink, sideNavDisabled: true }] : []; + return entityAnalyticsLink + ? [ + { + ...entityAnalyticsLink, + sideNavDisabled: true, // Link disabled from the side nav but configured in the navigationTree (breadcrumbs). It is displayed in the management cards landing. + }, + ] + : []; }; export const projectSettingsNavLinks: ProjectNavigationLink[] = [ @@ -37,12 +44,16 @@ export const projectSettingsNavLinks: ProjectNavigationLink[] = [ }, { id: ExternalPageName.maps, - title: i18n.CLOUD_MAPS_TITLE, - disabled: true, // the link will be available in the navigationTree (breadcrumbs) but not appear in the sideNav + title: i18n.MAPS_TITLE, + description: i18n.MAPS_DESCRIPTION, + landingIcon: 'graphApp', + disabled: true, // Link disabled from the side nav but configured in the navigationTree (breadcrumbs). It is displayed in the management cards landing. }, { id: ExternalPageName.visualize, - title: i18n.CLOUD_VISUALIZE_TITLE, - disabled: true, // the link will be available in the navigationTree (breadcrumbs) but not appear in the sideNav + title: i18n.VISUALIZE_TITLE, + description: i18n.VISUALIZE_DESCRIPTION, + landingIcon: 'visualizeApp', + disabled: true, // Link disabled from the side nav but configured in the navigationTree (breadcrumbs). It is displayed in the management cards landing. }, ]; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts index 50b8086bfb0d9..dec726efea277 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/project_settings_translations.ts @@ -38,15 +38,28 @@ export const CLOUD_BILLING_TITLE = i18n.translate( } ); -export const CLOUD_MAPS_TITLE = i18n.translate( +export const MAPS_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.projectSettings.maps.title', { defaultMessage: 'Maps', } ); -export const CLOUD_VISUALIZE_TITLE = i18n.translate( +export const MAPS_DESCRIPTION = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.projectSettings.maps.description', + { + defaultMessage: + 'Analyze geospatial data and identify geo patterns in multiple layers and indices.', + } +); +export const VISUALIZE_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.projectSettings.visualize.title', { defaultMessage: 'Visualize library', } ); +export const VISUALIZE_DESCRIPTION = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.projectSettings.visualize.description', + { + defaultMessage: 'Manage visualization library. Create, edit, and share visualizations.', + } +); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts index edaf40529e16d..e77e860e93538 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts @@ -54,5 +54,4 @@ export const isBottomNavItemId = (id: string) => id === ExternalPageName.management || id === ExternalPageName.integrationsSecurity || id === ExternalPageName.cloudUsersAndRoles || - id === ExternalPageName.cloudPerformance || id === ExternalPageName.cloudBilling; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/management_cards.ts b/x-pack/plugins/security_solution_serverless/public/navigation/management_cards.ts new file mode 100644 index 0000000000000..79587f8f0843b --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/navigation/management_cards.ts @@ -0,0 +1,50 @@ +/* + * 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 { + AppDefinition, + CardNavExtensionDefinition, +} from '@kbn/management-cards-navigation/src/types'; +import { getNavigationPropsFromId, SecurityPageName } from '@kbn/security-solution-navigation'; +import type { Services } from '../common/services'; +import { ExternalPageName } from './links/constants'; +import type { ProjectPageName } from './links/types'; + +const SecurityManagementCards = new Map([ + [ExternalPageName.visualize, 'content'], + [ExternalPageName.maps, 'content'], + [SecurityPageName.entityAnalyticsManagement, 'alerts'], +]); + +export const enableManagementCardsLanding = (services: Services) => { + const { management, application } = services; + + services.getProjectNavLinks$().subscribe((projectNavLinks) => { + const extendCardNavDefinitions = projectNavLinks.reduce< + Record + >((acc, projectNavLink) => { + if (SecurityManagementCards.has(projectNavLink.id)) { + const { appId, deepLinkId, path } = getNavigationPropsFromId(projectNavLink.id); + + acc[projectNavLink.id] = { + category: SecurityManagementCards.get(projectNavLink.id) ?? 'other', + title: projectNavLink.title, + description: projectNavLink.description ?? '', + icon: projectNavLink.landingIcon ?? '', + href: application.getUrlForApp(appId, { deepLinkId, path }), + skipValidation: true, + }; + } + return acc; + }, {}); + + management.setupCardsNavigation({ + enabled: true, + extendCardNavDefinitions, + }); + }); +}; diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json index 154284ca9fab8..a626590c08e1a 100644 --- a/x-pack/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/plugins/security_solution_serverless/tsconfig.json @@ -45,6 +45,7 @@ "@kbn/core-logging-server-mocks", "@kbn/shared-ux-chrome-navigation", "@kbn/stack-connectors-plugin", - "@kbn/actions-plugin" + "@kbn/actions-plugin", + "@kbn/management-cards-navigation" ] } From bccfaafa7c4db85573f11e8bff4433b66a67a6f9 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Wed, 25 Oct 2023 12:00:57 +0200 Subject: [PATCH 002/119] [Snapshot Restore] Remove duped version in snapshot details flyout (#169615) --- .../__jest__/client_integration/home.test.ts | 6 ++---- .../snapshot_list/snapshot_details/tabs/tab_summary.tsx | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 3e38602b4be89..efbf38bccbf94 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -755,13 +755,11 @@ describe('', () => { describe('summary tab', () => { test('should set the correct summary values', () => { - const { version, versionId, uuid, indices } = snapshot1; + const { version, uuid, indices } = snapshot1; const { find } = testBed; - expect(find('snapshotDetail.version.value').text()).toBe( - `${version} / ${versionId}` - ); + expect(find('snapshotDetail.version.value').text()).toBe(version); expect(find('snapshotDetail.uuid.value').text()).toBe(uuid); expect(find('snapshotDetail.state.value').text()).toBe('Snapshot complete'); expect(find('snapshotDetail.includeGlobalState.value').text()).toEqual('Yes'); diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx index 47313aaeea626..00ea3fa27109b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -37,7 +37,6 @@ interface Props { export const TabSummary: React.FC = ({ snapshotDetails }) => { const { - versionId, version, // TODO: Add a tooltip explaining that: a false value means that the cluster global state // is not stored as part of the snapshot. @@ -62,12 +61,12 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { - {version} / {versionId} + {version} From 6f334cd51015c1e4ff85d69f2fa83ee23f7902e3 Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:03:02 +0100 Subject: [PATCH 003/119] =?UTF-8?q?[ObsUX]=20Change=20link=20from=20instan?= =?UTF-8?q?ces=20table=20of=20java=20agent=20to=20open=20new=20Metrics=20p?= =?UTF-8?q?a=E2=80=A6=20(#169672)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/elastic/kibana/issues/169085 BEFORE https://github.com/elastic/kibana/assets/31922082/da089db4-9164-4eb6-8542-03ec8233cea6 AFTER https://github.com/elastic/kibana/assets/31922082/544ed491-1b69-4fa7-8053-0f711d9ecdb2 --- .../service_overview_instances_table/get_columns.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index d7cfc9aa0f76c..ea1cc64e9fff9 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -14,7 +14,6 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import { ActionMenu } from '@kbn/observability-shared-plugin/public'; import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options'; -import { isJavaAgentName } from '../../../../../common/agent_name'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { getServiceNodeName, @@ -27,7 +26,6 @@ import { } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; import { MetricOverviewLink } from '../../../shared/links/apm/metric_overview_link'; -import { ServiceNodeMetricOverviewLink } from '../../../shared/links/apm/service_node_metric_overview_link'; import { ListMetric } from '../../../shared/list_metric'; import { getLatencyColumnLabel } from '../../../shared/transactions_table/get_latency_column_label'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; @@ -87,14 +85,7 @@ export function getColumns({ serviceNodeName === SERVICE_NODE_NAME_MISSING; const text = getServiceNodeName(serviceNodeName); - const link = isJavaAgentName(agentName) ? ( - - {text} - - ) : ( + const link = ( ({ From 9a9a51b4545793a9a13da35e2b749f89be5e2914 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:07:55 +0200 Subject: [PATCH 004/119] Remove unused sharedux avatar components (#168686) ## Summary Closes https://github.com/elastic/kibana/issues/168689 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 - package.json | 1 - .../avatar/user_profile/impl/README.mdx | 12 - .../avatar/user_profile/impl/index.ts | 15 - .../avatar/user_profile/impl/jest.config.js | 13 - .../avatar/user_profile/impl/kibana.jsonc | 5 - .../avatar/user_profile/impl/package.json | 6 - .../avatar/user_profile/impl/tsconfig.json | 25 -- .../user_profile/impl/user_avatar.test.tsx | 94 ------ .../avatar/user_profile/impl/user_avatar.tsx | 79 ----- .../avatar/user_profile/impl/user_profile.ts | 137 -------- .../impl/user_profiles.stories.tsx | 60 ---- .../impl/user_profiles_popover.test.tsx | 125 ------- .../impl/user_profiles_popover.tsx | 48 --- .../impl/user_profiles_selectable.test.tsx | 203 ----------- .../impl/user_profiles_selectable.tsx | 319 ------------------ .../shared-ux/page/no_data/impl/tsconfig.json | 2 +- src/plugins/kibana_overview/tsconfig.json | 2 +- tsconfig.base.json | 2 - x-pack/plugins/spaces/tsconfig.json | 2 +- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - yarn.lock | 4 - 24 files changed, 3 insertions(+), 1164 deletions(-) delete mode 100644 packages/shared-ux/avatar/user_profile/impl/README.mdx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/index.ts delete mode 100644 packages/shared-ux/avatar/user_profile/impl/jest.config.js delete mode 100644 packages/shared-ux/avatar/user_profile/impl/kibana.jsonc delete mode 100644 packages/shared-ux/avatar/user_profile/impl/package.json delete mode 100644 packages/shared-ux/avatar/user_profile/impl/tsconfig.json delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_avatar.test.tsx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_avatar.tsx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_profile.ts delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_profiles.stories.tsx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.test.tsx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.tsx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.test.tsx delete mode 100644 packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 983754e6dd8db..9751dc527b95c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -676,7 +676,6 @@ examples/share_examples @elastic/appex-sharedux src/plugins/share @elastic/appex-sharedux packages/kbn-shared-svg @elastic/apm-ui packages/shared-ux/avatar/solution @elastic/appex-sharedux -packages/shared-ux/avatar/user_profile/impl @elastic/appex-sharedux packages/shared-ux/button/exit_full_screen/impl @elastic/appex-sharedux packages/shared-ux/button/exit_full_screen/mocks @elastic/appex-sharedux packages/shared-ux/button/exit_full_screen/types @elastic/appex-sharedux diff --git a/package.json b/package.json index 9291646fac18a..e5213d00d20d4 100644 --- a/package.json +++ b/package.json @@ -679,7 +679,6 @@ "@kbn/share-plugin": "link:src/plugins/share", "@kbn/shared-svg": "link:packages/kbn-shared-svg", "@kbn/shared-ux-avatar-solution": "link:packages/shared-ux/avatar/solution", - "@kbn/shared-ux-avatar-user-profile-components": "link:packages/shared-ux/avatar/user_profile/impl", "@kbn/shared-ux-button-exit-full-screen": "link:packages/shared-ux/button/exit_full_screen/impl", "@kbn/shared-ux-button-exit-full-screen-mocks": "link:packages/shared-ux/button/exit_full_screen/mocks", "@kbn/shared-ux-button-exit-full-screen-types": "link:packages/shared-ux/button/exit_full_screen/types", diff --git a/packages/shared-ux/avatar/user_profile/impl/README.mdx b/packages/shared-ux/avatar/user_profile/impl/README.mdx deleted file mode 100644 index 1522cec340b9b..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/README.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -id: sharedUX/Components/UserProfileAvatar -slug: /shared-ux/components/user-profile-avatar -title: User Profile Avatar -description: A wrapper around `EuiAvatar` -tags: ['shared-ux', 'component'] -date: 2022-09-01 ---- - -## Description - -A wrapper around `EuiAvatar` tailored for user profiles diff --git a/packages/shared-ux/avatar/user_profile/impl/index.ts b/packages/shared-ux/avatar/user_profile/impl/index.ts deleted file mode 100644 index e36215e36896a..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/index.ts +++ /dev/null @@ -1,15 +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 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 type { UserAvatarProps, UserProfileWithAvatar } from './user_avatar'; -export type { UserProfilesSelectableProps } from './user_profiles_selectable'; -export type { UserProfilesPopoverProps } from './user_profiles_popover'; -export { UserAvatar } from './user_avatar'; -export { UserProfilesSelectable } from './user_profiles_selectable'; -export { UserProfilesPopover } from './user_profiles_popover'; -export type { UserProfile, UserProfileUserInfo, UserProfileAvatarData } from './user_profile'; diff --git a/packages/shared-ux/avatar/user_profile/impl/jest.config.js b/packages/shared-ux/avatar/user_profile/impl/jest.config.js deleted file mode 100644 index 111a2a8105057..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/jest.config.js +++ /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 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../../..', - roots: ['/packages/shared-ux/avatar/user_profile/impl'], -}; diff --git a/packages/shared-ux/avatar/user_profile/impl/kibana.jsonc b/packages/shared-ux/avatar/user_profile/impl/kibana.jsonc deleted file mode 100644 index f5105c929224b..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/shared-ux-avatar-user-profile-components", - "owner": "@elastic/appex-sharedux" -} diff --git a/packages/shared-ux/avatar/user_profile/impl/package.json b/packages/shared-ux/avatar/user_profile/impl/package.json deleted file mode 100644 index 6af2682d533bf..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@kbn/shared-ux-avatar-user-profile-components", - "private": true, - "version": "1.0.0", - "license": "SSPL-1.0 OR Elastic License 2.0" -} \ No newline at end of file diff --git a/packages/shared-ux/avatar/user_profile/impl/tsconfig.json b/packages/shared-ux/avatar/user_profile/impl/tsconfig.json deleted file mode 100644 index 833908e045033..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../../../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "target/types", - "types": [ - "jest", - "node", - "react", - "@kbn/ambient-ui-types" - ] - }, - "include": [ - "*ts*", - "*.md*", - "**/*.ts", - "**/*.md*", - ], - "kbn_references": [ - "@kbn/i18n-react", - "@kbn/i18n", - ], - "exclude": [ - "target/**/*", - ] -} diff --git a/packages/shared-ux/avatar/user_profile/impl/user_avatar.test.tsx b/packages/shared-ux/avatar/user_profile/impl/user_avatar.test.tsx deleted file mode 100644 index 6a62d14c75642..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_avatar.test.tsx +++ /dev/null @@ -1,94 +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 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 { shallow } from 'enzyme'; -import React from 'react'; - -import { UserAvatar } from './user_avatar'; - -describe('UserAvatar', () => { - it('should render `EuiAvatar` correctly with image avatar', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchInlineSnapshot(` - - `); - }); - - it('should render `EuiAvatar` correctly with initials avatar', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchInlineSnapshot(` - - `); - }); - - it('should render `EuiAvatar` correctly without avatar data', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchInlineSnapshot(` - - `); - }); - - it('should render `EuiAvatar` correctly without user data', () => { - const wrapper = shallow(); - expect(wrapper).toMatchInlineSnapshot(` - - `); - }); -}); diff --git a/packages/shared-ux/avatar/user_profile/impl/user_avatar.tsx b/packages/shared-ux/avatar/user_profile/impl/user_avatar.tsx deleted file mode 100644 index 2413694317c27..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_avatar.tsx +++ /dev/null @@ -1,79 +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 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 type { EuiAvatarProps } from '@elastic/eui'; -import { EuiAvatar, useEuiTheme } from '@elastic/eui'; -import type { FunctionComponent } from 'react'; -import React from 'react'; - -import type { UserProfile, UserProfileUserInfo, UserProfileAvatarData } from './user_profile'; -import { - getUserAvatarColor, - getUserAvatarInitials, - getUserDisplayName, - USER_AVATAR_MAX_INITIALS, -} from './user_profile'; - -/** - * Convenience type for a {@link UserProfile} with avatar data - */ -export type UserProfileWithAvatar = UserProfile<{ avatar?: UserProfileAvatarData }>; - -/** - * Props of {@link UserAvatar} component - */ -export interface UserAvatarProps - extends Omit< - EuiAvatarProps, - | 'initials' - | 'initialsLength' - | 'imageUrl' - | 'iconType' - | 'iconSize' - | 'iconColor' - | 'name' - | 'color' - | 'type' - > { - /** - * User to be rendered - */ - user?: UserProfileUserInfo; - - /** - * Avatar data of user to be rendered - */ - avatar?: UserProfileAvatarData; -} - -/** - * Renders an avatar given a user profile - */ -export const UserAvatar: FunctionComponent = ({ user, avatar, ...rest }) => { - const { euiTheme } = useEuiTheme(); - - if (!user) { - return ; - } - - const displayName = getUserDisplayName(user); - - if (avatar?.imageUrl) { - return ; - } - - return ( - - ); -}; diff --git a/packages/shared-ux/avatar/user_profile/impl/user_profile.ts b/packages/shared-ux/avatar/user_profile/impl/user_profile.ts deleted file mode 100644 index a278e2bc61a9c..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_profile.ts +++ /dev/null @@ -1,137 +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 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 { VISUALIZATION_COLORS } from '@elastic/eui'; - -/** - * IMPORTANT: - * - * The types in this file have been imported from - * `x-pack/plugins/security/common/model/user_profile.ts` - * - * When making changes please ensure to keep both files in sync. - */ - -/** - * Describes basic properties stored in user profile. - */ -export interface UserProfile { - /** - * Unique ID for of the user profile. - */ - uid: string; - - /** - * Indicates whether user profile is enabled or not. - */ - enabled: boolean; - - /** - * Information about the user that owns profile. - */ - user: UserProfileUserInfo; - - /** - * User specific data associated with the profile. - */ - data: Partial; -} - -/** - * Basic user information returned in user profile. - */ -export interface UserProfileUserInfo { - /** - * Username of the user. - */ - username: string; - /** - * Optional email of the user. - */ - email?: string; - /** - * Optional full name of the user. - */ - fullName?: string; - /** - * Optional display name of the user. - */ - displayName?: string; -} - -/** - * Placeholder for data stored in user profile. - */ -export type UserProfileData = Record; - -/** - * Avatar stored in user profile. - */ -export interface UserProfileAvatarData { - /** - * Optional initials (two letters) of the user to use as avatar if avatar picture isn't specified. - */ - initials?: string; - /** - * Background color of the avatar when initials are used. - */ - color?: string; - /** - * Base64 data URL for the user avatar image. - */ - imageUrl?: string | null; -} - -export const USER_AVATAR_FALLBACK_CODE_POINT = 97; // code point for lowercase "a" -export const USER_AVATAR_MAX_INITIALS = 2; - -/** - * Determines the color for the provided user profile. - * If a color is present on the user profile itself, then that is used. - * Otherwise, a color is provided from EUI's Visualization Colors based on the display name. - * - * @param {UserProfileUserInfo} user User info - * @param {UserProfileAvatarData} avatar User avatar - */ -export function getUserAvatarColor( - user: Pick, - avatar?: UserProfileAvatarData -) { - const firstCodePoint = getUserDisplayName(user).codePointAt(0) || USER_AVATAR_FALLBACK_CODE_POINT; - - return avatar?.color ?? VISUALIZATION_COLORS[firstCodePoint % VISUALIZATION_COLORS.length]; -} - -/** - * Determines the initials for the provided user profile. - * If initials are present on the user profile itself, then that is used. - * Otherwise, the initials are calculated based off the words in the display name, with a max length of 2 characters. - * - * @param {UserProfileUserInfo} user User info - * @param {UserProfileAvatarData} avatar User avatar - */ -export function getUserAvatarInitials( - user: Pick, - avatar?: UserProfileAvatarData -) { - const words = getUserDisplayName(user).split(' '); - const numInitials = Math.min(USER_AVATAR_MAX_INITIALS, words.length); - - words.splice(numInitials, words.length); - - return avatar?.initials ?? words.map((word) => word.substring(0, 1)).join(''); -} - -/** - * Determines the display name for the provided user profile. - * - * @param {UserProfileUserInfo} user User info - */ -export function getUserDisplayName(user: Pick) { - return user.fullName || user.username; -} diff --git a/packages/shared-ux/avatar/user_profile/impl/user_profiles.stories.tsx b/packages/shared-ux/avatar/user_profile/impl/user_profiles.stories.tsx deleted file mode 100644 index e7a7fa719f2e8..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_profiles.stories.tsx +++ /dev/null @@ -1,60 +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 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 React from 'react'; -import { UserAvatar, UserAvatarProps } from './user_avatar'; -import mdx from './README.mdx'; -import { UserProfileUserInfo } from './user_profile'; - -export default { - title: 'Avatar/User Profile', - description: '', - parameters: { - docs: { - page: mdx, - }, - }, -}; - -type UserAvatarParams = Pick; -const sampleUsers = [ - { - username: 'Peggy', - email: 'test@email.com', - fullName: 'Peggy Simms', - displayName: 'Peggy', - }, - { - username: 'Martin', - email: 'test@email.com', - fullName: 'Martin Gatsby', - displayName: 'Martin', - }, - { - username: 'Leonardo DiCaprio', - email: 'test@email.com', - fullName: 'Leonardo DiCaprio', - displayName: 'Leonardo DiCaprio', - }, -]; - -export const userAvatar = ( - params: Pick, - rest: UserAvatarParams -) => { - const username = params; - return ; -}; - -userAvatar.argTypes = { - username: { - control: { type: 'radio' }, - options: sampleUsers.map(({ username }) => username), - defaultValue: sampleUsers.map(({ username }) => username)[0], - }, -}; diff --git a/packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.test.tsx b/packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.test.tsx deleted file mode 100644 index 9412904c8b5a4..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.test.tsx +++ /dev/null @@ -1,125 +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 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 { shallow } from 'enzyme'; -import React from 'react'; - -import { UserProfilesPopover } from './user_profiles_popover'; - -const userProfiles = [ - { - uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0', - enabled: true, - data: {}, - user: { - username: 'delighted_nightingale', - email: 'delighted_nightingale@profiles.elastic.co', - fullName: 'Delighted Nightingale', - }, - }, - { - uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', - enabled: true, - data: {}, - user: { - username: 'damaged_raccoon', - email: 'damaged_raccoon@profiles.elastic.co', - fullName: 'Damaged Raccoon', - }, - }, - { - uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0', - enabled: true, - data: {}, - user: { - username: 'physical_dinosaur', - email: 'physical_dinosaur@profiles.elastic.co', - fullName: 'Physical Dinosaur', - }, - }, - { - uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0', - enabled: true, - data: {}, - user: { - username: 'wet_dingo', - email: 'wet_dingo@profiles.elastic.co', - fullName: 'Wet Dingo', - }, - }, -]; - -describe('UserProfilesPopover', () => { - it('should render `EuiPopover` and `UserProfilesSelectable` correctly', () => { - const [firstOption, secondOption] = userProfiles; - const wrapper = shallow( - Toggle} - closePopover={jest.fn()} - selectableProps={{ - selectedOptions: [firstOption], - defaultOptions: [secondOption], - }} - /> - ); - expect(wrapper).toMatchInlineSnapshot(` - - Toggle - - } - closePopover={[MockFunction]} - display="inline-block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - repositionToCrossAxis={true} - > - - - - - `); - }); -}); diff --git a/packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.tsx b/packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.tsx deleted file mode 100644 index 9fc553d9be689..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_profiles_popover.tsx +++ /dev/null @@ -1,48 +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 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 type { EuiPopoverProps, EuiContextMenuPanelProps } from '@elastic/eui'; -import type { FunctionComponent } from 'react'; -import React from 'react'; -import { EuiPopover, EuiContextMenuPanel } from '@elastic/eui'; - -import { UserProfilesSelectable, UserProfilesSelectableProps } from './user_profiles_selectable'; - -/** - * Props of {@link UserProfilesPopover} component - */ -export interface UserProfilesPopoverProps extends EuiPopoverProps { - /** - * Title of the popover - * @see EuiContextMenuPanelProps - */ - title?: EuiContextMenuPanelProps['title']; - - /** - * Props forwarded to selectable component - * @see UserProfilesSelectableProps - */ - selectableProps: UserProfilesSelectableProps; -} - -/** - * Renders a selectable component inside a popover given a list of user profiles - */ -export const UserProfilesPopover: FunctionComponent = ({ - title, - selectableProps, - ...popoverProps -}) => { - return ( - - - - - - ); -}; diff --git a/packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.test.tsx b/packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.test.tsx deleted file mode 100644 index d17e70c566f43..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.test.tsx +++ /dev/null @@ -1,203 +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 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 { mount } from 'enzyme'; -import React from 'react'; - -import { UserProfilesSelectable } from './user_profiles_selectable'; - -const userProfiles = [ - { - uid: 'u_BOulL4QMPSyV9jg5lQI2JmCkUnokHTazBnet3xVHNv0_0', - enabled: true, - data: {}, - user: { - username: 'delighted_nightingale', - email: 'delighted_nightingale@profiles.elastic.co', - fullName: 'Delighted Nightingale', - }, - }, - { - uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', - enabled: true, - data: {}, - user: { - username: 'damaged_raccoon', - email: 'damaged_raccoon@profiles.elastic.co', - fullName: 'Damaged Raccoon', - }, - }, - { - uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0', - enabled: true, - data: {}, - user: { - username: 'physical_dinosaur', - email: 'physical_dinosaur@profiles.elastic.co', - fullName: 'Physical Dinosaur', - }, - }, - { - uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0', - enabled: true, - data: {}, - user: { - username: 'wet_dingo', - email: 'wet_dingo@profiles.elastic.co', - fullName: 'Wet Dingo', - }, - }, -]; - -describe('UserProfilesSelectable', () => { - it('should render `selectedOptions` before `defaultOptions` separated by a group label', () => { - const [firstOption, secondOption, thirdOption] = userProfiles; - const wrapper = mount( - - ); - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: firstOption.uid, - checked: 'on', - }), - expect.objectContaining({ - isGroupLabel: true, - label: 'Suggested', - }), - expect.objectContaining({ - key: secondOption.uid, - checked: undefined, - }), - expect.objectContaining({ - key: thirdOption.uid, - checked: undefined, - }), - ]); - }); - - it('should hide `selectedOptions` and `defaultOptions` when `options` has been provided', () => { - const [firstOption, secondOption, thirdOption] = userProfiles; - const wrapper = mount( - - ); - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: thirdOption.uid, - checked: undefined, - }), - ]); - }); - - it('should hide `selectedOptions` and `defaultOptions` when `options` gets updated', () => { - const [firstOption, secondOption, thirdOption] = userProfiles; - const wrapper = mount( - - ); - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: firstOption.uid, - checked: 'on', - }), - expect.objectContaining({ - isGroupLabel: true, - label: 'Suggested', - }), - expect.objectContaining({ - key: secondOption.uid, - checked: undefined, - }), - ]); - - wrapper.setProps({ options: [thirdOption] }).update(); - - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: thirdOption.uid, - checked: undefined, - }), - ]); - }); - - it('should render `options` with correct checked status', () => { - const [firstOption, secondOption] = userProfiles; - const wrapper = mount( - - ); - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: firstOption.uid, - checked: 'on', - }), - expect.objectContaining({ - key: secondOption.uid, - checked: undefined, - }), - ]); - }); - - it('should trigger `onChange` callback when selection changes', () => { - const onChange = jest.fn(); - const [firstOption, secondOption] = userProfiles; - const wrapper = mount( - - ); - wrapper.find('EuiSelectableListItem').last().simulate('click'); - expect(onChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - uid: firstOption.uid, - }), - expect.objectContaining({ - uid: secondOption.uid, - }), - ]) - ); - }); - - it('should continue to display `selectedOptions` when getting unchecked', () => { - const onChange = jest.fn(); - const [firstOption] = userProfiles; - const wrapper = mount( - - ); - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: firstOption.uid, - checked: 'on', - }), - ]); - wrapper.setProps({ selectedOptions: [] }).update(); - expect(wrapper.find('EuiSelectable').prop('options')).toEqual([ - expect.objectContaining({ - key: firstOption.uid, - checked: undefined, - }), - ]); - }); - - it('should trigger `onSearchChange` callback when search term changes', () => { - const onSearchChange = jest.fn(); - const wrapper = mount(); - wrapper.find('input[type="search"]').simulate('change', { target: { value: 'search' } }); - expect(onSearchChange).toHaveBeenCalledWith('search', []); - }); -}); diff --git a/packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.tsx b/packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.tsx deleted file mode 100644 index ea0e2260f44fd..0000000000000 --- a/packages/shared-ux/avatar/user_profile/impl/user_profiles_selectable.tsx +++ /dev/null @@ -1,319 +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 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 type { EuiSelectableOption, EuiSelectableProps } from '@elastic/eui'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPanel, - EuiSelectable, - EuiSpacer, - EuiText, - EuiTextColor, -} from '@elastic/eui'; -import type { FunctionComponent, ReactNode } from 'react'; -import React, { useEffect, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { getUserDisplayName } from './user_profile'; -import type { UserProfileWithAvatar } from './user_avatar'; -import { UserAvatar } from './user_avatar'; - -/** - * Props of {@link UserProfilesSelectable} component - */ -export interface UserProfilesSelectableProps - extends Pick< - EuiSelectableProps, - | 'height' - | 'singleSelection' - | 'loadingMessage' - | 'noMatchesMessage' - | 'emptyMessage' - | 'errorMessage' - > { - /** - * List of users to be rendered as suggestions. - */ - defaultOptions?: UserProfileWithAvatar[]; - - /** - * List of selected users. - */ - selectedOptions?: UserProfileWithAvatar[]; - - /** - * List of users from search results. Should be updated based on the search term provided by `onSearchChange` callback. - */ - options?: UserProfileWithAvatar[]; - - /** - * Passes back the list of selected users. - * @param options List of selected users - */ - onChange?(options: UserProfileWithAvatar[]): void; - - /** - * Passes back the search term. - * @param searchTerm Search term - */ - onSearchChange?(searchTerm: string): void; - - /** - * Loading indicator for asynchronous search operations. - */ - isLoading?: boolean; - - /** - * Placeholder text for search box. - */ - searchPlaceholder?: string; - - /** - * Returns text for selected status. - * @param selectedCount Number of selected users - */ - selectedStatusMessage?(selectedCount: number): ReactNode; - - /** - * Text for label of clear button. - */ - clearButtonLabel?: ReactNode; -} - -/** - * Renders a selectable component given a list of user profiles - */ -export const UserProfilesSelectable: FunctionComponent = ({ - selectedOptions, - defaultOptions, - options, - onChange, - onSearchChange, - isLoading = false, - singleSelection = false, - height, - loadingMessage, - noMatchesMessage, - emptyMessage, - errorMessage, - searchPlaceholder, - selectedStatusMessage, - clearButtonLabel, -}) => { - const [displayedOptions, setDisplayedOptions] = useState([]); - - // Resets all displayed options - const resetDisplayedOptions = () => { - if (options) { - setDisplayedOptions(options.map(toSelectableOption)); - return; - } - - setDisplayedOptions([]); - updateDisplayedOptions(); - }; - - const ensureSeparator = (values: SelectableOption[]) => { - let index = values.findIndex((option) => option.isGroupLabel); - if (index === -1) { - const length = values.push({ - label: i18n.translate( - 'sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel', - { - defaultMessage: 'Suggested', - } - ), - isGroupLabel: true, - } as SelectableOption); - index = length - 1; - } - return index; - }; - - // Updates displayed options without removing or resorting exiting options - const updateDisplayedOptions = () => { - if (options) { - return; - } - - setDisplayedOptions((values) => { - // Copy all displayed options - const nextOptions: SelectableOption[] = [...values]; - - // Get any newly added selected options - const selectedOptionsToAdd: SelectableOption[] = selectedOptions - ? selectedOptions - .filter((profile) => !nextOptions.find((option) => option.key === profile.uid)) - .map(toSelectableOption) - : []; - - // Get any newly added default options - const defaultOptionsToAdd: SelectableOption[] = defaultOptions - ? defaultOptions - .filter( - (profile) => - !nextOptions.find((option) => option.key === profile.uid) && - !selectedOptionsToAdd.find((option) => option.key === profile.uid) - ) - .map(toSelectableOption) - : []; - - // Merge in any new options and add group separator if necessary - if (defaultOptionsToAdd.length) { - const separatorIndex = ensureSeparator(nextOptions); - nextOptions.splice(separatorIndex, 0, ...selectedOptionsToAdd); - nextOptions.push(...defaultOptionsToAdd); - } else { - nextOptions.push(...selectedOptionsToAdd); - } - - return nextOptions; - }); - }; - - // Marks displayed options as checked or unchecked depending on `props.selectedOptions` - const updateCheckedStatus = () => { - setDisplayedOptions((values) => - values.map((option) => { - if (selectedOptions) { - const match = selectedOptions.find((p) => p.uid === option.key); - return { ...option, checked: match ? 'on' : undefined }; - } - return { ...option, checked: undefined }; - }) - ); - }; - - useEffect(resetDisplayedOptions, [options]); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(updateDisplayedOptions, [defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]); - - const selectedCount = selectedOptions ? selectedOptions.length : 0; - - const placeholder = - searchPlaceholder ?? - i18n.translate( - 'sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder', - { - defaultMessage: 'Search', - } - ); - - return ( - >) => { - if (!onChange) { - return; - } - - // Take all selected options from `nextOptions` unless already in `props.selectedOptions` - const values: UserProfileWithAvatar[] = nextOptions - .filter((option) => { - if (option.isGroupLabel || option.checked !== 'on') { - return false; - } - if (selectedOptions && selectedOptions.find((p) => p.uid === option.key)) { - return false; - } - return true; - }) - .map((option) => option.data); - - // Add all options from `props.selectedOptions` unless they have been deselected in `nextOptions` - if (selectedOptions && !singleSelection) { - selectedOptions.forEach((profile) => { - const match = nextOptions.find((o) => o.key === profile.uid); - if (!match || match.checked === 'on') { - values.push(profile); - } - }); - } - - onChange(values); - }} - style={{ maxHeight: height }} - singleSelection={singleSelection} - searchable - searchProps={{ - placeholder, - onChange: onSearchChange, - isLoading, - isClearable: !isLoading, - }} - isPreFiltered - listProps={{ onFocusBadge: false }} - loadingMessage={loadingMessage} - noMatchesMessage={noMatchesMessage} - emptyMessage={emptyMessage} - errorMessage={errorMessage} - > - {(list, search) => ( - <> - - {search} - - - - - {selectedStatusMessage ? ( - selectedStatusMessage(selectedCount) - ) : ( - - )} - - - - {selectedCount ? ( - onChange?.([])} - style={{ height: '1rem' }} - > - {clearButtonLabel ?? ( - - )} - - ) : null} - - - - - {list} - - )} - - ); -}; - -type SelectableOption = EuiSelectableOption; - -function toSelectableOption(userProfile: UserProfileWithAvatar): SelectableOption { - // @ts-ignore: `isGroupLabel` is not required here but TS complains - return { - key: userProfile.uid, - prepend: , - label: getUserDisplayName(userProfile.user), - append: {userProfile.user.email}, - data: userProfile, - }; -} diff --git a/packages/shared-ux/page/no_data/impl/tsconfig.json b/packages/shared-ux/page/no_data/impl/tsconfig.json index 99daacfdb7087..d36c0c9a9ff95 100644 --- a/packages/shared-ux/page/no_data/impl/tsconfig.json +++ b/packages/shared-ux/page/no_data/impl/tsconfig.json @@ -14,13 +14,13 @@ "**/*.tsx", ], "kbn_references": [ - "@kbn/shared-ux-avatar-solution", "@kbn/shared-ux-card-no-data", "@kbn/shared-ux-page-no-data-types", "@kbn/test-jest-helpers", "@kbn/shared-ux-page-no-data-mocks", "@kbn/i18n", "@kbn/i18n-react", + "@kbn/shared-ux-avatar-solution", ], "exclude": [ "target/**/*", diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index 2f450f1e57609..976eed58d31aa 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -23,9 +23,9 @@ "@kbn/test-jest-helpers", "@kbn/shared-ux-page-kibana-template", "@kbn/shared-ux-page-analytics-no-data", - "@kbn/shared-ux-avatar-solution", "@kbn/shared-ux-link-redirect-app", "@kbn/shared-ux-router", + "@kbn/shared-ux-avatar-solution", ], "exclude": [ "target/**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index b0fd477a6aff3..79e998206483d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1346,8 +1346,6 @@ "@kbn/shared-svg/*": ["packages/kbn-shared-svg/*"], "@kbn/shared-ux-avatar-solution": ["packages/shared-ux/avatar/solution"], "@kbn/shared-ux-avatar-solution/*": ["packages/shared-ux/avatar/solution/*"], - "@kbn/shared-ux-avatar-user-profile-components": ["packages/shared-ux/avatar/user_profile/impl"], - "@kbn/shared-ux-avatar-user-profile-components/*": ["packages/shared-ux/avatar/user_profile/impl/*"], "@kbn/shared-ux-button-exit-full-screen": ["packages/shared-ux/button/exit_full_screen/impl"], "@kbn/shared-ux-button-exit-full-screen/*": ["packages/shared-ux/button/exit_full_screen/impl/*"], "@kbn/shared-ux-button-exit-full-screen-mocks": ["packages/shared-ux/button/exit_full_screen/mocks"], diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index d59e30e3a0194..060fd35cffcbc 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -16,7 +16,6 @@ "@kbn/core", "@kbn/test-jest-helpers", "@kbn/i18n-react", - "@kbn/shared-ux-avatar-solution", "@kbn/core-saved-objects-api-server", "@kbn/config-schema", "@kbn/utility-types", @@ -32,6 +31,7 @@ "@kbn/core-custom-branding-common", "@kbn/shared-ux-link-redirect-app", "@kbn/config", + "@kbn/shared-ux-avatar-solution", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b8221e1102355..bd3f3f1395d8b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5214,7 +5214,6 @@ "sharedUXPackages.noDataPage.intro": "Ajoutez vos données pour commencer, ou {link} sur {solution}.", "sharedUXPackages.noDataPage.welcomeTitle": "Bienvenue dans Elastic {solution} !", "sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage": "{count, plural, one {# utilisateur sélectionné} many {# utilisateurs sélectionnés} other {# utilisateurs sélectionnés}}", "sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "Ajouter depuis la bibliothèque", "sharedUXPackages.buttonToolbar.toolbar.errorToolbarText": "Il y a plus de 120 boutons supplémentaires. Nous vous invitons à limiter le nombre de boutons.", "sharedUXPackages.card.noData.description": "Utilisez Elastic Agent pour collecter de manière simple et unifiée les données de vos machines.", @@ -5268,9 +5267,6 @@ "sharedUXPackages.solutionNav.collapsibleLabel": "Réduire la navigation latérale", "sharedUXPackages.solutionNav.menuText": "menu", "sharedUXPackages.solutionNav.openLabel": "Ouvrir la navigation latérale", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel": "Retirer tous les utilisateurs", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder": "Recherche", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "Suggérée", "telemetry.callout.appliesSettingTitle": "Les modifications apportées à ce paramètre s'appliquent dans {allOfKibanaText} et sont enregistrées automatiquement.", "telemetry.dataManagementDisclaimerPrivacy": "{optInStatus} Ceci nous permet de savoir ce qui intéresse le plus nos utilisateurs, afin d’améliorer nos produits et services. Consultez notre {privacyStatementLink}.", "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "Découvrez des exemples des {clusterData} et {securityData} que nous collectons.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cf97123016c9d..938c92c1aa10e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5230,7 +5230,6 @@ "sharedUXPackages.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。", "sharedUXPackages.noDataPage.welcomeTitle": "Elastic {solution}へようこそ!", "sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage": "{count, plural, other {#人のユーザーが選択されました}}", "sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "ライブラリから追加", "sharedUXPackages.buttonToolbar.toolbar.errorToolbarText": "120以上のボタンがあります。ボタンの数を制限することを検討してください。", "sharedUXPackages.card.noData.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。", @@ -5284,9 +5283,6 @@ "sharedUXPackages.solutionNav.collapsibleLabel": "サイドナビゲーションを折りたたむ", "sharedUXPackages.solutionNav.menuText": "メニュー", "sharedUXPackages.solutionNav.openLabel": "サイドナビゲーションを開く", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel": "すべてのユーザーを削除", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder": "検索", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "候補", "telemetry.callout.appliesSettingTitle": "この設定に加えた変更は{allOfKibanaText}に適用され、自動的に保存されます。", "telemetry.dataManagementDisclaimerPrivacy": "{optInStatus} これにより、ユーザーが最も関心を持っている項目を把握できるため、製品とサービスを改善できます。{privacyStatementLink}を参照してください。", "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "収集される{clusterData}および{securityData}の例を参照してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 821c07ad58fa1..74f0152cb1dfc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5229,7 +5229,6 @@ "sharedUXPackages.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。", "sharedUXPackages.noDataPage.welcomeTitle": "欢迎使用 Elastic {solution}!", "sharedUXPackages.solutionNav.mobileTitleText": "{solutionName} {menuText}", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.selectedStatusMessage": "{count, plural, other {# 个用户已选择}}", "sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "从库中添加", "sharedUXPackages.buttonToolbar.toolbar.errorToolbarText": "有 120 多个附加按钮。请考虑限制按钮数量。", "sharedUXPackages.card.noData.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。", @@ -5283,9 +5282,6 @@ "sharedUXPackages.solutionNav.collapsibleLabel": "折叠侧边导航", "sharedUXPackages.solutionNav.menuText": "菜单", "sharedUXPackages.solutionNav.openLabel": "打开侧边导航", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.clearButtonLabel": "移除所有用户", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.searchPlaceholder": "搜索", - "sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "已建议", "telemetry.callout.appliesSettingTitle": "对此设置的更改将应用到{allOfKibanaText} 且会自动保存。", "telemetry.dataManagementDisclaimerPrivacy": "{optInStatus} 这便于我们了解用户最感兴趣的内容,以便我们改善产品和服务。请参阅我们的{privacyStatementLink}。", "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和{securityData}示例。", diff --git a/yarn.lock b/yarn.lock index c2b7799faa3f7..28df3eac7c3f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5596,10 +5596,6 @@ version "0.0.0" uid "" -"@kbn/shared-ux-avatar-user-profile-components@link:packages/shared-ux/avatar/user_profile/impl": - version "0.0.0" - uid "" - "@kbn/shared-ux-button-exit-full-screen-mocks@link:packages/shared-ux/button/exit_full_screen/mocks": version "0.0.0" uid "" From 1d66dcad4ca148bbf1ad8ce8c2d933f861e51b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:19:07 +0100 Subject: [PATCH 005/119] [Profiling] Self-managed set up (#168488) Profiling running in a self-managed environment: Screenshot 2023-10-12 at 13 47 30 Screenshot 2023-10-12 at 13 47 42 --------- Co-authored-by: Francesco Gualazzi --- .../e2e/profiling_views/functions.cy.ts | 2 +- x-pack/plugins/profiling/kibana.jsonc | 6 +- x-pack/plugins/profiling/public/services.ts | 2 +- .../server/lib/setup/cluster_settings.ts | 12 +- .../server/lib/setup/fleet_policies.ts | 10 +- .../server/lib/setup/security_role.ts | 2 +- .../profiling/server/lib/setup/types.ts | 13 +- .../plugins/profiling/server/routes/index.ts | 2 +- .../setup/get_cloud_setup_instructions.ts} | 2 +- .../setup/get_self_managed_instructions.ts | 21 ++ .../routes/{setup.ts => setup/route.ts} | 140 +++++++------- .../server/routes/setup/setup_cloud.ts | 39 ++++ .../server/routes/setup/setup_self_managed.ts | 26 +++ x-pack/plugins/profiling/server/types.ts | 8 +- .../utils/create_profiling_es_client.ts | 8 +- .../common/cloud_setup.test.ts | 181 ++++++++++++++++++ .../common/cloud_setup.ts | 75 ++++++++ .../common/cluster_settings.ts | 1 + .../common/fleet_policies.ts | 8 +- .../common/has_profiling_data.ts | 4 +- .../profiling_data_access/common/index.ts | 2 + .../common/profiling_es_client.ts | 2 +- .../common/setup.test.ts | 87 +-------- .../profiling_data_access/common/setup.ts | 54 ++---- .../profiling_data_access/kibana.jsonc | 4 +- .../server/services/register_services.ts | 8 +- .../cloud_setup_state.ts} | 37 ++-- .../server/services/setup_state/index.ts | 77 ++++++++ .../setup_state/self_managed_setup_state.ts | 34 ++++ .../server/services/status/index.ts | 45 +---- .../profiling_data_access/server/types.ts | 4 +- .../utils/create_profiling_es_client.ts | 6 +- .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 35 files changed, 622 insertions(+), 306 deletions(-) rename x-pack/plugins/profiling/server/{lib/setup/get_setup_instructions.ts => routes/setup/get_cloud_setup_instructions.ts} (97%) create mode 100644 x-pack/plugins/profiling/server/routes/setup/get_self_managed_instructions.ts rename x-pack/plugins/profiling/server/routes/{setup.ts => setup/route.ts} (54%) create mode 100644 x-pack/plugins/profiling/server/routes/setup/setup_cloud.ts create mode 100644 x-pack/plugins/profiling/server/routes/setup/setup_self_managed.ts create mode 100644 x-pack/plugins/profiling_data_access/common/cloud_setup.test.ts create mode 100644 x-pack/plugins/profiling_data_access/common/cloud_setup.ts rename x-pack/plugins/profiling_data_access/server/services/{get_setup_state/index.ts => setup_state/cloud_setup_state.ts} (55%) create mode 100644 x-pack/plugins/profiling_data_access/server/services/setup_state/index.ts create mode 100644 x-pack/plugins/profiling_data_access/server/services/setup_state/self_managed_setup_state.ts diff --git a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts index ac678e4650d02..738f800756072 100644 --- a/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts +++ b/x-pack/plugins/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts @@ -10,7 +10,7 @@ import { profilingPerCoreWatt, } from '@kbn/observability-plugin/common'; -describe.skip('Functions page', () => { +describe('Functions page', () => { const rangeFrom = '2023-04-18T00:00:00.000Z'; const rangeTo = '2023-04-18T00:00:30.000Z'; diff --git a/x-pack/plugins/profiling/kibana.jsonc b/x-pack/plugins/profiling/kibana.jsonc index 1eae495f8c85e..aa1ae58a2b190 100644 --- a/x-pack/plugins/profiling/kibana.jsonc +++ b/x-pack/plugins/profiling/kibana.jsonc @@ -9,15 +9,15 @@ "configPath": ["xpack", "profiling"], "optionalPlugins": [ "spaces", - "usageCollection" + "usageCollection", + "cloud", + "fleet" ], "requiredPlugins": [ "charts", - "cloud", "data", "dataViews", "features", - "fleet", "licensing", "observability", "observabilityShared", diff --git a/x-pack/plugins/profiling/public/services.ts b/x-pack/plugins/profiling/public/services.ts index 785179b5bc65c..750e9eab65a96 100644 --- a/x-pack/plugins/profiling/public/services.ts +++ b/x-pack/plugins/profiling/public/services.ts @@ -19,7 +19,7 @@ import type { StorageHostDetailsAPIResponse, } from '../common/storage_explorer'; import { TopNResponse } from '../common/topn'; -import type { SetupDataCollectionInstructions } from '../server/lib/setup/get_setup_instructions'; +import type { SetupDataCollectionInstructions } from '../server/routes/setup/get_cloud_setup_instructions'; import { AutoAbortedHttpService } from './hooks/use_auto_aborted_http_client'; export interface ProfilingSetupStatus { diff --git a/x-pack/plugins/profiling/server/lib/setup/cluster_settings.ts b/x-pack/plugins/profiling/server/lib/setup/cluster_settings.ts index 3d838bc7a5c31..b1b2fb8a24724 100644 --- a/x-pack/plugins/profiling/server/lib/setup/cluster_settings.ts +++ b/x-pack/plugins/profiling/server/lib/setup/cluster_settings.ts @@ -6,7 +6,7 @@ */ import { MAX_BUCKETS } from '@kbn/profiling-data-access-plugin/common'; -import { ProfilingSetupOptions } from './types'; +import { ProfilingSetupOptions } from '@kbn/profiling-data-access-plugin/common/setup'; export async function setMaximumBuckets({ client }: ProfilingSetupOptions) { await client.getEsClient().cluster.putSettings({ @@ -20,14 +20,6 @@ export async function setMaximumBuckets({ client }: ProfilingSetupOptions) { export async function enableResourceManagement({ client }: ProfilingSetupOptions) { await client.getEsClient().cluster.putSettings({ - persistent: { - xpack: { - profiling: { - templates: { - enabled: true, - }, - }, - }, - }, + persistent: { xpack: { profiling: { templates: { enabled: true } } } }, }); } diff --git a/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts b/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts index 326ebc19dd9f1..0342f12eb8284 100644 --- a/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts +++ b/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts @@ -9,12 +9,12 @@ import { fetchFindLatestPackageOrThrow } from '@kbn/fleet-plugin/server/services import { COLLECTOR_PACKAGE_POLICY_NAME, ELASTIC_CLOUD_APM_POLICY, - SYMBOLIZER_PACKAGE_POLICY_NAME, getApmPolicy, + SYMBOLIZER_PACKAGE_POLICY_NAME, } from '@kbn/profiling-data-access-plugin/common'; import { omit } from 'lodash'; import { PackageInputType } from '../..'; -import { ProfilingSetupOptions } from './types'; +import { ProfilingCloudSetupOptions } from './types'; const CLOUD_AGENT_POLICY_ID = 'policy-elastic-agent-on-cloud'; @@ -60,7 +60,7 @@ export async function createCollectorPackagePolicy({ soClient, packagePolicyClient, config, -}: ProfilingSetupOptions) { +}: ProfilingCloudSetupOptions) { const packageName = 'profiler_collector'; const { version } = await fetchFindLatestPackageOrThrow(packageName, { prerelease: true }); const packagePolicy = { @@ -96,7 +96,7 @@ export async function createSymbolizerPackagePolicy({ soClient, packagePolicyClient, config, -}: ProfilingSetupOptions) { +}: ProfilingCloudSetupOptions) { const packageName = 'profiler_symbolizer'; const { version } = await fetchFindLatestPackageOrThrow(packageName, { prerelease: true }); const packagePolicy = { @@ -132,7 +132,7 @@ export async function removeProfilingFromApmPackagePolicy({ client, soClient, packagePolicyClient, -}: ProfilingSetupOptions) { +}: ProfilingCloudSetupOptions) { const apmPackagePolicy = await getApmPolicy({ packagePolicyClient, soClient }); if (!apmPackagePolicy) { throw new Error(`Could not find APM package policy`); diff --git a/x-pack/plugins/profiling/server/lib/setup/security_role.ts b/x-pack/plugins/profiling/server/lib/setup/security_role.ts index b578c2ef2cff6..b48a1d9f63a28 100644 --- a/x-pack/plugins/profiling/server/lib/setup/security_role.ts +++ b/x-pack/plugins/profiling/server/lib/setup/security_role.ts @@ -9,7 +9,7 @@ import { METADATA_VERSION, PROFILING_READER_ROLE_NAME, } from '@kbn/profiling-data-access-plugin/common'; -import { ProfilingSetupOptions } from './types'; +import { ProfilingSetupOptions } from '@kbn/profiling-data-access-plugin/common/setup'; export async function setSecurityRole({ client }: ProfilingSetupOptions) { const esClient = client.getEsClient(); diff --git a/x-pack/plugins/profiling/server/lib/setup/types.ts b/x-pack/plugins/profiling/server/lib/setup/types.ts index 40001649f861c..0ef5a5a4dd826 100644 --- a/x-pack/plugins/profiling/server/lib/setup/types.ts +++ b/x-pack/plugins/profiling/server/lib/setup/types.ts @@ -5,18 +5,9 @@ * 2.0. */ -import { SavedObjectsClientContract } from '@kbn/core/server'; -import { PackagePolicyClient } from '@kbn/fleet-plugin/server'; -import { Logger } from '@kbn/logging'; +import { ProfilingCloudSetupOptions as BaseProfilingCloudSetupOptions } from '@kbn/profiling-data-access-plugin/common'; import { ProfilingConfig } from '../..'; -import { ProfilingESClient } from '../../utils/create_profiling_es_client'; -export interface ProfilingSetupOptions { - client: ProfilingESClient; - soClient: SavedObjectsClientContract; - packagePolicyClient: PackagePolicyClient; - logger: Logger; - spaceId: string; - isCloudEnabled: boolean; +export interface ProfilingCloudSetupOptions extends BaseProfilingCloudSetupOptions { config: ProfilingConfig; } diff --git a/x-pack/plugins/profiling/server/routes/index.ts b/x-pack/plugins/profiling/server/routes/index.ts index d4b75e3cc1fa5..14473897720d9 100644 --- a/x-pack/plugins/profiling/server/routes/index.ts +++ b/x-pack/plugins/profiling/server/routes/index.ts @@ -18,7 +18,7 @@ import { import { ProfilingESClient } from '../utils/create_profiling_es_client'; import { registerFlameChartSearchRoute } from './flamechart'; import { registerTopNFunctionsSearchRoute } from './functions'; -import { registerSetupRoute } from './setup'; +import { registerSetupRoute } from './setup/route'; import { registerStorageExplorerRoute } from './storage_explorer/route'; import { registerTraceEventsTopNContainersSearchRoute, diff --git a/x-pack/plugins/profiling/server/lib/setup/get_setup_instructions.ts b/x-pack/plugins/profiling/server/routes/setup/get_cloud_setup_instructions.ts similarity index 97% rename from x-pack/plugins/profiling/server/lib/setup/get_setup_instructions.ts rename to x-pack/plugins/profiling/server/routes/setup/get_cloud_setup_instructions.ts index 64b857f93de27..5fd6513e15dff 100644 --- a/x-pack/plugins/profiling/server/lib/setup/get_setup_instructions.ts +++ b/x-pack/plugins/profiling/server/routes/setup/get_cloud_setup_instructions.ts @@ -25,7 +25,7 @@ export interface SetupDataCollectionInstructions { stackVersion: string; } -export async function getSetupInstructions({ +export async function getCloudSetupInstructions({ packagePolicyClient, soClient, apmServerHost, diff --git a/x-pack/plugins/profiling/server/routes/setup/get_self_managed_instructions.ts b/x-pack/plugins/profiling/server/routes/setup/get_self_managed_instructions.ts new file mode 100644 index 0000000000000..edd62bbc85aef --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/setup/get_self_managed_instructions.ts @@ -0,0 +1,21 @@ +/* + * 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 { SetupDataCollectionInstructions } from './get_cloud_setup_instructions'; + +export function getSelfManagedInstructions({ + stackVersion, +}: { + stackVersion: string; +}): SetupDataCollectionInstructions { + return { + collector: { host: '', secretToken: '' }, + profilerAgent: { version: '' }, + symbolizer: { host: '' }, + stackVersion, + }; +} diff --git a/x-pack/plugins/profiling/server/routes/setup.ts b/x-pack/plugins/profiling/server/routes/setup/route.ts similarity index 54% rename from x-pack/plugins/profiling/server/routes/setup.ts rename to x-pack/plugins/profiling/server/routes/setup/route.ts index 45a5f9d691307..5ee297ee68791 100644 --- a/x-pack/plugins/profiling/server/routes/setup.ts +++ b/x-pack/plugins/profiling/server/routes/setup/route.ts @@ -6,18 +6,15 @@ */ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; -import { RouteRegisterParameters } from '.'; -import { getRoutePaths } from '../../common'; -import { enableResourceManagement, setMaximumBuckets } from '../lib/setup/cluster_settings'; -import { - createCollectorPackagePolicy, - createSymbolizerPackagePolicy, - removeProfilingFromApmPackagePolicy, -} from '../lib/setup/fleet_policies'; -import { getSetupInstructions } from '../lib/setup/get_setup_instructions'; -import { setSecurityRole } from '../lib/setup/security_role'; -import { handleRouteHandlerError } from '../utils/handle_route_error_handler'; -import { getClient } from './compat'; +import { ProfilingSetupOptions } from '@kbn/profiling-data-access-plugin/common/setup'; +import { RouteRegisterParameters } from '..'; +import { getRoutePaths } from '../../../common'; +import { getCloudSetupInstructions } from './get_cloud_setup_instructions'; +import { handleRouteHandlerError } from '../../utils/handle_route_error_handler'; +import { getClient } from '../compat'; +import { setupCloud } from './setup_cloud'; +import { setupSelfManaged } from './setup_self_managed'; +import { getSelfManagedInstructions } from './get_self_managed_instructions'; export function registerSetupRoute({ router, @@ -55,7 +52,7 @@ export function registerSetupRoute({ } } ); - // Set up Elasticsearch and Fleet for Universal Profiling + router.post( { path: paths.HasSetupESResources, @@ -64,19 +61,6 @@ export function registerSetupRoute({ }, async (context, request, response) => { try { - const isCloudEnabled = dependencies.setup.cloud.isCloudEnabled; - - if (!isCloudEnabled) { - const msg = `Elastic Cloud is required to set up Elasticsearch and Fleet for Universal Profiling`; - logger.error(msg); - return response.custom({ - statusCode: 500, - body: { - message: msg, - }, - }); - } - const esClient = await getClient(context); const core = await context.core; const clientWithDefaultAuth = createProfilingEsClient({ @@ -90,45 +74,59 @@ export function registerSetupRoute({ useDefaultAuth: false, }); - const commonParams = { + const commonSetupParams: ProfilingSetupOptions = { client: clientWithDefaultAuth, + clientWithProfilingAuth, logger, - packagePolicyClient: dependencies.start.fleet.packagePolicyService, soClient: core.savedObjects.client, spaceId: dependencies.setup.spaces?.spacesService?.getSpaceId(request) ?? DEFAULT_SPACE_ID, - isCloudEnabled, }; - const setupState = await dependencies.start.profilingDataAccess.services.getSetupState( - commonParams, - clientWithProfilingAuth - ); - - const executeAdminFunctions = [ - ...(setupState.resource_management.enabled ? [] : [enableResourceManagement]), - ...(setupState.permissions.configured ? [] : [setSecurityRole]), - ...(setupState.settings.configured ? [] : [setMaximumBuckets]), - ]; - - const executeViewerFunctions = [ - ...(setupState.policies.collector.installed ? [] : [createCollectorPackagePolicy]), - ...(setupState.policies.symbolizer.installed ? [] : [createSymbolizerPackagePolicy]), - ...(setupState.policies.apm.profilingEnabled - ? [removeProfilingFromApmPackagePolicy] - : []), - ]; - - if (!executeAdminFunctions.length && !executeViewerFunctions.length) { - return response.ok(); + const { type, setupState } = + await dependencies.start.profilingDataAccess.services.getSetupState({ + esClient, + soClient: core.savedObjects.client, + spaceId: + dependencies.setup.spaces?.spacesService?.getSpaceId(request) ?? DEFAULT_SPACE_ID, + }); + + const isCloudEnabled = dependencies.setup.cloud?.isCloudEnabled; + if (isCloudEnabled && type === 'cloud') { + if (!dependencies.start.fleet) { + const msg = `Elastic Fleet is required to set up Universal Profiling on Cloud`; + logger.error(msg); + return response.custom({ + statusCode: 500, + body: { message: msg }, + }); + } + logger.debug('Setting up Universal Profiling on Cloud'); + + await setupCloud({ + setupState, + setupParams: { + ...commonSetupParams, + packagePolicyClient: dependencies.start.fleet.packagePolicyService, + isCloudEnabled, + config: dependencies.config, + }, + }); + + logger.debug('[DONE] Setting up Universal Profiling on Cloud'); + } else { + logger.debug('Setting up self-managed Universal Profiling'); + + await setupSelfManaged({ + setupState, + setupParams: commonSetupParams, + }); + + logger.debug('[DONE] Setting up self-managed Universal Profiling'); } - const setupParams = { - ...commonParams, - config: dependencies.config, - }; - await Promise.all(executeAdminFunctions.map((fn) => fn(setupParams))); - await Promise.all(executeViewerFunctions.map((fn) => fn(setupParams))); + // Wait until Profiling ES plugin creates all resources + await clientWithDefaultAuth.profilingStatus({ waitForResourcesCreated: true }); if (dependencies.telemetryUsageCounter) { dependencies.telemetryUsageCounter.incrementCounter({ @@ -166,16 +164,30 @@ export function registerSetupRoute({ }, async (context, request, response) => { try { - const apmServerHost = dependencies.setup.cloud?.apm?.url; const stackVersion = dependencies.stackVersion; - const setupInstructions = await getSetupInstructions({ - packagePolicyClient: dependencies.start.fleet.packagePolicyService, - soClient: (await context.core).savedObjects.client, - apmServerHost, - stackVersion, - }); + const isCloudEnabled = dependencies.setup.cloud?.isCloudEnabled; + if (isCloudEnabled) { + if (!dependencies.start.fleet) { + const msg = `Elastic Fleet is required to set up Universal Profiling on Cloud`; + logger.error(msg); + return response.custom({ + statusCode: 500, + body: { message: msg }, + }); + } + + const apmServerHost = dependencies.setup.cloud?.apm?.url; + const setupInstructions = await getCloudSetupInstructions({ + packagePolicyClient: dependencies.start.fleet?.packagePolicyService, + soClient: (await context.core).savedObjects.client, + apmServerHost, + stackVersion, + }); + + return response.ok({ body: setupInstructions }); + } - return response.ok({ body: setupInstructions }); + return response.ok({ body: getSelfManagedInstructions({ stackVersion }) }); } catch (error) { return handleRouteHandlerError({ error, diff --git a/x-pack/plugins/profiling/server/routes/setup/setup_cloud.ts b/x-pack/plugins/profiling/server/routes/setup/setup_cloud.ts new file mode 100644 index 0000000000000..c4978710991ce --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/setup/setup_cloud.ts @@ -0,0 +1,39 @@ +/* + * 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 { CloudSetupState } from '@kbn/profiling-data-access-plugin/common/cloud_setup'; +import { enableResourceManagement, setMaximumBuckets } from '../../lib/setup/cluster_settings'; +import { + createCollectorPackagePolicy, + createSymbolizerPackagePolicy, + removeProfilingFromApmPackagePolicy, +} from '../../lib/setup/fleet_policies'; +import { setSecurityRole } from '../../lib/setup/security_role'; +import { ProfilingCloudSetupOptions } from '../../lib/setup/types'; + +export async function setupCloud({ + setupState, + setupParams, +}: { + setupState: CloudSetupState; + setupParams: ProfilingCloudSetupOptions; +}) { + const executeAdminFunctions = [ + ...(setupState.resource_management.enabled ? [] : [enableResourceManagement]), + ...(setupState.permissions.configured ? [] : [setSecurityRole]), + ...(setupState.settings.configured ? [] : [setMaximumBuckets]), + ]; + + const executeViewerFunctions = [ + ...(setupState.policies.collector.installed ? [] : [createCollectorPackagePolicy]), + ...(setupState.policies.symbolizer.installed ? [] : [createSymbolizerPackagePolicy]), + ...(setupState.policies.apm.profilingEnabled ? [removeProfilingFromApmPackagePolicy] : []), + ]; + // Give priority to admin functions as if something fails we won't procceed to viewer functions + await Promise.all(executeAdminFunctions.map((fn) => fn(setupParams))); + await Promise.all(executeViewerFunctions.map((fn) => fn(setupParams))); +} diff --git a/x-pack/plugins/profiling/server/routes/setup/setup_self_managed.ts b/x-pack/plugins/profiling/server/routes/setup/setup_self_managed.ts new file mode 100644 index 0000000000000..c82721780cd0c --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/setup/setup_self_managed.ts @@ -0,0 +1,26 @@ +/* + * 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 { ProfilingSetupOptions, SetupState } from '@kbn/profiling-data-access-plugin/common/setup'; +import { enableResourceManagement, setMaximumBuckets } from '../../lib/setup/cluster_settings'; +import { setSecurityRole } from '../../lib/setup/security_role'; + +export async function setupSelfManaged({ + setupState, + setupParams, +}: { + setupState: SetupState; + setupParams: ProfilingSetupOptions; +}) { + const executeFunctions = [ + ...(setupState.resource_management.enabled ? [] : [enableResourceManagement]), + ...(setupState.permissions.configured ? [] : [setSecurityRole]), + ...(setupState.settings.configured ? [] : [setMaximumBuckets]), + ]; + + await Promise.all(executeFunctions.map((fn) => fn(setupParams))); +} diff --git a/x-pack/plugins/profiling/server/types.ts b/x-pack/plugins/profiling/server/types.ts index 6ee94e238effa..24705921bbbf9 100644 --- a/x-pack/plugins/profiling/server/types.ts +++ b/x-pack/plugins/profiling/server/types.ts @@ -20,8 +20,8 @@ import { export interface ProfilingPluginSetupDeps { observability: ObservabilityPluginSetup; features: FeaturesPluginSetup; - cloud: CloudSetup; - fleet: FleetSetupContract; + cloud?: CloudSetup; + fleet?: FleetSetupContract; spaces?: SpacesPluginSetup; usageCollection?: UsageCollectionSetup; profilingDataAccess: ProfilingDataAccessPluginSetup; @@ -30,8 +30,8 @@ export interface ProfilingPluginSetupDeps { export interface ProfilingPluginStartDeps { observability: {}; features: {}; - cloud: CloudStart; - fleet: FleetStartContract; + cloud?: CloudStart; + fleet?: FleetStartContract; spaces?: SpacesPluginStart; profilingDataAccess: ProfilingDataAccessPluginStart; } diff --git a/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts b/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts index 76ea2788fea57..f085a89b2f3db 100644 --- a/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts +++ b/x-pack/plugins/profiling/server/utils/create_profiling_es_client.ts @@ -38,7 +38,7 @@ export interface ProfilingESClient { query: QueryDslQueryContainer; sampleSize: number; }): Promise; - profilingStatus(): Promise; + profilingStatus(params?: { waitForResourcesCreated?: boolean }): Promise; getEsClient(): ElasticsearchClient; profilingFlamegraph({}: { query: QueryDslQueryContainer; @@ -101,7 +101,7 @@ export function createProfilingEsClient({ return unwrapEsResponse(promise) as Promise; }, - profilingStatus() { + profilingStatus({ waitForResourcesCreated = false } = {}) { const controller = new AbortController(); const promise = withProfilingSpan('_profiling/status', () => { @@ -109,7 +109,9 @@ export function createProfilingEsClient({ esClient.transport.request( { method: 'GET', - path: encodeURI('/_profiling/status'), + path: encodeURI( + `/_profiling/status?wait_for_resources_created=${waitForResourcesCreated}` + ), }, { signal: controller.signal, diff --git a/x-pack/plugins/profiling_data_access/common/cloud_setup.test.ts b/x-pack/plugins/profiling_data_access/common/cloud_setup.test.ts new file mode 100644 index 0000000000000..1d99c6346c4c6 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/common/cloud_setup.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { + areCloudResourcesSetup, + createDefaultCloudSetupState, + PartialCloudSetupState, +} from './cloud_setup'; +import { mergePartialSetupStates } from './setup'; + +const createCloudState = (available: boolean): PartialCloudSetupState => ({ cloud: { available } }); +const createDataState = (available: boolean): PartialCloudSetupState => ({ data: { available } }); +const createPermissionState = (configured: boolean): PartialCloudSetupState => ({ + permissions: { configured }, +}); +const createCollectorPolicyState = (installed: boolean): PartialCloudSetupState => ({ + policies: { collector: { installed } }, +}); +const createSymbolizerPolicyState = (installed: boolean): PartialCloudSetupState => ({ + policies: { symbolizer: { installed } }, +}); +const createProfilingInApmPolicyState = (profilingEnabled: boolean): PartialCloudSetupState => ({ + policies: { apm: { profilingEnabled } }, +}); + +function createResourceState({ + enabled, + created, +}: { + enabled: boolean; + created: boolean; +}): PartialCloudSetupState { + return { + resource_management: { + enabled, + }, + resources: { + created, + }, + }; +} + +function createSettingsState(configured: boolean): PartialCloudSetupState { + return { + settings: { + configured, + }, + }; +} + +describe('Merging partial state operations', () => { + const defaultSetupState = createDefaultCloudSetupState(); + + it('returns partial states with missing key', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCloudState(true), + createDataState(true), + ]); + + expect(mergedState.cloud.available).toEqual(true); + expect(mergedState.cloud.required).toEqual(true); + expect(mergedState.data.available).toEqual(true); + }); + + it('should deeply nested partial states with overlap', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + ]); + + expect(mergedState.policies.collector.installed).toEqual(true); + expect(mergedState.policies.symbolizer.installed).toEqual(true); + }); + it('returns false when permission is not configured', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(false), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); + + it('returns false when resource management is not enabled', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), + createResourceState({ enabled: false, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); + + it('returns false when resources are not created', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), + createResourceState({ enabled: true, created: false }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); + + it('returns false when settings are not configured', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), + createResourceState({ enabled: true, created: true }), + createSettingsState(false), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); + + it('returns true when all checks are valid', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(false), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeTruthy(); + }); + + it('returns false when collector is not found', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(false), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(false), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); + + it('returns false when symbolizer is not found', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(false), + createProfilingInApmPolicyState(false), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); + + it('returns false when profiling is in APM server', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areCloudResourcesSetup(mergedState)).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/profiling_data_access/common/cloud_setup.ts b/x-pack/plugins/profiling_data_access/common/cloud_setup.ts new file mode 100644 index 0000000000000..1c03451cbd2b2 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/common/cloud_setup.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { RecursivePartial } from '@elastic/eui'; +import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import { + areResourcesSetup, + createDefaultSetupState, + ProfilingSetupOptions, + SetupState, +} from './setup'; + +export interface ProfilingCloudSetupOptions extends ProfilingSetupOptions { + packagePolicyClient: PackagePolicyClient; + isCloudEnabled: boolean; +} + +export interface CloudSetupStateType { + type: 'cloud'; + setupState: CloudSetupState; +} + +export interface CloudSetupState extends SetupState { + cloud: { + available: boolean; + required: boolean; + }; + policies: { + collector: { + installed: boolean; + }; + symbolizer: { + installed: boolean; + }; + apm: { + profilingEnabled: boolean; + }; + }; +} + +export type PartialCloudSetupState = RecursivePartial; + +export function createDefaultCloudSetupState(): CloudSetupState { + const defaultSetupState = createDefaultSetupState(); + return { + cloud: { + available: false, + required: true, + }, + policies: { + collector: { + installed: false, + }, + symbolizer: { + installed: false, + }, + apm: { + profilingEnabled: false, + }, + }, + ...defaultSetupState, + }; +} + +export function areCloudResourcesSetup(state: CloudSetupState): boolean { + return ( + areResourcesSetup(state) && + state.policies.collector.installed && + state.policies.symbolizer.installed && + !state.policies.apm.profilingEnabled + ); +} diff --git a/x-pack/plugins/profiling_data_access/common/cluster_settings.ts b/x-pack/plugins/profiling_data_access/common/cluster_settings.ts index a1e92ab1c996f..e1e65330f47d4 100644 --- a/x-pack/plugins/profiling_data_access/common/cluster_settings.ts +++ b/x-pack/plugins/profiling_data_access/common/cluster_settings.ts @@ -30,6 +30,7 @@ export async function validateResourceManagement({ enabled: statusResponse.resource_management.enabled, }, resources: { + // If the flag is true, that means that all index templates / data streams and indices have been created created: statusResponse.resources.created, pre_8_9_1_data: statusResponse.resources.pre_8_9_1_data, }, diff --git a/x-pack/plugins/profiling_data_access/common/fleet_policies.ts b/x-pack/plugins/profiling_data_access/common/fleet_policies.ts index d3a9b51dd55d5..ad599bb1e2551 100644 --- a/x-pack/plugins/profiling_data_access/common/fleet_policies.ts +++ b/x-pack/plugins/profiling_data_access/common/fleet_policies.ts @@ -9,7 +9,7 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PackagePolicy } from '@kbn/fleet-plugin/common'; import { getApmPolicy } from './get_apm_policy'; -import { PartialSetupState, ProfilingSetupOptions } from './setup'; +import { PartialCloudSetupState, ProfilingCloudSetupOptions } from './cloud_setup'; export const COLLECTOR_PACKAGE_POLICY_NAME = 'elastic-universal-profiling-collector'; export const SYMBOLIZER_PACKAGE_POLICY_NAME = 'elastic-universal-profiling-symbolizer'; @@ -46,7 +46,7 @@ export async function getCollectorPolicy({ export async function validateCollectorPackagePolicy({ soClient, packagePolicyClient, -}: ProfilingSetupOptions): Promise { +}: ProfilingCloudSetupOptions): Promise { const collectorPolicy = await getCollectorPolicy({ soClient, packagePolicyClient }); return { policies: { collector: { installed: !!collectorPolicy } } }; } @@ -80,7 +80,7 @@ export async function getSymbolizerPolicy({ export async function validateSymbolizerPackagePolicy({ soClient, packagePolicyClient, -}: ProfilingSetupOptions): Promise { +}: ProfilingCloudSetupOptions): Promise { const symbolizerPackagePolicy = await getSymbolizerPolicy({ soClient, packagePolicyClient }); return { policies: { symbolizer: { installed: !!symbolizerPackagePolicy } } }; } @@ -88,7 +88,7 @@ export async function validateSymbolizerPackagePolicy({ export async function validateProfilingInApmPackagePolicy({ soClient, packagePolicyClient, -}: ProfilingSetupOptions): Promise { +}: ProfilingCloudSetupOptions): Promise { try { const apmPolicy = await getApmPolicy({ packagePolicyClient, soClient }); return { diff --git a/x-pack/plugins/profiling_data_access/common/has_profiling_data.ts b/x-pack/plugins/profiling_data_access/common/has_profiling_data.ts index 13ed8a2830543..1f62b6e1510a9 100644 --- a/x-pack/plugins/profiling_data_access/common/has_profiling_data.ts +++ b/x-pack/plugins/profiling_data_access/common/has_profiling_data.ts @@ -8,9 +8,9 @@ import { PartialSetupState, ProfilingSetupOptions } from './setup'; export async function hasProfilingData({ - client, + clientWithProfilingAuth, }: ProfilingSetupOptions): Promise { - const hasProfilingDataResponse = await client.search('has_any_profiling_data', { + const hasProfilingDataResponse = await clientWithProfilingAuth.search('has_any_profiling_data', { index: 'profiling*', size: 0, track_total_hits: 1, diff --git a/x-pack/plugins/profiling_data_access/common/index.ts b/x-pack/plugins/profiling_data_access/common/index.ts index 8e654f7a85144..8482620dcb474 100644 --- a/x-pack/plugins/profiling_data_access/common/index.ts +++ b/x-pack/plugins/profiling_data_access/common/index.ts @@ -14,3 +14,5 @@ export { COLLECTOR_PACKAGE_POLICY_NAME, SYMBOLIZER_PACKAGE_POLICY_NAME, } from './fleet_policies'; + +export type { ProfilingCloudSetupOptions } from './cloud_setup'; diff --git a/x-pack/plugins/profiling_data_access/common/profiling_es_client.ts b/x-pack/plugins/profiling_data_access/common/profiling_es_client.ts index 820aefea45090..ae25dfe57b3cd 100644 --- a/x-pack/plugins/profiling_data_access/common/profiling_es_client.ts +++ b/x-pack/plugins/profiling_data_access/common/profiling_es_client.ts @@ -23,7 +23,7 @@ export interface ProfilingESClient { query: QueryDslQueryContainer; sampleSize: number; }): Promise; - profilingStatus(): Promise; + profilingStatus(params?: { waitForResourcesCreated?: boolean }): Promise; getEsClient(): ElasticsearchClient; profilingFlamegraph({}: { query: QueryDslQueryContainer; diff --git a/x-pack/plugins/profiling_data_access/common/setup.test.ts b/x-pack/plugins/profiling_data_access/common/setup.test.ts index 48b8136e39020..01826ac7fa913 100644 --- a/x-pack/plugins/profiling_data_access/common/setup.test.ts +++ b/x-pack/plugins/profiling_data_access/common/setup.test.ts @@ -6,26 +6,16 @@ */ import { - areResourcesSetup, - createDefaultSetupState, mergePartialSetupStates, PartialSetupState, + areResourcesSetup, + createDefaultSetupState, } from './setup'; -const createCloudState = (available: boolean): PartialSetupState => ({ cloud: { available } }); const createDataState = (available: boolean): PartialSetupState => ({ data: { available } }); const createPermissionState = (configured: boolean): PartialSetupState => ({ permissions: { configured }, }); -const createCollectorPolicyState = (installed: boolean): PartialSetupState => ({ - policies: { collector: { installed } }, -}); -const createSymbolizerPolicyState = (installed: boolean): PartialSetupState => ({ - policies: { symbolizer: { installed } }, -}); -const createProfilingInApmPolicyState = (profilingEnabled: boolean): PartialSetupState => ({ - policies: { apm: { profilingEnabled } }, -}); function createResourceState({ enabled, @@ -56,30 +46,24 @@ describe('Merging partial state operations', () => { const defaultSetupState = createDefaultSetupState(); it('returns partial states with missing key', () => { - const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCloudState(true), - createDataState(true), - ]); - - expect(mergedState.cloud.available).toEqual(true); - expect(mergedState.cloud.required).toEqual(true); + const mergedState = mergePartialSetupStates(defaultSetupState, [createDataState(true)]); expect(mergedState.data.available).toEqual(true); + expect(mergedState.settings.configured).toEqual(false); + expect(mergedState.permissions.configured).toEqual(false); + expect(mergedState.resources.created).toEqual(false); }); it('should deeply nested partial states with overlap', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), + createResourceState({ created: true, enabled: true }), ]); - expect(mergedState.policies.collector.installed).toEqual(true); - expect(mergedState.policies.symbolizer.installed).toEqual(true); + expect(mergedState.resource_management.enabled).toEqual(true); + expect(mergedState.resources.created).toEqual(true); }); + it('returns false when permission is not configured', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(true), createResourceState({ enabled: true, created: true }), createSettingsState(true), createPermissionState(false), @@ -90,9 +74,6 @@ describe('Merging partial state operations', () => { it('returns false when resource management is not enabled', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(true), createResourceState({ enabled: false, created: true }), createSettingsState(true), createPermissionState(true), @@ -103,9 +84,6 @@ describe('Merging partial state operations', () => { it('returns false when resources are not created', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(true), createResourceState({ enabled: true, created: false }), createSettingsState(true), createPermissionState(true), @@ -116,9 +94,6 @@ describe('Merging partial state operations', () => { it('returns false when settings are not configured', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(true), createResourceState({ enabled: true, created: true }), createSettingsState(false), createPermissionState(true), @@ -129,9 +104,6 @@ describe('Merging partial state operations', () => { it('returns true when all checks are valid', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(false), createResourceState({ enabled: true, created: true }), createSettingsState(true), createPermissionState(true), @@ -139,43 +111,4 @@ describe('Merging partial state operations', () => { expect(areResourcesSetup(mergedState)).toBeTruthy(); }); - - it('returns false when collector is not found', () => { - const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(false), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(false), - createResourceState({ enabled: true, created: true }), - createSettingsState(true), - createPermissionState(true), - ]); - - expect(areResourcesSetup(mergedState)).toBeFalsy(); - }); - - it('returns false when symbolizer is not found', () => { - const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(false), - createProfilingInApmPolicyState(false), - createResourceState({ enabled: true, created: true }), - createSettingsState(true), - createPermissionState(true), - ]); - - expect(areResourcesSetup(mergedState)).toBeFalsy(); - }); - - it('returns false when profiling is in APM server', () => { - const mergedState = mergePartialSetupStates(defaultSetupState, [ - createCollectorPolicyState(true), - createSymbolizerPolicyState(true), - createProfilingInApmPolicyState(true), - createResourceState({ enabled: true, created: true }), - createSettingsState(true), - createPermissionState(true), - ]); - - expect(areResourcesSetup(mergedState)).toBeFalsy(); - }); }); diff --git a/x-pack/plugins/profiling_data_access/common/setup.ts b/x-pack/plugins/profiling_data_access/common/setup.ts index d3411ea9ee020..934c425ed0af9 100644 --- a/x-pack/plugins/profiling_data_access/common/setup.ts +++ b/x-pack/plugins/profiling_data_access/common/setup.ts @@ -4,43 +4,31 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { RecursivePartial } from '@elastic/eui'; +import { RecursivePartial } from '@elastic/eui'; import { Logger, SavedObjectsClientContract } from '@kbn/core/server'; -import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import { merge } from 'lodash'; import { ProfilingESClient } from './profiling_es_client'; export interface ProfilingSetupOptions { client: ProfilingESClient; + clientWithProfilingAuth: ProfilingESClient; soClient: SavedObjectsClientContract; - packagePolicyClient: PackagePolicyClient; logger: Logger; spaceId: string; - isCloudEnabled: boolean; +} + +export interface SetupStateType { + type: 'self-managed'; + setupState: SetupState; } export interface SetupState { - cloud: { - available: boolean; - required: boolean; - }; data: { available: boolean; }; permissions: { configured: boolean; }; - policies: { - collector: { - installed: boolean; - }; - symbolizer: { - installed: boolean; - }; - apm: { - profilingEnabled: boolean; - }; - }; resource_management: { enabled: boolean; }; @@ -57,27 +45,12 @@ export type PartialSetupState = RecursivePartial; export function createDefaultSetupState(): SetupState { return { - cloud: { - available: false, - required: true, - }, data: { available: false, }, permissions: { configured: false, }, - policies: { - collector: { - installed: false, - }, - symbolizer: { - installed: false, - }, - apm: { - profilingEnabled: false, - }, - }, resource_management: { enabled: false, }, @@ -93,9 +66,6 @@ export function createDefaultSetupState(): SetupState { export function areResourcesSetup(state: SetupState): boolean { return ( - state.policies.collector.installed && - state.policies.symbolizer.installed && - !state.policies.apm.profilingEnabled && state.resource_management.enabled && state.resources.created && state.permissions.configured && @@ -107,9 +77,9 @@ function mergeRecursivePartial(base: T, partial: RecursivePartial): T { return merge(base, partial); } -export function mergePartialSetupStates( - base: SetupState, - partials: PartialSetupState[] -): SetupState { - return partials.reduce(mergeRecursivePartial, base); +export function mergePartialSetupStates( + base: T, + partials: Array> +): T { + return partials.reduce(mergeRecursivePartial, base); } diff --git a/x-pack/plugins/profiling_data_access/kibana.jsonc b/x-pack/plugins/profiling_data_access/kibana.jsonc index 654cb93a0460c..a6bcd9f7ecff4 100644 --- a/x-pack/plugins/profiling_data_access/kibana.jsonc +++ b/x-pack/plugins/profiling_data_access/kibana.jsonc @@ -9,10 +9,8 @@ "configPath": ["xpack", "profiling"], "requiredPlugins": [ "data", - "fleet", - "cloud" ], - "optionalPlugins": [], + "optionalPlugins": ["cloud", "fleet"], "requiredBundles": [] } } diff --git a/x-pack/plugins/profiling_data_access/server/services/register_services.ts b/x-pack/plugins/profiling_data_access/server/services/register_services.ts index 095f8e1826794..60f582ac16b7e 100644 --- a/x-pack/plugins/profiling_data_access/server/services/register_services.ts +++ b/x-pack/plugins/profiling_data_access/server/services/register_services.ts @@ -10,9 +10,9 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { FleetStartContract } from '@kbn/fleet-plugin/server'; import { createFetchFlamechart } from './fetch_flamechart'; import { createGetStatusService } from './status'; -import { createGetSetupState } from './get_setup_state'; import { ProfilingESClient } from '../../common/profiling_es_client'; import { createFetchFunctions } from './functions'; +import { createSetupState } from './setup_state'; export interface RegisterServicesParams { createProfilingEsClient: (params: { @@ -21,8 +21,8 @@ export interface RegisterServicesParams { }) => ProfilingESClient; logger: Logger; deps: { - fleet: FleetStartContract; - cloud: CloudStart; + fleet?: FleetStartContract; + cloud?: CloudStart; }; } @@ -30,7 +30,7 @@ export function registerServices(params: RegisterServicesParams) { return { fetchFlamechartData: createFetchFlamechart(params), getStatus: createGetStatusService(params), - getSetupState: createGetSetupState(params), + getSetupState: createSetupState(params), fetchFunction: createFetchFunctions(params), }; } diff --git a/x-pack/plugins/profiling_data_access/server/services/get_setup_state/index.ts b/x-pack/plugins/profiling_data_access/server/services/setup_state/cloud_setup_state.ts similarity index 55% rename from x-pack/plugins/profiling_data_access/server/services/get_setup_state/index.ts rename to x-pack/plugins/profiling_data_access/server/services/setup_state/cloud_setup_state.ts index 9110883900c27..ed05677d21dfb 100644 --- a/x-pack/plugins/profiling_data_access/server/services/get_setup_state/index.ts +++ b/x-pack/plugins/profiling_data_access/server/services/setup_state/cloud_setup_state.ts @@ -5,6 +5,9 @@ * 2.0. */ +import { RecursivePartial } from '@elastic/eui'; +import { ProfilingCloudSetupOptions } from '../../../common'; +import { CloudSetupState, createDefaultCloudSetupState } from '../../../common/cloud_setup'; import { validateMaximumBuckets, validateResourceManagement, @@ -15,21 +18,14 @@ import { validateSymbolizerPackagePolicy, } from '../../../common/fleet_policies'; import { hasProfilingData } from '../../../common/has_profiling_data'; -import { ProfilingESClient } from '../../../common/profiling_es_client'; import { validateSecurityRole } from '../../../common/security_role'; -import { - ProfilingSetupOptions, - createDefaultSetupState, - mergePartialSetupStates, -} from '../../../common/setup'; -import { RegisterServicesParams } from '../register_services'; +import { mergePartialSetupStates } from '../../../common/setup'; -export async function getSetupState( - options: ProfilingSetupOptions, - clientWithProfilingAuth: ProfilingESClient -) { - const state = createDefaultSetupState(); - state.cloud.available = options.isCloudEnabled; +export async function cloudSetupState( + params: ProfilingCloudSetupOptions +): Promise { + const state = createDefaultCloudSetupState(); + state.cloud.available = params.isCloudEnabled; const verifyFunctions = [ validateMaximumBuckets, @@ -38,19 +34,12 @@ export async function getSetupState( validateCollectorPackagePolicy, validateSymbolizerPackagePolicy, validateProfilingInApmPackagePolicy, + hasProfilingData, ]; - const partialStates = await Promise.all([ - ...verifyFunctions.map((fn) => fn(options)), - hasProfilingData({ - ...options, - client: clientWithProfilingAuth, - }), - ]); + const partialStates = (await Promise.all(verifyFunctions.map((fn) => fn(params)))) as Array< + RecursivePartial + >; return mergePartialSetupStates(state, partialStates); } - -export function createGetSetupState(params: RegisterServicesParams) { - return getSetupState; -} diff --git a/x-pack/plugins/profiling_data_access/server/services/setup_state/index.ts b/x-pack/plugins/profiling_data_access/server/services/setup_state/index.ts new file mode 100644 index 0000000000000..99d81ab771793 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/services/setup_state/index.ts @@ -0,0 +1,77 @@ +/* + * 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 { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import { CloudSetupStateType } from '../../../common/cloud_setup'; +import { SetupStateType } from '../../../common/setup'; +import { RegisterServicesParams } from '../register_services'; +import { cloudSetupState } from './cloud_setup_state'; +import { selfManagedSetupState } from './self_managed_setup_state'; + +export interface SetupStateParams { + soClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; + spaceId?: string; +} + +export async function getSetupState({ + createProfilingEsClient, + deps, + esClient, + logger, + soClient, + spaceId, +}: RegisterServicesParams & SetupStateParams): Promise { + const clientWithDefaultAuth = createProfilingEsClient({ + esClient, + useDefaultAuth: true, + }); + const clientWithProfilingAuth = createProfilingEsClient({ + esClient, + useDefaultAuth: false, + }); + + const isCloudEnabled = deps.cloud?.isCloudEnabled; + if (isCloudEnabled) { + if (!deps.fleet) { + throw new Error('Elastic Fleet is required to set up Universal Profiling on Cloud'); + } + + const setupState = await cloudSetupState({ + client: clientWithDefaultAuth, + clientWithProfilingAuth, + logger, + soClient, + spaceId: spaceId ?? DEFAULT_SPACE_ID, + packagePolicyClient: deps.fleet.packagePolicyService, + isCloudEnabled, + }); + + return { + type: 'cloud', + setupState, + }; + } + + const setupState = await selfManagedSetupState({ + client: clientWithDefaultAuth, + clientWithProfilingAuth, + logger, + soClient, + spaceId: spaceId ?? DEFAULT_SPACE_ID, + }); + + return { + type: 'self-managed', + setupState, + }; +} + +export function createSetupState(params: RegisterServicesParams) { + return async ({ esClient, soClient, spaceId }: SetupStateParams) => + getSetupState({ ...params, esClient, soClient, spaceId }); +} diff --git a/x-pack/plugins/profiling_data_access/server/services/setup_state/self_managed_setup_state.ts b/x-pack/plugins/profiling_data_access/server/services/setup_state/self_managed_setup_state.ts new file mode 100644 index 0000000000000..062a75f0f1f02 --- /dev/null +++ b/x-pack/plugins/profiling_data_access/server/services/setup_state/self_managed_setup_state.ts @@ -0,0 +1,34 @@ +/* + * 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 { + validateMaximumBuckets, + validateResourceManagement, +} from '../../../common/cluster_settings'; +import { hasProfilingData } from '../../../common/has_profiling_data'; +import { validateSecurityRole } from '../../../common/security_role'; +import { + createDefaultSetupState, + mergePartialSetupStates, + ProfilingSetupOptions, + SetupState, +} from '../../../common/setup'; + +export async function selfManagedSetupState(params: ProfilingSetupOptions): Promise { + const state = createDefaultSetupState(); + + const verifyFunctions = [ + validateMaximumBuckets, + validateResourceManagement, + validateSecurityRole, + hasProfilingData, + ]; + + const partialStates = await Promise.all(verifyFunctions.map((fn) => fn(params))); + + return mergePartialSetupStates(state, partialStates); +} diff --git a/x-pack/plugins/profiling_data_access/server/services/status/index.ts b/x-pack/plugins/profiling_data_access/server/services/status/index.ts index 31581140b12e0..0e32989ea8828 100644 --- a/x-pack/plugins/profiling_data_access/server/services/status/index.ts +++ b/x-pack/plugins/profiling_data_access/server/services/status/index.ts @@ -7,10 +7,10 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ProfilingStatus } from '@kbn/profiling-utils'; -import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; -import { getSetupState } from '../get_setup_state'; +import { areCloudResourcesSetup } from '../../../common/cloud_setup'; +import { areResourcesSetup } from '../../../common/setup'; import { RegisterServicesParams } from '../register_services'; -import { ProfilingSetupOptions, areResourcesSetup } from '../../../common/setup'; +import { getSetupState } from '../setup_state'; export interface HasSetupParams { soClient: SavedObjectsClientContract; @@ -18,45 +18,16 @@ export interface HasSetupParams { spaceId?: string; } -export function createGetStatusService({ - createProfilingEsClient, - deps, - logger, -}: RegisterServicesParams) { +export function createGetStatusService(params: RegisterServicesParams) { return async ({ esClient, soClient, spaceId }: HasSetupParams): Promise => { try { - const isCloudEnabled = deps.cloud.isCloudEnabled; - if (!isCloudEnabled) { - // When not on cloud just return that is has not set up and has no data - return { - has_setup: false, - has_data: false, - pre_8_9_1_data: false, - }; - } - - const clientWithDefaultAuth = createProfilingEsClient({ - esClient, - useDefaultAuth: true, - }); - const clientWithProfilingAuth = createProfilingEsClient({ - esClient, - useDefaultAuth: false, - }); - - const setupOptions: ProfilingSetupOptions = { - client: clientWithDefaultAuth, - logger, - packagePolicyClient: deps.fleet.packagePolicyService, - soClient, - spaceId: spaceId ?? DEFAULT_SPACE_ID, - isCloudEnabled, - }; + const { type, setupState } = await getSetupState({ ...params, esClient, soClient, spaceId }); - const setupState = await getSetupState(setupOptions, clientWithProfilingAuth); + params.logger.debug(`Set up state for: ${type}: ${JSON.stringify(setupState, null, 2)}`); return { - has_setup: areResourcesSetup(setupState), + has_setup: + type === 'cloud' ? areCloudResourcesSetup(setupState) : areResourcesSetup(setupState), has_data: setupState.data.available, pre_8_9_1_data: setupState.resources.pre_8_9_1_data, }; diff --git a/x-pack/plugins/profiling_data_access/server/types.ts b/x-pack/plugins/profiling_data_access/server/types.ts index 4092b18c6b953..f7adb62b63b19 100644 --- a/x-pack/plugins/profiling_data_access/server/types.ts +++ b/x-pack/plugins/profiling_data_access/server/types.ts @@ -8,6 +8,6 @@ import { CloudStart } from '@kbn/cloud-plugin/server'; import { FleetStartContract } from '@kbn/fleet-plugin/server'; export interface ProfilingPluginStartDeps { - fleet: FleetStartContract; - cloud: CloudStart; + fleet?: FleetStartContract; + cloud?: CloudStart; } diff --git a/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts b/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts index 258b18c3e44f9..617e27a458b72 100644 --- a/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts +++ b/x-pack/plugins/profiling_data_access/server/utils/create_profiling_es_client.ts @@ -60,14 +60,16 @@ export function createProfilingEsClient({ return unwrapEsResponse(promise) as Promise; }, - profilingStatus() { + profilingStatus({ waitForResourcesCreated = false } = {}) { const controller = new AbortController(); const promise = withProfilingSpan('_profiling/status', () => { return esClient.transport.request( { method: 'GET', - path: encodeURI('/_profiling/status'), + path: encodeURI( + `/_profiling/status?wait_for_resources_created=${waitForResourcesCreated}` + ), }, { signal: controller.signal, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index bd3f3f1395d8b..25530be35e9a7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -39413,4 +39413,4 @@ "xpack.painlessLab.walkthroughButtonLabel": "Présentation", "xpack.serverlessObservability.nav.getStarted": "Démarrer" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 938c92c1aa10e..3fa06a6d8c35f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39404,4 +39404,4 @@ "xpack.painlessLab.walkthroughButtonLabel": "実地検証", "xpack.serverlessObservability.nav.getStarted": "使ってみる" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 74f0152cb1dfc..2c57adbd57df1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -39398,4 +39398,4 @@ "xpack.painlessLab.walkthroughButtonLabel": "指导", "xpack.serverlessObservability.nav.getStarted": "开始使用" } -} +} \ No newline at end of file From 31accd60a034d4d50ecd961adf086a06a3a6008a Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 25 Oct 2023 12:33:50 +0200 Subject: [PATCH 006/119] [Cases] Change the error message for missing required custom fields. (#169758) ## Summary Updated the error message when the user tried to update a case with missing required custom fields.
BeforeScreenshot
2023-10-24 at 14 42 18
After Screenshot 2023-10-25 at 10 38 14
--- .../cases/server/client/cases/create.test.ts | 12 ++++++------ .../cases/server/client/cases/update.test.ts | 4 ++-- .../server/client/cases/validators.test.ts | 18 +++++++++++------- .../cases/server/client/cases/validators.ts | 8 +++++--- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index be58f866cb556..b7cc876c47655 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -550,7 +550,7 @@ describe('create', () => { { key: 'first_key', type: CustomFieldTypes.TEXT, - label: 'foo', + label: 'missing field 1', required: true, }, { @@ -566,7 +566,7 @@ describe('create', () => { await expect( create({ ...theCase }, clientArgs, casesClient) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to create case: Error: Missing required custom fields: first_key"` + `"Failed to create case: Error: Missing required custom fields: \\"missing field 1\\""` ); }); @@ -578,13 +578,13 @@ describe('create', () => { { key: 'first_key', type: CustomFieldTypes.TEXT, - label: 'foo', + label: 'missing field 1', required: true, }, { key: 'second_key', type: CustomFieldTypes.TOGGLE, - label: 'foo', + label: 'missing field 2', required: true, }, ], @@ -612,7 +612,7 @@ describe('create', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to create case: Error: Missing required custom fields: first_key,second_key"` + `"Failed to create case: Error: Missing required custom fields: \\"missing field 1\\", \\"missing field 2\\""` ); }); @@ -695,7 +695,7 @@ describe('create', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to create case: Error: Missing required custom fields: first_key"` + `"Failed to create case: Error: Missing required custom fields: \\"missing field 1\\""` ); }); diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index f6d1640fe9448..94d4767e93b25 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -920,7 +920,7 @@ describe('update', () => { { key: 'first_key', type: CustomFieldTypes.TEXT, - label: 'foo', + label: 'missing field 1', required: true, }, { @@ -1156,7 +1156,7 @@ describe('update', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Missing required custom fields: first_key"` + `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Missing required custom fields: \\"missing field 1\\""` ); }); diff --git a/x-pack/plugins/cases/server/client/cases/validators.test.ts b/x-pack/plugins/cases/server/client/cases/validators.test.ts index b90f579435093..888852e6f90ed 100644 --- a/x-pack/plugins/cases/server/client/cases/validators.test.ts +++ b/x-pack/plugins/cases/server/client/cases/validators.test.ts @@ -355,7 +355,7 @@ describe('validators', () => { { key: 'first_key', type: CustomFieldTypes.TEXT, - label: 'foo', + label: 'missing field 1', required: true, }, { @@ -370,7 +370,9 @@ describe('validators', () => { requestCustomFields, customFieldsConfiguration, }) - ).toThrowErrorMatchingInlineSnapshot(`"Missing required custom fields: first_key"`); + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required custom fields: \\"missing field 1\\""` + ); }); it('throws if required custom fields have null value', () => { @@ -385,13 +387,13 @@ describe('validators', () => { { key: 'first_key', type: CustomFieldTypes.TEXT, - label: 'foo', + label: 'missing field 1', required: true, }, { key: 'second_key', type: CustomFieldTypes.TOGGLE, - label: 'foo', + label: 'missing field 2', required: true, }, ]; @@ -401,7 +403,7 @@ describe('validators', () => { customFieldsConfiguration, }) ).toThrowErrorMatchingInlineSnapshot( - `"Missing required custom fields: first_key,second_key"` + `"Missing required custom fields: \\"missing field 1\\", \\"missing field 2\\""` ); }); @@ -425,7 +427,7 @@ describe('validators', () => { { key: 'first_key', type: CustomFieldTypes.TEXT, - label: 'foo', + label: 'missing field 1', required: true, }, ]; @@ -433,7 +435,9 @@ describe('validators', () => { validateRequiredCustomFields({ customFieldsConfiguration, }) - ).toThrowErrorMatchingInlineSnapshot(`"Missing required custom fields: first_key"`); + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required custom fields: \\"missing field 1\\""` + ); }); }); }); diff --git a/x-pack/plugins/cases/server/client/cases/validators.ts b/x-pack/plugins/cases/server/client/cases/validators.ts index 87e677dca457b..7817e6646695a 100644 --- a/x-pack/plugins/cases/server/client/cases/validators.ts +++ b/x-pack/plugins/cases/server/client/cases/validators.ts @@ -115,7 +115,7 @@ export const validateRequiredCustomFields = ({ requiredCustomFields, requestCustomFields ?? [], (requiredVal, requestedVal) => requiredVal.key === requestedVal.key - ).map((e) => e.key); + ).map((e) => `"${e.label}"`); requiredCustomFields.forEach((requiredField) => { const found = requestCustomFields?.find( @@ -123,11 +123,13 @@ export const validateRequiredCustomFields = ({ ); if (found && found.value === null) { - missingRequiredCustomFields.push(found.key); + missingRequiredCustomFields.push(`"${requiredField.label}"`); } }); if (missingRequiredCustomFields.length) { - throw Boom.badRequest(`Missing required custom fields: ${missingRequiredCustomFields}`); + throw Boom.badRequest( + `Missing required custom fields: ${missingRequiredCustomFields.join(', ')}` + ); } }; From 9505fa97540a0124258ef528d22e2cc60290ddf3 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 25 Oct 2023 12:34:47 +0200 Subject: [PATCH 007/119] [Ops] create-deploy-tag workflow: Add link to deployed commits overview (#169496) --- .github/workflows/create-deploy-tag.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-deploy-tag.yml b/.github/workflows/create-deploy-tag.yml index 6488aa0dc6417..7ce79f2bd1c07 100644 --- a/.github/workflows/create-deploy-tag.yml +++ b/.github/workflows/create-deploy-tag.yml @@ -48,6 +48,10 @@ jobs: echo "This workflow can only be run on the main branch" exit 1 fi + - name: Find previous tag + run: | + prev_tag_name=`git tag -l 'deploy@[0-9]*' | tail -1` + echo "PREV_TAG_NAME=${prev_tag_name}" >> "${GITHUB_ENV}" - name: Prepare tag run: | tag_name="deploy@$(date +%s)" @@ -103,7 +107,7 @@ jobs: JSON_USEFUL_LINKS_ARRAY: | [ "*Useful links:*\\n", - "", + "", "", " (use Elastic Cloud Staging VPN)", "", @@ -152,6 +156,5 @@ jobs: JSON_USEFUL_LINKS_ARRAY: | [ "*Useful links:*\\n", - "", "" ] From aff871a84a504cb0b4f7fa29e13f670ab75b447a Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 25 Oct 2023 11:45:28 +0100 Subject: [PATCH 008/119] [ML] Always sync before serverless trained model tests (#169658) Ensuring saved objects are synced before running the tests for the trained models. This should fix the occasional test failure where the build in `lang_ident_model_1` is missing from the list due to a sync being needed. Flaky test runner, all have passed. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3703 --- .../functional/test_suites/search/ml/trained_models_list.ts | 1 + .../functional/test_suites/security/ml/trained_models_list.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts b/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts index 22adf85dc3926..cc2a98291923f 100644 --- a/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts +++ b/x-pack/test_serverless/functional/test_suites/search/ml/trained_models_list.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Trained models list', () => { before(async () => { await PageObjects.svlCommonPage.login(); + await ml.api.syncSavedObjects(); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts index 19b1430dda1ff..92bdbd6ffab65 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ml/trained_models_list.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Trained models list', () => { before(async () => { await PageObjects.svlCommonPage.login(); + await ml.api.syncSavedObjects(); }); after(async () => { From 560006f44491ae022077aced542fd114c30eee07 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 25 Oct 2023 13:08:13 +0200 Subject: [PATCH 009/119] Reindexing into a new index might convince ES it cannot complete in 0s (#169650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes https://github.com/elastic/kibana/issues/166190 🤞 We were reusing an existing index as the reindex target. Maybe if we force ES to create a new index it would take a bit more time and stop the flakiness. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### 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) --- .../migrations/group3/actions/actions_test_suite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts index ccdf39133f93e..682478f8d5f83 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts @@ -1155,7 +1155,7 @@ export const runActionTestSuite = ({ const res = (await reindex({ client, sourceIndex: 'existing_index_with_100k_docs', - targetIndex: 'reindex_target', + targetIndex: 'reindex_target_7', reindexScript: Option.none, requireAlias: false, excludeOnUpgradeQuery: { match_all: {} }, From eaddc54f6c088abd7278c6da3cdabeaf39801924 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Wed, 25 Oct 2023 12:53:51 +0100 Subject: [PATCH 010/119] [Fleet] Output secrets UI (#169429) ## Summary Continuation of #169221. Part of https://github.com/elastic/kibana/issues/157458 _Note: The experimental feature flag `outputSecretsStorage` must be enabled to see these changes._ Introduces the UI components to create and edit output secrets, currently there are only 3 output secrets: - Kafka output password - Kafka output SSL key - Logstash output SSL key Some key behaviours of the new UI: - on creating an output, the user can opt to revert to using plain text values if they want - once an output has been created with a secret, when editing the output, the secret values can only be replaced, never viewed - If an output uses plain values, there currently isn't a way to convert to using secrets. **Create** Screenshot 2023-10-24 at 14 48 49 **Edit** https://github.com/elastic/kibana/assets/3315046/d8d44911-81d3-4a06-a0ff-ece981a36496 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../components/edit_output_flyout/index.tsx | 81 +++++++--- .../edit_output_flyout/output_form_kafka.tsx | 10 +- .../output_form_kafka_authentication.tsx | 116 ++++++++++---- .../output_form_secret_form_row.test.tsx | 70 +++++++++ .../output_form_secret_form_row.tsx | 144 ++++++++++++++++++ .../output_form_validators.tsx | 14 ++ .../edit_output_flyout/use_output_form.tsx | 96 +++++++++++- .../plugins/fleet/public/hooks/use_input.ts | 66 ++++++++ .../plugins/fleet/server/services/secrets.ts | 2 +- 9 files changed, 535 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index 05f44bb8b92a9..bbf0ca39ffd41 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; + import { EuiFlyout, EuiFlyoutBody, @@ -44,13 +45,14 @@ import { FLYOUT_MAX_WIDTH } from '../../constants'; import { LogstashInstructions } from '../logstash_instructions'; import { useBreadcrumbs, useStartServices } from '../../../../hooks'; +import { SecretFormRow } from './output_form_secret_form_row'; + import { OutputFormKafkaSection } from './output_form_kafka'; import { YamlCodeEditorWithPlaceholder } from './yaml_code_editor_with_placeholder'; import { useOutputForm } from './use_output_form'; import { EncryptionKeyRequiredCallout } from './encryption_key_required_callout'; import { AdvancedOptionsSection } from './advanced_options_section'; - export interface EditOutputFlyoutProps { output?: Output; onClose: () => void; @@ -67,6 +69,12 @@ export const EditOutputFlyout: React.FunctionComponent = const inputs = form.inputs; const { docLinks } = useStartServices(); const { euiTheme } = useEuiTheme(); + const { outputSecretsStorage: isOutputSecretsStorageEnabled } = ExperimentalFeaturesService.get(); + const [useSecretsStorage, setUseSecretsStorage] = React.useState(isOutputSecretsStorageEnabled); + + const onUsePlainText = () => { + setUseSecretsStorage(false); + }; const proxiesOptions = useMemo( () => proxies.map((proxy) => ({ value: proxy.id, label: proxy.name })), @@ -161,28 +169,51 @@ export const EditOutputFlyout: React.FunctionComponent = )} /> - + } + {...inputs.sslKeyInput.formRowProps} + > + - } - {...inputs.sslKeyInput.formRowProps} - > - + ) : ( + - + title={i18n.translate('xpack.fleet.settings.editOutputFlyout.sslKeySecretInputTitle', { + defaultMessage: 'Client SSL certificate key', + })} + {...inputs.sslKeySecretInput.formRowProps} + onUsePlainText={onUsePlainText} + > + + + )} ); }; @@ -231,7 +262,13 @@ export const EditOutputFlyout: React.FunctionComponent = const renderKafkaSection = () => { if (isKafkaOutputEnabled) { - return ; + return ( + + ); } return null; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx index 18a25601e1836..66493b328697c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka.tsx @@ -32,10 +32,12 @@ import type { OutputFormInputsType } from './use_output_form'; interface Props { inputs: OutputFormInputsType; + useSecretsStorage: boolean; + onUsePlainText: () => void; } export const OutputFormKafkaSection: React.FunctionComponent = (props) => { - const { inputs } = props; + const { inputs, useSecretsStorage, onUsePlainText } = props; const { docLinks } = useStartServices(); @@ -104,7 +106,11 @@ export const OutputFormKafkaSection: React.FunctionComponent = (props) => /> - + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx index 42a7b66727597..30665eb0e7c44 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_authentication.tsx @@ -31,6 +31,7 @@ import { } from '../../../../../../../common/constants'; import type { OutputFormInputsType } from './use_output_form'; +import { SecretFormRow } from './output_form_secret_form_row'; const kafkaSaslOptions = [ { @@ -70,8 +71,10 @@ const kafkaAuthenticationsOptions = [ export const OutputFormKafkaAuthentication: React.FunctionComponent<{ inputs: OutputFormInputsType; + useSecretsStorage: boolean; + onUsePlainText: () => void; }> = (props) => { - const { inputs } = props; + const { inputs, useSecretsStorage, onUsePlainText } = props; const kafkaVerificationModeOptions = useMemo( () => @@ -145,28 +148,54 @@ export const OutputFormKafkaAuthentication: React.FunctionComponent<{ )} /> - + } + {...inputs.kafkaSslKeyInput.formRowProps} + > + - } - {...inputs.kafkaSslKeyInput.formRowProps} - > - + ) : ( + - + {...inputs.kafkaSslKeySecretInput.formRowProps} + onUsePlainText={onUsePlainText} + > + + + )} ); default: @@ -189,23 +218,44 @@ export const OutputFormKafkaAuthentication: React.FunctionComponent<{ {...inputs.kafkaAuthUsernameInput.props} /> - + } + {...inputs.kafkaAuthPasswordInput.formRowProps} + > + - } - {...inputs.kafkaAuthPasswordInput.formRowProps} - > - + ) : ( + - + title={i18n.translate( + 'xpack.fleet.settings.editOutputFlyout.kafkaPasswordInputtitle', + { + defaultMessage: 'Password', + } + )} + {...inputs.kafkaAuthPasswordSecretInput.formRowProps} + onUsePlainText={onUsePlainText} + > + + + )} { + const title = 'Test Secret'; + const initialValue = 'initial value'; + const clear = jest.fn(); + const onUsePlainText = jest.fn(); + + it('should switch to edit mode when the replace button is clicked', () => { + const { getByText, queryByText, container } = render( + + + + ); + + expect(container.querySelector('#myinput')).not.toBeInTheDocument(); + + fireEvent.click(getByText('Replace Test Secret')); + + expect(container.querySelector('#myinput')).toBeInTheDocument(); + expect(getByText(title)).toBeInTheDocument(); + expect(queryByText('Replace Test Secret')).not.toBeInTheDocument(); + expect(queryByText(initialValue)).not.toBeInTheDocument(); + }); + + it('should call the clear function when the cancel button is clicked', () => { + const { getByText } = render( + + + + ); + + fireEvent.click(getByText('Replace Test Secret')); + fireEvent.click(getByText('Cancel Test Secret change')); + + expect(clear).toHaveBeenCalled(); + }); + + it('should call the onUsePlainText function when the revert link is clicked', () => { + const { getByText } = render( + + + + ); + + fireEvent.click(getByText('Click to use plain text storage instead')); + + expect(onUsePlainText).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx new file mode 100644 index 0000000000000..fc55835557403 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx @@ -0,0 +1,144 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; + +export const SecretFormRow: React.FC<{ + fullWidth?: boolean; + children: ConstructorParameters[0]['children']; + error?: string[]; + isInvalid?: boolean; + title: string; + clear: () => void; + initialValue?: any; + onUsePlainText: () => void; +}> = ({ fullWidth, error, isInvalid, children, clear, title, initialValue, onUsePlainText }) => { + const hasInitialValue = initialValue !== undefined; + const [editMode, setEditMode] = useState(!initialValue); + const valueHiddenPanel = ( + + + + + + setEditMode(true)} + color="primary" + iconType="refresh" + iconSide="left" + size="xs" + > + + + + ); + + const cancelButton = ( + { + setEditMode(false); + clear(); + }} + color="primary" + iconType="refresh" + iconSide="left" + size="xs" + > + + + ); + + const editValue = ( + <> + {children} + {hasInitialValue && ( + + {cancelButton} + + )} + + ); + + const label = ( + + +   + {title} +   + + + + + ); + + const helpText = !initialValue ? ( + + + + ), + }} + /> + ) : undefined; + + const inputComponent = editMode ? editValue : valueHiddenPanel; + + return ( + + {inputComponent} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx index dec10c9e88cd8..f163c5bd9950a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx @@ -8,6 +8,16 @@ import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; +const toSecretValidator = + (validator: (value: string) => string[] | undefined) => + (value: string | { id: string } | undefined) => { + if (!value || typeof value === 'object') { + return undefined; + } + + return validator(value); + }; + export function validateKafkaHosts(value: string[]) { const res: Array<{ message: string; index?: number }> = []; const urlIndexes: { [key: string]: number[] } = {}; @@ -237,6 +247,8 @@ export function validateKafkaPassword(value: string) { } } +export const validateKafkaPasswordSecret = toSecretValidator(validateKafkaPassword); + export function validateCATrustedFingerPrint(value: string) { if (value !== '' && !value.match(/^[a-zA-Z0-9]+$/)) { return [ @@ -268,6 +280,8 @@ export function validateSSLKey(value: string) { } } +export const validateSSLKeySecret = toSecretValidator(validateSSLKey); + export function validateKafkaDefaultTopic(value: string) { if (!value || value === '') { return [ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx index 3f650734dff80..282b3433268ec 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx @@ -32,6 +32,7 @@ import { sendPostOutput, useComboInput, useInput, + useSecretInput, useNumberInput, useSelectInput, useSwitchInput, @@ -54,8 +55,10 @@ import { validateCATrustedFingerPrint, validateSSLCertificate, validateSSLKey, + validateSSLKeySecret, validateKafkaUsername, validateKafkaPassword, + validateKafkaPasswordSecret, validateKafkaHeaders, validateKafkaDefaultTopic, validateKafkaTopics, @@ -84,6 +87,7 @@ export interface OutputFormInputsType { caTrustedFingerprintInput: ReturnType; sslCertificateInput: ReturnType; sslKeyInput: ReturnType; + sslKeySecretInput: ReturnType; sslCertificateAuthoritiesInput: ReturnType; proxyIdInput: ReturnType; loadBalanceEnabledInput: ReturnType; @@ -98,6 +102,7 @@ export interface OutputFormInputsType { kafkaSaslMechanismInput: ReturnType; kafkaAuthUsernameInput: ReturnType; kafkaAuthPasswordInput: ReturnType; + kafkaAuthPasswordSecretInput: ReturnType; kafkaPartitionTypeInput: ReturnType; kafkaPartitionTypeRandomInput: ReturnType; kafkaPartitionTypeHashInput: ReturnType; @@ -115,9 +120,34 @@ export interface OutputFormInputsType { kafkaKeyInput: ReturnType; kafkaSslCertificateInput: ReturnType; kafkaSslKeyInput: ReturnType; + kafkaSslKeySecretInput: ReturnType; kafkaSslCertificateAuthoritiesInput: ReturnType; } +function extractKafkaOutputSecrets( + inputs: Pick< + OutputFormInputsType, + | 'kafkaSslKeyInput' + | 'kafkaSslKeySecretInput' + | 'kafkaAuthPasswordInput' + | 'kafkaAuthPasswordSecretInput' + > +): KafkaOutput['secrets'] | null { + const secrets: KafkaOutput['secrets'] = {}; + + if (!inputs.kafkaSslKeyInput.value && inputs.kafkaSslKeySecretInput.value) { + secrets.ssl = { + key: inputs.kafkaSslKeySecretInput.value, + }; + } + + if (!inputs.kafkaAuthPasswordInput.value && inputs.kafkaAuthPasswordSecretInput.value) { + secrets.password = inputs.kafkaAuthPasswordSecretInput.value; + } + + return Object.keys(secrets).length ? secrets : null; +} + export function useOutputForm(onSucess: () => void, output?: Output) { const fleetStatus = useFleetStatus(); @@ -250,6 +280,12 @@ export function useOutputForm(onSucess: () => void, output?: Output) { ); const sslKeyInput = useInput(output?.ssl?.key ?? '', validateSSLKey, isSSLEditable); + const sslKeySecretInput = useSecretInput( + output?.secrets?.ssl?.key, + validateSSLKeySecret, + isSSLEditable + ); + const proxyIdInput = useInput(output?.proxy_id ?? '', () => undefined, isDisabled('proxy_id')); /** @@ -309,6 +345,12 @@ export function useOutputForm(onSucess: () => void, output?: Output) { isDisabled('password') ); + const kafkaAuthPasswordSecretInput = useSecretInput( + kafkaOutput?.secrets?.password, + kafkaAuthMethodInput.value === kafkaAuthType.Userpass ? validateKafkaPasswordSecret : undefined, + isDisabled('password') + ); + const kafkaSslCertificateAuthoritiesInput = useComboInput( 'kafkaSslCertificateAuthoritiesComboBox', kafkaOutput?.ssl?.certificate_authorities ?? [], @@ -326,6 +368,12 @@ export function useOutputForm(onSucess: () => void, output?: Output) { isSSLEditable ); + const kafkaSslKeySecretInput = useSecretInput( + kafkaOutput?.ssl?.certificate, + kafkaAuthMethodInput.value === kafkaAuthType.Ssl ? validateSSLKeySecret : undefined, + isSSLEditable + ); + const kafkaVerificationModeInput = useInput( kafkaOutput?.ssl?.verification_mode ?? kafkaVerificationModes.Full, undefined, @@ -443,6 +491,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { caTrustedFingerprintInput, sslCertificateInput, sslKeyInput, + sslKeySecretInput, sslCertificateAuthoritiesInput, proxyIdInput, loadBalanceEnabledInput, @@ -456,6 +505,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaConnectionTypeInput, kafkaAuthUsernameInput, kafkaAuthPasswordInput, + kafkaAuthPasswordSecretInput, kafkaSaslMechanismInput, kafkaPartitionTypeInput, kafkaPartitionTypeRandomInput, @@ -473,6 +523,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaSslCertificateAuthoritiesInput, kafkaSslCertificateInput, kafkaSslKeyInput, + kafkaSslKeySecretInput, kafkaDefaultTopicInput, kafkaTopicsInput, }; @@ -484,10 +535,12 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); const kafkaHostsValid = kafkaHostsInput.validate(); const kafkaUsernameValid = kafkaAuthUsernameInput.validate(); - const kafkaPasswordValid = kafkaAuthPasswordInput.validate(); + const kafkaPasswordPlainValid = kafkaAuthPasswordInput.validate(); + const kafkaPasswordSecretValid = kafkaAuthPasswordSecretInput.validate(); const kafkaClientIDValid = kafkaClientIdInput.validate(); const kafkaSslCertificateValid = kafkaSslCertificateInput.validate(); - const kafkaSslKeyValid = kafkaSslKeyInput.validate(); + const kafkaSslKeyPlainValid = kafkaSslKeyInput.validate(); + const kafkaSslKeySecretValid = kafkaSslKeySecretInput.validate(); const kafkaDefaultTopicValid = kafkaDefaultTopicInput.validate(); const kafkaTopicsValid = kafkaTopicsInput.validate(); const kafkaHeadersValid = kafkaHeadersInput.validate(); @@ -496,10 +549,19 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const caTrustedFingerprintValid = caTrustedFingerprintInput.validate(); const sslCertificateValid = sslCertificateInput.validate(); const sslKeyValid = sslKeyInput.validate(); + const sslKeySecretValid = sslKeySecretInput.validate(); const diskQueuePathValid = diskQueuePathInput.validate(); const partitioningRandomGroupEventsValid = kafkaPartitionTypeRandomInput.validate(); const partitioningRoundRobinGroupEventsValid = kafkaPartitionTypeRoundRobinInput.validate(); + const kafkaSslKeyValid = kafkaSslKeyInput.value + ? kafkaSslKeyPlainValid + : kafkaSslKeySecretValid; + + const kafkaPasswordValid = kafkaAuthPasswordInput.value + ? kafkaPasswordPlainValid + : kafkaPasswordSecretValid; + if (isLogstash) { // validate logstash return ( @@ -507,7 +569,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { additionalYamlConfigValid && nameInputValid && sslCertificateValid && - sslKeyValid + ((sslKeyInput.value && sslKeyValid) || (sslKeySecretInput.value && sslKeySecretValid)) ); } if (isKafka) { @@ -543,9 +605,11 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaHostsInput, kafkaAuthUsernameInput, kafkaAuthPasswordInput, + kafkaAuthPasswordSecretInput, kafkaClientIdInput, kafkaSslCertificateInput, kafkaSslKeyInput, + kafkaSslKeySecretInput, kafkaDefaultTopicInput, kafkaTopicsInput, kafkaHeadersInput, @@ -554,6 +618,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { caTrustedFingerprintInput, sslCertificateInput, sslKeyInput, + sslKeySecretInput, diskQueuePathInput, kafkaPartitionTypeRandomInput, kafkaPartitionTypeRoundRobinInput, @@ -623,6 +688,13 @@ export function useOutputForm(onSucess: () => void, output?: Output) { (val) => val !== '' ).length; + const maybeSecrets = extractKafkaOutputSecrets({ + kafkaSslKeyInput, + kafkaSslKeySecretInput, + kafkaAuthPasswordInput, + kafkaAuthPasswordSecretInput, + }); + return { name: nameInput.value, type: outputType.Kafka, @@ -721,6 +793,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { ), required_acks: parseIntegerIfStringDefined(kafkaBrokerAckReliabilityInput.value), ...shipperParams, + ...(maybeSecrets ? { secrets: maybeSecrets } : {}), } as KafkaOutput; case outputType.Logstash: return { @@ -732,11 +805,19 @@ export function useOutputForm(onSucess: () => void, output?: Output) { config_yaml: additionalYamlConfigInput.value, ssl: { certificate: sslCertificateInput.value, - key: sslKeyInput.value, + key: sslKeyInput.value || undefined, certificate_authorities: sslCertificateAuthoritiesInput.value.filter( (val) => val !== '' ), }, + ...(!sslKeyInput.value && + sslKeySecretInput.value && { + secrets: { + ssl: { + key: sslKeySecretInput.value, + }, + }, + }), proxy_id: proxyIdValue, ...shipperParams, } as NewLogstashOutput; @@ -812,7 +893,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { additionalYamlConfigInput.value, kafkaAuthMethodInput.value, kafkaSslCertificateInput.value, - kafkaSslKeyInput.value, + kafkaSslKeyInput, + kafkaSslKeySecretInput, kafkaVerificationModeInput.value, kafkaClientIdInput.value, kafkaVersionInput.value, @@ -821,7 +903,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) { kafkaCompressionLevelInput.value, kafkaConnectionTypeInput.value, kafkaAuthUsernameInput.value, - kafkaAuthPasswordInput.value, + kafkaAuthPasswordInput, + kafkaAuthPasswordSecretInput, kafkaSaslMechanismInput.value, kafkaPartitionTypeInput.value, kafkaPartitionTypeRandomInput.value, @@ -836,6 +919,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { logstashHostsInput.value, sslCertificateInput.value, sslKeyInput.value, + sslKeySecretInput.value, sslCertificateAuthoritiesInput.value, elasticsearchUrlInput.value, caTrustedFingerprintInput.value, diff --git a/x-pack/plugins/fleet/public/hooks/use_input.ts b/x-pack/plugins/fleet/public/hooks/use_input.ts index dbc60eafda833..42295aebd9f49 100644 --- a/x-pack/plugins/fleet/public/hooks/use_input.ts +++ b/x-pack/plugins/fleet/public/hooks/use_input.ts @@ -84,6 +84,72 @@ export function useInput( }; } +type MaybeSecret = string | { id: string } | undefined; + +export function useSecretInput( + initialValue: MaybeSecret, + validate?: (value: MaybeSecret) => string[] | undefined, + disabled: boolean = false +) { + const [value, setValue] = useState(initialValue); + const [errors, setErrors] = useState(); + const [hasChanged, setHasChanged] = useState(false); + + const onChange = useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value; + setValue(newValue); + if (errors && validate && validate(newValue) === undefined) { + setErrors(undefined); + } + }, + [errors, validate] + ); + + useEffect(() => { + if (hasChanged) { + return; + } + if (value !== initialValue) { + setHasChanged(true); + } + }, [hasChanged, value, initialValue]); + + const isInvalid = errors !== undefined; + + return { + value, + errors, + props: { + onChange, + value: typeof value === 'string' ? value : '', + isInvalid, + disabled, + }, + formRowProps: { + error: errors, + isInvalid, + initialValue, + clear: () => { + setValue(''); + }, + }, + cancelEdit: () => { + setValue(initialValue || ''); + }, + validate: () => { + if (validate) { + const newErrors = validate(value); + setErrors(newErrors); + return newErrors === undefined; + } + + return true; + }, + setValue, + hasChanged, + }; +} export function useRadioInput(defaultValue: string, disabled = false) { const [value, setValue] = useState(defaultValue); const [hasChanged, setHasChanged] = useState(false); diff --git a/x-pack/plugins/fleet/server/services/secrets.ts b/x-pack/plugins/fleet/server/services/secrets.ts index fcb6a0fdc2fe6..36a88b4a7a4c1 100644 --- a/x-pack/plugins/fleet/server/services/secrets.ts +++ b/x-pack/plugins/fleet/server/services/secrets.ts @@ -566,7 +566,7 @@ export async function isSecretStorageEnabled( const settings = await settingsService.getSettingsOrUndefined(soClient); if (settings && settings.secret_storage_requirements_met) { - logger.debug('Secrets storage already met, turned on is settings'); + logger.debug('Secrets storage requirements already met, turned on in settings'); return true; } From 980162b0b0138fb572a32c4f3901da3df7f53017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 25 Oct 2023 14:02:28 +0200 Subject: [PATCH 011/119] [Enterprise Search]Disable syncs for native connectors when EnterpriseSearch is down (#169671) ## Summary Disable syncs when Enterprise Search is down for native connectors. Screenshot 2023-10-24 at 17 15 00 Screenshot 2023-10-24 at 17 15 07 Screenshot 2023-10-24 at 17 15 32 Screenshot 2023-10-24 at 17 15 20 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --- .../header_actions/syncs_context_menu.tsx | 20 ++++++++---- .../native_connector_configuration.tsx | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/syncs_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/syncs_context_menu.tsx index 96efd9d7d8447..bbe8a039a3ef7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/syncs_context_menu.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/header_actions/syncs_context_menu.tsx @@ -23,6 +23,7 @@ import { import { i18n } from '@kbn/i18n'; import { Status } from '../../../../../../../common/types/api'; +import { HttpLogic } from '../../../../../shared/http'; import { KibanaLogic } from '../../../../../shared/kibana'; import { CancelSyncsApiLogic } from '../../../../api/connector/cancel_syncs_api_logic'; import { IngestionStatus } from '../../../../types'; @@ -30,8 +31,9 @@ import { CancelSyncsLogic } from '../../connector/cancel_syncs_logic'; import { IndexViewLogic } from '../../index_view_logic'; export const SyncsContextMenu: React.FC = () => { - const { productFeatures } = useValues(KibanaLogic); + const { config, productFeatures } = useValues(KibanaLogic); const { + connector, hasDocumentLevelSecurityFeature, hasIncrementalSyncFeature, ingestionMethod, @@ -43,7 +45,7 @@ export const SyncsContextMenu: React.FC = () => { const { cancelSyncs } = useActions(CancelSyncsLogic); const { status } = useValues(CancelSyncsApiLogic); const { startSync, startIncrementalSync, startAccessControlSync } = useActions(IndexViewLogic); - const { connector } = useValues(IndexViewLogic); + const { errorConnectingMessage } = useValues(HttpLogic); const [isPopoverOpen, setPopover] = useState(false); const togglePopover = () => setPopover(!isPopoverOpen); @@ -75,6 +77,13 @@ export const SyncsContextMenu: React.FC = () => { const shouldShowIncrementalSync = productFeatures.hasIncrementalSyncEnabled && hasIncrementalSyncFeature; + const isEnterpriseSearchNotAvailable = Boolean( + config.host && config.canDeployEntSearch && errorConnectingMessage + ); + const isSyncsDisabled = + (connector?.is_native && isEnterpriseSearchNotAvailable) || + ingestionStatus === IngestionStatus.INCOMPLETE; + const panels: EuiContextMenuProps['panels'] = [ { id: 0, @@ -86,7 +95,7 @@ export const SyncsContextMenu: React.FC = () => { // @ts-ignore - data-* attributes are applied but doesn't exist on types 'data-telemetry-id': `entSearchContent-${ingestionMethod}-header-sync-startSync`, 'data-test-subj': `entSearchContent-${ingestionMethod}-header-sync-startSync`, - disabled: ingestionStatus === IngestionStatus.INCOMPLETE, + disabled: isSyncsDisabled, icon: 'play', name: i18n.translate('xpack.enterpriseSearch.index.header.more.fullSync', { defaultMessage: 'Full Content', @@ -105,7 +114,7 @@ export const SyncsContextMenu: React.FC = () => { 'entSearchContent-${ingestionMethod}-header-sync-more-incrementalSync', 'data-test-subj': 'entSearchContent-${ingestionMethod}-header-sync-more-incrementalSync', - disabled: ingestionStatus === IngestionStatus.INCOMPLETE, + disabled: isSyncsDisabled, icon: 'play', name: i18n.translate('xpack.enterpriseSearch.index.header.more.incrementalSync', { defaultMessage: 'Incremental Content', @@ -126,8 +135,7 @@ export const SyncsContextMenu: React.FC = () => { 'data-test-subj': 'entSearchContent-${ingestionMethod}-header-sync-more-accessControlSync', disabled: Boolean( - ingestionStatus === IngestionStatus.INCOMPLETE || - connector?.configuration.use_document_level_security?.value + isSyncsDisabled || !connector?.configuration.use_document_level_security?.value ), icon: 'play', name: i18n.translate('xpack.enterpriseSearch.index.header.more.accessControlSync', { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx index 235ec644d3399..df4155cb28d65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { useValues } from 'kea'; import { + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, @@ -25,7 +26,9 @@ import { i18n } from '@kbn/i18n'; import { BetaConnectorCallout } from '../../../../../shared/beta/beta_connector_callout'; import { docLinks } from '../../../../../shared/doc_links'; +import { HttpLogic } from '../../../../../shared/http'; import { CONNECTOR_ICONS } from '../../../../../shared/icons/connector_icons'; +import { KibanaLogic } from '../../../../../shared/kibana'; import { hasConfiguredConfiguration } from '../../../../utils/has_configured_configuration'; import { isConnectorIndex } from '../../../../utils/indices'; @@ -40,6 +43,8 @@ import { ResearchConfiguration } from './research_configuration'; export const NativeConnectorConfiguration: React.FC = () => { const { index } = useValues(IndexViewLogic); + const { config } = useValues(KibanaLogic); + const { errorConnectingMessage } = useValues(HttpLogic); if (!isConnectorIndex(index)) { return <>; @@ -95,6 +100,33 @@ export const NativeConnectorConfiguration: React.FC = () => { + {config.host && config.canDeployEntSearch && errorConnectingMessage && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.entSearchWarning.text', + { + defaultMessage: + 'Native connectors require a running Enterprise Search instance to sync content from source.', + } + )} +

+
+ + + + )} Date: Wed, 25 Oct 2023 14:13:41 +0200 Subject: [PATCH 012/119] [EDR Workflows][E2E] Recreate agent on createEndpointHost task fail (#169092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restart vagrant vm on error during `beforeAll` task `createEndpointHost` Defend Workflows Cypress suite ran 300 times through flaky test runner: 1. 100x https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699 2. 50x https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3707 3. 50x https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3708 4. 50x https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3709 5. 50x https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3710 Flaky test runner runs with `createEndpointHost` task failure with successful recovery: 1. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3710#018b62fd-9ae9-4988-b1e0-ab0f04d8efdc 2. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3710#018b62fd-9ae6-4340-992b-1474ee0f114b 3. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3708#018b62fd-578e-4817-ae1c-8c58e8774eec 4. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3708#018b62fd-5787-4245-85a6-cb446e42bc73 5. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3707#018b62fc-fc17-407e-88de-d0b43b6b1d44 (failed due to unrelated issue) 6. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2c3-430c-b3e3-72b9fbb22d24 7. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2c6-4315-b828-b3218a70f209 8. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2c7-4ff7-9a70-7354f90179e0 9. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2d7-418f-b043-049e5effb26f 10. https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2da-47cc-b4ea-a4d4de3ba0a0 New errors not spotted before that got to do with env set up: 1. `vagrant up` failed: 1.1 https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3708#018b62fd-5787-4245-85a6-cb446e42bc73 1.2 https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2d0-4a52-87d9-34caa8927465 2. `CypressError: `cy.task('indexFleetEndpointPolicy')` timed out after waiting `60000ms`.: 2.1 https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3707#018b62fc-fc04-40d4-b155-46f094681edb 2.2 https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2c9-4ebb-9174-eb9d79d04d02 2.3 https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3699#018b61d9-d2dc-438f-94b0-9f94ae95701c Closes: https://github.com/elastic/kibana/issues/168284 https://github.com/elastic/kibana/issues/169343 https://github.com/elastic/kibana/issues/169468 https://github.com/elastic/kibana/issues/169469 https://github.com/elastic/kibana/issues/169467 https://github.com/elastic/kibana/issues/169465 https://github.com/elastic/kibana/issues/169466 https://github.com/elastic/kibana/issues/169157 https://github.com/elastic/kibana/issues/168719 https://github.com/elastic/kibana/issues/168427 https://github.com/elastic/kibana/issues/168359 https://github.com/elastic/kibana/issues/168340 https://github.com/elastic/kibana/issues/169689 --------- Co-authored-by: Patryk Kopyciński --- .buildkite/pipelines/flaky_tests/pipeline.ts | 3 +- .../automated_response_actions.cy.ts | 6 +-- .../cypress/e2e/endpoint_list/endpoints.cy.ts | 3 +- .../cypress/support/data_loaders.ts | 41 +++++++++++++++---- .../cypress/tasks/create_endpoint_host.ts | 2 +- .../endpoint/common/endpoint_host_services.ts | 7 +++- .../scripts/endpoint/common/fleet_services.ts | 17 ++++++-- 7 files changed, 57 insertions(+), 22 deletions(-) diff --git a/.buildkite/pipelines/flaky_tests/pipeline.ts b/.buildkite/pipelines/flaky_tests/pipeline.ts index 89505065b5809..1d12b2c840744 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.ts +++ b/.buildkite/pipelines/flaky_tests/pipeline.ts @@ -162,10 +162,11 @@ for (const testSuite of testSuites) { `Group configuration was not found in groups.json for the following cypress suite: {${suiteName}}.` ); } + const agentQueue = suiteName.includes('defend_workflows') ? 'n2-4-virt' : 'n2-4-spot'; steps.push({ command: `.buildkite/scripts/steps/functional/${suiteName}.sh`, label: group.name, - agents: { queue: 'n2-4-spot' }, + agents: { queue: agentQueue }, depends_on: 'build', parallelism: testSuite.count, concurrency, 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 f7257060e4ca9..b94f389958f9f 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 @@ -20,8 +20,7 @@ import { createEndpointHost } from '../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data'; import { enableAllPolicyProtections } from '../../tasks/endpoint_policy'; -// FLAKY: https://github.com/elastic/kibana/issues/168340 -describe.skip( +describe( 'Automated Response Actions', { tags: [ @@ -76,8 +75,7 @@ describe.skip( disableExpandableFlyoutAdvancedSettings(); }); - // FLAKY: https://github.com/elastic/kibana/issues/168427 - describe.skip('From alerts', () => { + describe('From alerts', () => { let ruleId: string; let ruleName: string; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts index b7d9244040aa0..2baf3b583aed4 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts @@ -32,8 +32,7 @@ import { createEndpointHost } from '../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data'; import { enableAllPolicyProtections } from '../../tasks/endpoint_policy'; -// FLAKY: https://github.com/elastic/kibana/issues/168284 -describe.skip('Endpoints page', { tags: ['@ess', '@serverless'] }, () => { +describe('Endpoints page', { tags: ['@ess', '@serverless'] }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index 164afe89086d1..fcead968801af 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -332,16 +332,39 @@ export const dataLoadersForRealEndpoints = ( options: Omit ): Promise => { const { kbnClient, log } = await stackServicesPromise; - return createAndEnrollEndpointHost({ - useClosestVersionMatch: true, - ...options, - log, - kbnClient, - }).then((newHost) => { - return waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000).then(() => { + + let retryAttempt = 0; + const attemptCreateEndpointHost = async (): Promise => { + try { + log.info(`Creating endpoint host, attempt ${retryAttempt}`); + const newHost = await createAndEnrollEndpointHost({ + useClosestVersionMatch: true, + ...options, + log, + kbnClient, + }); + await waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000); return newHost; - }); - }); + } catch (err) { + log.info(`Caught error when setting up the agent: ${err}`); + if (retryAttempt === 0 && err.agentId) { + retryAttempt++; + await destroyEndpointHost(kbnClient, { + hostname: err.hostname || '', // No hostname in CI env for vagrant + agentId: err.agentId, + }); + log.info(`Deleted endpoint host ${err.agentId} and retrying`); + return attemptCreateEndpointHost(); + } else { + log.info( + `${retryAttempt} attempts of creating endpoint host failed, reason for the last failure was ${err}` + ); + throw err; + } + } + }; + + return attemptCreateEndpointHost(); }, destroyEndpointHost: async ( diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/create_endpoint_host.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/create_endpoint_host.ts index 7db2ad37317fd..8b75c7e1b2af5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/create_endpoint_host.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/create_endpoint_host.ts @@ -17,6 +17,6 @@ export const createEndpointHost = ( { agentPolicyId, }, - { timeout: timeout ?? 900000 } // 15 minutes, since setup can take 10 minutes and more. Task will time out if is not resolved within this time. + { timeout: timeout ?? 30 * 60 * 1000 } ); }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts index ef38b120c38bb..b716b9299ba02 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts @@ -234,7 +234,7 @@ const createMultipassVm = async ({ }; }; -const deleteMultipassVm = async (vmName: string): Promise => { +export const deleteMultipassVm = async (vmName: string): Promise => { if (process.env.CI) { await execa.command(`vagrant destroy -f`, { env: { @@ -339,7 +339,10 @@ const enrollHostWithFleet = async ({ ]); } log.info(`Waiting for Agent to check-in with Fleet`); - const agent = await waitForHostToEnroll(kbnClient, vmName, 240000); + + const agent = await waitForHostToEnroll(kbnClient, vmName, 8 * 60 * 1000); + + log.info(`Agent enrolled with Fleet, status: `, agent.status); return { agentId: agent.id, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index a95b9f03caf2e..e254b8d3a1570 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -147,15 +147,19 @@ export const waitForHostToEnroll = async ( return elapsedTime > timeoutMs; }; let found: Agent | undefined; + let agentId: string | undefined; while (!found && !hasTimedOut()) { found = await retryOnError( async () => fetchFleetAgents(kbnClient, { perPage: 1, - kuery: `(local_metadata.host.hostname.keyword : "${hostname}") and (status:online)`, + kuery: `(local_metadata.host.hostname.keyword : "${hostname}")`, showInactive: false, - }).then((response) => response.items[0]), + }).then((response) => { + agentId = response.items[0]?.id; + return response.items.filter((agent) => agent.status === 'online')[0]; + }), RETRYABLE_TRANSIENT_ERRORS ); @@ -166,7 +170,14 @@ export const waitForHostToEnroll = async ( } if (!found) { - throw new Error(`Timed out waiting for host [${hostname}] to show up in Fleet`); + throw Object.assign( + new Error( + `Timed out waiting for host [${hostname}] to show up in Fleet in ${ + timeoutMs / 60 / 1000 + } seconds` + ), + { agentId, hostname } + ); } return found; From f378b87cad0d308a29af0659e963adf651aedfbe Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 25 Oct 2023 14:43:20 +0200 Subject: [PATCH 013/119] [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 8208616ffd028..49af462dc96f5 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 28f41fcabfcba..967590c36ce07 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 012c7811c5a80..c6c6e74edf62b 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 014/119] [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 b8d46bad40cc4..16b58be6c6c03 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 adc7f13c6c612..703af63c1027c 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 6974f956e2bc9..82a4eedcd9c34 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 70a538d212d77..2ad67febaa3da 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 015/119] [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 686d29c9cdf22..5f6558df992ca 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 016/119] [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 90346011c23e5..e2756d1955e30 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 0372765db9873..e9d482344a28e 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 be827e082db14..0000000000000 --- 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 16ed7f95b87c6..6aab4a0afbe68 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 b7bfb751e4fa8..52ed72dc1a2bd 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 5ef5280df7fa0..abb3e9d7b85ff 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 d29530f2cdfca..0000000000000 --- 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 5c45da1bb1ff2..7da3500775e48 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 fe20d4670f3a7..0000000000000 --- 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 6e10a6f0d0c86..78d8c9bc39c97 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 b180856da2b29..2b887808696bd 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 a05c351f3d22d..a04d8d197da1e 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 5c4e985abf717..0000000000000 --- 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 0771ffa5ccf9f..0000000000000 --- 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 35386ecf28dc2..0000000000000 --- 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 67a45a0098266..0000000000000 --- 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 ee24b2e636874..0000000000000 --- 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 bee3abe3bc156..0000000000000 --- 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 8935ff132f246..0000000000000 --- 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 c3952801e5ca4..0000000000000 --- 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 0dec93eb40b24..0000000000000 --- 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 32905019eb3f8..0000000000000 --- 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 4ffc16603cb0b..0000000000000 --- 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 72b469780097b..0000000000000 --- 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 ef55a3d158a83..0000000000000 --- 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 304e03a27de9f..0000000000000 --- 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 d078785bf93f8..0000000000000 --- 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 2688dd5cabf3c..0000000000000 --- 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 2f6d146c8cc9f..0000000000000 --- 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 4b67dcb87356c..0000000000000 --- 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 a41659fd2bcf6..0000000000000 --- 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 a2eec20864a2b..0000000000000 --- 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 6a509ff958735..0000000000000 --- 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 3fca60579b1da..0000000000000 --- 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 a2d5978d05e96..0000000000000 --- 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 575673a494a2f..0000000000000 --- 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 fd9cf26c7280e..0000000000000 --- 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 0a6ac1d91401f..0000000000000 --- 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 3b4138a9d3d7d..0000000000000 --- 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 2b6dca72bf078..0000000000000 --- 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 7d5dbc5440087..0000000000000 --- 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 540cd99ad9bde..0000000000000 --- 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 2f530424f9384..82d41e0eafb33 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 a40a9095ea111..3416636f71e17 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 cd0b49c515bb6..4ab99faba09c4 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 25530be35e9a7..ba03ddd298d5f 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 3fa06a6d8c35f..b2a00bc71bd43 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 2c57adbd57df1..5c97128b8c8f1 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 99569e7b3084f..fb34362f7fb9b 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 f5716f33ff288..99cbf31012d75 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 caa560f13aead..f10681a516146 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 d61ba89fa90b2..0000000000000 --- 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 86a1703959ea9..d0681a4348e06 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 9d57a2b502f33..70f43b35d1211 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 4950f2c65fab2..a81cc24ae9653 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 017/119] [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 8c66afcd0ef29..2b24ca538caa9 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 018/119] [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 70f0037cd17c3..c36000889d481 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 c60b7a8ed103c..0e60fa0125234 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 c3d8285857634..d9531a4529ee5 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 019/119] [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 47bdee0d5e6c6..5916464e90980 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 414e214fd1fe7..b63e17e2e0d86 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 0219b3ac87fc0..6e49f9fe0fa70 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 a574ae7abd09b..0393ab5e5a6fc 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 af5942024ec99..f9d2109d88df6 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 9bf35b0ca2b1a..ef7c3a431cc18 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 aa364a416a046..5714ae5283fb2 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 e0f86c68864bd..8b40d4c257434 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 1c0095046c735..67e8908f5c421 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 774c3ac6f9d2d..551e19a264463 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 020/119] [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 1a5b9152b3ac5..259e6d0d7b9ee 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 021/119] 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 20cc30d83176e..b7d45d3682971 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 2bb692bfc311b..4807b33a8e410 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 083364fad2fd2..39d8bec4696d2 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 0000000000000..125243aec84b4 --- /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 0000000000000..e258bdd75a3aa --- /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 f74fb472d08f6..18b3953da7e05 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 e69fad24c12e0..cac40b5ec8fa0 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 0000000000000..aa7250ef52230 --- /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 0000000000000..3352bf35963b9 --- /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 b84151fb9da83..d1d35ade56159 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 58272636bfe85..8d20eff18927c 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 0000000000000..0b7bc2e3fe0b6 --- /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 9bc25b66167fe..c4ae2732af15c 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 be75573b8d3db..0d1a53615da5f 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 520a313bc9786..bdcd9f58bf7bf 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 ec555e676e1e8..0cc9f82b84794 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 713392901adaf..a9daebef1e78f 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 b1afd9ea21af2..a65b51d13eb89 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 4c9879f25b591..2fbabc1fe4d67 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 396d9d0645fc3..a8379af455282 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 2fe293f072a25..b0cc1a243edd0 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 022/119] 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 f72f01c06a632..100f83d167b6e 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 d1157b7b16f10..6e01b89b23565 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 288e2692bd76a..d23a42f4ebcfb 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 4cbd6bb9e330d..687bfd31cfba2 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 aa545c9bb0ee0..10bdbda351892 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 251fb3b3752fc..67e2aaec256d7 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 4c5916831b6cb..dc938cd6effd3 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 a1e0da36921b6..ba31f6109075a 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 023/119] [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 8318a411ab86a..ebd08e55cb863 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 466b6ddec75a0..a55f25292cf5d 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 81ba44205d31b..5e03cd0732e74 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 27751b7dc3fd1..4b75cf4e0826a 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 ab676e0104b78..8836c241e55d8 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 40ee98f3234dc..97d7201f0140d 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 5ac7474324c50..c9fe22a472f4f 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 cfd812e4f435c..8aeccc6af9a97 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 024/119] [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 99ed44565dec4..5c116e26a2ff7 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 616d1aca201cf..ca118249d123b 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 500bad2a0483d..76b57064da160 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 2d9ad8c1bb2d5..bbab7fb56f057 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 5efdfb7d94c8f..2b5a85c28f22c 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 0000000000000..11885714a0dda --- /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 3dd31a0ec981c..90c380e8f88b4 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 dc87ebd5db9d3..7ff15c0d38215 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 2d80ebd9d413e..17ebe65b45194 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 24da4464e9fb3..c20c2da7235cb 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 b19e1746847ed..373782e69bb67 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 8a753e5d4829a..e672f99780fa8 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 d9531a4529ee5..d9429f93bc9af 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 998ad9a2096c9..ef33a898de4fb 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 5cc4aa637ec43..71df12e44d78a 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 025/119] [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 0869f10c73ef0..d1449672bed26 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 bf6dc8c57a478..06b33141bad1b 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 f80e3b17e6017..7ae7aa49f2e94 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 b434458c4dc1d..7ad73ad142eeb 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 30679364bd2f8..ac4d2e80ebe44 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 8f4f1e797910b..80f1aaba567a0 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 026/119] [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 a52446d389d7a..364b521c1ec30 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 027/119] [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 7552b3fac7e26..f33178ad4f6b0 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 c15286325495a..a75a7139c1189 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 028/119] [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 34b7defc289fc..4203742cc2fbc 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 0000000000000..a5a6996e4debb --- /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 1ec925d9ce148..a874e071e6c23 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 8cd6d92369076..47c4a6102798e 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 ed84b84288db0..1205382832eb7 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 024ffe9e5cdf5..61366d2fcf7a5 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 5ba552e1245e7..e3b71afa57001 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 029/119] 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 55edc2e8406b0..4ee62b760db04 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 030/119] 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 8838a5e0bbcdf..141372b9aae93 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 031/119] 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 6bee290df896d..05af7445d755e 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 032/119] 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 05af7445d755e..7a1e1408056fb 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 033/119] 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 b94f389958f9f..1948434b39c9f 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 034/119] 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 4ba7ab5befabf..72d4adcbe2669 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 035/119] [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 7c1ada1c6e1bc..f223214c3c768 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 1ff2eb43b744c..52d8ad920c968 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 b805b0a0a878e..556bd119c5247 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 a55d229bb1b97..4fe7b5378b1ce 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 1dc19e69dd8d7..cbbf27668ce8e 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 e1d9234cddc38..90ba699131625 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 30699d60912cb..a4971a66972e7 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 4587368488050..c5e862b3f7a0f 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 036/119] 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 25ab8fd65ae1f..cd20cd62fef79 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(); From 4a37b20143813fcca54a2443bf66c5d339f3aab5 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 20:05:50 +0100 Subject: [PATCH 037/119] skip flaky suite (#168505) --- .../plugins/cases/public/components/add_comment/index.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index cb13950b110de..689972b75a438 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -51,7 +51,8 @@ const sampleData: CaseAttachmentWithoutOwner = { const appId = 'testAppId'; const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; -describe('AddComment ', () => { +// FLAKY: https://github.com/elastic/kibana/issues/168505 +describe.skip('AddComment ', () => { let appMockRender: AppMockRenderer; beforeEach(() => { From f5bf0541c5f0e21509fa68cf2a6cb5140c872abd Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 20:08:02 +0100 Subject: [PATCH 038/119] skip flaky suite (#168506) --- .../plugins/cases/public/components/add_comment/index.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 689972b75a438..06f99b8a15774 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -52,6 +52,7 @@ const appId = 'testAppId'; const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; // FLAKY: https://github.com/elastic/kibana/issues/168505 +// FLAKY: https://github.com/elastic/kibana/issues/168506 describe.skip('AddComment ', () => { let appMockRender: AppMockRenderer; From cb0bc971f4d119b8eb23ada13204c8a37b2fa736 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 20:09:18 +0100 Subject: [PATCH 039/119] skip flaky suite (#168507) --- .../plugins/cases/public/components/add_comment/index.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 06f99b8a15774..bf1485bd618d4 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -53,6 +53,7 @@ const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id} // FLAKY: https://github.com/elastic/kibana/issues/168505 // FLAKY: https://github.com/elastic/kibana/issues/168506 +// FLAKY: https://github.com/elastic/kibana/issues/168507 describe.skip('AddComment ', () => { let appMockRender: AppMockRenderer; From 0a3c320534c61a6cca9b2c93190d703b0397d2d3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 20:09:37 +0100 Subject: [PATCH 040/119] skip flaky suite (#168508) --- .../plugins/cases/public/components/add_comment/index.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index bf1485bd618d4..2c58b34344b4e 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -54,6 +54,7 @@ const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id} // FLAKY: https://github.com/elastic/kibana/issues/168505 // FLAKY: https://github.com/elastic/kibana/issues/168506 // FLAKY: https://github.com/elastic/kibana/issues/168507 +// FLAKY: https://github.com/elastic/kibana/issues/168508 describe.skip('AddComment ', () => { let appMockRender: AppMockRenderer; From c2d24724ad7c99c5df928a466c8d444d52ceec7d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 25 Oct 2023 20:09:55 +0100 Subject: [PATCH 041/119] skip flaky suite (#168509) --- .../plugins/cases/public/components/add_comment/index.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 2c58b34344b4e..55ecb2df29435 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -55,6 +55,7 @@ const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id} // FLAKY: https://github.com/elastic/kibana/issues/168506 // FLAKY: https://github.com/elastic/kibana/issues/168507 // FLAKY: https://github.com/elastic/kibana/issues/168508 +// FLAKY: https://github.com/elastic/kibana/issues/168509 describe.skip('AddComment ', () => { let appMockRender: AppMockRenderer; From 9d0c7d7df2b191d116df7ebcd69ae0225b4e0649 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Wed, 25 Oct 2023 21:14:43 +0200 Subject: [PATCH 042/119] [ES UI Shared] Remove old ace based `EuiCodeEditor` (#169613) --- .../__snapshots__/code_editor.test.tsx.snap | 627 ------------------ .../components/code_editor/_code_editor.scss | 38 -- .../public/components/code_editor/_index.scss | 1 - .../code_editor/code_editor.test.tsx | 116 ---- .../components/code_editor/code_editor.tsx | 312 --------- .../public/components/code_editor/index.tsx | 32 - .../components/code_editor/jest_mock.tsx | 32 - src/plugins/es_ui_shared/public/index.ts | 2 - .../components/json_editor/json_editor.tsx | 6 +- .../public/application/shared_imports.ts | 3 - .../template_clone.test.tsx | 1 - .../template_create.test.tsx | 1 - .../template_edit.test.tsx | 1 - .../component_template_create.test.tsx | 2 - .../component_template_edit.test.tsx | 1 - .../helpers/setup_environment.tsx | 1 - .../load_mappings_provider.test.tsx | 1 - .../load_mappings/load_mappings_provider.tsx | 5 +- .../index_management/public/shared_imports.ts | 1 - .../index_management/test/global_mocks.tsx | 29 - .../ingest_pipelines_clone.test.tsx | 18 - .../ingest_pipelines_create.test.tsx | 18 - .../ingest_pipelines_edit.test.tsx | 18 - .../__jest__/test_pipeline.helpers.tsx | 1 - .../load_from_json/modal_provider.test.tsx | 1 - .../ml_job_editor/ml_job_editor.tsx | 6 +- .../plugins/osquery/public/shared_imports.ts | 2 - .../detail_panel/detail_panel.test.js | 6 +- 28 files changed, 11 insertions(+), 1271 deletions(-) delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/_code_editor.scss delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/_index.scss delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/code_editor.test.tsx delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/code_editor.tsx delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/index.tsx delete mode 100644 src/plugins/es_ui_shared/public/components/code_editor/jest_mock.tsx delete mode 100644 x-pack/plugins/index_management/test/global_mocks.tsx diff --git a/src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap deleted file mode 100644 index aeab9a66c7694..0000000000000 --- a/src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap +++ /dev/null @@ -1,627 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EuiCodeEditor behavior hint element should be disabled when the ui ace box gains focus 1`] = ` - -`; - -exports[`EuiCodeEditor behavior hint element should be enabled when the ui ace box loses focus 1`] = ` - -`; - -exports[`EuiCodeEditor behavior hint element should be tabable 1`] = ` - -`; - -exports[`EuiCodeEditor is rendered 1`] = ` -
- -
-