[AO] Implement Alert Summary Widget new design (#149348)

Closes #149239
Closes #149238

## 📝 Summary

This PR implements the new design of the Alert Summary Widget. In the
new design, we removed the recovered chart and count to make it easier
to understand.
([design](https://www.figma.com/file/xnSsLoEMntX3VLG0qwmmnt/Alert-summary-widget-V2?t=m2Xd16Obz5OJz7A1-0))
After discussion with @maciejforcone, we decided to use blue color
instead of black since when we have one alert that triggers the whole
time during the selected period, the chart was not readable.

|Full-size|Compact|
|---|---|

|![image](214273271-4d4f3ba4-196a-472a-903e-913583f2678f.png)|


https://user-images.githubusercontent.com/12370520/214258610-2d5d0b9b-9034-4cec-885f-c57959cd7d53.mov

## 🧪 How to test
- Check the component's new design in
[storybook](https://ci-artifacts.kibana.dev/storybooks/pr-149348/5181917c254d5a4e6038be7ceb5e551fcab03161/triggers_actions_ui/index.html?path=/story/app-alertsummarywidget--compact)
- Generate some alerts
- Check the Alerts page; you should see the Alert Summary Widget show
the correct data
- Check the Rule details of one of the alerts to see the compact version
there
- Clicking on Alert Summary Widget compact version should work as
expected

Co-authored-by: Katrin Freihofner <katrin.freihofner@elastic.co>
This commit is contained in:
Maryam Saeidi 2023-01-27 10:44:21 +01:00 committed by GitHub
parent 0d613e58cf
commit 7bce7c934d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 282 additions and 322 deletions

View file

@ -35086,10 +35086,8 @@
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.start": "Démarrer",
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.status": "Statut",
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage": "La durée dépasse le temps d'exécution attendu de la règle.",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel": "Actuellement actives",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingBody": "Une erreur s'est produite lors du chargement du récapitulatif des alertes. Contactez votre administrateur pour obtenir de l'aide.",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingTitle": "Impossible de charger le récapitulatif des alertes",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title": "Alertes",
"xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel": "Supprimer la règle",
"xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel": "Désactiver",
"xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "Modifier",
@ -35124,7 +35122,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "règle",
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "Alertes",
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "Historique",
"xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel": "Récupéré",
"xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.ruleIsEnabledDisabledTitle": "La règle est",
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrors": "Actions comportant des erreurs",
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.close": "Fermer",

View file

@ -35054,10 +35054,8 @@
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.start": "開始",
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.status": "ステータス",
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage": "期間がルールの想定実行時間を超えています。",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel": "現在アクティブ",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingBody": "アラート概要の読み込みエラーが発生しました。ヘルプについては、管理者にお問い合わせください。",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingTitle": "アラート概要を読み込めません",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title": "アラート",
"xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel": "ルールの削除",
"xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel": "無効にする",
"xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "編集",
@ -35092,7 +35090,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "ルール",
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "アラート",
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "履歴",
"xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel": "回復済み",
"xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.ruleIsEnabledDisabledTitle": "ルールは",
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrors": "エラーのアクション",
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.close": "閉じる",

View file

@ -35091,10 +35091,8 @@
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.start": "启动",
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.status": "状态",
"xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage": "持续时间超出了规则的预期运行时间。",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel": "当前处于活动状态",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingBody": "加载告警摘要时出现错误。请联系您的管理员寻求帮助。",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.errorLoadingTitle": "无法加载告警摘要",
"xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title": "告警",
"xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel": "删除规则",
"xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel": "禁用",
"xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "编辑",
@ -35129,7 +35127,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "规则",
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "告警",
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "历史记录",
"xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel": "已恢复",
"xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.ruleIsEnabledDisabledTitle": "规则为",
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrors": "错误操作",
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.close": "关闭",

View file

@ -46,7 +46,6 @@ describe('useLoadAlertSummary', () => {
activeAlertCount: 0,
activeAlerts: [],
recoveredAlertCount: 0,
recoveredAlerts: [],
},
});

View file

@ -27,7 +27,6 @@ interface AlertSummary {
activeAlertCount: number;
activeAlerts: Alert[];
recoveredAlertCount: number;
recoveredAlerts: Alert[];
}
interface LoadAlertSummaryResponse {
@ -44,7 +43,6 @@ export function useLoadAlertSummary({ featureIds, timeRange, filter }: UseLoadAl
activeAlertCount: 0,
activeAlerts: [],
recoveredAlertCount: 0,
recoveredAlerts: [],
},
});
const isCancelledRef = useRef(false);
@ -56,14 +54,13 @@ export function useLoadAlertSummary({ featureIds, timeRange, filter }: UseLoadAl
abortCtrlRef.current = new AbortController();
try {
const { activeAlertCount, activeAlerts, recoveredAlertCount, recoveredAlerts } =
await fetchAlertSummary({
featureIds,
filter,
http,
signal: abortCtrlRef.current.signal,
timeRange,
});
const { activeAlertCount, activeAlerts, recoveredAlertCount } = await fetchAlertSummary({
featureIds,
filter,
http,
signal: abortCtrlRef.current.signal,
timeRange,
});
if (!isCancelledRef.current) {
setAlertSummary(() => ({
@ -71,7 +68,6 @@ export function useLoadAlertSummary({ featureIds, timeRange, filter }: UseLoadAl
activeAlertCount,
activeAlerts,
recoveredAlertCount,
recoveredAlerts,
},
isLoading: false,
}));
@ -123,12 +119,10 @@ async function fetchAlertSummary({
const activeAlertCount = res?.activeAlertCount ?? 0;
const activeAlerts = res?.activeAlerts ?? [];
const recoveredAlertCount = res?.recoveredAlertCount ?? 0;
const recoveredAlerts = res?.recoveredAlerts ?? [];
return {
activeAlertCount,
activeAlerts,
recoveredAlertCount,
recoveredAlerts,
};
}

View file

@ -25,32 +25,14 @@ export const mockedAlertSummaryResponse = {
{ key: 1671808000000, doc_count: 6 },
{ key: 1671908000000, doc_count: 14 },
{ key: 1672008000000, doc_count: 15 },
{ key: 1672108000000, doc_count: 15 },
{ key: 1672108000000, doc_count: 20 },
{ key: 1672208000000, doc_count: 10 },
{ key: 1672308000000, doc_count: 9 },
{ key: 1672408000000, doc_count: 7 },
{ key: 1672508000000, doc_count: 2 },
{ key: 1672608000000, doc_count: 2 },
],
recoveredAlertCount: 15,
recoveredAlerts: [
{ key: 1671108000000, doc_count: 0 },
{ key: 1671208000000, doc_count: 0 },
{ key: 1671308000000, doc_count: 0 },
{ key: 1671408000000, doc_count: 0 },
{ key: 1671508000000, doc_count: 0 },
{ key: 1671608000000, doc_count: 0 },
{ key: 1671708000000, doc_count: 2 },
{ key: 1671808000000, doc_count: 0 },
{ key: 1671908000000, doc_count: 0 },
{ key: 1672008000000, doc_count: 0 },
{ key: 1672108000000, doc_count: 0 },
{ key: 1672208000000, doc_count: 5 },
{ key: 1672308000000, doc_count: 1 },
{ key: 1672408000000, doc_count: 2 },
{ key: 1672508000000, doc_count: 5 },
{ key: 1672608000000, doc_count: 0 },
],
recoveredAlertCount: 20,
};
export const mockedAlertSummaryTimeRange: AlertSummaryTimeRange = {

View file

@ -31,10 +31,6 @@ jest.mock('../../../../hooks/use_load_alert_summary', () => ({
{ key: 1671321600000, doc_count: 0 },
{ key: 1671408000000, doc_count: 1 },
],
recoveredAlerts: [
{ key: 1671321600000, doc_count: 2 },
{ key: 1671408000000, doc_count: 5 },
],
},
}),
}));
@ -74,7 +70,6 @@ describe('AlertSummaryWidget', () => {
it('should render counts and title correctly', async () => {
const alertSummaryWidget = renderComponent();
expect(alertSummaryWidget.queryByTestId('activeAlertsCount')).toHaveTextContent('1');
expect(alertSummaryWidget.queryByTestId('recoveredAlertsCount')).toHaveTextContent('7');
expect(alertSummaryWidget.queryByTestId('totalAlertsCount')).toHaveTextContent('8');
expect(alertSummaryWidget.queryByTestId(TITLE_DATA_TEST_SUBJ)).toBeTruthy();
});

View file

@ -24,7 +24,7 @@ export const AlertSummaryWidget = ({
chartThemes,
}: AlertSummaryWidgetProps) => {
const {
alertSummary: { activeAlertCount, activeAlerts, recoveredAlertCount, recoveredAlerts },
alertSummary: { activeAlertCount, activeAlerts, recoveredAlertCount },
isLoading,
error,
} = useLoadAlertSummary({
@ -41,7 +41,6 @@ export const AlertSummaryWidget = ({
activeAlertCount={activeAlertCount}
activeAlerts={activeAlerts}
recoveredAlertCount={recoveredAlertCount}
recoveredAlerts={recoveredAlerts}
dateFormat={timeRange.dateFormat}
chartThemes={chartThemes}
/>
@ -51,7 +50,6 @@ export const AlertSummaryWidget = ({
activeAlerts={activeAlerts}
onClick={onClick}
recoveredAlertCount={recoveredAlertCount}
recoveredAlerts={recoveredAlerts}
timeRangeTitle={timeRange.title}
chartThemes={chartThemes}
/>

View file

@ -0,0 +1,40 @@
/*
* 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 numeral from '@elastic/numeral';
import { EuiIcon, EuiText, useEuiTheme } from '@elastic/eui';
import { ACTIVE_ALERT_LABEL, ALERT_COUNT_FORMAT } from './constants';
interface Props {
activeAlertCount: number;
}
export const ActiveAlertCounts = ({ activeAlertCount }: Props) => {
const { euiTheme } = useEuiTheme();
return (
<>
<EuiText
color={!!activeAlertCount ? euiTheme.colors.dangerText : euiTheme.colors.successText}
>
<h3 data-test-subj={`activeAlertsCount`}>
{numeral(activeAlertCount).format(ALERT_COUNT_FORMAT)}
{!!activeAlertCount && (
<>
&nbsp;
<EuiIcon type="alert" ascent={10} />
</>
)}
</h3>
</EuiText>
<EuiText size="s" color="subdued">
{ACTIVE_ALERT_LABEL}
</EuiText>
</>
);
};

View file

@ -0,0 +1,45 @@
/*
* 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, { MouseEvent } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import { ALERT_STATUS_ACTIVE, AlertStatus } from '@kbn/rule-data-utils';
import { ActiveAlertCounts } from './active_alert_counts';
import { AllAlertCounts } from './all_alert_counts';
interface Props {
activeAlertCount: number;
recoveredAlertCount: number;
onActiveClick?: (
event: MouseEvent<HTMLAnchorElement | HTMLDivElement>,
status?: AlertStatus
) => void;
}
export const AlertCounts = ({ activeAlertCount, recoveredAlertCount, onActiveClick }: Props) => {
return (
<EuiFlexGroup gutterSize="l" responsive={false}>
<EuiFlexItem style={{ minWidth: 50, wordWrap: 'break-word' }} grow={false}>
<AllAlertCounts count={activeAlertCount + recoveredAlertCount} />
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: 50, wordWrap: 'break-word' }} grow={false}>
{!!onActiveClick ? (
<EuiLink
onClick={(event: React.MouseEvent<HTMLAnchorElement>) =>
onActiveClick(event, ALERT_STATUS_ACTIVE)
}
data-test-subj="activeAlerts"
>
<ActiveAlertCounts activeAlertCount={activeAlertCount} />
</EuiLink>
) : (
<ActiveAlertCounts activeAlertCount={activeAlertCount} />
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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 { Axis, Chart, CurveType, LineSeries, Position, ScaleType, Settings } from '@elastic/charts';
import { Color } from '@elastic/charts/dist/common/colors';
import { ColorVariant } from '@elastic/charts/dist/utils/common';
import {
EuiFlexGroup,
EuiFlexItem,
EuiListGroupItemProps,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme';
import React from 'react';
import { Alert, ChartThemes } from '../types';
interface AlertStateInfoProps {
chartThemes: ChartThemes;
count: number;
data: Alert[];
dataTestSubj: string;
domain: { min: number; max: number };
id: string;
stroke: Color | ColorVariant;
title: EuiListGroupItemProps['label'];
}
export const AlertStateInfo = ({
count,
data,
dataTestSubj,
domain,
id,
stroke,
chartThemes: { theme, baseTheme },
title,
}: AlertStateInfoProps) => {
const { euiTheme } = useEuiTheme();
const chartTheme = [
theme,
EUI_SPARKLINE_THEME_PARTIAL,
{
chartMargins: {
left: 10,
right: 10,
top: 10,
bottom: 10,
},
},
];
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={1} style={{ minWidth: '70px' }}>
<EuiText color={euiTheme.colors.text}>
<h3 data-test-subj={`${dataTestSubj}Count`}>{count}</h3>
</EuiText>
<EuiText size="s" color="subdued">
{title}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<Chart size={{ height: 50 }}>
<Settings theme={chartTheme} baseTheme={baseTheme} tooltip={{ type: 'none' }} />
<Axis
domain={domain}
hide
id={id + '-axis'}
position={Position.Left}
showGridLines={false}
/>
<LineSeries
id={id}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['doc_count']}
data={data}
lineSeriesStyle={{
line: {
strokeWidth: 2,
stroke,
},
}}
curve={CurveType.CURVE_MONOTONE_X}
/>
</Chart>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -14,7 +14,7 @@ import {
export default {
component: Component,
title: 'app/AlertsSummaryWidget',
title: 'app/AlertSummaryWidget',
};
export const Compact = {

View file

@ -0,0 +1,54 @@
/*
* 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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import {
AlertsSummaryWidgetCompact,
AlertsSummaryWidgetCompactProps,
} from './alert_summary_widget_compact';
import { render } from '@testing-library/react';
import {
mockedAlertSummaryResponse,
mockedChartThemes,
} from '../../../../../mock/alert_summary_widget';
describe('AlertsSummaryWidgetCompact', () => {
const renderComponent = (props: Partial<AlertsSummaryWidgetCompactProps> = {}) =>
render(
<IntlProvider locale="en">
<AlertsSummaryWidgetCompact
chartThemes={mockedChartThemes}
onClick={jest.fn}
{...mockedAlertSummaryResponse}
{...props}
/>
</IntlProvider>
);
it('should render AlertsSummaryWidgetCompact', async () => {
const alertSummaryWidget = renderComponent();
expect(alertSummaryWidget.queryByTestId('alertSummaryWidgetCompact')).toBeTruthy();
});
it('should render counts correctly', async () => {
const alertSummaryWidget = renderComponent();
expect(alertSummaryWidget.queryByTestId('activeAlertsCount')).toHaveTextContent('2');
expect(alertSummaryWidget.queryByTestId('totalAlertsCount')).toHaveTextContent('22');
});
it('should render higher counts correctly', async () => {
const alertSummaryWidget = renderComponent({
activeAlertCount: 2000,
});
expect(alertSummaryWidget.queryByTestId('activeAlertsCount')).toHaveTextContent('2k');
expect(alertSummaryWidget.queryByTestId('totalAlertsCount')).toHaveTextContent('2.02k');
});
});

View file

@ -6,18 +6,12 @@
*/
import React, { MouseEvent } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, AlertStatus } from '@kbn/rule-data-utils';
import { AlertStateInfo } from './alert_state_info';
import { ACTIVE_ALERT_LABEL, ALL_ALERT_LABEL, RECOVERED_ALERT_LABEL } from './constants';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { Axis, Chart, CurveType, LineSeries, Position, ScaleType, Settings } from '@elastic/charts';
import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme';
import { AlertStatus } from '@kbn/rule-data-utils';
import { AlertCounts } from './alert_counts';
import { ALL_ALERT_COLOR, WIDGET_TITLE } from './constants';
import { Alert, ChartThemes } from '../types';
export interface AlertsSummaryWidgetCompactProps {
@ -25,7 +19,6 @@ export interface AlertsSummaryWidgetCompactProps {
activeAlerts: Alert[];
chartThemes: ChartThemes;
recoveredAlertCount: number;
recoveredAlerts: Alert[];
timeRangeTitle?: JSX.Element | string;
onClick: (status?: AlertStatus) => void;
}
@ -33,19 +26,23 @@ export interface AlertsSummaryWidgetCompactProps {
export const AlertsSummaryWidgetCompact = ({
activeAlertCount,
activeAlerts,
chartThemes,
chartThemes: { theme, baseTheme },
recoveredAlertCount,
recoveredAlerts,
timeRangeTitle,
onClick,
}: AlertsSummaryWidgetCompactProps) => {
const domain = {
min: 0,
max: Math.max(
...activeAlerts.map((alert) => alert.doc_count),
...recoveredAlerts.map((alert) => alert.doc_count)
),
};
const chartTheme = [
theme,
EUI_SPARKLINE_THEME_PARTIAL,
{
chartMargins: {
left: 10,
right: 10,
top: 10,
bottom: 10,
},
},
];
const handleClick = (
event: MouseEvent<HTMLAnchorElement | HTMLDivElement>,
@ -66,63 +63,48 @@ export const AlertsSummaryWidgetCompact = ({
onClick={handleClick}
>
<EuiFlexGroup direction="column">
{!!timeRangeTitle && (
<EuiFlexItem>
<EuiTitle size="xxs">
<h5>{WIDGET_TITLE}</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued" data-test-subj="timeRangeTitle">
{timeRangeTitle}
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiTitle size="xxs">
<h5 data-test-subj="totalAlertsCount">
{ALL_ALERT_LABEL}&nbsp;({activeAlertCount + recoveredAlertCount})
</h5>
</EuiTitle>
{!!timeRangeTitle && (
<>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued" data-test-subj="timeRangeTitle">
{timeRangeTitle}
</EuiText>
</>
)}
<AlertCounts
activeAlertCount={activeAlertCount}
recoveredAlertCount={recoveredAlertCount}
onActiveClick={handleClick}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup wrap>
{/* Active */}
<EuiFlexItem style={{ minWidth: '200px' }}>
<EuiLink
onClick={(event: React.MouseEvent<HTMLAnchorElement>) =>
handleClick(event, ALERT_STATUS_ACTIVE)
}
data-test-subj="activeAlerts"
>
<AlertStateInfo
chartThemes={chartThemes}
count={activeAlertCount}
<Chart size={{ height: 50 }}>
<Settings theme={chartTheme} baseTheme={baseTheme} tooltip={{ type: 'none' }} />
<Axis hide id={'activeAlertsAxis'} position={Position.Left} showGridLines={false} />
<LineSeries
id={'activeAlertsChart'}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['doc_count']}
data={activeAlerts}
dataTestSubj="activeAlerts"
domain={domain}
id="active"
stroke="#E7664C"
title={ACTIVE_ALERT_LABEL}
lineSeriesStyle={{
line: {
strokeWidth: 2,
stroke: ALL_ALERT_COLOR,
},
}}
curve={CurveType.CURVE_MONOTONE_X}
/>
</EuiLink>
</EuiFlexItem>
{/* Recovered */}
<EuiFlexItem style={{ minWidth: '200px' }}>
<EuiLink
onClick={(event: React.MouseEvent<HTMLAnchorElement>) =>
handleClick(event, ALERT_STATUS_RECOVERED)
}
data-test-subj="recoveredAlerts"
>
<AlertStateInfo
chartThemes={chartThemes}
count={recoveredAlertCount}
data={recoveredAlerts}
dataTestSubj="recoveredAlerts"
domain={domain}
id="recovered"
stroke="#54B399"
title={RECOVERED_ALERT_LABEL}
/>
</EuiLink>
</Chart>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -13,7 +13,7 @@ import {
export default {
component: Component,
title: 'app/AlertsSummaryWidget',
title: 'app/AlertSummaryWidget',
};
export const FullSize = {

View file

@ -7,20 +7,18 @@
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { AlertsSummaryWidgetFullSize } from './alert_summary_widget_full_size';
import {
AlertsSummaryWidgetFullSize,
AlertsSummaryWidgetFullSizeProps,
} from './alert_summary_widget_full_size';
import { render } from '@testing-library/react';
import { AlertSummaryWidgetProps } from '..';
import {
mockedAlertSummaryResponse,
mockedChartThemes,
} from '../../../../../mock/alert_summary_widget';
jest.mock('@kbn/kibana-react-plugin/public', () => ({
useUiSetting: jest.fn(() => false),
}));
describe('AlertSummaryWidgetFullSize', () => {
const renderComponent = (props: Partial<AlertSummaryWidgetProps> = {}) =>
const renderComponent = (props: Partial<AlertsSummaryWidgetFullSizeProps> = {}) =>
render(
<IntlProvider locale="en">
<AlertsSummaryWidgetFullSize
@ -41,7 +39,15 @@ describe('AlertSummaryWidgetFullSize', () => {
const alertSummaryWidget = renderComponent();
expect(alertSummaryWidget.queryByTestId('activeAlertsCount')).toHaveTextContent('2');
expect(alertSummaryWidget.queryByTestId('recoveredAlertsCount')).toHaveTextContent('15');
expect(alertSummaryWidget.queryByTestId('totalAlertsCount')).toHaveTextContent('17');
expect(alertSummaryWidget.queryByTestId('totalAlertsCount')).toHaveTextContent('22');
});
it('should render higher counts correctly', async () => {
const alertSummaryWidget = renderComponent({
activeAlertCount: 2000,
});
expect(alertSummaryWidget.queryByTestId('activeAlertsCount')).toHaveTextContent('2k');
expect(alertSummaryWidget.queryByTestId('totalAlertsCount')).toHaveTextContent('2.02k');
});
});

View file

@ -8,15 +8,9 @@
import moment from 'moment';
import React from 'react';
import { Axis, Chart, CurveType, LineSeries, Position, ScaleType, Settings } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui';
import {
ACTIVE_ALERT_LABEL,
ACTIVE_COLOR,
ALL_ALERT_LABEL,
RECOVERED_ALERT_LABEL,
RECOVERED_COLOR,
TOOLTIP_DATE_FORMAT,
} from './constants';
import { EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import { AlertCounts } from './alert_counts';
import { ALL_ALERT_COLOR, TOOLTIP_DATE_FORMAT } from './constants';
import { Alert, ChartThemes } from '../types';
export interface AlertsSummaryWidgetFullSizeProps {
@ -24,7 +18,6 @@ export interface AlertsSummaryWidgetFullSizeProps {
activeAlerts: Alert[];
chartThemes: ChartThemes;
recoveredAlertCount: number;
recoveredAlerts: Alert[];
dateFormat?: string;
}
@ -34,9 +27,7 @@ export const AlertsSummaryWidgetFullSize = ({
chartThemes: { theme, baseTheme },
dateFormat,
recoveredAlertCount,
recoveredAlerts,
}: AlertsSummaryWidgetFullSizeProps) => {
const { euiTheme } = useEuiTheme();
const chartTheme = [
theme,
{
@ -53,48 +44,15 @@ export const AlertsSummaryWidgetFullSize = ({
hasShadow={false}
paddingSize="none"
>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup gutterSize="xl" alignItems="flexStart" responsive={false}>
<EuiFlexItem>
<EuiText color={euiTheme.colors.text}>
<h3 data-test-subj="totalAlertsCount">
{activeAlertCount + recoveredAlertCount}
</h3>
</EuiText>
<EuiText size="xs" color="subdued">
{ALL_ALERT_LABEL}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color={euiTheme.colors.dangerText}>
<h3 data-test-subj="activeAlertsCount">{activeAlertCount}</h3>
</EuiText>
<EuiText size="xs" color="subdued">
{ACTIVE_ALERT_LABEL}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexItem>
<EuiText color={euiTheme.colors.successText}>
<h3 data-test-subj="recoveredAlertsCount">{recoveredAlertCount}</h3>
</EuiText>
</EuiFlexItem>
<EuiText size="xs" color="subdued">
{RECOVERED_ALERT_LABEL}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem>
<AlertCounts
activeAlertCount={activeAlertCount}
recoveredAlertCount={recoveredAlertCount}
/>
</EuiFlexItem>
<EuiSpacer size="l" />
<Chart size={['100%', 170]}>
<Settings
showLegend
legendPosition={Position.Right}
theme={chartTheme}
baseTheme={baseTheme}
@ -121,33 +79,15 @@ export const AlertsSummaryWidgetFullSize = ({
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['doc_count']}
color={[ALL_ALERT_COLOR]}
data={activeAlerts}
color={[ACTIVE_COLOR]}
lineSeriesStyle={{
line: {
strokeWidth: 2,
},
point: { visible: true, radius: 3, strokeWidth: 2 },
point: { visible: false },
}}
curve={CurveType.CURVE_MONOTONE_X}
timeZone="UTC"
/>
<LineSeries
id="Recovered"
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="key"
yAccessors={['doc_count']}
data={recoveredAlerts}
color={[RECOVERED_COLOR]}
lineSeriesStyle={{
line: {
strokeWidth: 2,
},
point: { visible: true, radius: 3, strokeWidth: 2 },
}}
curve={CurveType.CURVE_MONOTONE_X}
timeZone="UTC"
/>
</Chart>
</EuiPanel>

View file

@ -0,0 +1,28 @@
/*
* 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 numeral from '@elastic/numeral';
import { EuiText } from '@elastic/eui';
import { ALERT_COUNT_FORMAT, ALERTS_LABEL, ALL_ALERT_COLOR } from './constants';
interface Props {
count: number;
}
export const AllAlertCounts = ({ count }: Props) => {
return (
<>
<EuiText color={ALL_ALERT_COLOR}>
<h3 data-test-subj="totalAlertsCount">{numeral(count).format(ALERT_COUNT_FORMAT)}</h3>
</EuiText>
<EuiText size="s" color="subdued">
{ALERTS_LABEL}
</EuiText>
</>
);
};

View file

@ -5,30 +5,33 @@
* 2.0.
*/
import { euiPaletteColorBlind } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
export const ACTIVE_COLOR = '#E7664C';
export const RECOVERED_COLOR = '#54B399';
export const TOOLTIP_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
export const ALERT_COUNT_FORMAT = '0.[00]a';
export const ALL_ALERT_LABEL = (
const visColors = euiPaletteColorBlind();
export const ALL_ALERT_COLOR = visColors[1];
export const WIDGET_TITLE = (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title"
defaultMessage="Alert activity"
/>
);
export const ALERTS_LABEL = (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.alerts"
defaultMessage="Alerts"
/>
);
export const ACTIVE_ALERT_LABEL = (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel"
defaultMessage="Active"
/>
);
export const RECOVERED_ALERT_LABEL = (
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel"
defaultMessage="Recovered"
id="xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeNow"
defaultMessage="Active now"
/>
);

View file

@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
const COMPACT_COMPONENT_SELECTOR = 'alertSummaryWidgetCompact';
const COMPACT_TIME_RANGE_TITLE_SELECTOR = 'timeRangeTitle';
const COMPACT_ACTIVE_ALERTS_SELECTOR = 'activeAlerts';
const COMPACT_RECOVERED_ALERTS_SELECTOR = 'recoveredAlerts';
export function ObservabilityAlertSummaryWidgetProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
@ -27,14 +26,14 @@ export function ObservabilityAlertSummaryWidgetProvider({ getService }: FtrProvi
return await testSubjects.find(COMPACT_ACTIVE_ALERTS_SELECTOR);
};
const getCompactRecoveredAlertSelector = async () => {
return await testSubjects.find(COMPACT_RECOVERED_ALERTS_SELECTOR);
const getCompactWidgetSelector = async () => {
return await testSubjects.find(COMPACT_COMPONENT_SELECTOR);
};
return {
getCompactActiveAlertSelector,
getCompactComponentSelectorOrFail,
getCompactTimeRangeTitle,
getCompactActiveAlertSelector,
getCompactRecoveredAlertSelector,
getCompactWidgetSelector,
};
}

View file

@ -169,16 +169,16 @@ export default ({ getService }: FtrProviderContext) => {
expect(url.includes(to.replaceAll(':', '%3A'))).to.be(true);
});
it('handles clicking on recovered correctly', async () => {
const recoveredAlerts =
await observability.components.alertSummaryWidget.getCompactRecoveredAlertSelector();
await recoveredAlerts.click();
it('handles clicking on widget correctly', async () => {
const compactWidget =
await observability.components.alertSummaryWidget.getCompactWidgetSelector();
await compactWidget.click();
const url = await browser.getCurrentUrl();
const { from, to } = await observability.components.alertSearchBar.getAbsoluteTimeRange();
expect(url.includes('tabId=alerts')).to.be(true);
expect(url.includes('status%3Arecovered')).to.be(true);
expect(url.includes('status%3Aall')).to.be(true);
expect(url.includes(from.replaceAll(':', '%3A'))).to.be(true);
expect(url.includes(to.replaceAll(':', '%3A'))).to.be(true);
});