From 521c2a4dcbe6064f56db0032d0b14b30f63a3933 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Tue, 2 Aug 2022 14:59:35 +0200 Subject: [PATCH] [ResponseOps][BUG] - UI fixes for Alerts Summary chart (#137476) * Use more efficient code/query, and add the 3rd bar (base one) * Update comment * Update comment * Add the shade color to the base bar * Update comment * Remove unused else * add tooltip and fix base bar * Update design * Add error handling message * review idea * fix no alerts + graph to get greyed out * review I * Fix the order of alertsChartData * Fix design and color scheme Co-authored-by: Xavier Mouligneau --- .../use_load_rule_alerts_aggregations.ts | 139 ++++++++++++++---- .../components/alert_summary/helpers.tsx | 18 ++- .../alert_summary/rule_alerts_summary.tsx | 86 +++++++++-- .../components/alert_summary/types.ts | 4 +- 4 files changed, 201 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts index 530fdbde82b59..aeba9605cb9a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts @@ -113,10 +113,6 @@ interface RuleAlertsAggs { error?: string; alertsChartData: AlertChartData[]; } -interface BucketAggsPerDay { - key_as_string: string; - doc_count: number; -} export async function fetchRuleAlertsAggByTimeRange({ http, @@ -146,28 +142,60 @@ export async function fetchRuleAlertsAggByTimeRange({ { range: { '@timestamp': { - // When needed, we can make this range configurable via a function argument. gte: 'now-30d', lt: 'now', }, }, }, + { + bool: { + should: [ + { + term: { + 'kibana.alert.status': 'active', + }, + }, + { + term: { + 'kibana.alert.status': 'recovered', + }, + }, + ], + }, + }, ], }, }, aggs: { - filterAggs: { + total: { filters: { filters: { - alert_active: { term: { 'kibana.alert.status': 'active' } }, - alert_recovered: { term: { 'kibana.alert.status': 'recovered' } }, + totalActiveAlerts: { + term: { + 'kibana.alert.status': 'active', + }, + }, + totalRecoveredAlerts: { + term: { + 'kibana.alert.status': 'recovered', + }, + }, + }, + }, + }, + statusPerDay: { + date_histogram: { + field: '@timestamp', + fixed_interval: '1d', + extended_bounds: { + min: 'now-30d', + max: 'now', }, }, aggs: { - status_per_day: { - date_histogram: { - field: '@timestamp', - fixed_interval: '1d', + alertStatus: { + terms: { + field: 'kibana.alert.status', }, }, }, @@ -175,29 +203,82 @@ export async function fetchRuleAlertsAggByTimeRange({ }, }), }); - const active = res?.aggregations?.filterAggs.buckets.alert_active?.doc_count ?? 0; - const recovered = res?.aggregations?.filterAggs.buckets.alert_recovered?.doc_count ?? 0; + + const active = res?.aggregations?.total.buckets.totalActiveAlerts?.doc_count ?? 0; + const recovered = res?.aggregations?.total.buckets.totalRecoveredAlerts?.doc_count ?? 0; + let maxTotalAlertPerDay = 0; + res?.aggregations?.statusPerDay.buckets.forEach( + (dayAlerts: { + key: number; + doc_count: number; + alertStatus: { + buckets: Array<{ + key: 'active' | 'recovered'; + doc_count: number; + }>; + }; + }) => { + if (dayAlerts.doc_count > maxTotalAlertPerDay) { + maxTotalAlertPerDay = dayAlerts.doc_count; + } + } + ); + const alertsChartData = [ - ...res?.aggregations?.filterAggs.buckets.alert_active.status_per_day.buckets.map( - (bucket: BucketAggsPerDay) => ({ - date: bucket.key_as_string, - status: 'active', - count: bucket.doc_count, - }) - ), - ...res?.aggregations?.filterAggs.buckets.alert_recovered.status_per_day.buckets.map( - (bucket: BucketAggsPerDay) => ({ - date: bucket.key_as_string, - status: 'recovered', - count: bucket.doc_count, - }) + ...res?.aggregations?.statusPerDay.buckets.reduce( + ( + acc: AlertChartData[], + dayAlerts: { + key: number; + doc_count: number; + alertStatus: { + buckets: Array<{ + key: 'active' | 'recovered'; + doc_count: number; + }>; + }; + } + ) => { + // We are adding this to each day to construct the 30 days bars (background bar) when there is no data for a given day or to show the delta today alerts/total alerts. + const totalDayAlerts = { + date: dayAlerts.key, + count: maxTotalAlertPerDay === 0 ? 1 : maxTotalAlertPerDay, + status: 'total', + }; + + if (dayAlerts.doc_count > 0) { + const localAlertChartData = acc; + // If there are alerts in this day, we construct the chart data + dayAlerts.alertStatus.buckets.forEach((alert) => { + localAlertChartData.push({ + date: dayAlerts.key, + count: alert.doc_count, + status: alert.key, + }); + }); + const deltaAlertsCount = maxTotalAlertPerDay - dayAlerts.doc_count; + if (deltaAlertsCount > 0) { + localAlertChartData.push({ + date: dayAlerts.key, + count: deltaAlertsCount, + status: 'total', + }); + } + return localAlertChartData; + } + return [...acc, totalDayAlerts]; + }, + [] ), ]; - return { active, recovered, - alertsChartData, + alertsChartData: [ + ...alertsChartData.filter((acd) => acd.status === 'recovered'), + ...alertsChartData.filter((acd) => acd.status === 'active'), + ...alertsChartData.filter((acd) => acd.status === 'total'), + ], }; } catch (error) { return { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx index f9cb7c773ab1c..167c98a678f52 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx @@ -10,7 +10,7 @@ import { AlertChartData } from './types'; export const formatChartAlertData = ( data: AlertChartData[] -): Array<{ x: string; y: number; g: string }> => +): Array<{ x: number; y: number; g: string }> => data.map((alert) => ({ x: alert.date, y: alert.count, @@ -20,10 +20,22 @@ export const formatChartAlertData = ( export const getColorSeries = ({ seriesKeys }: XYChartSeriesIdentifier) => { switch (seriesKeys[0]) { case 'active': - return LIGHT_THEME.colors.vizColors[1]; - case 'recovered': return LIGHT_THEME.colors.vizColors[2]; + case 'recovered': + return LIGHT_THEME.colors.vizColors[1]; + case 'total': + return '#f5f7fa'; default: return null; } }; + +/** + * This function may be passed to `Array.find()` to locate the `P1DT` + * configuration (sub) setting, a string array that contains two entries + * like the following example: `['P1DT', 'YYYY-MM-DD']`. + */ +export const isP1DTFormatterSetting = (formatNameFormatterPair?: string[]) => + Array.isArray(formatNameFormatterPair) && + formatNameFormatterPair[0] === 'P1DT' && + formatNameFormatterPair.length === 2; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx index 58d75d0a6d4bb..6fc657d051594 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import { BarSeries, Chart, ScaleType, Settings, TooltipType } from '@elastic/charts'; import { + BarSeries, + Chart, + FilterPredicate, + LIGHT_THEME, + ScaleType, + Settings, + TooltipType, +} from '@elastic/charts'; +import { + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiLoadingSpinner, EuiPanel, EuiSpacer, @@ -25,18 +33,30 @@ import { EUI_SPARKLINE_THEME_PARTIAL, } from '@elastic/eui/dist/eui_charts_theme'; import { useUiSetting } from '@kbn/kibana-react-plugin/public'; +import moment from 'moment'; import { useLoadRuleAlertsAggs } from '../../../../hooks/use_load_rule_alerts_aggregations'; import { useLoadRuleTypes } from '../../../../hooks/use_load_rule_types'; import { formatChartAlertData, getColorSeries } from '.'; import { RuleAlertsSummaryProps } from '.'; +import { isP1DTFormatterSetting } from './helpers'; const Y_ACCESSORS = ['y']; const X_ACCESSORS = ['x']; const G_ACCESSORS = ['g']; - +const FALLBACK_DATE_FORMAT_SCALED_P1DT = 'YYYY-MM-DD'; export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummaryProps) => { const [features, setFeatures] = useState(''); const isDarkMode = useUiSetting('theme:darkMode'); + + const scaledDateFormatPreference = useUiSetting('dateFormat:scaled'); + const maybeP1DTFormatter = Array.isArray(scaledDateFormatPreference) + ? scaledDateFormatPreference.find(isP1DTFormatterSetting) + : null; + const p1dtFormat = + Array.isArray(maybeP1DTFormatter) && maybeP1DTFormatter.length === 2 + ? maybeP1DTFormatter[1] + : FALLBACK_DATE_FORMAT_SCALED_P1DT; + const theme = useMemo( () => [ EUI_SPARKLINE_THEME_PARTIAL, @@ -57,6 +77,15 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary features, }); const chartData = useMemo(() => formatChartAlertData(alertsChartData), [alertsChartData]); + const tooltipSettings = useMemo( + () => ({ + type: TooltipType.VerticalCursor, + headerFormatter: ({ value }: { value: number }) => { + return <>{moment(value).format(p1dtFormat)}; + }, + }), + [p1dtFormat] + ); useEffect(() => { const matchedRuleType = ruleTypes.find((type) => type.id === rule.ruleTypeId); @@ -66,8 +95,33 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary }, [rule, ruleTypes]); if (isLoadingRuleAlertsAggs) return ; - if (errorRuleAlertsAggs) return Error; - + if (errorRuleAlertsAggs) + return ( + + + + } + body={ +

+ { + + } +

+ } + /> + ); + const isVisibleFunction: FilterPredicate = (series) => series.splitAccessors.get('g') !== 'total'; return ( @@ -82,12 +136,20 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary /> + + + + + - + - + - +

{active}

- + - +

{recovered}

@@ -128,7 +190,6 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary
-
@@ -145,7 +206,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary - +
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts index 6ee51dfad4caa..ae4d8696572b0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts @@ -12,9 +12,9 @@ export interface RuleAlertsSummaryProps { filteredRuleTypes: string[]; } export interface AlertChartData { - status: 'active' | 'recovered'; + status: 'active' | 'recovered' | 'total'; count: number; - date: string; + date: number; } export interface AlertsChartProps {