diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx index 1fc8cc6614a75..4b7a1907e206a 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, type EuiAccordionProps } from '@elastic/eui'; import { useSummaryTimeRange } from '@kbn/observability-plugin/public'; import type { TimeRange } from '@kbn/es-query'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; @@ -24,6 +24,8 @@ import { ALERT_STATUS_ALL } from '../../../../common/alerts/constants'; import { AlertsSectionTitle } from '../../components/section_titles'; import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { CollapsibleSection } from './section/collapsible_section'; +import { AlertsClosedContent } from './alerts_closed_content'; +import { type AlertsCount } from '../../../../hooks/use_alerts_count'; export const AlertsSummaryContent = ({ assetName, @@ -37,6 +39,9 @@ export const AlertsSummaryContent = ({ const { featureFlags } = usePluginConfig(); const [isAlertFlyoutVisible, { toggle: toggleAlertFlyout }] = useBoolean(false); const { overrides } = useAssetDetailsRenderPropsContext(); + const [collapsibleStatus, setCollapsibleStatus] = + useState('open'); + const [activeAlertsCount, setActiveAlertsCount] = useState(undefined); const alertsEsQueryByStatus = useMemo( () => @@ -48,6 +53,14 @@ export const AlertsSummaryContent = ({ [assetName, dateRange] ); + const onLoaded = (alertsCount?: AlertsCount) => { + const { activeAlertCount = 0 } = alertsCount ?? {}; + const hasActiveAlerts = activeAlertCount > 0; + + setCollapsibleStatus(hasActiveAlerts ? 'open' : 'closed'); + setActiveAlertsCount(alertsCount?.activeAlertCount); + }; + return ( <> } + initialTriggerValue={collapsibleStatus} extraAction={ {featureFlags.inventoryThresholdAlertRuleEnabled && ( @@ -72,9 +87,12 @@ export const AlertsSummaryContent = ({ } > - + - {featureFlags.inventoryThresholdAlertRuleEnabled && ( void; } const MemoAlertSummaryWidget = React.memo( - ({ alertsQuery, dateRange }: MemoAlertSummaryWidgetProps) => { + ({ alertsQuery, dateRange, onLoaded }: MemoAlertSummaryWidgetProps) => { const { services } = useKibanaContextForPlugin(); const summaryTimeRange = useSummaryTimeRange(dateRange); @@ -112,6 +131,7 @@ const MemoAlertSummaryWidget = React.memo( featureIds={infraAlertFeatureIds} filter={alertsQuery} timeRange={summaryTimeRange} + onLoaded={onLoaded} fullSize hideChart /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts_closed_content.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts_closed_content.tsx new file mode 100644 index 0000000000000..a08a0313230ee --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts_closed_content.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; + +export const AlertsClosedContent = ({ activeAlertCount }: { activeAlertCount?: number }) => { + const shouldRenderAlertsClosedContent = typeof activeAlertCount === 'number'; + + if (!shouldRenderAlertsClosedContent) { + return null; + } + + if (activeAlertCount > 0) { + return ( + + + {activeAlertCount} + + + ); + } + + return ( + + {i18n.translate('xpack.infra.assetDetails.noActiveAlertsContentClosedSection', { + defaultMessage: 'No active alerts', + })} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx index ec31851d89a6d..da0b993199ee1 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React, { useEffect, useState } from 'react'; + import { EuiAccordion, EuiFlexGroup, @@ -12,7 +14,6 @@ import { useGeneratedHtmlId, type EuiAccordionProps, } from '@elastic/eui'; -import React, { useState } from 'react'; export const CollapsibleSection = ({ title, @@ -22,6 +23,7 @@ export const CollapsibleSection = ({ collapsible, ['data-test-subj']: dataTestSubj, id, + initialTriggerValue, }: { title: React.FunctionComponent; closedSectionContent?: React.ReactNode; @@ -31,13 +33,18 @@ export const CollapsibleSection = ({ collapsible: boolean; ['data-test-subj']: string; id: string; + initialTriggerValue?: EuiAccordionProps['forceState']; }) => { const [trigger, setTrigger] = useState('open'); + useEffect(() => { + setTrigger(initialTriggerValue ?? 'open'); + }, [initialTriggerValue]); + const Title = title; const ButtonContent = () => closedSectionContent && trigger === 'closed' ? ( - + </EuiFlexItem> diff --git a/x-pack/plugins/infra/public/hooks/use_alerts_count.ts b/x-pack/plugins/infra/public/hooks/use_alerts_count.ts index 7d05a275d6eae..5c602d09b7d23 100644 --- a/x-pack/plugins/infra/public/hooks/use_alerts_count.ts +++ b/x-pack/plugins/infra/public/hooks/use_alerts_count.ts @@ -28,7 +28,7 @@ interface FetchAlertsCountParams { signal: AbortSignal; } -interface AlertsCount { +export interface AlertsCount { activeAlertCount: number; recoveredAlertCount: number; } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx index 15c4e568ad8ed..c3f778299ab69 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { EuiIcon, EuiLoadingSpinner, EuiNotificationBadge, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLoadingSpinner, EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsCount } from '../../../../../hooks/use_alerts_count'; import { infraAlertFeatureIds } from './config'; @@ -40,12 +40,12 @@ export const AlertsTabBadge = () => { typeof alertsCount?.activeAlertCount === 'number' && alertsCount.activeAlertCount > 0; return shouldRenderBadge ? ( - <EuiNotificationBadge + <EuiBadge + color="danger" className="eui-alignCenter" - size="m" data-test-subj="hostsView-tabs-alerts-count" > {alertsCount?.activeAlertCount} - </EuiNotificationBadge> + </EuiBadge> ) : null; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx index 2031c9a1f3fe2..2619ef2b25258 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx @@ -37,9 +37,9 @@ export const AlertSummaryWidget = ({ useEffect(() => { if (!isLoading && onLoaded) { - onLoaded(); + onLoaded({ activeAlertCount, recoveredAlertCount }); } - }, [isLoading, onLoaded]); + }, [activeAlertCount, isLoading, onLoaded, recoveredAlertCount]); if (isLoading) return <AlertSummaryWidgetLoader fullSize={fullSize} isLoadingWithoutChart={hideChart} />; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts index 48a49acf5ad7c..1bb02adff0653 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts @@ -30,6 +30,11 @@ export interface ChartProps { onBrushEnd?: BrushEndListener; } +interface AlertsCount { + activeAlertCount: number; + recoveredAlertCount: number; +} + export interface AlertSummaryWidgetProps { featureIds?: ValidFeatureId[]; filter?: estypes.QueryDslQueryContainer; @@ -38,5 +43,5 @@ export interface AlertSummaryWidgetProps { timeRange: AlertSummaryTimeRange; chartProps: ChartProps; hideChart?: boolean; - onLoaded?: () => void; + onLoaded?: (alertsCount?: AlertsCount) => void; } diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index 172ac410eb427..42147652f34a5 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -204,6 +204,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.assetDetails.overviewAlertsTitleExists(); }); + it('should show / hide alerts section with no alerts and show / hide closed section content', async () => { + await pageObjects.assetDetails.alertsSectionCollapsibleExist(); + // Collapsed by default + await pageObjects.assetDetails.alertsSectionClosedContentNoAlertsExist(); + // Expand + await pageObjects.assetDetails.alertsSectionCollapsibleClick(); + await pageObjects.assetDetails.alertsSectionClosedContentNoAlertsMissing(); + }); + it('shows the CPU Profiling prompt if UI setting for Profiling integration is enabled', async () => { await setInfrastructureProfilingIntegrationUiSetting(true); await pageObjects.assetDetails.cpuProfilingPromptExists(); @@ -213,6 +222,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await setInfrastructureProfilingIntegrationUiSetting(false); await pageObjects.assetDetails.cpuProfilingPromptMissing(); }); + + describe('Alerts Section with alerts', () => { + before(async () => { + await navigateToNodeDetails('demo-stack-apache-01', 'demo-stack-apache-01'); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await pageObjects.timePicker.setAbsoluteRange( + START_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT), + END_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT) + ); + + await pageObjects.assetDetails.clickOverviewTab(); + }); + + after(async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box'); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await pageObjects.timePicker.setAbsoluteRange( + START_HOST_PROCESSES_DATE.format(DATE_PICKER_FORMAT), + END_HOST_PROCESSES_DATE.format(DATE_PICKER_FORMAT) + ); + }); + + it('should show / hide alerts section with active alerts and show / hide closed section content', async () => { + await pageObjects.assetDetails.alertsSectionCollapsibleExist(); + // Expanded by default + await pageObjects.assetDetails.alertsSectionClosedContentMissing(); + // Collapse + await pageObjects.assetDetails.alertsSectionCollapsibleClick(); + await pageObjects.assetDetails.alertsSectionClosedContentExist(); + // Expand + await pageObjects.assetDetails.alertsSectionCollapsibleClick(); + await pageObjects.assetDetails.alertsSectionClosedContentMissing(); + }); + }); }); describe('Metadata Tab', () => { diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index 5e1ea574f8a81..cd34d9c2ca10b 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -89,6 +89,24 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return await testSubjects.existOrFail('infraAssetDetailsMetricsCollapsible'); }, + async alertsSectionCollapsibleClick() { + return await testSubjects.click('infraAssetDetailsAlertsCollapsible'); + }, + + async alertsSectionClosedContentExist() { + return await testSubjects.existOrFail('infraAssetDetailsAlertsClosedContentWithAlerts'); + }, + async alertsSectionClosedContentMissing() { + return await testSubjects.missingOrFail('infraAssetDetailsAlertsClosedContentWithAlerts'); + }, + + async alertsSectionClosedContentNoAlertsExist() { + return await testSubjects.existOrFail('infraAssetDetailsAlertsClosedContentNoAlerts'); + }, + async alertsSectionClosedContentNoAlertsMissing() { + return await testSubjects.missingOrFail('infraAssetDetailsAlertsClosedContentNoAlerts'); + }, + // Metadata async clickMetadataTab() { return testSubjects.click('infraAssetDetailsMetadataTab');