mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40: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 { waitFor } from '@testing-library/dom';
|
||||
import { OBSERVABILITY_OWNER } from '../../../../common/constants';
|
||||
import { alertCommentWithIndices, basicCase } from '../../../containers/mock';
|
||||
import type { AppMockRenderer } from '../../../common/mock';
|
||||
import { createAppMockRenderer } from '../../../common/mock';
|
||||
|
@ -54,6 +55,7 @@ describe('Case View Page activity tab', () => {
|
|||
},
|
||||
flyoutSize: 'm',
|
||||
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 () => {
|
||||
const getFeatureIdsMock = jest.spyOn(api, 'getFeatureIds');
|
||||
getFeatureIdsMock.mockResolvedValueOnce(['observability']);
|
||||
appMockRender.render(<CaseViewAlerts caseData={caseData} />);
|
||||
appMockRender.render(
|
||||
<CaseViewAlerts
|
||||
caseData={{
|
||||
...caseData,
|
||||
owner: OBSERVABILITY_OWNER,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(getAlertsStateTableMock).toHaveBeenCalledWith({
|
||||
alertsTableConfigurationRegistry: expect.anything(),
|
||||
configurationId: 'securitySolution',
|
||||
configurationId: 'observability',
|
||||
featureIds: ['observability'],
|
||||
id: 'case-details-alerts-securitySolution',
|
||||
id: 'case-details-alerts-observability',
|
||||
query: {
|
||||
ids: {
|
||||
values: ['alert-id-1'],
|
||||
|
@ -76,6 +85,7 @@ describe('Case View Page activity tab', () => {
|
|||
},
|
||||
flyoutSize: 's',
|
||||
showExpandToDetails: false,
|
||||
showAlertStatusWithFlapping: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { useMemo } from 'react';
|
|||
|
||||
import type { EuiFlyoutSize } from '@elastic/eui';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiProgress } from '@elastic/eui';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants';
|
||||
import type { Case } from '../../../../common';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getManualAlertIds, getRegistrationContextFromAlerts } from './helpers';
|
||||
|
@ -46,6 +47,7 @@ export const CaseViewAlerts = ({ caseData }: CaseViewAlertsProps) => {
|
|||
featureIds: alertFeatureIds ?? [],
|
||||
query: alertIdsQuery,
|
||||
showExpandToDetails: Boolean(alertFeatureIds?.includes('siem')),
|
||||
showAlertStatusWithFlapping: caseData.owner !== SECURITY_SOLUTION_OWNER,
|
||||
};
|
||||
|
||||
if (alertIdsQuery.ids.values.length === 0) {
|
||||
|
|
|
@ -199,6 +199,7 @@ function InternalAlertsPage() {
|
|||
featureIds={observabilityAlertFeatureIds}
|
||||
query={esQuery}
|
||||
showExpandToDetails={false}
|
||||
showAlertStatusWithFlapping
|
||||
pageSize={ALERTS_PER_PAGE}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -209,6 +209,7 @@ export function OverviewPage() {
|
|||
pageSize={ALERTS_PER_PAGE}
|
||||
query={esQuery}
|
||||
showExpandToDetails={false}
|
||||
showAlertStatusWithFlapping
|
||||
/>
|
||||
</CasesContext>
|
||||
</SectionContainer>
|
||||
|
|
|
@ -281,6 +281,7 @@ export function RuleDetailsPage() {
|
|||
featureIds={featureIds}
|
||||
query={esQuery}
|
||||
showExpandToDetails={false}
|
||||
showAlertStatusWithFlapping
|
||||
/>
|
||||
)}
|
||||
</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.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.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.enabledOptionText": "La règle est activé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.responseTooltip": "最大10,000件の直近のルール実行の応答。",
|
||||
"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.enabledOptionText": "ルールが有効です",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleStateFilter.snoozedOptionText": "ルールがスヌーズされました",
|
||||
|
|
|
@ -35124,8 +35124,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "无法提取事件日志 KPI。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "多达 10,000 次最近规则运行的响应。",
|
||||
"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.enabledOptionText": "规则已启用",
|
||||
"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 { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
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 { AlertsField, AlertsTableProps, BulkActionsState, RowSelectionState } from '../../../types';
|
||||
import { AlertsTableProps, BulkActionsState, RowSelectionState } from '../../../types';
|
||||
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { BulkActionsContext } from './bulk_actions/context';
|
||||
|
@ -25,27 +25,47 @@ jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
|||
|
||||
const columns = [
|
||||
{
|
||||
id: AlertsField.name,
|
||||
id: ALERT_RULE_NAME,
|
||||
displayAsText: 'Name',
|
||||
},
|
||||
{
|
||||
id: AlertsField.reason,
|
||||
id: ALERT_REASON,
|
||||
displayAsText: 'Reason',
|
||||
},
|
||||
{
|
||||
id: ALERT_STATUS,
|
||||
displayAsText: 'Alert status',
|
||||
},
|
||||
];
|
||||
|
||||
describe('AlertsTable', () => {
|
||||
const alerts = [
|
||||
{
|
||||
[AlertsField.name]: ['one'],
|
||||
[AlertsField.reason]: ['two'],
|
||||
},
|
||||
{
|
||||
[AlertsField.name]: ['three'],
|
||||
[AlertsField.reason]: ['four'],
|
||||
},
|
||||
] as unknown as EcsFieldsResponse[];
|
||||
const alerts = [
|
||||
{
|
||||
[ALERT_RULE_NAME]: ['one'],
|
||||
[ALERT_REASON]: ['two'],
|
||||
[ALERT_STATUS]: ['active'],
|
||||
[ALERT_FLAPPING]: [true],
|
||||
},
|
||||
{
|
||||
[ALERT_RULE_NAME]: ['three'],
|
||||
[ALERT_REASON]: ['four'],
|
||||
[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 = {
|
||||
activePage: 0,
|
||||
alerts,
|
||||
|
@ -164,6 +184,24 @@ describe('AlertsTable', () => {
|
|||
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', () => {
|
||||
it('should return at least the flyout action control', async () => {
|
||||
const wrapper = render(<AlertsTableWithLocale {...tableProps} />);
|
||||
|
@ -206,6 +244,7 @@ describe('AlertsTable', () => {
|
|||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testActionColumn"
|
||||
aria-label="testActionLabel"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -215,6 +254,7 @@ describe('AlertsTable', () => {
|
|||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testActionColumn2"
|
||||
aria-label="testActionLabel2"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
|
@ -249,6 +289,7 @@ describe('AlertsTable', () => {
|
|||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testActionColumn"
|
||||
aria-label="testActionLabel"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -258,6 +299,7 @@ describe('AlertsTable', () => {
|
|||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testActionColumn2"
|
||||
aria-label="testActionLabel2"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
|
@ -307,6 +349,7 @@ describe('AlertsTable', () => {
|
|||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testActionColumn"
|
||||
aria-label="testActionLabel"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
|
@ -332,8 +375,8 @@ describe('AlertsTable', () => {
|
|||
expect(within(selectedOptions[0]).queryByRole('checkbox')).not.toBeInTheDocument();
|
||||
|
||||
// second row, first column
|
||||
expect(within(selectedOptions[4]).queryByLabelText('Loading')).not.toBeInTheDocument();
|
||||
expect(within(selectedOptions[4]).getByRole('checkbox')).toBeDefined();
|
||||
expect(within(selectedOptions[5]).queryByLabelText('Loading')).not.toBeInTheDocument();
|
||||
expect(within(selectedOptions[5]).getByRole('checkbox')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show the row loader when callback triggered with false', async () => {
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 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 {
|
||||
EuiDataGrid,
|
||||
|
@ -23,6 +24,7 @@ import {
|
|||
ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL,
|
||||
ALERTS_TABLE_CONTROL_COLUMNS_VIEW_DETAILS_LABEL,
|
||||
} from './translations';
|
||||
import { AlertLifecycleStatusBadge } from '../../components/alert_lifecycle_status_badge';
|
||||
|
||||
import './alerts_table.scss';
|
||||
import { getToolbarVisibility } from './toolbar';
|
||||
|
@ -36,6 +38,40 @@ const GridStyles: EuiDataGridStyle = {
|
|||
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 [rowClasses, setRowClasses] = useState<EuiDataGridStyle['rowClasses']>({});
|
||||
const alertsData = props.useFetchAlertsData();
|
||||
|
@ -86,6 +122,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
updatedAt,
|
||||
browserFields,
|
||||
onChangeVisibleColumns,
|
||||
showAlertStatusWithFlapping = false,
|
||||
} = props;
|
||||
|
||||
// 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 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(
|
||||
() =>
|
||||
props.alertsTableConfiguration?.getRenderCellValue
|
||||
|
@ -249,6 +272,12 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
Object.entries(alert ?? {}).forEach(([key, value]) => {
|
||||
data.push({ field: key, value: value as string[] });
|
||||
});
|
||||
if (showAlertStatusWithFlapping && _props.columnId === ALERT_STATUS) {
|
||||
return renderAlertLifecycleStatus({
|
||||
..._props,
|
||||
data,
|
||||
});
|
||||
}
|
||||
return renderCellValue({
|
||||
..._props,
|
||||
data,
|
||||
|
@ -258,7 +287,14 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
}
|
||||
return null;
|
||||
},
|
||||
[alerts, isLoading, pagination.pageIndex, pagination.pageSize, renderCellValue]
|
||||
[
|
||||
alerts,
|
||||
isLoading,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
renderCellValue,
|
||||
showAlertStatusWithFlapping,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -59,6 +59,7 @@ export interface AlertsTableStateProps {
|
|||
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
|
||||
pageSize?: number;
|
||||
showExpandToDetails: boolean;
|
||||
showAlertStatusWithFlapping?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertsTableStorage {
|
||||
|
@ -96,6 +97,7 @@ const AlertsTableState = ({
|
|||
query,
|
||||
pageSize,
|
||||
showExpandToDetails,
|
||||
showAlertStatusWithFlapping,
|
||||
}: AlertsTableStateProps) => {
|
||||
const { cases } = useKibana<{ cases: CaseUi }>().services;
|
||||
|
||||
|
@ -246,6 +248,7 @@ const AlertsTableState = ({
|
|||
id,
|
||||
leadingControlColumns: [],
|
||||
showExpandToDetails,
|
||||
showAlertStatusWithFlapping,
|
||||
trailingControlColumns: [],
|
||||
useFetchAlertsData,
|
||||
visibleColumns,
|
||||
|
@ -265,6 +268,7 @@ const AlertsTableState = ({
|
|||
pagination.pageSize,
|
||||
id,
|
||||
showExpandToDetails,
|
||||
showAlertStatusWithFlapping,
|
||||
useFetchAlertsData,
|
||||
visibleColumns,
|
||||
updatedAt,
|
||||
|
|
|
@ -86,9 +86,9 @@ describe('rules', () => {
|
|||
|
||||
const rules: AlertListItem[] = [
|
||||
// active first
|
||||
alertToListItem(fakeNow.getTime(), ruleType, 'second_rule', ruleSummary.alerts.second_rule),
|
||||
alertToListItem(fakeNow.getTime(), 'second_rule', ruleSummary.alerts.second_rule),
|
||||
// ok second
|
||||
alertToListItem(fakeNow.getTime(), ruleType, 'first_rule', ruleSummary.alerts.first_rule),
|
||||
alertToListItem(fakeNow.getTime(), 'first_rule', ruleSummary.alerts.first_rule),
|
||||
];
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
|
@ -164,8 +164,8 @@ describe('rules', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.find(RuleAlertList).prop('items')).toEqual([
|
||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-central', alerts['us-central']),
|
||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-east', alerts['us-east']),
|
||||
alertToListItem(fakeNow.getTime(), 'us-central', alerts['us-central']),
|
||||
alertToListItem(fakeNow.getTime(), 'us-east', alerts['us-east']),
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -206,20 +206,14 @@ describe('rules', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.find(RuleAlertList).prop('items')).toEqual([
|
||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-west', ruleUsWest),
|
||||
alertToListItem(fakeNow.getTime(), ruleType, 'us-east', ruleUsEast),
|
||||
alertToListItem(fakeNow.getTime(), 'us-west', ruleUsWest),
|
||||
alertToListItem(fakeNow.getTime(), 'us-east', ruleUsEast),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alertToListItem', () => {
|
||||
it('handles active rules', () => {
|
||||
const ruleType = mockRuleType({
|
||||
actionGroups: [
|
||||
{ id: 'default', name: 'Default Action Group' },
|
||||
{ id: 'testing', name: 'Test Action Group' },
|
||||
],
|
||||
});
|
||||
const start = fake2MinutesAgo;
|
||||
const alert: AlertStatus = {
|
||||
status: 'Active',
|
||||
|
@ -229,9 +223,10 @@ describe('alertToListItem', () => {
|
|||
flapping: false,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
alert: 'id',
|
||||
status: { label: 'Active', actionGroup: 'Test Action Group', healthColor: 'primary' },
|
||||
status: 'Active',
|
||||
flapping: false,
|
||||
start,
|
||||
sortPriority: 0,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
|
@ -240,7 +235,6 @@ describe('alertToListItem', () => {
|
|||
});
|
||||
|
||||
it('handles active rules with no action group id', () => {
|
||||
const ruleType = mockRuleType();
|
||||
const start = fake2MinutesAgo;
|
||||
const alert: AlertStatus = {
|
||||
status: 'Active',
|
||||
|
@ -249,9 +243,10 @@ describe('alertToListItem', () => {
|
|||
flapping: false,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
alert: 'id',
|
||||
status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' },
|
||||
status: 'Active',
|
||||
flapping: false,
|
||||
start,
|
||||
sortPriority: 0,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
|
@ -260,7 +255,6 @@ describe('alertToListItem', () => {
|
|||
});
|
||||
|
||||
it('handles active muted rules', () => {
|
||||
const ruleType = mockRuleType();
|
||||
const start = fake2MinutesAgo;
|
||||
const alert: AlertStatus = {
|
||||
status: 'Active',
|
||||
|
@ -270,9 +264,10 @@ describe('alertToListItem', () => {
|
|||
flapping: false,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
alert: 'id',
|
||||
status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' },
|
||||
status: 'Active',
|
||||
flapping: false,
|
||||
start,
|
||||
sortPriority: 0,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
|
@ -281,7 +276,6 @@ describe('alertToListItem', () => {
|
|||
});
|
||||
|
||||
it('handles active rules with start date', () => {
|
||||
const ruleType = mockRuleType();
|
||||
const alert: AlertStatus = {
|
||||
status: 'Active',
|
||||
muted: false,
|
||||
|
@ -289,9 +283,10 @@ describe('alertToListItem', () => {
|
|||
flapping: false,
|
||||
};
|
||||
|
||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
alert: 'id',
|
||||
status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' },
|
||||
status: 'Active',
|
||||
flapping: false,
|
||||
start: undefined,
|
||||
duration: 0,
|
||||
sortPriority: 0,
|
||||
|
@ -300,16 +295,16 @@ describe('alertToListItem', () => {
|
|||
});
|
||||
|
||||
it('handles muted inactive rules', () => {
|
||||
const ruleType = mockRuleType();
|
||||
const alert: AlertStatus = {
|
||||
status: 'OK',
|
||||
muted: true,
|
||||
actionGroupId: 'default',
|
||||
flapping: false,
|
||||
};
|
||||
expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({
|
||||
expect(alertToListItem(fakeNow.getTime(), 'id', alert)).toEqual({
|
||||
alert: 'id',
|
||||
status: { label: 'Recovered', healthColor: 'subdued' },
|
||||
status: 'OK',
|
||||
flapping: false,
|
||||
start: undefined,
|
||||
duration: 0,
|
||||
sortPriority: 1,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types';
|
||||
import {
|
||||
|
@ -68,7 +68,7 @@ export function RuleComponent({
|
|||
const { ruleTypeRegistry, actionTypeRegistry } = useKibana().services;
|
||||
|
||||
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);
|
||||
|
||||
const onMuteAction = async (alert: AlertListItem) => {
|
||||
|
@ -179,39 +179,13 @@ export function 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(
|
||||
durationEpoch: number,
|
||||
ruleType: RuleType,
|
||||
alertId: string,
|
||||
alert: AlertStatus
|
||||
): AlertListItem {
|
||||
const isMuted = !!alert?.muted;
|
||||
const status =
|
||||
alert?.status === 'Active'
|
||||
? {
|
||||
label: ACTIVE_LABEL,
|
||||
actionGroup: getActionGroupName(ruleType, alert?.actionGroupId),
|
||||
healthColor: 'primary',
|
||||
}
|
||||
: { label: INACTIVE_LABEL, healthColor: 'subdued' };
|
||||
const status = alert.status;
|
||||
const start = alert?.activeStartDate ? new Date(alert.activeStartDate) : undefined;
|
||||
const duration = start ? durationEpoch - start.valueOf() : 0;
|
||||
const sortPriority = getSortPriorityByStatus(alert?.status);
|
||||
|
@ -222,6 +196,7 @@ export function alertToListItem(
|
|||
duration,
|
||||
isMuted,
|
||||
sortPriority,
|
||||
flapping: alert.flapping,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,23 @@
|
|||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import moment, { Duration } from 'moment';
|
||||
import { padStart, chunk } from 'lodash';
|
||||
import { EuiHealth, EuiBasicTable, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 { Pagination } from '../../../../types';
|
||||
import { AlertListItemStatus, AlertListItem } from './types';
|
||||
import { AlertListItem } from './types';
|
||||
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 => {
|
||||
return [duration.hours(), duration.minutes(), duration.seconds()]
|
||||
|
@ -49,13 +59,9 @@ const alertsTableColumns = (
|
|||
defaultMessage: 'Status',
|
||||
}),
|
||||
width: '15%',
|
||||
render: (value: AlertListItemStatus) => {
|
||||
return (
|
||||
<EuiHealth color={value.healthColor} className="alertsList__health">
|
||||
{value.label}
|
||||
{value.actionGroup ? ` (${value.actionGroup})` : ``}
|
||||
</EuiHealth>
|
||||
);
|
||||
render: (value: AlertStatusValues, alert: AlertListItem) => {
|
||||
const convertedStatus = getConvertedAlertStatus(value);
|
||||
return <AlertLifecycleStatusBadge alertStatus={convertedStatus} flapping={alert.flapping} />;
|
||||
},
|
||||
sortable: false,
|
||||
'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.
|
||||
*/
|
||||
|
||||
export interface AlertListItemStatus {
|
||||
label: string;
|
||||
healthColor: string;
|
||||
actionGroup?: string;
|
||||
}
|
||||
import { AlertStatusValues } from '@kbn/alerting-plugin/common';
|
||||
|
||||
export interface AlertListItem {
|
||||
alert: string;
|
||||
status: AlertListItemStatus;
|
||||
status: AlertStatusValues;
|
||||
start?: Date;
|
||||
duration: number;
|
||||
isMuted: boolean;
|
||||
sortPriority: number;
|
||||
flapping: boolean;
|
||||
}
|
||||
|
|
|
@ -81,7 +81,6 @@ import type {
|
|||
} from './application/sections/field_browser/types';
|
||||
import { RulesListVisibleColumns } from './application/sections/rules_list/components/rules_list_column_selector';
|
||||
import { TimelineItem } from './application/sections/alerts_table/bulk_actions/components/toolbar';
|
||||
|
||||
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
|
||||
// so the `Params` is a black-box of Record<string, unknown>
|
||||
type SanitizedRule<Params extends RuleTypeParams = never> = Omit<
|
||||
|
@ -475,6 +474,7 @@ export interface AlertsTableProps {
|
|||
id?: string;
|
||||
leadingControlColumns: EuiDataGridControlColumn[];
|
||||
showExpandToDetails: boolean;
|
||||
showAlertStatusWithFlapping?: boolean;
|
||||
trailingControlColumns: EuiDataGridControlColumn[];
|
||||
useFetchAlertsData: () => FetchAlertData;
|
||||
visibleColumns: string[];
|
||||
|
|
|
@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import { omit, mapValues, range, flatten } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { alwaysFiringAlertType } from '@kbn/alerting-fixture-plugin/server/plugin';
|
||||
import { RuleNotifyWhen } from '@kbn/alerting-plugin/common';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
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
|
||||
await browser.refresh();
|
||||
|
||||
// Get action groups
|
||||
const { actionGroups } = alwaysFiringAlertType;
|
||||
|
||||
// If the tab exists, click on the alert list
|
||||
await pageObjects.triggersActionsUI.maybeClickOnAlertTab();
|
||||
|
||||
// Verify content
|
||||
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 dateOnAllAlertsFromApiResponse: Record<string, string> = mapValues(
|
||||
summary.alerts,
|
||||
(a) => a.activeStartDate
|
||||
);
|
||||
|
||||
const actionGroupNameOnAllInstancesFromApiResponse = mapValues(summary.alerts, (a) => {
|
||||
const name = actionGroupNameFromId(a.actionGroupId);
|
||||
return name ? ` (${name})` : '';
|
||||
});
|
||||
|
||||
log.debug(
|
||||
`API RESULT: ${Object.entries(dateOnAllAlertsFromApiResponse)
|
||||
.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([
|
||||
{
|
||||
alert: 'us-central',
|
||||
status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-central']}`,
|
||||
status: `Active`,
|
||||
start: moment(dateOnAllAlertsFromApiResponse['us-central'])
|
||||
.utc()
|
||||
.format('D MMM YYYY @ HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
alert: 'us-east',
|
||||
status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-east']}`,
|
||||
status: `Active`,
|
||||
start: moment(dateOnAllAlertsFromApiResponse['us-east'])
|
||||
.utc()
|
||||
.format('D MMM YYYY @ HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
alert: 'us-west',
|
||||
status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-west']}`,
|
||||
status: `Active`,
|
||||
start: moment(dateOnAllAlertsFromApiResponse['us-west'])
|
||||
.utc()
|
||||
.format('D MMM YYYY @ HH:mm:ss'),
|
||||
|
|
|
@ -109,7 +109,6 @@
|
|||
"@kbn/apm-synthtrace-client",
|
||||
"@kbn/utils",
|
||||
"@kbn/journeys",
|
||||
"@kbn/alerting-fixture-plugin",
|
||||
"@kbn/stdio-dev-helpers",
|
||||
"@kbn/alerting-api-integration-helpers",
|
||||
"@kbn/securitysolution-ecs",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue