Skip to content

Commit

Permalink
[AO] Add AlertSummaryWidget full-size on the Alerts page (elastic#148539
Browse files Browse the repository at this point in the history
)

Resolves elastic#148279

## 📝 Summary

Add AlertSummaryWidget full-size on the Alerts page.


![image](https://user-images.githubusercontent.com/12370520/211568369-7d0847ba-e90e-4229-a55a-b68bf55209f8.png)

**Note**
The functional test will be added in another ticket
(elastic#148645)

## 🧪 How to test
- Create a rule that generates an alert and go to the observability >
alerts page, you should see the number of alerts correctly in the Alert
Summary Widget
- Change the time range to see different formats in the tooltip and the
number of buckets will also change accordingly

## 🐛 Known issue
When we don't have any rule, the `_alert_summary` API fails and we show
a failed message, I will discuss it with @XavierM, and based on the
decision, either I'll update this PR or create a separate PR.
Update: I created a ticket for it
(elastic#148653)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and pull[bot] committed Feb 14, 2024
1 parent e2b7e6a commit 3ce4aad
Show file tree
Hide file tree
Showing 18 changed files with 197 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@
* 2.0.
*/

import { EuiFlexGroup, EuiFlexItem, EuiFlyoutSize } from '@elastic/eui';

import React, { useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { TimeBuckets, UI_SETTINGS } from '@kbn/data-plugin/common';
import { BoolQuery } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public';
import {
loadRuleAggregations,
AlertSummaryTimeRange,
} from '@kbn/triggers-actions-ui-plugin/public';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { ObservabilityAlertSearchbarWithUrlSync } from '../../../../components/shared/alert_search_bar';
import { useToasts } from '../../../../hooks/use_toast';
import {
alertSearchBarStateContainer,
Provider,
useAlertSearchBarStateContainer,
} from '../../../../components/shared/alert_search_bar/containers';
import { getAlertSummaryTimeRange } from '../../../rule_details/helpers';
import { ObservabilityAlertSearchBar } from '../../../../components/shared/alert_search_bar';
import { observabilityAlertFeatureIds } from '../../../../config';
import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions';
import { observabilityFeatureId } from '../../../../../common';
Expand All @@ -33,15 +43,27 @@ import {
} from './constants';
import { RuleStatsState } from './types';

export function AlertsPage() {
function InternalAlertsPage() {
const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext();
const {
cases,
data: {
query: {
timefilter: { timefilter: timeFilterService },
},
},
docLinks,
http,
notifications: { toasts },
triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable },
triggersActionsUi: {
alertsTableConfigurationRegistry,
getAlertsSearchBar: AlertsSearchBar,
getAlertsStateTable: AlertsStateTable,
getAlertSummaryWidget: AlertSummaryWidget,
},
uiSettings,
} = useKibana<ObservabilityAppServices>().services;
const alertSearchBarStateProps = useAlertSearchBarStateContainer(URL_STORAGE_KEY);

const [ruleStatsLoading, setRuleStatsLoading] = useState<boolean>(false);
const [ruleStats, setRuleStats] = useState<RuleStatsState>({
Expand All @@ -53,6 +75,19 @@ export function AlertsPage() {
});
const { hasAnyData, isAllRequestsComplete } = useHasData();
const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>();
const timeBuckets = new TimeBuckets({
'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
dateFormat: uiSettings.get('dateFormat'),
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
});
const alertSummaryTimeRange: AlertSummaryTimeRange = getAlertSummaryTimeRange(
{
from: alertSearchBarStateProps.rangeFrom,
to: alertSearchBarStateProps.rangeTo,
},
timeBuckets
);

useBreadcrumbs([
{
Expand Down Expand Up @@ -132,15 +167,23 @@ export function AlertsPage() {
rightSideItems: renderRuleStats(ruleStats, manageRulesHref, ruleStatsLoading),
}}
>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<ObservabilityAlertSearchbarWithUrlSync
<ObservabilityAlertSearchBar
{...alertSearchBarStateProps}
appName={ALERTS_SEARCH_BAR_ID}
onEsQueryChange={setEsQuery}
urlStorageKey={URL_STORAGE_KEY}
services={{ timeFilterService, AlertsSearchBar, useToasts }}
/>
</EuiFlexItem>
<EuiFlexItem>
<AlertSummaryWidget
featureIds={observabilityAlertFeatureIds}
filter={esQuery}
fullSize
timeRange={alertSummaryTimeRange}
/>
</EuiFlexItem>

<EuiFlexItem>
<CasesContext
owner={[observabilityFeatureId]}
Expand All @@ -152,7 +195,7 @@ export function AlertsPage() {
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
configurationId={AlertConsumers.OBSERVABILITY}
id={ALERTS_TABLE_ID}
flyoutSize={'s' as EuiFlyoutSize}
flyoutSize="s"
featureIds={observabilityAlertFeatureIds}
query={esQuery}
showExpandToDetails={false}
Expand All @@ -165,3 +208,11 @@ export function AlertsPage() {
</ObservabilityPageTemplate>
);
}

export function AlertsPage() {
return (
<Provider value={alertSearchBarStateContainer}>
<InternalAlertsPage />
</Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,63 @@
*/

import moment from 'moment';
import { getAlertSummaryWidgetTimeRange } from '.';
import { TimeBuckets } from '@kbn/data-plugin/common';
import { getAlertSummaryTimeRange, getDefaultAlertSummaryTimeRange } from '.';

describe('getDefaultAlertSummaryTimeRange', () => {
it('should return default time in UTC format', () => {
const defaultTimeRange = getAlertSummaryWidgetTimeRange();
const utcFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
describe('AlertSummaryTimeRange', () => {
describe('getDefaultAlertSummaryTimeRange', () => {
it('should return default time in UTC format', () => {
const defaultTimeRange = getDefaultAlertSummaryTimeRange();
const utcFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ';

expect(moment(defaultTimeRange.utcFrom, utcFormat, true).isValid()).toBeTruthy();
expect(moment(defaultTimeRange.utcTo, utcFormat, true).isValid()).toBeTruthy();
expect(moment(defaultTimeRange.utcFrom, utcFormat, true).isValid()).toBeTruthy();
expect(moment(defaultTimeRange.utcTo, utcFormat, true).isValid()).toBeTruthy();
});
});

describe('getAlertSummaryTimeRange', () => {
const timeBucketConfig = {
'histogram:maxBars': 4,
'histogram:barTarget': 3,
dateFormat: 'YYYY-MM-DD',
'dateFormat:scaled': [
['', 'HH:mm:ss.SSS'],
['PT1S', 'HH:mm:ss'],
['PT1M', 'HH:mm'],
['PT1H', 'YYYY-MM-DD HH:mm'],
['P1DT', 'YYYY-MM-DD'],
['P1YT', 'YYYY'],
],
};
const timeBuckets = new TimeBuckets(timeBucketConfig);

it.each([
// 15 minutes
['2023-01-09T12:07:54.441Z', '2023-01-09T12:22:54.441Z', '30s', 'HH:mm:ss'],
// 30 minutes
['2023-01-09T11:53:43.605Z', '2023-01-09T12:23:43.605Z', '30s', 'HH:mm:ss'],
// 1 hour
['2023-01-09T11:22:05.728Z', '2023-01-09T12:22:05.728Z', '60s', 'HH:mm'],
// 24 hours
['2023-01-08T12:00:00.000Z', '2023-01-09T12:24:30.853Z', '1800s', 'HH:mm'],
// 7 days
['2023-01-01T23:00:00.000Z', '2023-01-09T12:29:38.101Z', '10800s', 'YYYY-MM-DD HH:mm'],
// 30 days
['2022-12-09T23:00:00.000Z', '2023-01-09T12:30:13.717Z', '43200s', 'YYYY-MM-DD HH:mm'],
// 90 days
['2022-10-10T22:00:00.000Z', '2023-01-09T12:32:11.537Z', '86400s', 'YYYY-MM-DD'],
// 1 year
['2022-01-08T23:00:00.000Z', '2023-01-09T12:33:09.906Z', '86400s', 'YYYY-MM-DD'],
])(
`Input: [%s, %s], Output: interval: %s, time format: %s `,
(from, to, fixedInterval, dateFormat) => {
expect(getAlertSummaryTimeRange({ from, to }, timeBuckets)).toEqual({
utcFrom: from,
utcTo: to,
fixedInterval,
dateFormat,
});
}
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
* 2.0.
*/

import type { AlertSummaryTimeRange } from '@kbn/triggers-actions-ui-plugin/public/application/hooks/use_load_alert_summary';
import React from 'react';
import { getAbsoluteTimeRange } from '@kbn/data-plugin/common';
import { getAbsoluteTimeRange, TimeBuckets } from '@kbn/data-plugin/common';
import { TimeRange } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import type { AlertSummaryTimeRange } from '@kbn/triggers-actions-ui-plugin/public';
import { getAbsoluteTime } from '../../../utils/date';
import { getBucketSize } from '../../../utils/get_bucket_size';

export const getAlertSummaryWidgetTimeRange = (): AlertSummaryTimeRange => {
export const getDefaultAlertSummaryTimeRange = (): AlertSummaryTimeRange => {
const { to, from } = getAbsoluteTimeRange({
from: 'now-30d',
to: 'now',
Expand All @@ -28,3 +31,30 @@ export const getAlertSummaryWidgetTimeRange = (): AlertSummaryTimeRange => {
),
};
};

export const getAlertSummaryTimeRange = (
timeRange: TimeRange,
timeBuckets: TimeBuckets
): AlertSummaryTimeRange => {
const { to, from } = getAbsoluteTimeRange(timeRange);
const fixedInterval = getFixedInterval(timeRange);
timeBuckets.setInterval(fixedInterval);

return {
utcFrom: from,
utcTo: to,
fixedInterval,
dateFormat: timeBuckets.getScaledDateFormat(),
};
};

const getFixedInterval = ({ to, from }: TimeRange) => {
const start = getAbsoluteTime(from);
const end = getAbsoluteTime(to, { roundUp: true });

if (start && end) {
return getBucketSize({ start, end, minInterval: '30s', buckets: 60 }).intervalString;
}

return '1m';
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
* 2.0.
*/

export { getAlertSummaryWidgetTimeRange } from './get_alert_summary_time_range';
export {
getDefaultAlertSummaryTimeRange,
getAlertSummaryTimeRange,
} from './get_alert_summary_time_range';
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { fromQuery, toQuery } from '../../utils/url';
import { ObservabilityAlertSearchbarWithUrlSync } from '../../components/shared/alert_search_bar';
import { DeleteModalConfirmation } from './components/delete_modal_confirmation';
import { CenterJustifiedSpinner } from './components/center_justified_spinner';
import { getAlertSummaryWidgetTimeRange } from './helpers';
import { getDefaultAlertSummaryTimeRange } from './helpers';

import {
EXECUTION_TAB,
Expand Down Expand Up @@ -112,7 +112,7 @@ export function RuleDetailsPage() {
const [isRuleEditPopoverOpen, setIsRuleEditPopoverOpen] = useState(false);
const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>();
const [alertSummaryWidgetTimeRange, setAlertSummaryWidgetTimeRange] = useState(
getAlertSummaryWidgetTimeRange
getDefaultAlertSummaryTimeRange
);
const ruleQuery = useRef<Query[]>([
{ query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' },
Expand All @@ -125,7 +125,7 @@ export function RuleDetailsPage() {
const tabsRef = useRef<HTMLDivElement>(null);

const onAlertSummaryWidgetClick = async (status: AlertStatus = ALERT_STATUS_ALL) => {
const timeRange = getAlertSummaryWidgetTimeRange();
const timeRange = getDefaultAlertSummaryTimeRange();
setAlertSummaryWidgetTimeRange(timeRange);
await locators.get(ruleDetailsLocatorID)?.navigate(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export function getBucketSize({
start,
end,
minInterval,
buckets = 100,
}: {
start: number;
end: number;
minInterval: string;
buckets?: number;
}) {
const duration = moment.duration(end - start, 'ms');
const bucketSize = Math.max(calculateAuto.near(100, duration)?.asSeconds() ?? 0, 1);
const bucketSize = Math.max(calculateAuto.near(buckets, duration)?.asSeconds() ?? 0, 1);
const intervalString = `${bucketSize}s`;
const matches = minInterval && minInterval.match(/^([\d]+)([shmdwMy]|ms)$/);
const minBucketSize = matches ? Number(matches[1]) * unitToSeconds(matches[2]) : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('getAlertSummaryRoute', () => {
"attributes": Object {
"success": false,
},
"message": "fixed_interval is not following the expected format 1m, 1h, 1d, 1w",
"message": "fixed_interval (value: xx) is not following the expected format 1s, 1m, 1h, 1d with at most 6 digits",
}
`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ export const getAlertSummaryRoute = (router: IRouter<RacRequestHandlerContext>)
throw Boom.badRequest('gte and/or lte are not following the UTC format');
}

if (fixedInterval && fixedInterval?.match(/^\d{1,2}['m','h','d','w']$/) == null) {
if (fixedInterval && fixedInterval?.match(/^\d{1,6}['s','m','h','d']$/) == null) {
throw Boom.badRequest(
'fixed_interval is not following the expected format 1m, 1h, 1d, 1w'
`fixed_interval (value: ${fixedInterval}) is not following the expected format 1s, 1m, 1h, 1d with at most 6 digits`
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,10 @@ import { AsApiContract } from '@kbn/actions-plugin/common';
import { HttpSetup } from '@kbn/core/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants';
import { useKibana } from '../../common/lib/kibana';

export interface AlertSummaryTimeRange {
utcFrom: string;
utcTo: string;
// fixed_interval condition in ES query such as '1m', '1d'
fixedInterval: string;
title: JSX.Element | string;
}

export interface Alert {
key: number;
doc_count: number;
}
import {
Alert,
AlertSummaryTimeRange,
} from '../sections/rule_details/components/alert_summary/types';

interface UseLoadAlertSummaryProps {
featureIds?: ValidFeatureId[];
Expand Down Expand Up @@ -75,8 +66,7 @@ export function useLoadAlertSummary({ featureIds, timeRange, filter }: UseLoadAl
});

if (!isCancelledRef.current) {
setAlertSummary((oldState) => ({
...oldState,
setAlertSummary(() => ({
alertSummary: {
activeAlertCount,
activeAlerts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { AlertSummaryTimeRange } from '../../hooks/use_load_alert_summary';
import { AlertSummaryTimeRange } from '../../sections/rule_details/components/alert_summary/types';

export const mockAlertSummaryResponse = {
activeAlertCount: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const AlertSummaryWidget = ({
activeAlerts={activeAlerts}
recoveredAlertCount={recoveredAlertCount}
recoveredAlerts={recoveredAlerts}
dateFormat={timeRange.dateFormat}
/>
) : (
<AlertsSummaryWidgetCompact
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from '@elastic/eui/dist/eui_charts_theme';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { Alert } from '../../../../../hooks/use_load_alert_summary';
import { Alert } from '../types';

interface AlertStateInfoProps {
count: number;
Expand Down
Loading

0 comments on commit 3ce4aad

Please sign in to comment.