Skip to content

Commit

Permalink
[ResponseOps][BUG] - UI fixes for Alerts Summary chart (#137476) (#13…
Browse files Browse the repository at this point in the history
…7840)

* 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 <xavier.mouligneau@elastic.co>
(cherry picked from commit 521c2a4)

Co-authored-by: Faisal Kanout <faisal.kanout@elastic.co>
  • Loading branch information
kibanamachine and fkanout authored Aug 2, 2022
1 parent 8ef28b3 commit 00145be
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,6 @@ interface RuleAlertsAggs {
error?: string;
alertsChartData: AlertChartData[];
}
interface BucketAggsPerDay {
key_as_string: string;
doc_count: number;
}

export async function fetchRuleAlertsAggByTimeRange({
http,
Expand Down Expand Up @@ -146,58 +142,143 @@ 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',
},
},
},
},
},
}),
});
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string>('');
const isDarkMode = useUiSetting<boolean>('theme:darkMode');

const scaledDateFormatPreference = useUiSetting<string[][]>('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,
Expand All @@ -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);
Expand All @@ -66,8 +95,33 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary
}, [rule, ruleTypes]);

if (isLoadingRuleAlertsAggs) return <EuiLoadingSpinner />;
if (errorRuleAlertsAggs) return <EuiFlexItem>Error</EuiFlexItem>;

if (errorRuleAlertsAggs)
return (
<EuiEmptyPrompt
iconType="alert"
color="danger"
title={
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingTitle"
defaultMessage="Unable to load the alerts summary"
/>
</h5>
}
body={
<p>
{
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingBody"
defaultMessage=" There was an error loading the alerts summary. Contact your
administrator for help."
/>
}
</p>
}
/>
);
const isVisibleFunction: FilterPredicate = (series) => series.splitAccessors.get('g') !== 'total';
return (
<EuiPanel hasShadow={false} hasBorder>
<EuiFlexGroup direction="column">
Expand All @@ -82,12 +136,20 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.last30days"
defaultMessage="Last 30 days"
/>
</EuiText>
</EuiFlexItem>

<EuiPanel hasShadow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText size="s" color="subdued">
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.allAlertsLabel"
defaultMessage="All alerts"
Expand All @@ -100,35 +162,34 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary
</EuiFlexGroup>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText size="s" color="subdued">
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel"
defaultMessage="Active"
/>
</EuiText>
<EuiText color="#4A7194">
<EuiText color={LIGHT_THEME.colors.vizColors[2]}>
<h4>{active}</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText size="s" color="subdued">
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel"
defaultMessage="Recovered"
/>
</EuiText>
<EuiFlexItem>
<EuiText color="#C4407C">
<EuiText color={LIGHT_THEME.colors.vizColors[1]}>
<h4>{recovered}</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
<EuiHorizontalRule margin="none" />
</EuiFlexGroup>
</EuiFlexItem>

Expand All @@ -145,7 +206,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary
</EuiFlexGroup>
<EuiSpacer size="m" />
<Chart size={{ height: 50 }}>
<Settings tooltip={TooltipType.None} theme={theme} />
<Settings tooltip={tooltipSettings} theme={theme} />
<BarSeries
id="bars"
xScaleType={ScaleType.Time}
Expand All @@ -156,6 +217,7 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary
splitSeriesAccessors={G_ACCESSORS}
color={getColorSeries}
data={chartData}
filterSeriesInTooltip={isVisibleFunction}
/>
</Chart>
</EuiPanel>
Expand Down
Loading

0 comments on commit 00145be

Please sign in to comment.