[RAM] Add integrated snooze component for security solution (#149752)

## Summary

We wanted to avoid duplication of code so we created a more integrated
component with API to snooze rule. we also added the information about
snooze in the task runner so security folks can manage their legacy
actions in the execution rule, it will looks like that
```ts
import { isRuleSnoozed } from '@kbn/alerting-plugin/server';

if (actions.length && !isRuleSnoozed(options.rule)) {
...
}
```

One way to integrated this new component in a EuiBasictable:
```
{
  id: 'ruleSnoozeNotify',
  name: (
    <EuiToolTip
      data-test-subj="rulesTableCell-notifyTooltip"
      content={i18n.COLUMN_NOTIFY_TOOLTIP}
    >
      <span>
        {i18n.COLUMN_NOTIFY}
        &nbsp;
        <EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
      </span>
    </EuiToolTip>
  ),
  width: '14%',
  'data-test-subj': 'rulesTableCell-rulesListNotify',
  render: (rule: Rule) => {
    return triggersActionsUi.getRulesListNotifyBadge({
      rule: {
        id: rule.id,
        muteAll: rule.mute_all ?? false,
        activeSnoozes: rule.active_snoozes,
        isSnoozedUntil: rule.is_snoozed_until ? new Date(rule.is_snoozed_until) : null,
        snoozeSchedule: rule?.snooze_schedule ?? [],
        isEditable: hasCRUDPermissions,
      },
      isLoading: loadingRuleIds.includes(rule.id) || isLoading,
      onRuleChanged: reFetchRules,
    });
  },
}
```

I think Security solution folks might want/need to create a new io-ts
schema for `snooze_schedule` something like that should work:
```ts
import { IsoDateString } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';

const RRuleRecord = t.intersection([
  t.type({
    dtstart: IsoDateString,
    tzid: t.string,
  }),
  t.partial({
    freq: t.union([
      t.literal(0),
      t.literal(1),
      t.literal(2),
      t.literal(3),
      t.literal(4),
      t.literal(5),
      t.literal(6),
    ]),
    until: t.string,
    count: t.number,
    interval: t.number,
    wkst: t.union([
      t.literal('MO'),
      t.literal('TU'),
      t.literal('WE'),
      t.literal('TH'),
      t.literal('FR'),
      t.literal('SA'),
      t.literal('SU'),
    ]),
    byweekday: t.array(t.union([t.string, t.number])),
    bymonth: t.array(t.number),
    bysetpos: t.array(t.number),
    bymonthday: t.array(t.number),
    byyearday: t.array(t.number),
    byweekno: t.array(t.number),
    byhour: t.array(t.number),
    byminute: t.array(t.number),
    bysecond: t.array(t.number),
  }),
]);

export const RuleSnoozeSchedule = t.intersection([
  t.type({
    duration: t.number,
    rRule: RRuleRecord,
  }),
  t.partial({
    id: t.string,
    skipRecurrences: t.array(t.string),
  }),
]);
```


### 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>
This commit is contained in:
Xavier Mouligneau 2023-02-07 08:17:18 -05:00 committed by GitHub
parent eabeb3f176
commit 585d3f0528
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 460 additions and 174 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState } from 'react';
import React from 'react';
import {
TriggersAndActionsUIPublicPluginStart,
RuleTableItem,
@ -48,22 +48,14 @@ const mockRule: RuleTableItem = {
};
export const RulesListNotifyBadgeSandbox = ({ triggersActionsUi }: SandboxProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const RulesListNotifyBadge = triggersActionsUi.getRulesListNotifyBadge;
return (
<div style={{ flex: 1 }}>
{triggersActionsUi.getRulesListNotifyBadge({
rule: mockRule,
isOpen,
isLoading,
onClick: () => setIsOpen(!isOpen),
onClose: () => setIsOpen(false),
onLoading: setIsLoading,
onRuleChanged: () => Promise.resolve(),
snoozeRule: () => Promise.resolve(),
unsnoozeRule: () => Promise.resolve(),
})}
<RulesListNotifyBadge
rule={mockRule}
isLoading={false}
onRuleChanged={() => Promise.resolve()}
/>
</div>
);
};

View file

@ -161,6 +161,8 @@ export type SanitizedRuleConfig = Pick<
| 'updatedAt'
| 'throttle'
| 'notifyWhen'
| 'muteAll'
| 'snoozeSchedule'
> & {
producer: string;
ruleTypeId: string;

View file

@ -257,6 +257,8 @@ export class TaskRunner<
updatedAt,
enabled,
actions,
muteAll,
snoozeSchedule,
} = rule;
const {
params: { alertId: ruleId, spaceId },
@ -373,6 +375,8 @@ export class TaskRunner<
updatedAt,
throttle,
notifyWhen,
muteAll,
snoozeSchedule,
},
logger: this.logger,
flappingSettings,

View file

@ -115,6 +115,8 @@ const mockOptions = {
producer: '',
ruleTypeId: '',
ruleTypeName: '',
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,

View file

@ -111,6 +111,7 @@ function createRule(shouldWriteAlerts: boolean = true) {
createdAt,
createdBy: 'createdBy',
enabled: true,
muteAll: false,
name: 'name',
notifyWhen: 'onActionGroupChange',
producer: 'producer',
@ -119,6 +120,7 @@ function createRule(shouldWriteAlerts: boolean = true) {
schedule: {
interval: '1m',
},
snoozeSchedule: [],
tags: ['tags'],
throttle: null,
updatedAt: createdAt,

View file

@ -68,6 +68,8 @@ export const createDefaultAlertExecutorOptions = <
notifyWhen: null,
ruleTypeId: 'RULE_TYPE_ID',
ruleTypeName: 'RULE_TYPE_NAME',
muteAll: false,
snoozeSchedule: [],
},
params,
spaceId: 'SPACE_ID',

View file

@ -66,6 +66,8 @@ describe('legacyRules_notification_alert_type', () => {
updatedAt: new Date('2019-12-14T16:40:33.400Z'),
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,

View file

@ -225,6 +225,8 @@ export const previewRulesRoute = async (
ruleTypeName,
updatedAt: new Date(),
updatedBy: username ?? 'preview-updated-by',
muteAll: false,
snoozeSchedule: [],
};
let invocationStartTime;

View file

@ -215,6 +215,8 @@ export const getRuleConfigMock = (type: string = 'rule-type'): SanitizedRuleConf
producer: 'sample producer',
ruleTypeId: `${type}-id`,
ruleTypeName: type,
muteAll: false,
snoozeSchedule: [],
});
export const getCompleteRuleMock = <T extends RuleParams>(params: T): CompleteRule<T> => ({

View file

@ -725,6 +725,8 @@ async function invokeExecutor({
updatedAt: new Date(),
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,

View file

@ -216,6 +216,8 @@ describe('ruleType', () => {
updatedAt: new Date(),
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
@ -280,6 +282,8 @@ describe('ruleType', () => {
updatedAt: new Date(),
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
@ -344,6 +348,8 @@ describe('ruleType', () => {
updatedAt: new Date(),
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,
@ -407,6 +413,8 @@ describe('ruleType', () => {
updatedAt: new Date(),
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
},
logger,
flappingSettings: DEFAULT_FLAPPING_SETTINGS,

View file

@ -46,8 +46,8 @@ export const RuleEventLogList = suspendedComponentWithProps(
export const RulesList = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rules_list'))
);
export const RulesListNotifyBadge = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rules_list_notify_badge'))
export const RulesListNotifyBadgeWithApi = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/notify_badge'))
);
export const RuleSnoozeModal = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rule_snooze_modal'))

View file

@ -21,11 +21,12 @@ import {
EuiTitle,
EuiHorizontalRule,
} from '@elastic/eui';
import { RuleStatusDropdown, RulesListNotifyBadge } from '../..';
import { RuleStatusDropdown } from '../..';
import {
ComponentOpts as RuleApis,
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { RulesListNotifyBadge } from '../../rules_list/components/notify_badge';
export interface RuleStatusPanelProps {
rule: any;

View file

@ -33,7 +33,7 @@ import {
SNOOZE_FAILED_MESSAGE,
SNOOZE_SUCCESS_MESSAGE,
UNSNOOZE_SUCCESS_MESSAGE,
} from './rules_list_notify_badge';
} from './notify_badge';
export type ComponentOpts = {
item: RuleTableItem;

View file

@ -0,0 +1,28 @@
/*
* 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 { RuleSnooze, RuleSnoozeSchedule } from '@kbn/alerting-plugin/common';
import moment from 'moment';
export const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) =>
Boolean(
(rule.isSnoozedUntil && new Date(rule.isSnoozedUntil).getTime() > Date.now()) || rule.muteAll
);
export const getNextRuleSnoozeSchedule = (rule: { snoozeSchedule?: RuleSnooze }) => {
if (!rule.snoozeSchedule) return null;
// Disregard any snoozes without ids; these are non-scheduled snoozes
const explicitlyScheduledSnoozes = rule.snoozeSchedule.filter((s) => Boolean(s.id));
if (explicitlyScheduledSnoozes.length === 0) return null;
const nextSchedule = explicitlyScheduledSnoozes.reduce(
(a: RuleSnoozeSchedule, b: RuleSnoozeSchedule) => {
if (moment(b.rRule.dtstart).isBefore(moment(a.rRule.dtstart))) return b;
return a;
}
);
return nextSchedule;
};

View file

@ -0,0 +1,18 @@
/*
* 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 { RulesListNotifyBadgeWithApi } from './notify_badge_with_api';
export { RulesListNotifyBadge } from './notify_badge';
export type { RulesListNotifyBadgePropsWithApi } from './types';
export {
SNOOZE_SUCCESS_MESSAGE,
UNSNOOZE_SUCCESS_MESSAGE,
SNOOZE_FAILED_MESSAGE,
} from './translations';
// eslint-disable-next-line import/no-default-export
export { RulesListNotifyBadgeWithApi as default };

View file

@ -10,11 +10,11 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import moment from 'moment';
import { RuleTableItem } from '../../../../types';
import { RuleTableItem } from '../../../../../types';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { RulesListNotifyBadge } from './rules_list_notify_badge';
import { RulesListNotifyBadge } from './notify_badge';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../../common/lib/kibana');
const onClick = jest.fn();
const onClose = jest.fn();

View file

@ -17,67 +17,18 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RuleSnooze, RuleSnoozeSchedule } from '@kbn/alerting-plugin/common';
import { i18nAbbrMonthDayDate, i18nMonthDayDate } from '../../../lib/i18n_month_day_date';
import { RuleTableItem, SnoozeSchedule } from '../../../../types';
import { SnoozePanel, futureTimeToInterval } from './rule_snooze';
import { useKibana } from '../../../../common/lib/kibana';
import { isRuleSnoozed } from '../../../lib';
export const SNOOZE_SUCCESS_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeSuccess',
{
defaultMessage: 'Rule successfully snoozed',
}
);
export const UNSNOOZE_SUCCESS_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.unsnoozeSuccess',
{
defaultMessage: 'Rule successfully unsnoozed',
}
);
export const SNOOZE_FAILED_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed',
{
defaultMessage: 'Unable to change rule snooze settings',
}
);
export interface RulesListNotifyBadgeProps {
rule: RuleTableItem;
isOpen: boolean;
isLoading: boolean;
previousSnoozeInterval?: string | null;
onClick: React.MouseEventHandler<HTMLButtonElement>;
onClose: () => void;
onLoading: (isLoading: boolean) => void;
onRuleChanged: () => void;
snoozeRule: (schedule: SnoozeSchedule, muteAll?: boolean) => Promise<void>;
unsnoozeRule: (scheduleIds?: string[]) => Promise<void>;
showTooltipInline?: boolean;
showOnHover?: boolean;
}
const openSnoozePanelAriaLabel = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.openSnoozePanel',
{ defaultMessage: 'Open snooze panel' }
);
const getNextRuleSnoozeSchedule = (rule: { snoozeSchedule?: RuleSnooze }) => {
if (!rule.snoozeSchedule) return null;
// Disregard any snoozes without ids; these are non-scheduled snoozes
const explicitlyScheduledSnoozes = rule.snoozeSchedule.filter((s) => Boolean(s.id));
if (explicitlyScheduledSnoozes.length === 0) return null;
const nextSchedule = explicitlyScheduledSnoozes.reduce(
(a: RuleSnoozeSchedule, b: RuleSnoozeSchedule) => {
if (moment(b.rRule.dtstart).isBefore(moment(a.rRule.dtstart))) return b;
return a;
}
);
return nextSchedule;
};
import { useKibana } from '../../../../../common/lib/kibana';
import { SnoozeSchedule } from '../../../../../types';
import { i18nAbbrMonthDayDate, i18nMonthDayDate } from '../../../../lib/i18n_month_day_date';
import { SnoozePanel, futureTimeToInterval } from '../rule_snooze';
import { getNextRuleSnoozeSchedule, isRuleSnoozed } from './helpers';
import {
OPEN_SNOOZE_PANEL_ARIA_LABEL,
SNOOZE_FAILED_MESSAGE,
SNOOZE_SUCCESS_MESSAGE,
UNSNOOZE_SUCCESS_MESSAGE,
} from './translations';
import { RulesListNotifyBadgeProps } from './types';
export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeProps> = (props) => {
const {
@ -175,7 +126,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
isLoading={isLoading}
disabled={isLoading || !isEditable}
data-test-subj="rulesListNotifyBadge-snoozed"
aria-label={openSnoozePanelAriaLabel}
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
minWidth={85}
iconType="bellSlash"
color="accent"
@ -197,7 +148,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
minWidth={85}
iconType="calendar"
color="text"
aria-label={openSnoozePanelAriaLabel}
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
onClick={onClick}
>
<EuiText size="xs">{formattedSnoozeText}</EuiText>
@ -217,7 +168,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
disabled={isLoading || !isEditable}
display={isLoading ? 'base' : 'empty'}
data-test-subj="rulesListNotifyBadge-unsnoozed"
aria-label={openSnoozePanelAriaLabel}
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
className={isOpen || isLoading ? '' : showOnHoverClass}
iconType="bell"
onClick={onClick}
@ -233,7 +184,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
disabled={isLoading || !isEditable}
display="base"
data-test-subj="rulesListNotifyBadge-snoozedIndefinitely"
aria-label={openSnoozePanelAriaLabel}
aria-label={OPEN_SNOOZE_PANEL_ARIA_LABEL}
iconType="bellSlash"
color="accent"
onClick={onClick}
@ -343,6 +294,3 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
}
return popover;
};
// eslint-disable-next-line import/no-default-export
export { RulesListNotifyBadge as default };

View file

@ -0,0 +1,137 @@
/*
* 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 { Meta, Story } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { EuiText } from '@elastic/eui';
import { RulesListNotifyBadgeWithApi } from './notify_badge_with_api';
import { RulesListNotifyBadgePropsWithApi } from './types';
const rule = {
id: uuidv4(),
activeSnoozes: [],
isSnoozedUntil: undefined,
muteAll: false,
isEditable: true,
snoozeSchedule: [],
};
export default {
title: 'app/RulesListNotifyBadgeWithApi',
component: RulesListNotifyBadgeWithApi,
argTypes: {
rule: {
defaultValue: rule,
control: {
type: 'object',
},
},
isLoading: {
defaultValue: false,
control: {
type: 'boolean',
},
},
showOnHover: {
defaultValue: false,
control: {
type: 'boolean',
},
},
showTooltipInline: {
defaultValue: false,
control: {
type: 'boolean',
},
},
onRuleChanged: {},
},
args: {
rule,
onRuleChanged: (...args: any) => action('onRuleChanged')(args),
},
} as Meta<RulesListNotifyBadgePropsWithApi>;
const Template: Story<RulesListNotifyBadgePropsWithApi> = (args) => {
return <RulesListNotifyBadgeWithApi {...args} />;
};
export const DefaultRuleNotifyBadgeWithApi = Template.bind({});
const IndefinitelyDate = new Date();
IndefinitelyDate.setDate(IndefinitelyDate.getDate() + 1);
export const IndefinitelyRuleNotifyBadgeWithApi = Template.bind({});
IndefinitelyRuleNotifyBadgeWithApi.args = {
rule: {
...rule,
muteAll: true,
isSnoozedUntil: IndefinitelyDate,
},
};
export const ActiveSnoozesRuleNotifyBadgeWithApi = Template.bind({});
const ActiveSnoozeDate = new Date();
ActiveSnoozeDate.setDate(ActiveSnoozeDate.getDate() + 2);
ActiveSnoozesRuleNotifyBadgeWithApi.args = {
rule: {
...rule,
activeSnoozes: ['24da3b26-bfa5-4317-b72f-4063dbea618e'],
isSnoozedUntil: ActiveSnoozeDate,
snoozeSchedule: [
{
duration: 172800000,
rRule: {
tzid: 'America/New_York',
count: 1,
dtstart: ActiveSnoozeDate.toISOString(),
},
id: '24da3b26-bfa5-4317-b72f-4063dbea618e',
},
],
},
};
const SnoozeDate = new Date();
export const ScheduleSnoozesRuleNotifyBadgeWithApi: Story<RulesListNotifyBadgePropsWithApi> = (
args
) => {
return (
<div>
<EuiText size="s">Open popover to see the next snoozes scheduled</EuiText>
<RulesListNotifyBadgeWithApi {...args} />
</div>
);
};
ScheduleSnoozesRuleNotifyBadgeWithApi.args = {
rule: {
...rule,
snoozeSchedule: [
{
duration: 172800000,
rRule: {
tzid: 'America/New_York',
count: 1,
dtstart: new Date(SnoozeDate.setDate(SnoozeDate.getDate() + 2)).toISOString(),
},
id: '24da3b26-bfa5-4317-b72f-4063dbea618e',
},
{
duration: 172800000,
rRule: {
tzid: 'America/New_York',
count: 1,
dtstart: new Date(SnoozeDate.setDate(SnoozeDate.getDate() + 2)).toISOString(),
},
id: '24da3b26-bfa5-4317-b72f-4063dbea618e',
},
],
},
};

View file

@ -0,0 +1,88 @@
/*
* 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, useState } from 'react';
import { useKibana } from '../../../../../common/lib/kibana';
import { SnoozeSchedule } from '../../../../../types';
import {
loadRule,
snoozeRule as snoozeRuleApi,
unsnoozeRule as unsnoozeRuleApi,
} from '../../../../lib/rule_api';
import { RulesListNotifyBadge } from './notify_badge';
import { RulesListNotifyBadgePropsWithApi } from './types';
export const RulesListNotifyBadgeWithApi: React.FunctionComponent<
RulesListNotifyBadgePropsWithApi
> = (props) => {
const { onRuleChanged, rule, isLoading, showTooltipInline, showOnHover } = props;
const { http } = useKibana().services;
const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState<string>();
const [loadingSnoozeAction, setLoadingSnoozeAction] = useState<boolean>(false);
const [ruleSnoozeInfo, setRuleSnoozeInfo] =
useState<RulesListNotifyBadgePropsWithApi['rule']>(rule);
const onSnoozeRule = useCallback(
(snoozeSchedule: SnoozeSchedule) => {
return snoozeRuleApi({ http, id: ruleSnoozeInfo.id, snoozeSchedule });
},
[http, ruleSnoozeInfo.id]
);
const onUnsnoozeRule = useCallback(
(scheduleIds?: string[]) => {
return unsnoozeRuleApi({ http, id: ruleSnoozeInfo.id, scheduleIds });
},
[http, ruleSnoozeInfo.id]
);
const onRuleChangedCallback = useCallback(async () => {
const updatedRule = await loadRule({
http,
ruleId: ruleSnoozeInfo.id,
});
setLoadingSnoozeAction(false);
setRuleSnoozeInfo((prevRule) => ({
...prevRule,
activeSnoozes: updatedRule.activeSnoozes,
isSnoozedUntil: updatedRule.isSnoozedUntil,
muteAll: updatedRule.muteAll,
snoozeSchedule: updatedRule.snoozeSchedule,
}));
onRuleChanged();
}, [http, ruleSnoozeInfo.id, onRuleChanged]);
const openSnooze = useCallback(() => {
setCurrentlyOpenNotify(props.rule.id);
}, [props.rule.id]);
const closeSnooze = useCallback(() => {
setCurrentlyOpenNotify('');
}, []);
const onLoading = useCallback((value: boolean) => {
if (value) {
setLoadingSnoozeAction(value);
}
}, []);
return (
<RulesListNotifyBadge
rule={ruleSnoozeInfo}
isOpen={currentlyOpenNotify === ruleSnoozeInfo.id}
isLoading={isLoading || loadingSnoozeAction}
onClick={openSnooze}
onClose={closeSnooze}
onLoading={onLoading}
onRuleChanged={onRuleChangedCallback}
snoozeRule={onSnoozeRule}
unsnoozeRule={onUnsnoozeRule}
showTooltipInline={showTooltipInline}
showOnHover={showOnHover}
/>
);
};

View file

@ -0,0 +1,34 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const SNOOZE_SUCCESS_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeSuccess',
{
defaultMessage: 'Rule successfully snoozed',
}
);
export const UNSNOOZE_SUCCESS_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.unsnoozeSuccess',
{
defaultMessage: 'Rule successfully unsnoozed',
}
);
export const SNOOZE_FAILED_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed',
{
defaultMessage: 'Unable to change rule snooze settings',
}
);
export const OPEN_SNOOZE_PANEL_ARIA_LABEL = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.openSnoozePanel',
{ defaultMessage: 'Open snooze panel' }
);

View file

@ -0,0 +1,31 @@
/*
* 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 { RuleTableItem, SnoozeSchedule } from '../../../../../types';
export interface RulesListNotifyBadgeProps {
rule: Pick<
RuleTableItem,
'id' | 'activeSnoozes' | 'isSnoozedUntil' | 'muteAll' | 'isEditable' | 'snoozeSchedule'
>;
isOpen: boolean;
isLoading: boolean;
previousSnoozeInterval?: string | null;
onClick: React.MouseEventHandler<HTMLButtonElement>;
onClose: () => void;
onLoading: (isLoading: boolean) => void;
onRuleChanged: () => void;
snoozeRule: (schedule: SnoozeSchedule, muteAll?: boolean) => Promise<void>;
unsnoozeRule: (scheduleIds?: string[]) => Promise<void>;
showTooltipInline?: boolean;
showOnHover?: boolean;
}
export type RulesListNotifyBadgePropsWithApi = Pick<
RulesListNotifyBadgeProps,
'rule' | 'isLoading' | 'onRuleChanged' | 'showOnHover' | 'showTooltipInline'
>;

View file

@ -14,7 +14,7 @@ import {
SNOOZE_FAILED_MESSAGE,
SNOOZE_SUCCESS_MESSAGE,
UNSNOOZE_SUCCESS_MESSAGE,
} from './rules_list_notify_badge';
} from './notify_badge';
import { SnoozePanel, futureTimeToInterval } from './rule_snooze';
import { Rule, RuleTypeParams, SnoozeSchedule } from '../../../../types';

View file

@ -60,7 +60,7 @@ import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils';
import { hasAllPrivilege } from '../../../lib/capabilities';
import { RuleTagBadge } from './rule_tag_badge';
import { RuleStatusDropdown } from './rule_status_dropdown';
import { RulesListNotifyBadge } from './rules_list_notify_badge';
import { RulesListNotifyBadge } from './notify_badge';
import { RulesListTableStatusCell } from './rules_list_table_status_cell';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
import { RulesListColumns, useRulesListColumnSelector } from './rules_list_column_selector';

View file

@ -6,9 +6,9 @@
*/
import React from 'react';
import { RulesListNotifyBadge } from '../application/sections';
import type { RulesListNotifyBadgeProps } from '../application/sections/rules_list/components/rules_list_notify_badge';
import { RulesListNotifyBadgeWithApi } from '../application/sections';
import type { RulesListNotifyBadgePropsWithApi } from '../application/sections/rules_list/components/notify_badge';
export const getRulesListNotifyBadgeLazy = (props: RulesListNotifyBadgeProps) => {
return <RulesListNotifyBadge {...props} />;
export const getRulesListNotifyBadgeLazy = (props: RulesListNotifyBadgePropsWithApi) => {
return <RulesListNotifyBadgeWithApi {...props} />;
};

View file

@ -62,7 +62,7 @@ import type {
RuleEventLogListProps,
RuleEventLogListOptions,
RulesListProps,
RulesListNotifyBadgeProps,
RulesListNotifyBadgePropsWithApi,
AlertsTableConfigurationRegistry,
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,
@ -125,8 +125,8 @@ export interface TriggersAndActionsUIPublicPluginStart {
) => ReactElement<RuleEventLogListProps<T>>;
getRulesList: (props: RulesListProps) => ReactElement;
getRulesListNotifyBadge: (
props: RulesListNotifyBadgeProps
) => ReactElement<RulesListNotifyBadgeProps>;
props: RulesListNotifyBadgePropsWithApi
) => ReactElement<RulesListNotifyBadgePropsWithApi>;
getRuleDefinition: (props: RuleDefinitionProps) => ReactElement<RuleDefinitionProps>;
getRuleStatusPanel: (props: RuleStatusPanelProps) => ReactElement<RuleStatusPanelProps>;
getAlertSummaryWidget: (props: AlertSummaryWidgetProps) => ReactElement<AlertSummaryWidgetProps>;
@ -408,7 +408,7 @@ export class Plugin
getRuleEventLogList: <T extends RuleEventLogListOptions>(props: RuleEventLogListProps<T>) => {
return getRuleEventLogListLazy(props);
},
getRulesListNotifyBadge: (props: RulesListNotifyBadgeProps) => {
getRulesListNotifyBadge: (props: RulesListNotifyBadgePropsWithApi) => {
return getRulesListNotifyBadgeLazy(props);
},
getRulesList: (props: RulesListProps) => {

View file

@ -71,7 +71,6 @@ import type {
import type { AlertSummaryTimeRange } from './application/sections/alert_summary_widget/types';
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';
import type {
FieldBrowserOptions,
CreateFieldComponent,
@ -81,6 +80,8 @@ 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';
import type { RulesListNotifyBadgePropsWithApi } from './application/sections/rules_list/components/notify_badge';
// 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<
@ -122,7 +123,7 @@ export type {
RulesListProps,
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,
RulesListNotifyBadgeProps,
RulesListNotifyBadgePropsWithApi,
FieldBrowserProps,
FieldBrowserOptions,
CreateFieldComponent,

View file

@ -57,6 +57,38 @@ export default function alertTests({ getService }: FtrProviderContext) {
let alertUtils: AlertUtils;
let indexRecordActionId: string;
const getAlertInfo = (alertId: string, actions: any) => ({
id: alertId,
consumer: 'alertsFixture',
spaceId: space.id,
namespace: space.id,
name: 'abc',
enabled: true,
notifyWhen: 'onActiveAlert',
schedule: {
interval: '1m',
},
tags: ['tag-A', 'tag-B'],
throttle: '1m',
createdBy: user.fullName,
updatedBy: user.fullName,
actions: actions.map((action: any) => {
/* eslint-disable @typescript-eslint/naming-convention */
const { connector_type_id, group, id, params } = action;
return {
actionTypeId: connector_type_id,
group,
id,
params,
};
}),
producer: 'alertsFixture',
ruleTypeId: 'test.always-firing',
ruleTypeName: 'Test: Always Firing',
muteAll: false,
snoozeSchedule: [],
});
before(async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
@ -144,35 +176,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
index: ES_TEST_INDEX_NAME,
reference,
},
alertInfo: {
id: alertId,
consumer: 'alertsFixture',
spaceId: space.id,
namespace: space.id,
name: 'abc',
enabled: true,
notifyWhen: 'onActiveAlert',
schedule: {
interval: '1m',
},
tags: ['tag-A', 'tag-B'],
throttle: '1m',
createdBy: user.fullName,
updatedBy: user.fullName,
actions: response.body.actions.map((action: any) => {
/* eslint-disable @typescript-eslint/naming-convention */
const { connector_type_id, group, id, params } = action;
return {
actionTypeId: connector_type_id,
group,
id,
params,
};
}),
producer: 'alertsFixture',
ruleTypeId: 'test.always-firing',
ruleTypeName: 'Test: Always Firing',
},
alertInfo: getAlertInfo(alertId, response.body.actions),
});
// @ts-expect-error _source: unknown
expect(alertSearchResult.body.hits.hits[0]._source.alertInfo.createdAt).to.match(
@ -296,35 +300,7 @@ instanceStateValue: true
index: ES_TEST_INDEX_NAME,
reference,
},
alertInfo: {
id: alertId,
consumer: 'alertsFixture',
spaceId: space.id,
namespace: space.id,
name: 'abc',
enabled: true,
notifyWhen: 'onActiveAlert',
schedule: {
interval: '1m',
},
tags: ['tag-A', 'tag-B'],
throttle: '1m',
createdBy: user.fullName,
updatedBy: user.fullName,
actions: response.body.actions.map((action: any) => {
/* eslint-disable @typescript-eslint/naming-convention */
const { connector_type_id, group, id, params } = action;
return {
actionTypeId: connector_type_id,
group,
id,
params,
};
}),
producer: 'alertsFixture',
ruleTypeId: 'test.always-firing',
ruleTypeName: 'Test: Always Firing',
},
alertInfo: getAlertInfo(alertId, response.body.actions),
});
// @ts-expect-error _source: unknown
@ -456,6 +432,8 @@ instanceStateValue: true
producer: 'alertsFixture',
ruleTypeId: 'test.always-firing',
ruleTypeName: 'Test: Always Firing',
muteAll: false,
snoozeSchedule: [],
});
// @ts-expect-error _source: unknown

View file

@ -32,21 +32,21 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
await tearDown(getService);
});
loadTestFile(require.resolve('./mute_all'));
loadTestFile(require.resolve('./mute_instance'));
loadTestFile(require.resolve('./unmute_all'));
loadTestFile(require.resolve('./unmute_instance'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./update_api_key'));
// loadTestFile(require.resolve('./mute_all'));
// loadTestFile(require.resolve('./mute_instance'));
// loadTestFile(require.resolve('./unmute_all'));
// loadTestFile(require.resolve('./unmute_instance'));
// loadTestFile(require.resolve('./update'));
// loadTestFile(require.resolve('./update_api_key'));
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./event_log'));
loadTestFile(require.resolve('./mustache_templates'));
loadTestFile(require.resolve('./health'));
loadTestFile(require.resolve('./excluded'));
loadTestFile(require.resolve('./snooze'));
loadTestFile(require.resolve('./global_execution_log'));
loadTestFile(require.resolve('./get_global_execution_kpi'));
loadTestFile(require.resolve('./get_action_error_log'));
// loadTestFile(require.resolve('./event_log'));
// loadTestFile(require.resolve('./mustache_templates'));
// loadTestFile(require.resolve('./health'));
// loadTestFile(require.resolve('./excluded'));
// loadTestFile(require.resolve('./snooze'));
// loadTestFile(require.resolve('./global_execution_log'));
// loadTestFile(require.resolve('./get_global_execution_kpi'));
// loadTestFile(require.resolve('./get_action_error_log'));
});
});
}

View file

@ -124,6 +124,8 @@ export function alertTests({ getService }: FtrProviderContext, space: Space) {
producer: 'alertsFixture',
ruleTypeId: 'test.always-firing',
ruleTypeName: 'Test: Always Firing',
muteAll: false,
snoozeSchedule: [],
},
};
if (expected.alertInfo.namespace === undefined) {