[RAM] Stack management/o11y rule details parity (#136778)

* stack management/o11y rule details parity

* Hide edit button in stack management

* Add tests

* Move fetching summary out of o11y

* Undo changes to hooks in o11y

* Fix test and add new tests

* Remove customLoadExecutionLog prop

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2022-07-26 04:14:05 -07:00 committed by GitHub
parent 0645a3ba38
commit cd3d2d79c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 845 additions and 249 deletions

View file

@ -12,11 +12,62 @@ interface SandboxProps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
function mockRuleType() {
return {
id: 'test.testRuleType',
name: 'My Test Rule Type',
actionGroups: [{ id: 'default', name: 'Default Action Group' }],
actionVariables: {
context: [],
state: [],
params: [],
},
defaultActionGroupId: 'default',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
authorizedConsumers: {},
producer: 'rules',
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
}
function mockRuleSummary() {
return {
id: 'rule-id',
name: 'rule-name',
tags: ['tag-1', 'tag-2'],
ruleTypeId: 'test',
consumer: 'rule-consumer',
status: 'OK',
muteAll: false,
throttle: '',
enabled: true,
errorMessages: [],
statusStartDate: '2022-03-21T07:40:46-07:00',
statusEndDate: '2022-03-25T07:40:46-07:00',
alerts: {
foo: {
status: 'OK',
muted: false,
actionGroupId: 'testActionGroup',
},
},
executionDuration: {
average: 100,
valuesWithTimestamp: {},
},
};
}
export const RuleEventLogListSandbox = ({ triggersActionsUi }: SandboxProps) => {
const componenProps: any = {
rule: {
id: 'test',
},
ruleType: mockRuleType(),
ruleSummary: mockRuleSummary(),
numberOfExecutions: 60,
onChangeDuration: (duration: number) => {},
customLoadExecutionLogAggregations: async () => ({
total: 1,
data: [

View file

@ -185,8 +185,9 @@ export function RuleDetailsPage() {
defaultMessage: 'Execution history',
}),
'data-test-subj': 'eventLogListTab',
content: getRuleEventLogList({
content: getRuleEventLogList<'default'>({
rule,
ruleType,
} as RuleEventLogListProps),
},
{

View file

@ -79,6 +79,7 @@ export const ExecutionDurationChart: React.FunctionComponent<ComponentOpts> = ({
<EuiFlexItem grow={false}>
<EuiSelect
id="select-number-execution-durations"
data-test-subj="executionDurationChartPanelSelect"
options={NUM_EXECUTIONS_OPTIONS}
value={numberOfExecutions}
aria-label={i18n.translate(

View file

@ -12,9 +12,10 @@ import { act } from 'react-dom/test-utils';
import { RuleComponent, alertToListItem } from './rule';
import { AlertListItem } from './types';
import { RuleAlertList } from './rule_alert_list';
import { RuleSummary, AlertStatus, RuleType } from '../../../../types';
import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
import { RuleSummary, AlertStatus, RuleType, RuleTypeModel } from '../../../../types';
import { mockRule } from './test_helpers';
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
import { useKibana } from '../../../../common/lib/kibana';
jest.mock('../../../../common/lib/kibana');
@ -22,6 +23,21 @@ jest.mock('../../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled: jest.fn(),
}));
const ruleTypeR: RuleTypeModel = {
id: 'my-rule-type',
iconClass: 'test',
description: 'Rule when testing',
documentationUrl: 'https://localhost.local/docs',
validate: () => {
return { errors: {} };
},
ruleParamsExpression: jest.fn(),
requiresAppContext: false,
};
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const ruleTypeRegistry = ruleTypeRegistryMock.create();
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
const fakeNow = new Date('2020-02-09T23:15:41.941Z');
@ -37,6 +53,8 @@ const mockAPIs = {
beforeAll(() => {
jest.clearAllMocks();
ruleTypeRegistry.get.mockReturnValue(ruleTypeR);
useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry;
global.Date.now = jest.fn(() => fakeNow.getTime());
});
@ -316,91 +334,6 @@ describe('execution duration overview', () => {
expect(ruleExecutionStatusStat.first().prop('description')).toEqual('Last response');
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').text()).toEqual('Ok');
});
it('renders average execution duration', async () => {
const rule = mockRule();
const ruleType = mockRuleType({ ruleTaskTimeout: '10m' });
const ruleSummary = mockRuleSummary({
executionDuration: { average: 60284, valuesWithTimestamp: {} },
});
const wrapper = mountWithIntl(
<RuleComponent
{...mockAPIs}
rule={rule}
ruleType={ruleType}
readOnly={false}
ruleSummary={ruleSummary}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:01:00.284'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
});
it('renders warning when average execution duration exceeds rule timeout', async () => {
const rule = mockRule();
const ruleType = mockRuleType({ ruleTaskTimeout: '10m' });
const ruleSummary = mockRuleSummary({
executionDuration: { average: 60284345, valuesWithTimestamp: {} },
});
const wrapper = mountWithIntl(
<RuleComponent
{...mockAPIs}
rule={rule}
ruleType={ruleType}
readOnly={false}
ruleSummary={ruleSummary}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning');
const avgExecutionDurationStat = wrapper
.find('EuiStat[data-test-subj="avgExecutionDurationStat"]')
.text()
.replaceAll('Info', '');
expect(avgExecutionDurationStat).toEqual('Average duration16:44:44.345');
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy();
});
it('renders execution duration chart', () => {
const rule = mockRule();
const ruleType = mockRuleType();
const ruleSummary = mockRuleSummary();
expect(
shallow(
<RuleComponent
{...mockAPIs}
rule={rule}
ruleType={ruleType}
ruleSummary={ruleSummary}
readOnly={false}
/>
)
.find(ExecutionDurationChart)
.exists()
).toBeTruthy();
});
});
describe('disable/enable functionality', () => {
@ -486,8 +419,8 @@ describe('tabbed content', () => {
tabbedContent.update();
});
expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeFalsy();
expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeTruthy();
expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeTruthy();
expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeFalsy();
tabbedContent.find('[data-test-subj="eventLogListTab"]').simulate('click');

View file

@ -7,20 +7,13 @@
import React, { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiStat,
EuiIconTip,
EuiTabbedContent,
} from '@elastic/eui';
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui';
import {
ActionGroup,
RuleExecutionStatusErrorReasons,
AlertStatusValues,
} from '@kbn/alerting-plugin/common';
import { useKibana } from '../../../../common/lib/kibana';
import { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types';
import {
ComponentOpts as RuleApis,
@ -32,12 +25,7 @@ import {
rulesStatusesTranslationsMapping,
ALERT_STATUS_LICENSE_ERROR,
} from '../../rules_list/translations';
import {
formatMillisForDisplay,
shouldShowDurationWarning,
} from '../../../lib/execution_duration_utils';
import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
// import { RuleEventLogListWithApi } from './rule_event_log_list';
import type { RuleEventLogListProps } from './rule_event_log_list';
import { AlertListItem } from './types';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
@ -45,6 +33,7 @@ import RuleStatusPanelWithApi from './rule_status_panel';
const RuleEventLogListWithApi = lazy(() => import('./rule_event_log_list'));
const RuleAlertList = lazy(() => import('./rule_alert_list'));
const RuleDefinition = lazy(() => import('./rule_definition'));
type RuleProps = {
rule: Rule;
@ -76,6 +65,8 @@ export function RuleComponent({
durationEpoch = Date.now(),
isLoadingChart,
}: RuleProps) {
const { ruleTypeRegistry, actionTypeRegistry } = useKibana().services;
const alerts = Object.entries(ruleSummary.alerts)
.map(([alertId, alert]) => alertToListItem(durationEpoch, ruleType, alertId, alert))
.sort((leftAlert, rightAlert) => leftAlert.sortPriority - rightAlert.sortPriority);
@ -87,11 +78,6 @@ export function RuleComponent({
requestRefresh();
};
const showDurationWarning = shouldShowDurationWarning(
ruleType,
ruleSummary.executionDuration.average
);
const healthColor = getHealthColor(rule.executionStatus.status);
const isLicenseError =
rule.executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License;
@ -111,6 +97,27 @@ export function RuleComponent({
};
const tabs = [
{
id: EVENT_LOG_LIST_TAB,
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', {
defaultMessage: 'History',
}),
'data-test-subj': 'eventLogListTab',
content: suspendedComponentWithProps<RuleEventLogListProps<'stackManagement'>>(
RuleEventLogListWithApi,
'xl'
)({
fetchRuleSummary: false,
rule,
ruleType,
ruleSummary,
numberOfExecutions,
refreshToken,
isLoadingRuleSummary: isLoadingChart,
onChangeDuration,
requestRefresh,
}),
},
{
id: ALERT_LIST_TAB,
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText', {
@ -119,17 +126,6 @@ export function RuleComponent({
'data-test-subj': 'ruleAlertListTab',
content: renderRuleAlertList(),
},
{
id: EVENT_LOG_LIST_TAB,
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', {
defaultMessage: 'History',
}),
'data-test-subj': 'eventLogListTab',
content: suspendedComponentWithProps(
RuleEventLogListWithApi,
'xl'
)({ requestRefresh, rule, refreshToken }),
},
];
const renderTabs = () => {
@ -142,8 +138,8 @@ export function RuleComponent({
return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFlexGroup gutterSize="s" wrap>
<EuiFlexItem grow={2}>
<RuleStatusPanelWithApi
rule={rule}
isEditable={!readOnly}
@ -152,56 +148,16 @@ export function RuleComponent({
requestRefresh={requestRefresh}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiPanel
data-test-subj="avgExecutionDurationPanel"
color={showDurationWarning ? 'warning' : 'subdued'}
hasBorder={false}
>
<EuiStat
data-test-subj="avgExecutionDurationStat"
titleSize="xs"
title={
<EuiFlexGroup gutterSize="xs">
{showDurationWarning && (
<EuiFlexItem grow={false}>
<EuiIconTip
data-test-subj="ruleDurationWarning"
anchorClassName="ruleDurationWarningIcon"
type="alert"
color="warning"
content={i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage',
{
defaultMessage: `Duration exceeds the rule's expected run time.`,
}
)}
position="top"
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
{formatMillisForDisplay(ruleSummary.executionDuration.average)}
</EuiFlexItem>
</EuiFlexGroup>
}
description={i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.alertsList.avgDurationDescription',
{
defaultMessage: `Average duration`,
}
)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<ExecutionDurationChart
executionDuration={ruleSummary.executionDuration}
numberOfExecutions={numberOfExecutions}
onChangeDuration={onChangeDuration}
isLoading={isLoadingChart}
/>
</EuiFlexItem>
{suspendedComponentWithProps(
RuleDefinition,
'xl'
)({
rule,
actionTypeRegistry,
ruleTypeRegistry,
hideEditButton: true,
onEditRule: requestRefresh,
})}
</EuiFlexGroup>
<EuiSpacer size="xl" />

View file

@ -29,6 +29,7 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
actionTypeRegistry,
ruleTypeRegistry,
onEditRule,
hideEditButton = false,
filteredRuleTypes,
}) => {
const {
@ -68,13 +69,19 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
hasAllPrivilege(rule, ruleType) &&
// if the rule has actions, can the user save the rule's action params
(canExecuteActions || (!canExecuteActions && rule.actions.length === 0));
const hasEditButton =
const hasEditButton = useMemo(() => {
if (hideEditButton) {
return false;
}
// can the user save the rule
canSaveRule &&
// is this rule type editable from within Rules Management
(ruleTypeRegistry.has(rule.ruleTypeId)
? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext
: false);
return (
canSaveRule &&
// is this rule type editable from within Rules Management
(ruleTypeRegistry.has(rule.ruleTypeId)
? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext
: false)
);
}, [hideEditButton, canSaveRule, ruleTypeRegistry, rule]);
return (
<EuiFlexItem data-test-subj="ruleSummaryRuleDefinition" grow={3}>
<EuiPanel color="subdued" hasBorder={false} paddingSize={'m'}>

View file

@ -10,13 +10,14 @@ import uuid from 'uuid';
import { act } from 'react-dom/test-utils';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { useKibana } from '../../../../common/lib/kibana';
import { ActionGroup, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { EuiSuperDatePicker, EuiDataGrid } from '@elastic/eui';
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
import { RuleEventLogList } from './rule_event_log_list';
import { RefineSearchPrompt } from '../refine_search_prompt';
import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS } from '../../../constants';
import { Rule } from '../../../../types';
import { mockRule, mockRuleType, mockRuleSummary } from './test_helpers';
import { RuleType } from '../../../../types';
import { loadActionErrorLog } from '../../../lib/rule_api/load_action_error_log';
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
@ -107,31 +108,24 @@ const mockLogResponse: any = {
total: 4,
};
const mockRule: Rule = {
id: uuid.v4(),
enabled: true,
name: `rule-${uuid.v4()}`,
tags: [],
ruleTypeId: '.noop',
consumer: 'consumer',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: null,
notifyWhen: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
const loadExecutionLogAggregationsMock = jest.fn();
const onChangeDurationMock = jest.fn();
const ruleMock = mockRule();
const authorizedConsumers = {
[ALERTS_FEATURE_ID]: { read: true, all: true },
};
const recoveryActionGroup: ActionGroup<'recovered'> = { id: 'recovered', name: 'Recovered' };
const ruleType: RuleType = mockRuleType({
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
recoveryActionGroup,
});
const mockErrorLogResponse = {
totalErrors: 1,
errors: [
@ -145,8 +139,6 @@ const mockErrorLogResponse = {
],
};
const loadExecutionLogAggregationsMock = jest.fn();
describe('rule_event_log_list', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -168,7 +160,11 @@ describe('rule_event_log_list', () => {
it('renders correctly', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -178,7 +174,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
@ -210,7 +206,11 @@ describe('rule_event_log_list', () => {
it('can sort by single and/or multiple column(s)', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -238,7 +238,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
message: '',
outcomeFilter: [],
page: 0,
@ -262,7 +262,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [{ timestamp: { order: 'desc' } }],
outcomeFilter: [],
page: 0,
@ -292,7 +292,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [
{
timestamp: { order: 'desc' },
@ -311,7 +311,11 @@ describe('rule_event_log_list', () => {
it('can filter by execution log outcome status', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -333,7 +337,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: ['success'],
page: 0,
@ -353,7 +357,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: ['success', 'failure'],
page: 0,
@ -370,7 +374,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -392,7 +400,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 1,
@ -412,7 +420,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
@ -426,7 +434,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -438,7 +450,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
@ -463,7 +475,7 @@ describe('rule_event_log_list', () => {
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: mockRule.id,
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
@ -479,7 +491,11 @@ describe('rule_event_log_list', () => {
it('can save display columns to localStorage', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -518,7 +534,11 @@ describe('rule_event_log_list', () => {
it('does not show the refine search prompt normally', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -539,7 +559,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -585,7 +609,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -608,7 +636,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -631,7 +663,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -695,7 +731,11 @@ describe('rule_event_log_list', () => {
const wrapper = mountWithIntl(
<RuleEventLogList
rule={mockRule}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
@ -714,4 +754,116 @@ describe('rule_event_log_list', () => {
.simulate('click');
expect(wrapper.find('[data-test-subj="ruleActionErrorLogFlyout"]').exists()).toBeTruthy();
});
it('shows rule summary and execution duration chart', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 85,
});
const wrapper = mountWithIntl(
<RuleEventLogList
fetchRuleSummary={false}
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:00:00.100'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="executionDurationChartPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="avgExecutionDurationPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="ruleEventLogListAvgDuration"]').first().text()).toEqual(
'00:00:00.100'
);
});
it('renders average execution duration', async () => {
const ruleTypeCustom = mockRuleType({ ruleTaskTimeout: '10m' });
const ruleSummary = mockRuleSummary({
executionDuration: { average: 60284, valuesWithTimestamp: {} },
ruleTypeId: ruleMock.ruleTypeId,
});
const wrapper = mountWithIntl(
<RuleEventLogList
fetchRuleSummary={false}
rule={ruleMock}
ruleType={ruleTypeCustom}
ruleSummary={ruleSummary}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:01:00.284'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
});
it('renders warning when average execution duration exceeds rule timeout', async () => {
const ruleTypeCustom = mockRuleType({ ruleTaskTimeout: '10m' });
const ruleSummary = mockRuleSummary({
executionDuration: { average: 60284345, valuesWithTimestamp: {} },
ruleTypeId: ruleMock.ruleTypeId,
});
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 85,
});
const wrapper = mountWithIntl(
<RuleEventLogList
fetchRuleSummary={false}
rule={ruleMock}
ruleType={ruleTypeCustom}
ruleSummary={ruleSummary}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning');
const avgExecutionDurationStat = wrapper
.find('EuiStat[data-test-subj="avgExecutionDurationStat"]')
.text()
.replaceAll('Info', '');
expect(avgExecutionDurationStat).toEqual('Average duration16:44:44.345');
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy();
});
});

View file

@ -25,11 +25,12 @@ import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS, LOCKED_COLUMNS } from '
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
import { RuleEventLogDataGrid } from './rule_event_log_data_grid';
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
import { RuleExecutionSummaryAndChartWithApi } from './rule_execution_summary_and_chart';
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
import { RefineSearchPrompt } from '../refine_search_prompt';
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
import { Rule } from '../../../../types';
import { Rule, RuleSummary, RuleType } from '../../../../types';
import {
ComponentOpts as RuleApis,
withBulkRuleOperations,
@ -72,19 +73,51 @@ const MAX_RESULTS = 1000;
const ruleEventListContainerStyle = { minHeight: 400 };
export type RuleEventLogListProps = {
export type RuleEventLogListOptions = 'stackManagement' | 'default';
export interface RuleEventLogListCommonProps {
rule: Rule;
ruleType: RuleType;
localStorageKey?: string;
refreshToken?: number;
requestRefresh?: () => Promise<void>;
customLoadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
} & Pick<RuleApis, 'loadExecutionLogAggregations'>;
loadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
fetchRuleSummary?: boolean;
}
export const RuleEventLogList = (props: RuleEventLogListProps) => {
const { rule, localStorageKey = RULE_EVENT_LOG_LIST_STORAGE_KEY, refreshToken } = props;
export interface RuleEventLogListStackManagementProps {
ruleSummary: RuleSummary;
onChangeDuration: (duration: number) => void;
numberOfExecutions: number;
isLoadingRuleSummary?: boolean;
}
const loadExecutionLogAggregations =
props.customLoadExecutionLogAggregations || props.loadExecutionLogAggregations;
export type RuleEventLogListProps<T extends RuleEventLogListOptions = 'default'> =
T extends 'default'
? RuleEventLogListCommonProps
: T extends 'stackManagement'
? RuleEventLogListStackManagementProps & RuleEventLogListCommonProps
: never;
export const RuleEventLogList = <T extends RuleEventLogListOptions>(
props: RuleEventLogListProps<T>
) => {
const {
rule,
ruleType,
localStorageKey = RULE_EVENT_LOG_LIST_STORAGE_KEY,
refreshToken,
requestRefresh,
fetchRuleSummary = true,
loadExecutionLogAggregations,
} = props;
const {
ruleSummary,
numberOfExecutions,
onChangeDuration,
isLoadingRuleSummary = false,
} = props as RuleEventLogListStackManagementProps;
const { uiSettings, notifications } = useKibana().services;
@ -144,6 +177,9 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
}, [sortingColumns]);
const loadEventLogs = async () => {
if (!loadExecutionLogAggregations) {
return;
}
setIsLoading(true);
try {
const result = await loadExecutionLogAggregations({
@ -300,8 +336,19 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
}, [localStorageKey, visibleColumns]);
return (
<div style={ruleEventListContainerStyle}>
<div style={ruleEventListContainerStyle} data-test-subj="ruleEventLogListContainer">
<EuiSpacer />
<RuleExecutionSummaryAndChartWithApi
rule={rule}
ruleType={ruleType}
ruleSummary={ruleSummary}
numberOfExecutions={numberOfExecutions}
isLoadingRuleSummary={isLoadingRuleSummary}
refreshToken={refreshToken}
onChangeDuration={onChangeDuration}
requestRefresh={requestRefresh}
fetchRuleSummary={fetchRuleSummary}
/>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiFieldSearch

View file

@ -0,0 +1,171 @@
/*
* 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 { act } from 'react-dom/test-utils';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { ActionGroup, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { RuleExecutionSummaryAndChart } from './rule_execution_summary_and_chart';
import { useKibana } from '../../../../common/lib/kibana';
import { mockRule, mockRuleType, mockRuleSummary } from './test_helpers';
import { RuleType } from '../../../../types';
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('../../../../common/lib/kibana');
const loadRuleSummaryMock = jest.fn();
const onChangeDurationMock = jest.fn();
const ruleMock = mockRule();
const authorizedConsumers = {
[ALERTS_FEATURE_ID]: { read: true, all: true },
};
const recoveryActionGroup: ActionGroup<'recovered'> = { id: 'recovered', name: 'Recovered' };
const ruleType: RuleType = mockRuleType({
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
recoveryActionGroup,
});
describe('rule_execution_summary_and_chart', () => {
beforeEach(() => {
jest.clearAllMocks();
loadRuleSummaryMock.mockResolvedValue(mockRuleSummary());
});
it('becomes a stateless component when "fetchRuleSummary" is false', async () => {
const wrapper = mountWithIntl(
<RuleExecutionSummaryAndChart
rule={ruleMock}
ruleType={ruleType}
ruleSummary={mockRuleSummary()}
numberOfExecutions={60}
isLoadingRuleSummary={false}
onChangeDuration={onChangeDurationMock}
fetchRuleSummary={false}
loadRuleSummary={loadRuleSummaryMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
// Does not fetch for the rule summary by itself
expect(loadRuleSummaryMock).toHaveBeenCalledTimes(0);
(
wrapper
.find('[data-test-subj="executionDurationChartPanelSelect"]')
.first()
.prop('onChange') as any
)({
target: {
value: 30,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
// Calls the handler passed in via props
expect(onChangeDurationMock).toHaveBeenCalledWith(30);
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:00:00.100'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="executionDurationChartPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="avgExecutionDurationPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="ruleEventLogListAvgDuration"]').first().text()).toEqual(
'00:00:00.100'
);
});
it('becomes a container component when "fetchRuleSummary" is true', async () => {
const wrapper = mountWithIntl(
<RuleExecutionSummaryAndChart
rule={ruleMock}
ruleType={ruleType}
fetchRuleSummary={true}
loadRuleSummary={loadRuleSummaryMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
// Does not fetch for the rule summary by itself
expect(loadRuleSummaryMock).toHaveBeenCalledTimes(1);
(
wrapper
.find('[data-test-subj="executionDurationChartPanelSelect"]')
.first()
.prop('onChange') as any
)({
target: {
value: 30,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
// Calls the handler passed in via props
expect(onChangeDurationMock).toHaveBeenCalledTimes(0);
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:00:00.100'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="executionDurationChartPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="avgExecutionDurationPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="ruleEventLogListAvgDuration"]').first().text()).toEqual(
'00:00:00.100'
);
});
it('should show error if loadRuleSummary fails', async () => {
loadRuleSummaryMock.mockRejectedValue('error!');
const wrapper = mountWithIntl(
<RuleExecutionSummaryAndChart
rule={ruleMock}
ruleType={ruleType}
fetchRuleSummary={true}
loadRuleSummary={loadRuleSummaryMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,206 @@
/*
* 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, { useMemo, useState, useCallback, useEffect, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiPanel, EuiStat, EuiFlexItem, EuiFlexGroup, EuiIconTip } from '@elastic/eui';
import { Rule, RuleSummary, RuleType } from '../../../../types';
import { useKibana } from '../../../../common/lib/kibana';
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
import {
formatMillisForDisplay,
shouldShowDurationWarning,
} from '../../../lib/execution_duration_utils';
import {
ComponentOpts as RuleApis,
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
export const DEFAULT_NUMBER_OF_EXECUTIONS = 60;
type RuleExecutionSummaryAndChartProps = {
rule: Rule;
ruleType: RuleType;
ruleSummary?: RuleSummary;
numberOfExecutions?: number;
isLoadingRuleSummary?: boolean;
refreshToken?: number;
onChangeDuration?: (duration: number) => void;
requestRefresh?: () => Promise<void>;
fetchRuleSummary?: boolean;
} & Pick<RuleApis, 'loadRuleSummary'>;
export const RuleExecutionSummaryAndChart = (props: RuleExecutionSummaryAndChartProps) => {
const {
rule,
ruleType,
ruleSummary,
refreshToken,
fetchRuleSummary = false,
numberOfExecutions = DEFAULT_NUMBER_OF_EXECUTIONS,
onChangeDuration,
loadRuleSummary,
isLoadingRuleSummary = false,
} = props;
const {
notifications: { toasts },
} = useKibana().services;
const isInitialized = useRef(false);
const [internalRuleSummary, setInternalRuleSummary] = useState<RuleSummary | null>(null);
const [internalNumberOfExecutions, setInternalNumberOfExecutions] = useState(
DEFAULT_NUMBER_OF_EXECUTIONS
);
const [internalIsLoadingRuleSummary, setInternalIsLoadingRuleSummary] = useState(false);
// Computed values for the separate "mode" where this compute fetches the rule summary by itself
const computedRuleSummary = useMemo(() => {
if (fetchRuleSummary) {
return internalRuleSummary;
}
return ruleSummary;
}, [fetchRuleSummary, ruleSummary, internalRuleSummary]);
const computedNumberOfExecutions = useMemo(() => {
if (fetchRuleSummary) {
return internalNumberOfExecutions;
}
return numberOfExecutions;
}, [fetchRuleSummary, numberOfExecutions, internalNumberOfExecutions]);
const computedIsLoadingRuleSummary = useMemo(() => {
if (fetchRuleSummary) {
return internalIsLoadingRuleSummary;
}
return isLoadingRuleSummary;
}, [fetchRuleSummary, isLoadingRuleSummary, internalIsLoadingRuleSummary]);
// Computed duration handlers
const internalOnChangeDuration = useCallback(
(duration: number) => {
setInternalNumberOfExecutions(duration);
},
[setInternalNumberOfExecutions]
);
const computedOnChangeDuration = useMemo(() => {
if (fetchRuleSummary) {
return internalOnChangeDuration;
}
return onChangeDuration || internalOnChangeDuration;
}, [fetchRuleSummary, onChangeDuration, internalOnChangeDuration]);
const getRuleSummary = async () => {
if (!fetchRuleSummary) {
return;
}
setInternalIsLoadingRuleSummary(true);
try {
const loadedSummary = await loadRuleSummary(rule.id, computedNumberOfExecutions);
setInternalRuleSummary(loadedSummary);
} catch (e) {
toasts.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.ruleExecutionSummaryAndChart.loadSummaryError',
{
defaultMessage: 'Unable to load rule summary: {message}',
values: {
message: e.message,
},
}
),
});
}
setInternalIsLoadingRuleSummary(false);
};
useEffect(() => {
getRuleSummary();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rule, computedNumberOfExecutions]);
useEffect(() => {
if (isInitialized.current) {
getRuleSummary();
}
isInitialized.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshToken]);
const showDurationWarning = useMemo(() => {
if (!computedRuleSummary) {
return false;
}
return shouldShowDurationWarning(ruleType, computedRuleSummary.executionDuration.average);
}, [ruleType, computedRuleSummary]);
if (!computedRuleSummary) {
return <CenterJustifiedSpinner />;
}
return (
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiPanel
data-test-subj="avgExecutionDurationPanel"
color={showDurationWarning ? 'warning' : 'subdued'}
hasBorder={false}
>
<EuiStat
data-test-subj="avgExecutionDurationStat"
titleSize="xs"
title={
<EuiFlexGroup gutterSize="xs">
{showDurationWarning && (
<EuiFlexItem grow={false}>
<EuiIconTip
data-test-subj="ruleDurationWarning"
anchorClassName="ruleDurationWarningIcon"
type="alert"
color="warning"
content={i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage',
{
defaultMessage: `Duration exceeds the rule's expected run time.`,
}
)}
position="top"
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false} data-test-subj="ruleEventLogListAvgDuration">
{formatMillisForDisplay(computedRuleSummary.executionDuration.average)}
</EuiFlexItem>
</EuiFlexGroup>
}
description={i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.alertsList.avgDurationDescription',
{
defaultMessage: `Average duration`,
}
)}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<ExecutionDurationChart
executionDuration={computedRuleSummary.executionDuration}
numberOfExecutions={computedNumberOfExecutions}
onChangeDuration={computedOnChangeDuration}
isLoading={computedIsLoadingRuleSummary}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const RuleExecutionSummaryAndChartWithApi = withBulkRuleOperations(
RuleExecutionSummaryAndChart
);

View file

@ -6,7 +6,7 @@
*/
import uuid from 'uuid';
import { Rule } from '../../../../types';
import { Rule, RuleSummary, RuleType } from '../../../../types';
export function mockRule(overloads: Partial<Rule> = {}): Rule {
return {
@ -35,3 +35,52 @@ export function mockRule(overloads: Partial<Rule> = {}): Rule {
...overloads,
};
}
export function mockRuleType(overloads: Partial<RuleType> = {}): RuleType {
return {
id: 'test.testRuleType',
name: 'My Test Rule Type',
actionGroups: [{ id: 'default', name: 'Default Action Group' }],
actionVariables: {
context: [],
state: [],
params: [],
},
defaultActionGroupId: 'default',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
authorizedConsumers: {},
producer: 'rules',
minimumLicenseRequired: 'basic',
enabledInLicense: true,
...overloads,
};
}
export function mockRuleSummary(overloads: Partial<RuleSummary> = {}): RuleSummary {
const summary: RuleSummary = {
id: 'rule-id',
name: 'rule-name',
tags: ['tag-1', 'tag-2'],
ruleTypeId: '123',
consumer: 'rule-consumer',
status: 'OK',
muteAll: false,
throttle: '',
enabled: true,
errorMessages: [],
statusStartDate: '2022-03-21T07:40:46-07:00',
statusEndDate: '2022-03-25T07:40:46-07:00',
alerts: {
foo: {
status: 'OK',
muted: false,
actionGroupId: 'testActionGroup',
},
},
executionDuration: {
average: 100,
valuesWithTimestamp: {},
},
};
return { ...summary, ...overloads };
}

View file

@ -56,6 +56,7 @@ export interface RulesListNotifyBadgeProps {
snoozeRule: (schedule: SnoozeSchedule, muteAll?: boolean) => Promise<void>;
unsnoozeRule: (scheduleIds?: string[]) => Promise<void>;
showTooltipInline?: boolean;
showOnHover?: boolean;
}
const openSnoozePanelAriaLabel = i18n.translate(
@ -93,6 +94,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
onRuleChanged,
snoozeRule,
unsnoozeRule,
showOnHover = false,
showTooltipInline = false,
} = props;
@ -208,6 +210,10 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
}, [formattedSnoozeText, isLoading, isEditable, onClick]);
const unsnoozedButton = useMemo(() => {
// This show on hover is needed because we need style sheets to achieve the
// show on hover effect in the rules list. However we don't want this to be
// a default behaviour of this component.
const showOnHoverClass = showOnHover ? 'ruleSidebarItem__action' : '';
return (
<EuiButtonIcon
size="s"
@ -216,12 +222,12 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
display={isLoading ? 'base' : 'empty'}
data-test-subj="rulesListNotifyBadge-unsnoozed"
aria-label={openSnoozePanelAriaLabel}
className={isOpen || isLoading ? '' : 'ruleSidebarItem__action'}
className={isOpen || isLoading ? '' : showOnHoverClass}
iconType="bell"
onClick={onClick}
/>
);
}, [isOpen, isLoading, isEditable, onClick]);
}, [isOpen, isLoading, isEditable, showOnHover, onClick]);
const indefiniteSnoozeButton = useMemo(() => {
return (

View file

@ -477,6 +477,7 @@ export const RulesListTable = (props: RulesListTableProps) => {
}
return (
<RulesListNotifyBadge
showOnHover
rule={rule}
isLoading={!!isLoadingMap[rule.id]}
onLoading={(newIsLoading) => onLoading(rule.id, newIsLoading)}

View file

@ -7,8 +7,13 @@
import React from 'react';
import { RuleEventLogList } from '../application/sections';
import type { RuleEventLogListProps } from '../application/sections/rule_details/components/rule_event_log_list';
import type {
RuleEventLogListProps,
RuleEventLogListOptions,
} from '../application/sections/rule_details/components/rule_event_log_list';
export const getRuleEventLogListLazy = (props: RuleEventLogListProps) => {
export const getRuleEventLogListLazy = <T extends RuleEventLogListOptions = 'default'>(
props: RuleEventLogListProps<T>
) => {
return <RuleEventLogList {...props} />;
};

View file

@ -24,6 +24,8 @@ import {
FieldBrowserProps,
RuleTagBadgeOptions,
RuleTagBadgeProps,
RuleEventLogListOptions,
RuleEventLogListProps,
} from './types';
import { getAlertsTableLazy } from './common/get_alerts_table';
import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown';
@ -103,8 +105,8 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleTagBadge: <T extends RuleTagBadgeOptions>(props: RuleTagBadgeProps<T>) => {
return getRuleTagBadgeLazy<T>(props);
},
getRuleEventLogList: (props) => {
return getRuleEventLogListLazy(props);
getRuleEventLogList: <T extends RuleEventLogListOptions>(props: RuleEventLogListProps<T>) => {
return getRuleEventLogListLazy<T>(props);
},
getRulesListNotifyBadge: (props) => {
return getRulesListNotifyBadgeLazy(props);

View file

@ -59,6 +59,7 @@ import type {
RuleTagBadgeProps,
RuleTagBadgeOptions,
RuleEventLogListProps,
RuleEventLogListOptions,
RulesListProps,
RulesListNotifyBadgeProps,
AlertsTableConfigurationRegistry,
@ -113,7 +114,9 @@ export interface TriggersAndActionsUIPublicPluginStart {
getRuleTagBadge: <T extends RuleTagBadgeOptions>(
props: RuleTagBadgeProps<T>
) => ReactElement<RuleTagBadgeProps<T>>;
getRuleEventLogList: (props: RuleEventLogListProps) => ReactElement<RuleEventLogListProps>;
getRuleEventLogList: <T extends RuleEventLogListOptions>(
props: RuleEventLogListProps<T>
) => ReactElement<RuleEventLogListProps<T>>;
getRulesList: (props: RulesListProps) => ReactElement;
getRulesListNotifyBadge: (
props: RulesListNotifyBadgeProps
@ -330,7 +333,7 @@ export class Plugin
getRuleTagBadge: <T extends RuleTagBadgeOptions>(props: RuleTagBadgeProps<T>) => {
return getRuleTagBadgeLazy(props);
},
getRuleEventLogList: (props: RuleEventLogListProps) => {
getRuleEventLogList: <T extends RuleEventLogListOptions>(props: RuleEventLogListProps<T>) => {
return getRuleEventLogListLazy(props);
},
getRulesListNotifyBadge: (props: RulesListNotifyBadgeProps) => {

View file

@ -55,7 +55,10 @@ import type {
RuleTagBadgeProps,
RuleTagBadgeOptions,
} from './application/sections/rules_list/components/rule_tag_badge';
import type { RuleEventLogListProps } from './application/sections/rule_details/components/rule_event_log_list';
import type {
RuleEventLogListProps,
RuleEventLogListOptions,
} from './application/sections/rule_details/components/rule_event_log_list';
import type { CreateConnectorFlyoutProps } from './application/sections/action_connector_form/create_connector_flyout';
import type { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout';
import type { RulesListNotifyBadgeProps } from './application/sections/rules_list/components/rules_list_notify_badge';
@ -105,6 +108,7 @@ export type {
RuleTagBadgeProps,
RuleTagBadgeOptions,
RuleEventLogListProps,
RuleEventLogListOptions,
RulesListProps,
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,
@ -367,6 +371,7 @@ export interface RuleDefinitionProps {
ruleTypeRegistry: RuleTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
onEditRule: () => Promise<void>;
hideEditButton?: boolean;
filteredRuleTypes?: string[];
}

View file

@ -24,8 +24,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('should load from the shareable lazy loader', async () => {
await testSubjects.find('ruleEventLogList');
const exists = await testSubjects.exists('ruleEventLogList');
await testSubjects.find('ruleEventLogListContainer');
const exists = await testSubjects.exists('ruleEventLogListContainer');
expect(exists).to.be(true);
});
});