mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[RAM][Flapping] Add flapping alert status to alert table (#149176)
## Summary Resolves: https://github.com/elastic/kibana/issues/148759 Adds a new component that will display an alert's flapping status in addition to its `active/recovered` status in the alerts table. This component is used both in the O11Y alert table and the stack management alerts table. This PR also allows the new alert status badge component to be shareable. ### Alerts Table: Active  ### Alerts Table: Flapping  ### Alerts Table: Recovered (Recovered is preferred over flapping)  ### Stack Management Alerts List:  ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
4e66e01c73
commit
4a4138dc3a
21 changed files with 383 additions and 132 deletions
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { waitFor } from '@testing-library/dom';
|
import { waitFor } from '@testing-library/dom';
|
||||||
|
import { OBSERVABILITY_OWNER } from '../../../../common/constants';
|
||||||
import { alertCommentWithIndices, basicCase } from '../../../containers/mock';
|
import { alertCommentWithIndices, basicCase } from '../../../containers/mock';
|
||||||
import type { AppMockRenderer } from '../../../common/mock';
|
import type { AppMockRenderer } from '../../../common/mock';
|
||||||
import { createAppMockRenderer } from '../../../common/mock';
|
import { createAppMockRenderer } from '../../../common/mock';
|
||||||
|
@ -54,6 +55,7 @@ describe('Case View Page activity tab', () => {
|
||||||
},
|
},
|
||||||
flyoutSize: 'm',
|
flyoutSize: 'm',
|
||||||
showExpandToDetails: true,
|
showExpandToDetails: true,
|
||||||
|
showAlertStatusWithFlapping: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -61,14 +63,21 @@ describe('Case View Page activity tab', () => {
|
||||||
it('should call the alerts table with correct props for observability', async () => {
|
it('should call the alerts table with correct props for observability', async () => {
|
||||||
const getFeatureIdsMock = jest.spyOn(api, 'getFeatureIds');
|
const getFeatureIdsMock = jest.spyOn(api, 'getFeatureIds');
|
||||||
getFeatureIdsMock.mockResolvedValueOnce(['observability']);
|
getFeatureIdsMock.mockResolvedValueOnce(['observability']);
|
||||||
appMockRender.render(<CaseViewAlerts caseData={caseData} />);
|
appMockRender.render(
|
||||||
|
<CaseViewAlerts
|
||||||
|
caseData={{
|
||||||
|
...caseData,
|
||||||
|
owner: OBSERVABILITY_OWNER,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
expect(getAlertsStateTableMock).toHaveBeenCalledWith({
|
expect(getAlertsStateTableMock).toHaveBeenCalledWith({
|
||||||
alertsTableConfigurationRegistry: expect.anything(),
|
alertsTableConfigurationRegistry: expect.anything(),
|
||||||
configurationId: 'securitySolution',
|
configurationId: 'observability',
|
||||||
featureIds: ['observability'],
|
featureIds: ['observability'],
|
||||||
id: 'case-details-alerts-securitySolution',
|
id: 'case-details-alerts-observability',
|
||||||
query: {
|
query: {
|
||||||
ids: {
|
ids: {
|
||||||
values: ['alert-id-1'],
|
values: ['alert-id-1'],
|
||||||
|
@ -76,6 +85,7 @@ describe('Case View Page activity tab', () => {
|
||||||
},
|
},
|
||||||
flyoutSize: 's',
|
flyoutSize: 's',
|
||||||
showExpandToDetails: false,
|
showExpandToDetails: false,
|
||||||
|
showAlertStatusWithFlapping: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import type { EuiFlyoutSize } from '@elastic/eui';
|
import type { EuiFlyoutSize } from '@elastic/eui';
|
||||||
import { EuiFlexItem, EuiFlexGroup, EuiProgress } from '@elastic/eui';
|
import { EuiFlexItem, EuiFlexGroup, EuiProgress } from '@elastic/eui';
|
||||||
|
import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants';
|
||||||
import type { Case } from '../../../../common';
|
import type { Case } from '../../../../common';
|
||||||
import { useKibana } from '../../../common/lib/kibana';
|
import { useKibana } from '../../../common/lib/kibana';
|
||||||
import { getManualAlertIds, getRegistrationContextFromAlerts } from './helpers';
|
import { getManualAlertIds, getRegistrationContextFromAlerts } from './helpers';
|
||||||
|
@ -46,6 +47,7 @@ export const CaseViewAlerts = ({ caseData }: CaseViewAlertsProps) => {
|
||||||
featureIds: alertFeatureIds ?? [],
|
featureIds: alertFeatureIds ?? [],
|
||||||
query: alertIdsQuery,
|
query: alertIdsQuery,
|
||||||
showExpandToDetails: Boolean(alertFeatureIds?.includes('siem')),
|
showExpandToDetails: Boolean(alertFeatureIds?.includes('siem')),
|
||||||
|
showAlertStatusWithFlapping: caseData.owner !== SECURITY_SOLUTION_OWNER,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (alertIdsQuery.ids.values.length === 0) {
|
if (alertIdsQuery.ids.values.length === 0) {
|
||||||
|
|
|
@ -199,6 +199,7 @@ function InternalAlertsPage() {
|
||||||
featureIds={observabilityAlertFeatureIds}
|
featureIds={observabilityAlertFeatureIds}
|
||||||
query={esQuery}
|
query={esQuery}
|
||||||
showExpandToDetails={false}
|
showExpandToDetails={false}
|
||||||
|
showAlertStatusWithFlapping
|
||||||
pageSize={ALERTS_PER_PAGE}
|
pageSize={ALERTS_PER_PAGE}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -209,6 +209,7 @@ export function OverviewPage() {
|
||||||
pageSize={ALERTS_PER_PAGE}
|
pageSize={ALERTS_PER_PAGE}
|
||||||
query={esQuery}
|
query={esQuery}
|
||||||
showExpandToDetails={false}
|
showExpandToDetails={false}
|
||||||
|
showAlertStatusWithFlapping
|
||||||
/>
|
/>
|
||||||
</CasesContext>
|
</CasesContext>
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
|
|
|
@ -281,6 +281,7 @@ export function RuleDetailsPage() {
|
||||||
featureIds={featureIds}
|
featureIds={featureIds}
|
||||||
query={esQuery}
|
query={esQuery}
|
||||||
showExpandToDetails={false}
|
showExpandToDetails={false}
|
||||||
|
showAlertStatusWithFlapping
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
|
@ -35119,8 +35119,6 @@
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "Impossible de récupérer le KPI du log d'événements.",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "Impossible de récupérer le KPI du log d'événements.",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "Réponses pour un maximum de 10 000 exécutions de règles les plus récentes.",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "Réponses pour un maximum de 10 000 exécutions de règles les plus récentes.",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "Dernière réponse",
|
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "Dernière réponse",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "Actif",
|
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "Récupéré",
|
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText": "La règle est désactivée",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText": "La règle est désactivée",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText": "La règle est activée",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText": "La règle est activée",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "La règle s’est répétée",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "La règle s’est répétée",
|
||||||
|
|
|
@ -35087,8 +35087,6 @@
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "イベントログKPIを取得できませんでした。",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "イベントログKPIを取得できませんでした。",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "最大10,000件の直近のルール実行の応答。",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "最大10,000件の直近のルール実行の応答。",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "前回の応答",
|
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "前回の応答",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "アクティブ",
|
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "回復済み",
|
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText": "ルールが無効です",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText": "ルールが無効です",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText": "ルールが有効です",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText": "ルールが有効です",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "ルールがスヌーズされました",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "ルールがスヌーズされました",
|
||||||
|
|
|
@ -35124,8 +35124,6 @@
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "无法提取事件日志 KPI。",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "无法提取事件日志 KPI。",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "多达 10,000 次最近规则运行的响应。",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "多达 10,000 次最近规则运行的响应。",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "上次响应",
|
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "上次响应",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "活动",
|
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "已恢复",
|
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText": "规则已禁用",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.disabledOptionText": "规则已禁用",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText": "规则已启用",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.enabledOptionText": "规则已启用",
|
||||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "规则已暂停",
|
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "规则已暂停",
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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, { ComponentProps } from 'react';
|
||||||
|
import { Story } from '@storybook/react';
|
||||||
|
import { ALERT_STATUS_RECOVERED, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
|
||||||
|
import {
|
||||||
|
AlertLifecycleStatusBadge,
|
||||||
|
AlertLifecycleStatusBadgeProps,
|
||||||
|
} from './alert_lifecycle_status_badge';
|
||||||
|
|
||||||
|
type Args = ComponentProps<typeof AlertLifecycleStatusBadge>;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'app/AlertLifecyceStatusBadge',
|
||||||
|
component: AlertLifecycleStatusBadge,
|
||||||
|
argTypes: {
|
||||||
|
alertStatus: {
|
||||||
|
defaultValue: ALERT_STATUS_ACTIVE,
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
options: [ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flapping: {
|
||||||
|
defaultValue: false,
|
||||||
|
control: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template: Story<Args> = (args: AlertLifecycleStatusBadgeProps) => {
|
||||||
|
return <AlertLifecycleStatusBadge {...args} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Active = Template.bind({});
|
||||||
|
Active.args = {
|
||||||
|
alertStatus: ALERT_STATUS_ACTIVE,
|
||||||
|
flapping: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Flapping = Template.bind({});
|
||||||
|
Flapping.args = {
|
||||||
|
alertStatus: ALERT_STATUS_ACTIVE,
|
||||||
|
flapping: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Recovered = Template.bind({});
|
||||||
|
Recovered.args = {
|
||||||
|
alertStatus: ALERT_STATUS_RECOVERED,
|
||||||
|
flapping: false,
|
||||||
|
};
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 { render } from '@testing-library/react';
|
||||||
|
import { AlertLifecycleStatusBadge } from './alert_lifecycle_status_badge';
|
||||||
|
|
||||||
|
describe('alertLifecycleStatusBadge', () => {
|
||||||
|
it('should display the alert status correctly when active and not flapping', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<AlertLifecycleStatusBadge alertStatus="active" flapping={false} />
|
||||||
|
);
|
||||||
|
expect(getByText('Active')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the alert status correctly when active and flapping', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<AlertLifecycleStatusBadge alertStatus="active" flapping={true} />
|
||||||
|
);
|
||||||
|
expect(getByText('Flapping')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the alert status correctly when recovered and not flapping', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<AlertLifecycleStatusBadge alertStatus="recovered" flapping={false} />
|
||||||
|
);
|
||||||
|
expect(getByText('Recovered')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize recovered over flapping when recovered and flapping', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<AlertLifecycleStatusBadge alertStatus="recovered" flapping={true} />
|
||||||
|
);
|
||||||
|
expect(getByText('Recovered')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display active alert status correctly is flapping is not defined', () => {
|
||||||
|
const { getByText } = render(<AlertLifecycleStatusBadge alertStatus="active" />);
|
||||||
|
expect(getByText('Active')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display recovered alert status correctly is flapping is not defined', () => {
|
||||||
|
const { getByText } = render(<AlertLifecycleStatusBadge alertStatus="recovered" />);
|
||||||
|
expect(getByText('Recovered')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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, { memo } from 'react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { EuiBadge, EuiBadgeProps } from '@elastic/eui';
|
||||||
|
import { AlertStatus, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||||
|
|
||||||
|
export interface AlertLifecycleStatusBadgeProps {
|
||||||
|
alertStatus: AlertStatus;
|
||||||
|
flapping?: boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTIVE_LABEL = i18n.translate(
|
||||||
|
'xpack.triggersActionsUI.components.alertLifecycleStatusBadge.activeLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Active',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const RECOVERED_LABEL = i18n.translate(
|
||||||
|
'xpack.triggersActionsUI.components.alertLifecycleStatusBadge.recoveredLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Recovered',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const FLAPPING_LABEL = i18n.translate(
|
||||||
|
'xpack.triggersActionsUI.components.alertLifecycleStatusBadge.flappingLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Flapping',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
iconProps?: {
|
||||||
|
iconType: EuiBadgeProps['iconType'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBadgeProps = (alertStatus: AlertStatus, flapping: boolean | undefined): BadgeProps => {
|
||||||
|
// Prefer recovered over flapping
|
||||||
|
if (alertStatus === ALERT_STATUS_RECOVERED) {
|
||||||
|
return {
|
||||||
|
label: RECOVERED_LABEL,
|
||||||
|
color: 'success',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flapping) {
|
||||||
|
return {
|
||||||
|
label: FLAPPING_LABEL,
|
||||||
|
color: 'danger',
|
||||||
|
iconProps: {
|
||||||
|
iconType: 'visGauge',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: ACTIVE_LABEL,
|
||||||
|
color: 'danger',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const castFlapping = (flapping: boolean | string | undefined) => {
|
||||||
|
if (typeof flapping === 'string') {
|
||||||
|
return flapping === 'true';
|
||||||
|
}
|
||||||
|
return flapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AlertLifecycleStatusBadge = memo((props: AlertLifecycleStatusBadgeProps) => {
|
||||||
|
const { alertStatus, flapping } = props;
|
||||||
|
|
||||||
|
const castedFlapping = castFlapping(flapping);
|
||||||
|
|
||||||
|
const { label, color, iconProps } = getBadgeProps(alertStatus, castedFlapping);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiBadge data-test-subj="alertLifecycleStatusBadge" color={color} {...iconProps}>
|
||||||
|
{label}
|
||||||
|
</EuiBadge>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export { AlertLifecycleStatusBadge as default };
|
|
@ -10,9 +10,9 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||||
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
|
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
|
||||||
|
import { ALERT_RULE_NAME, ALERT_REASON, ALERT_FLAPPING, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||||
import { AlertsTable } from './alerts_table';
|
import { AlertsTable } from './alerts_table';
|
||||||
import { AlertsField, AlertsTableProps, BulkActionsState, RowSelectionState } from '../../../types';
|
import { AlertsTableProps, BulkActionsState, RowSelectionState } from '../../../types';
|
||||||
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
|
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
|
||||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||||
import { BulkActionsContext } from './bulk_actions/context';
|
import { BulkActionsContext } from './bulk_actions/context';
|
||||||
|
@ -25,27 +25,47 @@ jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
id: AlertsField.name,
|
id: ALERT_RULE_NAME,
|
||||||
displayAsText: 'Name',
|
displayAsText: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: AlertsField.reason,
|
id: ALERT_REASON,
|
||||||
displayAsText: 'Reason',
|
displayAsText: 'Reason',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: ALERT_STATUS,
|
||||||
|
displayAsText: 'Alert status',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('AlertsTable', () => {
|
const alerts = [
|
||||||
const alerts = [
|
{
|
||||||
{
|
[ALERT_RULE_NAME]: ['one'],
|
||||||
[AlertsField.name]: ['one'],
|
[ALERT_REASON]: ['two'],
|
||||||
[AlertsField.reason]: ['two'],
|
[ALERT_STATUS]: ['active'],
|
||||||
},
|
[ALERT_FLAPPING]: [true],
|
||||||
{
|
},
|
||||||
[AlertsField.name]: ['three'],
|
{
|
||||||
[AlertsField.reason]: ['four'],
|
[ALERT_RULE_NAME]: ['three'],
|
||||||
},
|
[ALERT_REASON]: ['four'],
|
||||||
] as unknown as EcsFieldsResponse[];
|
[ALERT_STATUS]: ['active'],
|
||||||
|
[ALERT_FLAPPING]: [false],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[ALERT_RULE_NAME]: ['five'],
|
||||||
|
[ALERT_REASON]: ['six'],
|
||||||
|
[ALERT_STATUS]: ['recovered'],
|
||||||
|
[ALERT_FLAPPING]: [true],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[ALERT_RULE_NAME]: ['seven'],
|
||||||
|
[ALERT_REASON]: ['eight'],
|
||||||
|
[ALERT_STATUS]: ['recovered'],
|
||||||
|
[ALERT_FLAPPING]: [false],
|
||||||
|
},
|
||||||
|
] as unknown as EcsFieldsResponse[];
|
||||||
|
|
||||||
|
describe('AlertsTable', () => {
|
||||||
const fetchAlertsData = {
|
const fetchAlertsData = {
|
||||||
activePage: 0,
|
activePage: 0,
|
||||||
alerts,
|
alerts,
|
||||||
|
@ -164,6 +184,24 @@ describe('AlertsTable', () => {
|
||||||
expect(getByTestId('toolbar-alerts-count')).not.toBe(null);
|
expect(getByTestId('toolbar-alerts-count')).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show alert status', () => {
|
||||||
|
const props = {
|
||||||
|
...tableProps,
|
||||||
|
showAlertStatusWithFlapping: true,
|
||||||
|
pageSize: alerts.length,
|
||||||
|
alertsTableConfiguration: {
|
||||||
|
...alertsTableConfiguration,
|
||||||
|
getRenderCellValue: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { queryAllByTestId } = render(<AlertsTableWithLocale {...props} />);
|
||||||
|
expect(queryAllByTestId('alertLifecycleStatusBadge')[0].textContent).toEqual('Flapping');
|
||||||
|
expect(queryAllByTestId('alertLifecycleStatusBadge')[1].textContent).toEqual('Active');
|
||||||
|
expect(queryAllByTestId('alertLifecycleStatusBadge')[2].textContent).toEqual('Recovered');
|
||||||
|
expect(queryAllByTestId('alertLifecycleStatusBadge')[3].textContent).toEqual('Recovered');
|
||||||
|
});
|
||||||
|
|
||||||
describe('leading control columns', () => {
|
describe('leading control columns', () => {
|
||||||
it('should return at least the flyout action control', async () => {
|
it('should return at least the flyout action control', async () => {
|
||||||
const wrapper = render(<AlertsTableWithLocale {...tableProps} />);
|
const wrapper = render(<AlertsTableWithLocale {...tableProps} />);
|
||||||
|
@ -206,6 +244,7 @@ describe('AlertsTable', () => {
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
size="s"
|
size="s"
|
||||||
data-test-subj="testActionColumn"
|
data-test-subj="testActionColumn"
|
||||||
|
aria-label="testActionLabel"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
|
@ -215,6 +254,7 @@ describe('AlertsTable', () => {
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
size="s"
|
size="s"
|
||||||
data-test-subj="testActionColumn2"
|
data-test-subj="testActionColumn2"
|
||||||
|
aria-label="testActionLabel2"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</>
|
</>
|
||||||
|
@ -249,6 +289,7 @@ describe('AlertsTable', () => {
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
size="s"
|
size="s"
|
||||||
data-test-subj="testActionColumn"
|
data-test-subj="testActionColumn"
|
||||||
|
aria-label="testActionLabel"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
|
@ -258,6 +299,7 @@ describe('AlertsTable', () => {
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
size="s"
|
size="s"
|
||||||
data-test-subj="testActionColumn2"
|
data-test-subj="testActionColumn2"
|
||||||
|
aria-label="testActionLabel2"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</>
|
</>
|
||||||
|
@ -307,6 +349,7 @@ describe('AlertsTable', () => {
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
size="s"
|
size="s"
|
||||||
data-test-subj="testActionColumn"
|
data-test-subj="testActionColumn"
|
||||||
|
aria-label="testActionLabel"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</>
|
</>
|
||||||
|
@ -332,8 +375,8 @@ describe('AlertsTable', () => {
|
||||||
expect(within(selectedOptions[0]).queryByRole('checkbox')).not.toBeInTheDocument();
|
expect(within(selectedOptions[0]).queryByRole('checkbox')).not.toBeInTheDocument();
|
||||||
|
|
||||||
// second row, first column
|
// second row, first column
|
||||||
expect(within(selectedOptions[4]).queryByLabelText('Loading')).not.toBeInTheDocument();
|
expect(within(selectedOptions[5]).queryByLabelText('Loading')).not.toBeInTheDocument();
|
||||||
expect(within(selectedOptions[4]).getByRole('checkbox')).toBeDefined();
|
expect(within(selectedOptions[5]).getByRole('checkbox')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the row loader when callback triggered with false', async () => {
|
it('should show the row loader when callback triggered with false', async () => {
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ALERT_UUID } from '@kbn/rule-data-utils';
|
import { ALERT_UUID, ALERT_STATUS, ALERT_FLAPPING } from '@kbn/rule-data-utils';
|
||||||
|
import { AlertStatus } from '@kbn/rule-data-utils';
|
||||||
import React, { useState, Suspense, lazy, useCallback, useMemo, useEffect } from 'react';
|
import React, { useState, Suspense, lazy, useCallback, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
EuiDataGrid,
|
EuiDataGrid,
|
||||||
|
@ -23,6 +24,7 @@ import {
|
||||||
ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL,
|
ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL,
|
||||||
ALERTS_TABLE_CONTROL_COLUMNS_VIEW_DETAILS_LABEL,
|
ALERTS_TABLE_CONTROL_COLUMNS_VIEW_DETAILS_LABEL,
|
||||||
} from './translations';
|
} from './translations';
|
||||||
|
import { AlertLifecycleStatusBadge } from '../../components/alert_lifecycle_status_badge';
|
||||||
|
|
||||||
import './alerts_table.scss';
|
import './alerts_table.scss';
|
||||||
import { getToolbarVisibility } from './toolbar';
|
import { getToolbarVisibility } from './toolbar';
|
||||||
|
@ -36,6 +38,40 @@ const GridStyles: EuiDataGridStyle = {
|
||||||
fontSize: 's',
|
fontSize: 's',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const basicRenderCellValue = ({
|
||||||
|
data,
|
||||||
|
columnId,
|
||||||
|
}: {
|
||||||
|
data: Array<{ field: string; value: string[] }>;
|
||||||
|
columnId: string;
|
||||||
|
}) => {
|
||||||
|
const value = data.find((d) => d.field === columnId)?.value ?? [];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return <>{value.length ? value.join() : '--'}</>;
|
||||||
|
}
|
||||||
|
return <>{value}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAlertLifecycleStatus = ({
|
||||||
|
data,
|
||||||
|
columnId,
|
||||||
|
}: {
|
||||||
|
data: Array<{ field: string; value: string[] }>;
|
||||||
|
columnId: string;
|
||||||
|
}) => {
|
||||||
|
const alertStatus = data.find((d) => d.field === ALERT_STATUS)?.value ?? [];
|
||||||
|
if (Array.isArray(alertStatus) && alertStatus.length) {
|
||||||
|
const flapping = data.find((d) => d.field === ALERT_FLAPPING)?.value ?? [];
|
||||||
|
return (
|
||||||
|
<AlertLifecycleStatusBadge
|
||||||
|
alertStatus={alertStatus.join() as AlertStatus}
|
||||||
|
flapping={flapping[0]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return basicRenderCellValue({ data, columnId });
|
||||||
|
};
|
||||||
|
|
||||||
const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTableProps) => {
|
const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTableProps) => {
|
||||||
const [rowClasses, setRowClasses] = useState<EuiDataGridStyle['rowClasses']>({});
|
const [rowClasses, setRowClasses] = useState<EuiDataGridStyle['rowClasses']>({});
|
||||||
const alertsData = props.useFetchAlertsData();
|
const alertsData = props.useFetchAlertsData();
|
||||||
|
@ -86,6 +122,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
||||||
updatedAt,
|
updatedAt,
|
||||||
browserFields,
|
browserFields,
|
||||||
onChangeVisibleColumns,
|
onChangeVisibleColumns,
|
||||||
|
showAlertStatusWithFlapping = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// TODO when every solution is using this table, we will be able to simplify it by just passing the alert index
|
// TODO when every solution is using this table, we will be able to simplify it by just passing the alert index
|
||||||
|
@ -216,20 +253,6 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
||||||
|
|
||||||
const handleFlyoutClose = useCallback(() => setFlyoutAlertIndex(-1), [setFlyoutAlertIndex]);
|
const handleFlyoutClose = useCallback(() => setFlyoutAlertIndex(-1), [setFlyoutAlertIndex]);
|
||||||
|
|
||||||
const basicRenderCellValue = ({
|
|
||||||
data,
|
|
||||||
columnId,
|
|
||||||
}: {
|
|
||||||
data: Array<{ field: string; value: string[] }>;
|
|
||||||
columnId: string;
|
|
||||||
}) => {
|
|
||||||
const value = data.find((d) => d.field === columnId)?.value ?? [];
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return <>{value.length ? value.join() : '--'}</>;
|
|
||||||
}
|
|
||||||
return <>{value}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCellValue = useCallback(
|
const renderCellValue = useCallback(
|
||||||
() =>
|
() =>
|
||||||
props.alertsTableConfiguration?.getRenderCellValue
|
props.alertsTableConfiguration?.getRenderCellValue
|
||||||
|
@ -249,6 +272,12 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
||||||
Object.entries(alert ?? {}).forEach(([key, value]) => {
|
Object.entries(alert ?? {}).forEach(([key, value]) => {
|
||||||
data.push({ field: key, value: value as string[] });
|
data.push({ field: key, value: value as string[] });
|
||||||
});
|
});
|
||||||
|
if (showAlertStatusWithFlapping && _props.columnId === ALERT_STATUS) {
|
||||||
|
return renderAlertLifecycleStatus({
|
||||||
|
..._props,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
return renderCellValue({
|
return renderCellValue({
|
||||||
..._props,
|
..._props,
|
||||||
data,
|
data,
|
||||||
|
@ -258,7 +287,14 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[alerts, isLoading, pagination.pageIndex, pagination.pageSize, renderCellValue]
|
[
|
||||||
|
alerts,
|
||||||
|
isLoading,
|
||||||
|
pagination.pageIndex,
|
||||||
|
pagination.pageSize,
|
||||||
|
renderCellValue,
|
||||||
|
showAlertStatusWithFlapping,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -59,6 +59,7 @@ export interface AlertsTableStateProps {
|
||||||
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
|
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
showExpandToDetails: boolean;
|
showExpandToDetails: boolean;
|
||||||
|
showAlertStatusWithFlapping?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlertsTableStorage {
|
export interface AlertsTableStorage {
|
||||||
|
@ -96,6 +97,7 @@ const AlertsTableState = ({
|
||||||
query,
|
query,
|
||||||
pageSize,
|
pageSize,
|
||||||
showExpandToDetails,
|
showExpandToDetails,
|
||||||
|
showAlertStatusWithFlapping,
|
||||||
}: AlertsTableStateProps) => {
|
}: AlertsTableStateProps) => {
|
||||||
const { cases } = useKibana<{ cases: CaseUi }>().services;
|
const { cases } = useKibana<{ cases: CaseUi }>().services;
|
||||||
|
|
||||||
|
@ -246,6 +248,7 @@ const AlertsTableState = ({
|
||||||
id,
|
id,
|
||||||
leadingControlColumns: [],
|
leadingControlColumns: [],
|
||||||
showExpandToDetails,
|
showExpandToDetails,
|
||||||
|
showAlertStatusWithFlapping,
|
||||||
trailingControlColumns: [],
|
trailingControlColumns: [],
|
||||||
useFetchAlertsData,
|
useFetchAlertsData,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
|
@ -265,6 +268,7 @@ const AlertsTableState = ({
|
||||||
pagination.pageSize,
|
pagination.pageSize,
|
||||||
id,
|
id,
|
||||||
showExpandToDetails,
|
showExpandToDetails,
|
||||||
|
showAlertStatusWithFlapping,
|
||||||
useFetchAlertsData,
|
useFetchAlertsData,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
|
|
|
@ -86,9 +86,9 @@ describe('rules', () => {
|
||||||
|
|
||||||
const rules: AlertListItem[] = [
|
const rules: AlertListItem[] = [
|
||||||
// active first
|
// active first
|
||||||
alertToListItem(fakeNow.getTime(), ruleType, 'second_rule', ruleSummary.alerts.second_rule),
|
alertToListItem(fakeNow.getTime(), 'second_rule', ruleSummary.alerts.second_rule),
|
||||||
// ok second
|
// ok second
|
||||||
alertToListItem(fakeNow.getTime(), ruleType, 'first_rule', ruleSummary.alerts.first_rule),
|
alertToListItem(fakeNow.getTime(), 'first_rule', ruleSummary.alerts.first_rule),
|
||||||
];
|
];
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
const wrapper = mountWithIntl(
|
||||||
|
@ -164,8 +164,8 @@ describe('rules', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find(RuleAlertList).prop('items')).toEqual([
|
expect(wrapper.find(RuleAlertList).prop('items')).toEqual([
|
||||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-central', alerts['us-central']),
|
alertToListItem(fakeNow.getTime(), 'us-central', alerts['us-central']),
|
||||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-east', alerts['us-east']),
|
alertToListItem(fakeNow.getTime(), 'us-east', alerts['us-east']),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,20 +206,14 @@ describe('rules', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find(RuleAlertList).prop('items')).toEqual([
|
expect(wrapper.find(RuleAlertList).prop('items')).toEqual([
|
||||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-west', ruleUsWest),
|
alertToListItem(fakeNow.getTime(), 'us-west', ruleUsWest),
|
||||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-east', ruleUsEast),
|
alertToListItem(fakeNow.getTime(), 'us-east', ruleUsEast),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('alertToListItem', () => {
|
describe('alertToListItem', () => {
|
||||||
it('handles active rules', () => {
|
it('handles active rules', () => {
|
||||||
const ruleType = mockRuleType({
|
|
||||||
actionGroups: [
|
|
||||||
{ id: 'default', name: 'Default Action Group' },
|
|
||||||
{ id: 'testing', name: 'Test Action Group' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const start = fake2MinutesAgo;
|
const start = fake2MinutesAgo;
|
||||||
const alert: AlertStatus = {
|
const alert: AlertStatus = {
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
|
@ -229,9 +223,10 @@ describe('alertToListItem', () => {
|
||||||
flapping: false,
|
flapping: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||||
alert: 'id',
|
alert: 'id',
|
||||||
status: { label: 'Active', actionGroup: 'Test Action Group', healthColor: 'primary' },
|
status: 'Active',
|
||||||
|
flapping: false,
|
||||||
start,
|
start,
|
||||||
sortPriority: 0,
|
sortPriority: 0,
|
||||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||||
|
@ -240,7 +235,6 @@ describe('alertToListItem', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles active rules with no action group id', () => {
|
it('handles active rules with no action group id', () => {
|
||||||
const ruleType = mockRuleType();
|
|
||||||
const start = fake2MinutesAgo;
|
const start = fake2MinutesAgo;
|
||||||
const alert: AlertStatus = {
|
const alert: AlertStatus = {
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
|
@ -249,9 +243,10 @@ describe('alertToListItem', () => {
|
||||||
flapping: false,
|
flapping: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||||
alert: 'id',
|
alert: 'id',
|
||||||
status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' },
|
status: 'Active',
|
||||||
|
flapping: false,
|
||||||
start,
|
start,
|
||||||
sortPriority: 0,
|
sortPriority: 0,
|
||||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||||
|
@ -260,7 +255,6 @@ describe('alertToListItem', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles active muted rules', () => {
|
it('handles active muted rules', () => {
|
||||||
const ruleType = mockRuleType();
|
|
||||||
const start = fake2MinutesAgo;
|
const start = fake2MinutesAgo;
|
||||||
const alert: AlertStatus = {
|
const alert: AlertStatus = {
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
|
@ -270,9 +264,10 @@ describe('alertToListItem', () => {
|
||||||
flapping: false,
|
flapping: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||||
alert: 'id',
|
alert: 'id',
|
||||||
status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' },
|
status: 'Active',
|
||||||
|
flapping: false,
|
||||||
start,
|
start,
|
||||||
sortPriority: 0,
|
sortPriority: 0,
|
||||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||||
|
@ -281,7 +276,6 @@ describe('alertToListItem', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles active rules with start date', () => {
|
it('handles active rules with start date', () => {
|
||||||
const ruleType = mockRuleType();
|
|
||||||
const alert: AlertStatus = {
|
const alert: AlertStatus = {
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
muted: false,
|
muted: false,
|
||||||
|
@ -289,9 +283,10 @@ describe('alertToListItem', () => {
|
||||||
flapping: false,
|
flapping: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||||
alert: 'id',
|
alert: 'id',
|
||||||
status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' },
|
status: 'Active',
|
||||||
|
flapping: false,
|
||||||
start: undefined,
|
start: undefined,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
sortPriority: 0,
|
sortPriority: 0,
|
||||||
|
@ -300,16 +295,16 @@ describe('alertToListItem', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles muted inactive rules', () => {
|
it('handles muted inactive rules', () => {
|
||||||
const ruleType = mockRuleType();
|
|
||||||
const alert: AlertStatus = {
|
const alert: AlertStatus = {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
muted: true,
|
muted: true,
|
||||||
actionGroupId: 'default',
|
actionGroupId: 'default',
|
||||||
flapping: false,
|
flapping: false,
|
||||||
};
|
};
|
||||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||||
alert: 'id',
|
alert: 'id',
|
||||||
status: { label: 'Recovered', healthColor: 'subdued' },
|
status: 'OK',
|
||||||
|
flapping: false,
|
||||||
start: undefined,
|
start: undefined,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
sortPriority: 1,
|
sortPriority: 1,
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import React, { lazy } from 'react';
|
import React, { lazy } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui';
|
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui';
|
||||||
import { ActionGroup, AlertStatusValues } from '@kbn/alerting-plugin/common';
|
import { AlertStatusValues } from '@kbn/alerting-plugin/common';
|
||||||
import { useKibana } from '../../../../common/lib/kibana';
|
import { useKibana } from '../../../../common/lib/kibana';
|
||||||
import { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types';
|
import { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types';
|
||||||
import {
|
import {
|
||||||
|
@ -68,7 +68,7 @@ export function RuleComponent({
|
||||||
const { ruleTypeRegistry, actionTypeRegistry } = useKibana().services;
|
const { ruleTypeRegistry, actionTypeRegistry } = useKibana().services;
|
||||||
|
|
||||||
const alerts = Object.entries(ruleSummary.alerts)
|
const alerts = Object.entries(ruleSummary.alerts)
|
||||||
.map(([alertId, alert]) => alertToListItem(durationEpoch, ruleType, alertId, alert))
|
.map(([alertId, alert]) => alertToListItem(durationEpoch, alertId, alert))
|
||||||
.sort((leftAlert, rightAlert) => leftAlert.sortPriority - rightAlert.sortPriority);
|
.sort((leftAlert, rightAlert) => leftAlert.sortPriority - rightAlert.sortPriority);
|
||||||
|
|
||||||
const onMuteAction = async (alert: AlertListItem) => {
|
const onMuteAction = async (alert: AlertListItem) => {
|
||||||
|
@ -179,39 +179,13 @@ export function RuleComponent({
|
||||||
}
|
}
|
||||||
export const RuleWithApi = withBulkRuleOperations(RuleComponent);
|
export const RuleWithApi = withBulkRuleOperations(RuleComponent);
|
||||||
|
|
||||||
const ACTIVE_LABEL = i18n.translate(
|
|
||||||
'xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active',
|
|
||||||
{ defaultMessage: 'Active' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const INACTIVE_LABEL = i18n.translate(
|
|
||||||
'xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive',
|
|
||||||
{ defaultMessage: 'Recovered' }
|
|
||||||
);
|
|
||||||
|
|
||||||
function getActionGroupName(ruleType: RuleType, actionGroupId?: string): string | undefined {
|
|
||||||
actionGroupId = actionGroupId || ruleType.defaultActionGroupId;
|
|
||||||
const actionGroup = ruleType?.actionGroups?.find(
|
|
||||||
(group: ActionGroup<string>) => group.id === actionGroupId
|
|
||||||
);
|
|
||||||
return actionGroup?.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alertToListItem(
|
export function alertToListItem(
|
||||||
durationEpoch: number,
|
durationEpoch: number,
|
||||||
ruleType: RuleType,
|
|
||||||
alertId: string,
|
alertId: string,
|
||||||
alert: AlertStatus
|
alert: AlertStatus
|
||||||
): AlertListItem {
|
): AlertListItem {
|
||||||
const isMuted = !!alert?.muted;
|
const isMuted = !!alert?.muted;
|
||||||
const status =
|
const status = alert.status;
|
||||||
alert?.status === 'Active'
|
|
||||||
? {
|
|
||||||
label: ACTIVE_LABEL,
|
|
||||||
actionGroup: getActionGroupName(ruleType, alert?.actionGroupId),
|
|
||||||
healthColor: 'primary',
|
|
||||||
}
|
|
||||||
: { label: INACTIVE_LABEL, healthColor: 'subdued' };
|
|
||||||
const start = alert?.activeStartDate ? new Date(alert.activeStartDate) : undefined;
|
const start = alert?.activeStartDate ? new Date(alert.activeStartDate) : undefined;
|
||||||
const duration = start ? durationEpoch - start.valueOf() : 0;
|
const duration = start ? durationEpoch - start.valueOf() : 0;
|
||||||
const sortPriority = getSortPriorityByStatus(alert?.status);
|
const sortPriority = getSortPriorityByStatus(alert?.status);
|
||||||
|
@ -222,6 +196,7 @@ export function alertToListItem(
|
||||||
duration,
|
duration,
|
||||||
isMuted,
|
isMuted,
|
||||||
sortPriority,
|
sortPriority,
|
||||||
|
flapping: alert.flapping,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,23 @@
|
||||||
import React, { useMemo, useCallback, useState } from 'react';
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
import moment, { Duration } from 'moment';
|
import moment, { Duration } from 'moment';
|
||||||
import { padStart, chunk } from 'lodash';
|
import { padStart, chunk } from 'lodash';
|
||||||
import { EuiHealth, EuiBasicTable, EuiToolTip } from '@elastic/eui';
|
import { EuiBasicTable, EuiToolTip } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||||
|
import { AlertStatus, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||||
|
import { AlertStatusValues } from '@kbn/alerting-plugin/common';
|
||||||
import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
|
import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
|
||||||
import { Pagination } from '../../../../types';
|
import { Pagination } from '../../../../types';
|
||||||
import { AlertListItemStatus, AlertListItem } from './types';
|
import { AlertListItem } from './types';
|
||||||
import { AlertMutedSwitch } from './alert_muted_switch';
|
import { AlertMutedSwitch } from './alert_muted_switch';
|
||||||
|
import { AlertLifecycleStatusBadge } from '../../../components/alert_lifecycle_status_badge';
|
||||||
|
|
||||||
|
export const getConvertedAlertStatus = (status: AlertStatusValues): AlertStatus => {
|
||||||
|
if (status === 'Active') {
|
||||||
|
return ALERT_STATUS_ACTIVE;
|
||||||
|
}
|
||||||
|
return ALERT_STATUS_RECOVERED;
|
||||||
|
};
|
||||||
|
|
||||||
const durationAsString = (duration: Duration): string => {
|
const durationAsString = (duration: Duration): string => {
|
||||||
return [duration.hours(), duration.minutes(), duration.seconds()]
|
return [duration.hours(), duration.minutes(), duration.seconds()]
|
||||||
|
@ -49,13 +59,9 @@ const alertsTableColumns = (
|
||||||
defaultMessage: 'Status',
|
defaultMessage: 'Status',
|
||||||
}),
|
}),
|
||||||
width: '15%',
|
width: '15%',
|
||||||
render: (value: AlertListItemStatus) => {
|
render: (value: AlertStatusValues, alert: AlertListItem) => {
|
||||||
return (
|
const convertedStatus = getConvertedAlertStatus(value);
|
||||||
<EuiHealth color={value.healthColor} className="alertsList__health">
|
return <AlertLifecycleStatusBadge alertStatus={convertedStatus} flapping={alert.flapping} />;
|
||||||
{value.label}
|
|
||||||
{value.actionGroup ? ` (${value.actionGroup})` : ``}
|
|
||||||
</EuiHealth>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
sortable: false,
|
sortable: false,
|
||||||
'data-test-subj': 'alertsTableCell-status',
|
'data-test-subj': 'alertsTableCell-status',
|
||||||
|
|
|
@ -4,18 +4,14 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
import { AlertStatusValues } from '@kbn/alerting-plugin/common';
|
||||||
export interface AlertListItemStatus {
|
|
||||||
label: string;
|
|
||||||
healthColor: string;
|
|
||||||
actionGroup?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlertListItem {
|
export interface AlertListItem {
|
||||||
alert: string;
|
alert: string;
|
||||||
status: AlertListItemStatus;
|
status: AlertStatusValues;
|
||||||
start?: Date;
|
start?: Date;
|
||||||
duration: number;
|
duration: number;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
sortPriority: number;
|
sortPriority: number;
|
||||||
|
flapping: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,6 @@ import type {
|
||||||
} from './application/sections/field_browser/types';
|
} from './application/sections/field_browser/types';
|
||||||
import { RulesListVisibleColumns } from './application/sections/rules_list/components/rules_list_column_selector';
|
import { RulesListVisibleColumns } from './application/sections/rules_list/components/rules_list_column_selector';
|
||||||
import { TimelineItem } from './application/sections/alerts_table/bulk_actions/components/toolbar';
|
import { TimelineItem } from './application/sections/alerts_table/bulk_actions/components/toolbar';
|
||||||
|
|
||||||
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
|
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
|
||||||
// so the `Params` is a black-box of Record<string, unknown>
|
// so the `Params` is a black-box of Record<string, unknown>
|
||||||
type SanitizedRule<Params extends RuleTypeParams = never> = Omit<
|
type SanitizedRule<Params extends RuleTypeParams = never> = Omit<
|
||||||
|
@ -475,6 +474,7 @@ export interface AlertsTableProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
leadingControlColumns: EuiDataGridControlColumn[];
|
leadingControlColumns: EuiDataGridControlColumn[];
|
||||||
showExpandToDetails: boolean;
|
showExpandToDetails: boolean;
|
||||||
|
showAlertStatusWithFlapping?: boolean;
|
||||||
trailingControlColumns: EuiDataGridControlColumn[];
|
trailingControlColumns: EuiDataGridControlColumn[];
|
||||||
useFetchAlertsData: () => FetchAlertData;
|
useFetchAlertsData: () => FetchAlertData;
|
||||||
visibleColumns: string[];
|
visibleColumns: string[];
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
import { omit, mapValues, range, flatten } from 'lodash';
|
import { omit, mapValues, range, flatten } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { asyncForEach } from '@kbn/std';
|
import { asyncForEach } from '@kbn/std';
|
||||||
import { alwaysFiringAlertType } from '@kbn/alerting-fixture-plugin/server/plugin';
|
|
||||||
import { RuleNotifyWhen } from '@kbn/alerting-plugin/common';
|
import { RuleNotifyWhen } from '@kbn/alerting-plugin/common';
|
||||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
import { ObjectRemover } from '../../lib/object_remover';
|
import { ObjectRemover } from '../../lib/object_remover';
|
||||||
|
@ -747,31 +746,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
// refresh to ensure Api call and UI are looking at freshest output
|
// refresh to ensure Api call and UI are looking at freshest output
|
||||||
await browser.refresh();
|
await browser.refresh();
|
||||||
|
|
||||||
// Get action groups
|
|
||||||
const { actionGroups } = alwaysFiringAlertType;
|
|
||||||
|
|
||||||
// If the tab exists, click on the alert list
|
// If the tab exists, click on the alert list
|
||||||
await pageObjects.triggersActionsUI.maybeClickOnAlertTab();
|
await pageObjects.triggersActionsUI.maybeClickOnAlertTab();
|
||||||
|
|
||||||
// Verify content
|
// Verify content
|
||||||
await testSubjects.existOrFail('alertsList');
|
await testSubjects.existOrFail('alertsList');
|
||||||
|
|
||||||
const actionGroupNameFromId = (actionGroupId: string) =>
|
|
||||||
actionGroups.find(
|
|
||||||
(actionGroup: { id: string; name: string }) => actionGroup.id === actionGroupId
|
|
||||||
)?.name;
|
|
||||||
|
|
||||||
const summary = await getAlertSummary(rule.id);
|
const summary = await getAlertSummary(rule.id);
|
||||||
const dateOnAllAlertsFromApiResponse: Record<string, string> = mapValues(
|
const dateOnAllAlertsFromApiResponse: Record<string, string> = mapValues(
|
||||||
summary.alerts,
|
summary.alerts,
|
||||||
(a) => a.activeStartDate
|
(a) => a.activeStartDate
|
||||||
);
|
);
|
||||||
|
|
||||||
const actionGroupNameOnAllInstancesFromApiResponse = mapValues(summary.alerts, (a) => {
|
|
||||||
const name = actionGroupNameFromId(a.actionGroupId);
|
|
||||||
return name ? ` (${name})` : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
`API RESULT: ${Object.entries(dateOnAllAlertsFromApiResponse)
|
`API RESULT: ${Object.entries(dateOnAllAlertsFromApiResponse)
|
||||||
.map(([id, date]) => `${id}: ${moment(date).utc()}`)
|
.map(([id, date]) => `${id}: ${moment(date).utc()}`)
|
||||||
|
@ -782,21 +768,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
expect(alertsList.map((a) => omit(a, 'duration'))).to.eql([
|
expect(alertsList.map((a) => omit(a, 'duration'))).to.eql([
|
||||||
{
|
{
|
||||||
alert: 'us-central',
|
alert: 'us-central',
|
||||||
status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-central']}`,
|
status: `Active`,
|
||||||
start: moment(dateOnAllAlertsFromApiResponse['us-central'])
|
start: moment(dateOnAllAlertsFromApiResponse['us-central'])
|
||||||
.utc()
|
.utc()
|
||||||
.format('D MMM YYYY @ HH:mm:ss'),
|
.format('D MMM YYYY @ HH:mm:ss'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
alert: 'us-east',
|
alert: 'us-east',
|
||||||
status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-east']}`,
|
status: `Active`,
|
||||||
start: moment(dateOnAllAlertsFromApiResponse['us-east'])
|
start: moment(dateOnAllAlertsFromApiResponse['us-east'])
|
||||||
.utc()
|
.utc()
|
||||||
.format('D MMM YYYY @ HH:mm:ss'),
|
.format('D MMM YYYY @ HH:mm:ss'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
alert: 'us-west',
|
alert: 'us-west',
|
||||||
status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-west']}`,
|
status: `Active`,
|
||||||
start: moment(dateOnAllAlertsFromApiResponse['us-west'])
|
start: moment(dateOnAllAlertsFromApiResponse['us-west'])
|
||||||
.utc()
|
.utc()
|
||||||
.format('D MMM YYYY @ HH:mm:ss'),
|
.format('D MMM YYYY @ HH:mm:ss'),
|
||||||
|
|
|
@ -109,7 +109,6 @@
|
||||||
"@kbn/apm-synthtrace-client",
|
"@kbn/apm-synthtrace-client",
|
||||||
"@kbn/utils",
|
"@kbn/utils",
|
||||||
"@kbn/journeys",
|
"@kbn/journeys",
|
||||||
"@kbn/alerting-fixture-plugin",
|
|
||||||
"@kbn/stdio-dev-helpers",
|
"@kbn/stdio-dev-helpers",
|
||||||
"@kbn/alerting-api-integration-helpers",
|
"@kbn/alerting-api-integration-helpers",
|
||||||
"@kbn/securitysolution-ecs",
|
"@kbn/securitysolution-ecs",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue