[RAM] Refactor alert 011y fly-out to just use hook (#135161)

* refactor alert flyout to just use hook

* fix unit test

* thanks to test, we find out discrepency between reconcilliation of components

* open rule details from o11y
This commit is contained in:
Xavier Mouligneau 2022-06-28 02:28:32 -04:00 committed by GitHub
parent 1916efbb1c
commit c4350f1f2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 239 additions and 439 deletions

View file

@ -51,7 +51,7 @@ describe('Case View Page activity tab', () => {
values: ['alert-id-1'],
},
},
flyoutState: 'internal',
flyoutSize: 'm',
showExpandToDetails: true,
});
});
@ -73,7 +73,7 @@ describe('Case View Page activity tab', () => {
values: ['alert-id-1'],
},
},
flyoutState: 'external',
flyoutSize: 's',
showExpandToDetails: false,
});
});

View file

@ -7,8 +7,7 @@
import React, { useMemo } from 'react';
import { AlertsTableFlyoutState } from '@kbn/triggers-actions-ui-plugin/public';
import { EuiFlexItem, EuiFlexGroup, EuiProgress } from '@elastic/eui';
import { EuiFlexItem, EuiFlexGroup, EuiProgress, EuiFlyoutSize } from '@elastic/eui';
import { Case } from '../../../../common';
import { useKibana } from '../../../common/lib/kibana';
import { getManualAlertIds, getRegistrationContextFromAlerts } from './helpers';
@ -41,9 +40,7 @@ export const CaseViewAlerts = ({ caseData }: CaseViewAlertsProps) => {
alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry,
configurationId: caseData.owner,
id: `case-details-alerts-${caseData.owner}`,
flyoutState: alertFeatureIds.includes('siem')
? AlertsTableFlyoutState.internal
: AlertsTableFlyoutState.external,
flyoutSize: (alertFeatureIds.includes('siem') ? 'm' : 's') as EuiFlyoutSize,
featureIds: alertFeatureIds,
query: alertIdsQuery,
showExpandToDetails: alertFeatureIds.includes('siem'),

View file

@ -5,36 +5,26 @@
* 2.0.
*/
import type {
AlertTableFlyoutComponent,
GetRenderCellValue,
} from '@kbn/triggers-actions-ui-plugin/public';
import { lazy } from 'react';
import type { GetRenderCellValue } from '@kbn/triggers-actions-ui-plugin/public';
import { observabilityFeatureId } from '../../common';
import { TopAlert, useToGetInternalFlyout } from '../pages/alerts';
import { getRenderCellValue } from '../pages/alerts/components/render_cell_value';
import { addDisplayNames } from '../pages/alerts/containers/alerts_table_t_grid/add_display_names';
import { columns as alertO11yColumns } from '../pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid';
import type { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry';
const AlertsPageFlyoutHeaderLazy = lazy(
() => import('../pages/alerts/components/alerts_flyout/alerts_flyout_header')
);
const AlertsPageFlyoutBodyLazy = lazy(
() => import('../pages/alerts/components/alerts_flyout/alerts_flyout_body')
);
const AlertsFlyoutFooterLazy = lazy(
() => import('../pages/alerts/components/alerts_flyout/alerts_flyout_footer')
);
const getO11yAlertsTableConfiguration = () => ({
const getO11yAlertsTableConfiguration = (
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry
) => ({
id: observabilityFeatureId,
columns: alertO11yColumns.map(addDisplayNames),
externalFlyout: {
header: AlertsPageFlyoutHeaderLazy as AlertTableFlyoutComponent,
body: AlertsPageFlyoutBodyLazy as AlertTableFlyoutComponent,
footer: AlertsFlyoutFooterLazy as AlertTableFlyoutComponent,
useInternalFlyout: () => {
const { header, body, footer } = useToGetInternalFlyout(observabilityRuleTypeRegistry);
return { header, body, footer };
},
getRenderCellValue: getRenderCellValue as GetRenderCellValue,
getRenderCellValue: (({ setFlyoutAlert }: { setFlyoutAlert: (data: TopAlert) => void }) => {
return getRenderCellValue({ observabilityRuleTypeRegistry, setFlyoutAlert });
}) as unknown as GetRenderCellValue,
});
export { getO11yAlertsTableConfiguration };

View file

@ -5,41 +5,15 @@
* 2.0.
*/
import {
EuiButton,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFlyoutProps,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
EuiHorizontalRule,
} from '@elastic/eui';
import {
ALERT_DURATION,
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_UUID,
ALERT_RULE_CATEGORY,
ALERT_RULE_NAME,
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
} from '@kbn/rule-data-utils';
import moment from 'moment-timezone';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutProps } from '@elastic/eui';
import { ALERT_UUID } from '@kbn/rule-data-utils';
import React, { useMemo } from 'react';
import { useKibana, useUiSetting } from '@kbn/kibana-react-plugin/public';
import type { TopAlert } from '../../containers';
import { asDuration } from '../../../../../common/utils/formatters';
import type { ObservabilityRuleTypeRegistry } from '../../../../rules/create_observability_rule_type_registry';
import { parseAlert } from '../parse_alert';
import { AlertStatusIndicator } from '../../../../components/shared/alert_status_indicator';
import { translations, paths } from '../../../../config';
import AlertsFlyoutHeader from './alerts_flyout_header';
import AlertsFlyoutBody from './alerts_flyout_body';
import AlertsFlyoutFooter from './alerts_flyout_footer';
type AlertsFlyoutProps = {
alert?: TopAlert;
@ -57,11 +31,6 @@ export function AlertsFlyout({
onClose,
selectedAlertId,
}: AlertsFlyoutProps) {
const dateFormat = useUiSetting<string>('dateFormat');
const { services } = useKibana();
const { http } = services;
const prepend = http?.basePath.prepend;
const decoratedAlerts = useMemo(() => {
const parseObservabilityAlert = parseAlert(observabilityRuleTypeRegistry);
return (alerts ?? []).map(parseObservabilityAlert);
@ -69,107 +38,19 @@ export function AlertsFlyout({
let alertData = alert;
if (!alertData) {
alertData = decoratedAlerts?.find((a) => a.fields[ALERT_UUID] === selectedAlertId);
alertData = decoratedAlerts?.find((a) => a.fields[ALERT_UUID] === selectedAlertId) as TopAlert;
}
if (!alertData) {
return null;
}
const ruleId = alertData.fields['kibana.alert.rule.uuid'] ?? null;
const linkToRule = ruleId && prepend ? prepend(paths.observability.ruleDetails(ruleId)) : null;
const overviewListItems = [
{
title: translations.alertsFlyout.statusLabel,
description: (
<AlertStatusIndicator
alertStatus={alertData.active ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED}
/>
),
},
{
title: translations.alertsFlyout.startedAtLabel,
description: (
<span title={alertData.start.toString()}>{moment(alertData.start).format(dateFormat)}</span>
),
},
{
title: translations.alertsFlyout.lastUpdatedLabel,
description: (
<span title={alertData.lastUpdated.toString()}>
{moment(alertData.lastUpdated).format(dateFormat)}
</span>
),
},
{
title: translations.alertsFlyout.durationLabel,
description: asDuration(alertData.fields[ALERT_DURATION], { extended: true }),
},
{
title: translations.alertsFlyout.expectedValueLabel,
description: alertData.fields[ALERT_EVALUATION_THRESHOLD] ?? '-',
},
{
title: translations.alertsFlyout.actualValueLabel,
description: alertData.fields[ALERT_EVALUATION_VALUE] ?? '-',
},
{
title: translations.alertsFlyout.ruleTypeLabel,
description: alertData.fields[ALERT_RULE_CATEGORY] ?? '-',
},
];
return (
<EuiFlyout onClose={onClose} size="s" data-test-subj="alertsFlyout">
<EuiFlyoutHeader hasBorder>
<EuiSpacer size="s" />
<EuiTitle size="m" data-test-subj="alertsFlyoutTitle">
<h2>{alertData.fields[ALERT_RULE_NAME]}</h2>
</EuiTitle>
<AlertsFlyoutHeader alert={alertData} />
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiTitle size="xs">
<h4>{translations.alertsFlyout.reasonTitle}</h4>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">{alertData.reason}</EuiText>
<EuiSpacer size="s" />
{!!linkToRule && (
<EuiLink href={linkToRule} data-test-subj="viewRuleDetailsFlyout">
{translations.alertsFlyout.viewRulesDetailsLinkText}
</EuiLink>
)}
<EuiHorizontalRule size="full" />
<EuiTitle size="xs">
<h4>{translations.alertsFlyout.documentSummaryTitle}</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescriptionList
compressed={true}
type="responsiveColumn"
listItems={overviewListItems}
titleProps={{
'data-test-subj': 'alertsFlyoutDescriptionListTitle',
}}
descriptionProps={{
'data-test-subj': 'alertsFlyoutDescriptionListDescription',
}}
/>
</EuiFlyoutBody>
{alertData.link && !isInApp && (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
href={prepend && prepend(alertData.link)}
data-test-subj="alertsFlyoutViewInAppButton"
fill
>
{translations.alertsFlyout.viewInAppButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
)}
<AlertsFlyoutBody alert={alertData} />
<AlertsFlyoutFooter alert={alertData} isInApp={isInApp} />
</EuiFlyout>
);
}

View file

@ -13,12 +13,14 @@ import {
EuiLink,
EuiHorizontalRule,
EuiDescriptionList,
EuiFlyoutBody,
} from '@elastic/eui';
import {
ALERT_DURATION,
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_RULE_CATEGORY,
ALERT_RULE_UUID,
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
} from '@kbn/rule-data-utils';
@ -27,22 +29,17 @@ import { useKibana, useUiSetting } from '@kbn/kibana-react-plugin/public';
import { asDuration } from '../../../../../common/utils/formatters';
import { translations, paths } from '../../../../config';
import { AlertStatusIndicator } from '../../../../components/shared/alert_status_indicator';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { parseAlert } from '../parse_alert';
import { FlyoutProps } from './types';
// eslint-disable-next-line import/no-default-export
export default function AlertsFlyoutBody(props: FlyoutProps) {
const { observabilityRuleTypeRegistry } = usePluginContext();
const alert = props.alert.start
? props.alert
: parseAlert(observabilityRuleTypeRegistry)(props.alert as unknown as Record<string, unknown>);
const alert = props.alert;
const { services } = useKibana();
const { http } = services;
const dateFormat = useUiSetting<string>('dateFormat');
const prepend = http?.basePath.prepend;
const ruleId = get(props.alert, 'kibana.alert.rule.uuid') ?? null;
const linkToRule = ruleId && prepend ? prepend(paths.management.ruleDetails(ruleId)) : null;
const ruleId = get(props.alert.fields, ALERT_RULE_UUID) ?? null;
const linkToRule = ruleId && prepend ? prepend(paths.observability.ruleDetails(ruleId)) : null;
const overviewListItems = [
{
title: translations.alertsFlyout.statusLabel,
@ -53,11 +50,19 @@ export default function AlertsFlyoutBody(props: FlyoutProps) {
),
},
{
title: translations.alertsFlyout.lastUpdatedLabel,
title: translations.alertsFlyout.startedAtLabel,
description: (
<span title={alert.start.toString()}>{moment(alert.start).format(dateFormat)}</span>
),
},
{
title: translations.alertsFlyout.lastUpdatedLabel,
description: (
<span title={alert.lastUpdated.toString()}>
{moment(alert.lastUpdated).format(dateFormat)}
</span>
),
},
{
title: translations.alertsFlyout.durationLabel,
description: asDuration(alert.fields[ALERT_DURATION], { extended: true }),
@ -77,7 +82,7 @@ export default function AlertsFlyoutBody(props: FlyoutProps) {
];
return (
<>
<EuiFlyoutBody>
<EuiTitle size="xs">
<h4>{translations.alertsFlyout.reasonTitle}</h4>
</EuiTitle>
@ -105,6 +110,6 @@ export default function AlertsFlyoutBody(props: FlyoutProps) {
'data-test-subj': 'alertsFlyoutDescriptionListDescription',
}}
/>
</>
</EuiFlyoutBody>
);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FlyoutProps } from './types';
import { translations } from '../../../../config';
@ -18,16 +18,18 @@ export default function AlertsFlyoutFooter({ alert, isInApp }: FlyoutProps & { i
if (!alert.link || isInApp) return <></>;
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
href={prepend && prepend(alert.link)}
data-test-subj="alertsFlyoutViewInAppButton"
fill
>
{translations.alertsFlyout.viewInAppButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
href={prepend && prepend(alert.link)}
data-test-subj="alertsFlyoutViewInAppButton"
fill
>
{translations.alertsFlyout.viewInAppButtonText}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);
}

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import { get } from 'lodash';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { FlyoutProps } from './types';
@ -16,7 +15,7 @@ export default function AlertsFlyoutHeader({ alert }: FlyoutProps) {
<>
<EuiSpacer size="s" />
<EuiTitle size="m" data-test-subj="alertsFlyoutTitle">
<h2>{get(alert, ALERT_RULE_NAME)}</h2>
<h2>{alert.fields[ALERT_RULE_NAME]}</h2>
</EuiTitle>
</>
);

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { AlertsFlyout } from './alerts_flyout';

View file

@ -0,0 +1,60 @@
/*
* 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, { useCallback, useMemo } from 'react';
import { AlertsTableFlyoutBaseProps } from '@kbn/triggers-actions-ui-plugin/public';
import { ObservabilityRuleTypeRegistry } from '../../../../rules/create_observability_rule_type_registry';
import { parseAlert } from '../parse_alert';
import AlertsFlyoutHeader from './alerts_flyout_header';
import AlertsFlyoutBody from './alerts_flyout_body';
import AlertsFlyoutFooter from './alerts_flyout_footer';
export { AlertsFlyout } from './alerts_flyout';
export const useToGetInternalFlyout = (
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry
) => {
const body = useCallback(
(props: AlertsTableFlyoutBaseProps) => {
const alert = parseAlert(observabilityRuleTypeRegistry)(
props.alert as unknown as Record<string, unknown>
);
return <AlertsFlyoutBody alert={alert} />;
},
[observabilityRuleTypeRegistry]
);
const header = useCallback(
(props: AlertsTableFlyoutBaseProps) => {
const alert = parseAlert(observabilityRuleTypeRegistry)(
props.alert as unknown as Record<string, unknown>
);
return <AlertsFlyoutHeader alert={alert} />;
},
[observabilityRuleTypeRegistry]
);
const footer = useCallback(
(props: AlertsTableFlyoutBaseProps) => {
const alert = parseAlert(observabilityRuleTypeRegistry)(
props.alert as unknown as Record<string, unknown>
);
return <AlertsFlyoutFooter isInApp={false} alert={alert} />;
},
[observabilityRuleTypeRegistry]
);
return useMemo(
() => ({
body,
header,
footer,
}),
[body, header, footer]
);
};

View file

@ -4,17 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AlertsTableFlyoutBaseProps } from '@kbn/triggers-actions-ui-plugin/public';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields';
import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
export type FlyoutProps = AlertsTableFlyoutBaseProps & {
alert: EcsFieldsResponse & {
fields: ParsedTechnicalFields & ParsedExperimentalFields;
start: number;
reason: string;
link?: string;
active: boolean;
};
};
import { TopAlert } from '../../containers';
export interface FlyoutProps {
alert: TopAlert;
}

View file

@ -8,7 +8,6 @@
import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import type { CellValueElementProps } from '@kbn/timelines-plugin/common';
import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock';
import * as PluginHook from '../../../../hooks/use_plugin_context';
import { render } from '../../../../utils/test_helper';
import { getRenderCellValue } from './render_cell_value';
@ -18,15 +17,10 @@ interface AlertsTableRow {
describe('getRenderCellValue', () => {
const observabilityRuleTypeRegistryMock = createObservabilityRuleTypeRegistryMock();
jest.spyOn(PluginHook, 'usePluginContext').mockImplementation(
() =>
({
observabilityRuleTypeRegistry: observabilityRuleTypeRegistryMock,
} as any)
);
const renderCellValue = getRenderCellValue({
setFlyoutAlert: jest.fn(),
observabilityRuleTypeRegistry: observabilityRuleTypeRegistryMock,
});
describe('when column is alert status', () => {

View file

@ -22,7 +22,7 @@ import { asDuration } from '../../../../../common/utils/formatters';
import { SeverityBadge } from '../severity_badge';
import { TopAlert } from '../..';
import { parseAlert } from '../parse_alert';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { ObservabilityRuleTypeRegistry } from '../../../../rules/create_observability_rule_type_registry';
export const getMappedNonEcsValue = ({
data,
@ -46,12 +46,13 @@ export const getMappedNonEcsValue = ({
export const getRenderCellValue = ({
setFlyoutAlert,
observabilityRuleTypeRegistry,
}: {
setFlyoutAlert: (data: TopAlert) => void;
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
}) => {
return ({ columnId, data }: CellValueElementProps) => {
if (!data) return null;
const { observabilityRuleTypeRegistry } = usePluginContext();
const value = getMappedNonEcsValue({
data,
fieldName: columnId,

View file

@ -325,6 +325,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
timelines,
application: { capabilities },
} = useKibana<ObservabilityAppServices>().services;
const { observabilityRuleTypeRegistry } = usePluginContext();
const [flyoutAlert, setFlyoutAlert] = useState<TopAlert | undefined>(undefined);
const [tGridState, setTGridState] = useState<Partial<TGridModel> | null>(
@ -432,7 +433,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
query: kuery ?? '',
language: 'kuery',
},
renderCellValue: getRenderCellValue({ setFlyoutAlert }),
renderCellValue: getRenderCellValue({ setFlyoutAlert, observabilityRuleTypeRegistry }),
rowRenderers: NO_ROW_RENDER,
// TODO: implement Kibana data view runtime fields in observability
runtimeMappings: {},
@ -471,6 +472,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
hasAlertsCrudPermissions,
indexNames,
itemsPerPage,
observabilityRuleTypeRegistry,
onStateChange,
kuery,
rangeFrom,
@ -480,7 +482,6 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
]);
const handleFlyoutClose = () => setFlyoutAlert(undefined);
const { observabilityRuleTypeRegistry } = usePluginContext();
return (
<>

View file

@ -15,6 +15,7 @@ import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutSize,
EuiButtonIcon,
EuiPanel,
EuiTitle,
@ -36,7 +37,6 @@ import {
RuleType,
getNotifyWhenOptions,
RuleEventLogListProps,
AlertsTableFlyoutState,
} from '@kbn/triggers-actions-ui-plugin/public';
// TODO: use a Delete modal from triggersActionUI when it's sharable
import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
@ -177,7 +177,7 @@ export function RuleDetailsPage() {
alertsTableConfigurationRegistry,
configurationId: observabilityFeatureId,
id: `case-details-alerts-o11y`,
flyoutState: AlertsTableFlyoutState.external,
flyoutSize: 's' as EuiFlyoutSize,
featureIds: [features] as AlertConsumers[],
query: {
bool: {

View file

@ -37,7 +37,10 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { observabilityAppId, observabilityFeatureId, casesPath } from '../common';
import { createLazyObservabilityPageTemplate } from './components/shared';
import { registerDataHandler } from './data_handler';
import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry';
import {
createObservabilityRuleTypeRegistry,
ObservabilityRuleTypeRegistry,
} from './rules/create_observability_rule_type_registry';
import { createCallObservabilityApi } from './services/call_observability_api';
import { createNavigationRegistry, NavigationEntry } from './services/navigation_registry';
import { updateGlobalNavigation } from './update_global_navigation';
@ -83,6 +86,8 @@ export class Plugin
{
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
private readonly navigationRegistry = createNavigationRegistry();
private observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry =
{} as ObservabilityRuleTypeRegistry;
// Define deep links as constant and hidden. Whether they are shown or hidden
// in the global navigation will happen in `updateGlobalNavigation`.
@ -134,7 +139,7 @@ export class Plugin
createCallObservabilityApi(coreSetup.http);
const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistry(
this.observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistry(
pluginsSetup.triggersActionsUi.ruleTypeRegistry
);
@ -158,7 +163,7 @@ export class Plugin
core: coreStart,
plugins: pluginsStart,
appMountParameters: params,
observabilityRuleTypeRegistry,
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
ObservabilityPageTemplate: navigation.PageTemplate,
kibanaFeatures,
usageCollection: pluginsSetup.usageCollection,
@ -257,7 +262,7 @@ export class Plugin
return {
dashboard: { register: registerDataHandler },
observabilityRuleTypeRegistry,
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
navigation: {
registerSections: this.navigationRegistry.registerSections,
},
@ -286,7 +291,7 @@ export class Plugin
const { getO11yAlertsTableConfiguration } = await import(
'./config/register_alerts_table_configuration'
);
return getO11yAlertsTableConfiguration();
return getO11yAlertsTableConfiguration(this.observabilityRuleTypeRegistry);
};
const { alertsTableConfigurationRegistry } = pluginsStart.triggersActionsUi;

View file

@ -8,7 +8,7 @@ import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { AlertsFlyout } from './alerts_flyout';
import { AlertsField, AlertsTableFlyoutState } from '../../../../types';
import { AlertsField } from '../../../../types';
const onClose = jest.fn();
const onPaginate = jest.fn();
@ -33,9 +33,6 @@ const props = {
initialWidth: 250,
},
],
externalFlyout: {
body: () => <h3>External flyout body</h3>,
},
useInternalFlyout: () => ({
body: () => <h3>Internal flyout body</h3>,
header: null,
@ -49,7 +46,6 @@ const props = {
flyoutIndex: 0,
alertsCount: 4,
isLoading: false,
state: AlertsTableFlyoutState.internal,
onClose,
onPaginate,
};
@ -66,123 +62,70 @@ describe('AlertsFlyout', () => {
wrapper.update();
});
expect(wrapper.find('h3').first().text()).toBe('Internal flyout body');
const externalWrapper = mountWithIntl(
<AlertsFlyout
{...{
...props,
state: AlertsTableFlyoutState.external,
}}
/>
);
await act(async () => {
await nextTick();
externalWrapper.update();
});
expect(externalWrapper.find('h3').first().text()).toBe('External flyout body');
});
const configurations = [AlertsTableFlyoutState.external, AlertsTableFlyoutState.internal];
for (const configuration of configurations) {
const base = {
body: () => <h5>Body</h5>,
footer: () => null,
const base = {
body: () => null,
header: () => null,
footer: () => null,
};
it(`should use header from useInternalFlyout configuration`, async () => {
const customProps = {
...props,
alertsTableConfiguration: {
...props.alertsTableConfiguration,
useInternalFlyout: () => ({
...base,
header: () => <h4>Header</h4>,
footer: () => null,
}),
},
};
it(`should use ${configuration} header configuration`, async () => {
const customProps = {
...props,
alertsTableConfiguration: {
...props.alertsTableConfiguration,
...(configuration === AlertsTableFlyoutState.external
? {
[`${configuration}Flyout`]: {
...base,
header: () => <h4>Header</h4>,
footer: () => null,
},
}
: {
useInternalFlyout: () => ({
...base,
header: () => <h4>Header</h4>,
footer: () => null,
}),
}),
},
state: configuration,
};
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('h4').first().text()).toBe('Header');
expect(wrapper.find('h5').first().text()).toBe('Body');
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('h4').first().text()).toBe('Header');
});
it(`should use ${configuration} body configuration`, async () => {
const customProps = {
...props,
alertsTableConfiguration: {
...props.alertsTableConfiguration,
...(configuration === AlertsTableFlyoutState.external
? {
[`${configuration}Flyout`]: {
...base,
},
}
: {
useInternalFlyout: () => ({
...base,
}),
}),
[`${configuration}Flyout`]: {
...base,
},
},
state: configuration,
};
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('h2').first().text()).toBe('one');
expect(wrapper.find('h5').first().text()).toBe('Body');
it(`should use body from useInternalFlyout configuration`, async () => {
const customProps = {
...props,
alertsTableConfiguration: {
...props.alertsTableConfiguration,
useInternalFlyout: () => ({
...base,
body: () => <h5>Body</h5>,
}),
},
};
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('h5').first().text()).toBe('Body');
});
it(`should use ${configuration} body configuration`, async () => {
const customProps = {
...props,
alertsTableConfiguration: {
...props.alertsTableConfiguration,
...(configuration === AlertsTableFlyoutState.external
? {
[`${configuration}Flyout`]: {
...base,
footer: () => <h6>Footer</h6>,
},
}
: {
useInternalFlyout: () => ({
...base,
footer: () => <h6>Footer</h6>,
}),
}),
},
state: configuration,
};
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('h2').first().text()).toBe('one');
expect(wrapper.find('h5').first().text()).toBe('Body');
expect(wrapper.find('h6').first().text()).toBe('Footer');
it(`should use footer from useInternalFlyout configuration`, async () => {
const customProps = {
...props,
alertsTableConfiguration: {
...props.alertsTableConfiguration,
useInternalFlyout: () => ({
...base,
footer: () => <h6>Footer</h6>,
}),
},
};
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
await act(async () => {
await nextTick();
wrapper.update();
});
}
expect(wrapper.find('h6').first().text()).toBe('Footer');
});
it('should allow pagination with next', async () => {
const wrapper = mountWithIntl(<AlertsFlyout {...props} />);

View file

@ -8,21 +8,16 @@ import React, { Suspense, lazy, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiPagination,
EuiProgress,
EuiFlyoutFooter,
EuiFlyoutSize,
} from '@elastic/eui';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import {
AlertsTableConfigurationRegistry,
AlertsTableFlyoutState,
AlertTableFlyoutComponent,
} from '../../../../types';
import { AlertsTableConfigurationRegistry } from '../../../../types';
const AlertsFlyoutHeader = lazy(() => import('./alerts_flyout_header'));
const PAGINATION_LABEL = i18n.translate(
@ -36,9 +31,9 @@ interface AlertsFlyoutProps {
alert: EcsFieldsResponse;
alertsTableConfiguration: AlertsTableConfigurationRegistry;
flyoutIndex: number;
flyoutSize?: EuiFlyoutSize;
alertsCount: number;
isLoading: boolean;
state: AlertsTableFlyoutState;
onClose: () => void;
onPaginate: (pageIndex: number) => void;
}
@ -46,39 +41,22 @@ export const AlertsFlyout: React.FunctionComponent<AlertsFlyoutProps> = ({
alert,
alertsTableConfiguration,
flyoutIndex,
flyoutSize = 'm',
alertsCount,
isLoading,
state,
onClose,
onPaginate,
}: AlertsFlyoutProps) => {
let Header: AlertTableFlyoutComponent;
let Body: AlertTableFlyoutComponent;
let Footer: AlertTableFlyoutComponent;
const {
header: internalHeader,
body: internalBody,
footer: internalFooter,
header: Header,
body: Body,
footer: Footer,
} = alertsTableConfiguration?.useInternalFlyout?.() ?? {
header: null,
header: AlertsFlyoutHeader,
body: null,
footer: null,
};
switch (state) {
case AlertsTableFlyoutState.external:
Header = alertsTableConfiguration?.externalFlyout?.header ?? AlertsFlyoutHeader;
Body = alertsTableConfiguration?.externalFlyout?.body ?? null;
Footer = alertsTableConfiguration?.externalFlyout?.footer ?? null;
break;
case AlertsTableFlyoutState.internal:
Header = internalHeader ?? AlertsFlyoutHeader;
Body = internalBody ?? null;
Footer = internalFooter ?? null;
break;
}
const passedProps = useMemo(
() => ({
alert,
@ -107,42 +85,22 @@ export const AlertsFlyout: React.FunctionComponent<AlertsFlyoutProps> = ({
[Footer, passedProps]
);
const FlyoutBodyMemo = useMemo(() => {
if (FlyoutBody) {
if (state === AlertsTableFlyoutState.external) {
return (
<EuiFlyoutBody>
<FlyoutBody />
</EuiFlyoutBody>
);
}
return <FlyoutBody />;
}
}, [FlyoutBody, state]);
const FlyoutFooterMemo = useMemo(() => {
if (FlyoutFooter) {
if (state === AlertsTableFlyoutState.external) {
return (
<EuiFlyoutFooter>
<FlyoutFooter />
</EuiFlyoutFooter>
);
}
return <FlyoutFooter />;
}
}, [FlyoutFooter, state]);
const FlyoutHeader = useCallback(
() =>
Header ? (
<Suspense fallback={null}>
<Header {...passedProps} />
</Suspense>
) : null,
[Header, passedProps]
);
return (
<EuiFlyout
onClose={onClose}
size={state === AlertsTableFlyoutState.external ? 's' : 'm'}
data-test-subj="alertsFlyout"
>
<EuiFlyout onClose={onClose} size={flyoutSize} data-test-subj="alertsFlyout">
{isLoading && <EuiProgress size="xs" color="accent" data-test-subj="alertsFlyoutLoading" />}
<EuiFlyoutHeader hasBorder>
<Suspense fallback={null}>
<Header {...passedProps} />
<FlyoutHeader />
</Suspense>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
@ -158,8 +116,8 @@ export const AlertsFlyout: React.FunctionComponent<AlertsFlyoutProps> = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
{FlyoutBodyMemo}
{FlyoutFooterMemo}
<FlyoutBody />
<FlyoutFooter />
</EuiFlyout>
);
};

View file

@ -10,7 +10,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils';
import { getAlertsTableStateLazy } from '../../../../common/get_alerts_table_state';
import { PLUGIN_ID } from '../../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
import { AlertsTableConfigurationRegistry, AlertsTableFlyoutState } from '../../../../types';
import { AlertsTableConfigurationRegistry } from '../../../../types';
import { TypeRegistry } from '../../../type_registry';
const consumers = [
@ -28,7 +28,6 @@ const AlertsPage: React.FunctionComponent = () => {
alertsTableConfigurationRegistry as TypeRegistry<AlertsTableConfigurationRegistry>,
configurationId: PLUGIN_ID,
id: `internal-alerts-page`,
flyoutState: AlertsTableFlyoutState.internal,
featureIds: consumers,
query: { bool: { must: [] } },
showExpandToDetails: true,

View file

@ -48,10 +48,6 @@ export function registerAlertsTableConfiguration({
},
],
useInternalFlyout,
externalFlyout: {
header: AlertsPageFlyoutHeader,
body: AlertsPageFlyoutBody,
},
getRenderCellValue: () => (props) => {
const myProps = props as any;
const value = myProps.data.find((d: any) => d.field === myProps.columnId)?.value ?? [];

View file

@ -11,7 +11,7 @@ import userEvent from '@testing-library/user-event';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { AlertsTable } from './alerts_table';
import { AlertsField, AlertsTableFlyoutState } from '../../../types';
import { AlertsField } from '../../../types';
jest.mock('@kbn/data-plugin/public');
@ -60,16 +60,11 @@ describe('AlertsTable', () => {
id: '',
columns,
sort: [],
externalFlyout: {
useInternalFlyout: jest.fn().mockImplementation(() => ({
header: jest.fn(),
body: jest.fn(),
footer: jest.fn(),
},
internalFlyout: {
header: jest.fn(),
body: jest.fn(),
footer: jest.fn(),
},
})),
getRenderCellValue: () =>
jest.fn().mockImplementation((props) => {
return `${props.colIndex}:${props.rowIndex}`;
@ -89,7 +84,6 @@ describe('AlertsTable', () => {
showExpandToDetails: true,
trailingControlColumns: [],
alerts,
flyoutState: AlertsTableFlyoutState.internal,
useFetchAlertsData,
visibleColumns: columns.map((c) => c.id),
'data-test-subj': 'testTable',

View file

@ -181,7 +181,6 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
<AlertsFlyout
alert={alerts[flyoutAlertIndex]}
alertsCount={alertsCount}
state={props.flyoutState}
onClose={handleFlyoutClose}
alertsTableConfiguration={props.alertsTableConfiguration}
flyoutIndex={flyoutAlertIndex + pagination.pageIndex * pagination.pageSize}

View file

@ -16,7 +16,6 @@ import {
AlertsField,
AlertsTableConfigurationRegistry,
AlertsTableFlyoutBaseProps,
AlertsTableFlyoutState,
} from '../../../types';
import { PLUGIN_ID } from '../../../common/constants';
import { TypeRegistry } from '../../type_registry';
@ -110,7 +109,6 @@ describe('AlertsTableState', () => {
configurationId: PLUGIN_ID,
id: `test-alerts`,
featureIds: [AlertConsumers.LOGS],
flyoutState: AlertsTableFlyoutState.internal,
query: {},
showExpandToDetails: true,
};

View file

@ -6,7 +6,13 @@
*/
import React, { useState, useCallback, useRef, useMemo } from 'react';
import { isEmpty } from 'lodash';
import { EuiDataGridColumn, EuiProgress, EuiDataGridSorting, EuiEmptyPrompt } from '@elastic/eui';
import {
EuiDataGridColumn,
EuiProgress,
EuiDataGridSorting,
EuiEmptyPrompt,
EuiFlyoutSize,
} from '@elastic/eui';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import type { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
import { Storage } from '@kbn/kibana-utils-plugin/public';
@ -16,7 +22,7 @@ import type {
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useFetchAlerts } from './hooks/use_fetch_alerts';
import { AlertsTable } from './alerts_table';
import { AlertsTableConfigurationRegistry, AlertsTableFlyoutState } from '../../../types';
import { AlertsTableConfigurationRegistry } from '../../../types';
import { ALERTS_TABLE_CONF_ERROR_MESSAGE, ALERTS_TABLE_CONF_ERROR_TITLE } from './translations';
import { TypeRegistry } from '../../type_registry';
@ -30,7 +36,7 @@ export interface AlertsTableStateProps {
configurationId: string;
id: string;
featureIds: ValidFeatureId[];
flyoutState: AlertsTableFlyoutState;
flyoutSize?: EuiFlyoutSize;
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
pageSize?: number;
showExpandToDetails: boolean;
@ -64,7 +70,7 @@ const AlertsTableState = ({
configurationId,
id,
featureIds,
flyoutState,
flyoutSize,
query,
pageSize,
showExpandToDetails,
@ -191,7 +197,7 @@ const AlertsTableState = ({
bulkActions: [],
deletedEventIds: [],
disabledCellActions: [],
flyoutState,
flyoutSize,
pageSize: pagination.pageSize,
pageSizeOptions: [10, 20, 50, 100],
leadingControlColumns: [],
@ -205,7 +211,7 @@ const AlertsTableState = ({
[
alertsTableConfiguration,
columns,
flyoutState,
flyoutSize,
pagination.pageSize,
showCheckboxes,
showExpandToDetails,

View file

@ -41,8 +41,6 @@ export type {
GetRenderCellValue,
} from './types';
export { AlertsTableFlyoutState } from './types';
export {
ActionForm,
CreateConnectorFlyout,

View file

@ -13,7 +13,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { IconType } from '@elastic/eui';
import type { IconType, EuiFlyoutSize } from '@elastic/eui';
import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui';
import {
ActionType,
@ -390,7 +390,7 @@ export interface AlertsTableProps {
// defaultCellActions: TGridCellAction[];
deletedEventIds: string[];
disabledCellActions: string[];
flyoutState: AlertsTableFlyoutState;
flyoutSize?: EuiFlyoutSize;
pageSize: number;
pageSizeOptions: number[];
leadingControlColumns: EuiDataGridControlColumn[];
@ -416,15 +416,10 @@ export type AlertTableFlyoutComponent =
export interface AlertsTableConfigurationRegistry {
id: string;
columns: EuiDataGridColumn[];
externalFlyout?: {
header?: AlertTableFlyoutComponent;
body?: AlertTableFlyoutComponent;
footer?: AlertTableFlyoutComponent;
};
useInternalFlyout?: () => {
header?: AlertTableFlyoutComponent;
body?: AlertTableFlyoutComponent;
footer?: AlertTableFlyoutComponent;
header: AlertTableFlyoutComponent;
body: AlertTableFlyoutComponent;
footer: AlertTableFlyoutComponent;
};
sort?: SortCombinations[];
getRenderCellValue?: GetRenderCellValue;
@ -435,11 +430,6 @@ export interface AlertsTableFlyoutBaseProps {
isLoading: boolean;
}
export enum AlertsTableFlyoutState {
internal = 'internal',
external = 'external',
}
export type RuleStatus = 'enabled' | 'disabled' | 'snoozed';
export enum RRuleFrequency {