This commit is contained in:
Edgar Santos 2025-06-15 15:24:16 +02:00
parent f5c2bb3d3a
commit aeef8f9eff
22 changed files with 1013 additions and 6 deletions

View file

@ -501,6 +501,8 @@ export const CASE_ATTACHMENT_ENDPOINT_TYPE_ID = 'endpoint' as const;
*/
export const MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS = 90;
export const MAX_MANUAL_RULE_RUN_BULK_SIZE = 100;
export const MAX_MANUAL_RULE_GAPS_FILLING_LOOKBACK_WINDOW_DAYS = 90;
export const MAX_MANUAL_RULE_GAPS_FILLING_BULK_SIZE = 100;
/*
* Whether it is a Jest environment

View file

@ -24,6 +24,7 @@ export const BULK_RULE_ACTIONS = {
DUPLICATE: `${APP_UI_ID} bulkRuleActions duplicate`,
EXPORT: `${APP_UI_ID} bulkRuleActions export`,
MANUAL_RULE_RUN: `${APP_UI_ID} bulkRuleActions manual rule run`,
FILL_GAPS: `${APP_UI_ID} bulkRuleActions fill gaps`,
DELETE: `${APP_UI_ID} bulkRuleActions delete`,
EDIT: `${APP_UI_ID} bulkRuleActions edit`,
};

View file

@ -110,6 +110,13 @@ export const BULK_ACTION_MANUAL_RULE_RUN = i18n.translate(
}
);
export const BULK_ACTION_FILL_GAPS = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.fillGapsTitle',
{
defaultMessage: 'Fill gaps',
}
);
export const BULK_ACTION_DUPLICATE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle',
{
@ -238,6 +245,29 @@ export const BULK_EDIT_WARNING_TOAST_NOTIFY = i18n.translate(
}
);
export const BULK_FILL_RULE_GAPS_WARNING_TOAST_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkFillRuleGapsWarningToastTitle',
{
defaultMessage: 'Rules updates are in progress',
}
);
export const BULK_FILL_RULE_GAPS_WARNING_TOAST_DESCRIPTION = (rulesCount: number) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkFillRuleGapsWarningToastDescription',
{
values: { rulesCount },
defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} updating.',
}
);
export const BULK_FILL_RULE_GAPS_WARNING_TOAST_NOTIFY = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkFillRuleGapsWarningToastNotifyButtonLabel',
{
defaultMessage: `Notify me when done`,
}
);
export const BULK_EXPORT_CONFIRMATION_REJECTED_TITLE = (rulesCount: number) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkExportConfirmationDeniedTitle',
@ -256,6 +286,15 @@ export const BULK_MANUAL_RULE_RUN_CONFIRMATION_REJECTED_TITLE = (rulesCount: num
}
);
export const BULK_FILL_RULE_GAPS_CONFIRMATION_REJECTED_TITLE = (rulesCount: number) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkFillRuleGapsConfirmationDeniedTitle',
{
values: { rulesCount },
defaultMessage: '{rulesCount, plural, =1 {# rule} other {# rules}} cannot be scheduled',
}
);
export const BULK_EDIT_CONFIRMATION_REJECTED_TITLE = (rulesCount: number) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationDeniedTitle',
@ -316,6 +355,15 @@ export const BULK_MANUAL_RULE_RUN_CONFIRMATION_CONFIRM = (rulesCount: number) =>
}
);
export const BULK_FILL_RULE_GAPS_CONFIRMATION_CONFIRM = (rulesCount: number) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkFillRuleGapsConfirmation.confirmButtonLabel',
{
values: { rulesCount },
defaultMessage: 'Schedule gaps filling for {rulesCount, plural, =1 {# rule} other {# rules}}',
}
);
export const BULK_MANUAL_RULE_RUN_LIMIT_ERROR_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkManualRuleRunLimitErrorMessage',
{

View file

@ -862,7 +862,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
</>
</Route>
<Route path={`/rules/id/:detailName/:tabName(${RuleDetailTabs.executionEvents})`}>
<ExecutionEventsTable ruleId={ruleId} />
<ExecutionEventsTable ruleId={ruleId} shouldRefetch={false} />
</Route>
</Routes>
</StyledMinHeightTabContainer>

View file

@ -0,0 +1,98 @@
/*
* 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 moment from 'moment';
import { fireEvent, render, screen } from '@testing-library/react';
import { ManualRuleRunModal } from '.';
import { MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS } from '../../../../../common/constants';
const convertToDatePickerFormat = (date: moment.Moment) => {
return `${date.format('L')} ${date.format('LT')}`;
};
describe('ManualRuleRunModal', () => {
const onCancelMock = jest.fn();
const onConfirmMock = jest.fn();
let startDatePicker: Element;
let endDatePicker: Element;
let confirmModalConfirmButton: HTMLElement;
let cancelModalConfirmButton: HTMLElement;
let timeRangeForm: HTMLElement;
afterEach(() => {
onCancelMock.mockReset();
onConfirmMock.mockReset();
});
beforeEach(() => {
render(<ManualRuleRunModal onCancel={onCancelMock} onConfirm={onConfirmMock} />);
timeRangeForm = screen.getByTestId('manual-rule-run-time-range-form');
startDatePicker = timeRangeForm.getElementsByClassName('start-date-picker')[0];
endDatePicker = timeRangeForm.getElementsByClassName('end-date-picker')[0];
confirmModalConfirmButton = screen.getByTestId('confirmModalConfirmButton');
cancelModalConfirmButton = screen.getByTestId('confirmModalCancelButton');
});
it('should render modal', () => {
expect(timeRangeForm).toBeInTheDocument();
expect(cancelModalConfirmButton).toBeEnabled();
expect(confirmModalConfirmButton).toBeEnabled();
});
it('should render confirmation button disabled if invalid time range has been selected', () => {
expect(confirmModalConfirmButton).toBeEnabled();
const now = moment();
const startDate = now.clone().subtract(1, 'd');
const endDate = now.clone().subtract(2, 'd');
fireEvent.change(startDatePicker, {
target: { value: convertToDatePickerFormat(startDate) },
});
fireEvent.change(endDatePicker, {
target: { value: convertToDatePickerFormat(endDate) },
});
expect(confirmModalConfirmButton).toBeDisabled();
expect(timeRangeForm).toHaveTextContent('Selected time range is invalid');
});
it('should render confirmation button disabled if selected start date is more than 90 days in the past', () => {
expect(confirmModalConfirmButton).toBeEnabled();
const now = moment();
const startDate = now.clone().subtract(MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS, 'd');
fireEvent.change(startDatePicker, {
target: {
value: convertToDatePickerFormat(startDate),
},
});
expect(confirmModalConfirmButton).toBeDisabled();
expect(timeRangeForm).toHaveTextContent(
'Manual rule run cannot be scheduled earlier than 90 days ago'
);
});
it('should render confirmation button disabled if selected end date is in future', () => {
expect(confirmModalConfirmButton).toBeEnabled();
const now = moment();
const endDate = now.clone().add(2, 'd');
fireEvent.change(endDatePicker, {
target: { value: convertToDatePickerFormat(endDate) },
});
expect(confirmModalConfirmButton).toBeDisabled();
expect(timeRangeForm).toHaveTextContent('Manual rule run cannot be scheduled for the future');
});
});

View file

@ -0,0 +1,139 @@
/*
* 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 {
EuiConfirmModal,
EuiDatePicker,
EuiDatePickerRange,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
useGeneratedHtmlId,
EuiCallOut,
EuiSpacer,
} from '@elastic/eui';
import moment from 'moment';
import React, { useCallback, useMemo, useState } from 'react';
import { MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS } from '../../../../../common/constants';
import * as i18n from './translations';
const MANUAL_RULE_GAPS_FILLING_MODAL_WIDTH = 600;
interface ManualRuleGapsFillingModalProps {
onCancel: () => void;
onConfirm: (timeRange: { startDate: moment.Moment; endDate: moment.Moment }) => void;
rulesCount: number
}
const ManualRuleGapsFillingModalComponent = ({ onCancel, onConfirm, rulesCount }: ManualRuleGapsFillingModalProps) => {
const modalTitleId = useGeneratedHtmlId();
const now = moment();
// By default we show three hours time range which user can then adjust
const [startDate, setStartDate] = useState(now.clone().subtract(3, 'h'));
const [endDate, setEndDate] = useState(now.clone());
const isStartDateOutOfRange = now
.clone()
.subtract(MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS, 'd')
.isAfter(startDate);
const isEndDateInFuture = endDate.isAfter(now);
const isInvalidTimeRange = startDate.isSameOrAfter(endDate);
const isInvalid = isStartDateOutOfRange || isEndDateInFuture || isInvalidTimeRange;
const errorMessage = useMemo(() => {
if (isStartDateOutOfRange) {
return i18n.MANUAL_RULE_GAPS_FILLING_START_DATE_OUT_OF_RANGE_ERROR(
MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS
);
}
if (isEndDateInFuture) {
return i18n.MANUAL_RULE_GAPS_FILLING_FUTURE_TIME_RANGE_ERROR;
}
if (isInvalidTimeRange) {
return i18n.MANUAL_RULE_GAPS_FILLING_INVALID_TIME_RANGE_ERROR;
}
return null;
}, [isEndDateInFuture, isInvalidTimeRange, isStartDateOutOfRange]);
const handleConfirm = useCallback(() => {
onConfirm({ startDate, endDate });
}, [endDate, onConfirm, startDate]);
return (
<EuiConfirmModal
aria-labelledby={modalTitleId}
title={
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>{i18n.MANUAL_RULE_GAPS_FILLING_MODAL_TITLE}</EuiFlexItem>
</EuiFlexGroup>
}
titleProps={{ id: modalTitleId, style: { width: '100%' } }}
onCancel={onCancel}
onConfirm={handleConfirm}
confirmButtonText={i18n.MANUAL_RULE_GAPS_FILLING_CONFIRM_BUTTON}
cancelButtonText={i18n.MANUAL_RULE_GAPS_FILLING_CANCEL_BUTTON}
confirmButtonDisabled={isInvalid}
style={{ width: MANUAL_RULE_GAPS_FILLING_MODAL_WIDTH }}
>
<EuiForm data-test-subj="manual-rule-gaps-filling-modal-form" fullWidth>
<EuiFormRow
data-test-subj="manual-rule-gaps-filling-time-range-form"
label={i18n.MANUAL_RULE_GAPS_FILLING_TIME_RANGE_TITLE}
isInvalid={isInvalid}
error={errorMessage}
>
<EuiDatePickerRange
data-test-subj="manual-rule-gaps-filling-time-range"
startDateControl={
<EuiDatePicker
className="start-date-picker"
aria-label="Start date range"
selected={startDate}
onChange={(date) => date && setStartDate(date)}
startDate={startDate}
endDate={endDate}
showTimeSelect={true}
/>
}
endDateControl={
<EuiDatePicker
className="end-date-picker"
aria-label="End date range"
selected={endDate}
onChange={(date) => date && setEndDate(date)}
startDate={startDate}
endDate={endDate}
showTimeSelect={true}
/>
}
/>
</EuiFormRow>
</EuiForm>
<EuiSpacer size="m" />
<EuiCallOut
size="s"
iconType="warning"
title={i18n.MANUAL_RULE_GAPS_FILLING_NOTIFIACTIONS_LIMITATIONS}
/>
{rulesCount > 1 && (
<EuiCallOut
size="s"
iconType="warning"
title={i18n.MANUAL_RULE_GAPS_FILLING_MAX_GAPS_LIMITATIONS}
/>
)}
</EuiConfirmModal>
);
};
export const ManualRuleGapsFillingModal = React.memo(ManualRuleGapsFillingModalComponent);
ManualRuleGapsFillingModal.displayName = 'ManualRuleGapsFillingModal';

View file

@ -0,0 +1,86 @@
/*
* 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 MANUAL_RULE_GAPS_FILLING_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.modalTitle',
{
defaultMessage: 'Manual gaps filling run',
}
);
export const MANUAL_RULE_GAPS_FILLING_TIME_RANGE_TITLE = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.timeRangeTitle',
{
defaultMessage: 'Select timerange for gaps filling',
}
);
export const MANUAL_RULE_GAPS_FILLING_START_AT_TITLE = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.startAtTitle',
{
defaultMessage: 'Start at',
}
);
export const MANUAL_RULE_GAPS_FILLING_END_AT_TITLE = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.endAtTitle',
{
defaultMessage: 'Finish at',
}
);
export const MANUAL_RULE_GAPS_FILLING_CONFIRM_BUTTON = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.confirmButton',
{
defaultMessage: 'Run',
}
);
export const MANUAL_RULE_GAPS_FILLING_CANCEL_BUTTON = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.cancelButton',
{
defaultMessage: 'Cancel',
}
);
export const MANUAL_RULE_GAPS_FILLING_INVALID_TIME_RANGE_ERROR = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.invalidTimeRangeError',
{
defaultMessage: 'Selected time range is invalid',
}
);
export const MANUAL_RULE_GAPS_FILLING_FUTURE_TIME_RANGE_ERROR = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.futureTimeRangeError',
{
defaultMessage: 'Manual rule gaps filling cannot be scheduled for the future',
}
);
export const MANUAL_RULE_GAPS_FILLING_START_DATE_OUT_OF_RANGE_ERROR = (maxDaysLookback: number) =>
i18n.translate('xpack.securitySolution.manuelRulaRun.startDateIsOutOfRangeError', {
values: { maxDaysLookback },
defaultMessage:
'Manual rule gaps filling cannot be scheduled earlier than {maxDaysLookback, plural, =1 {# day} other {# days}} ago',
});
export const MANUAL_RULE_GAPS_FILLING_NOTIFIACTIONS_LIMITATIONS = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.notificationsLimitations',
{
defaultMessage:
'Alert summary rule actions that run at a custom frequency are not performed during manual rule runs triggered by the gaps filling process.',
}
);
export const MANUAL_RULE_GAPS_FILLING_MAX_GAPS_LIMITATIONS = i18n.translate(
'xpack.securitySolution.manualRuleGapsFilling.maxGapsLimitations',
{
defaultMessage:
'A maximum of 1000 gaps will be filled per rule.',
}
);

View file

@ -0,0 +1,55 @@
/*
* 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 { useCallback, useRef } from 'react';
import { useBoolState } from '../../../../common/hooks/use_bool_state';
import type { TimeRange } from '../../types';
/**
* Hook that controls manual rule gaps filling confirmation modal window and its content
*/
export const useManualRuleGapsFillingConfirmation = () => {
const [isManualRuleGapsFillingConfirmationVisible, showModal, hideModal] = useBoolState();
const confirmationPromiseRef = useRef<(timerange: TimeRange | null) => void>();
const onConfirm = useCallback((timerange: TimeRange) => {
confirmationPromiseRef.current?.(timerange);
}, []);
const onCancel = useCallback(() => {
confirmationPromiseRef.current?.(null);
}, []);
const initModal = useCallback(() => {
showModal();
return new Promise<TimeRange | null>((resolve) => {
confirmationPromiseRef.current = resolve;
}).finally(() => {
hideModal();
});
}, [showModal, hideModal]);
const showManualRuleGapsFillingConfirmation = useCallback(async () => {
const confirmation = await initModal();
if (confirmation) {
onConfirm(confirmation);
} else {
onCancel();
}
return confirmation;
}, [initModal, onConfirm, onCancel]);
return {
isManualRuleGapsFillingConfirmationVisible,
showManualRuleGapsFillingConfirmation,
cancelManualRuleGapsFilling: onCancel,
confirmManualRuleGapsFilling: onConfirm,
};
};

View file

@ -0,0 +1,208 @@
import React, { useCallback, useState } from 'react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { Toast } from '@kbn/core/public';
import { EuiButton, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import * as i18n from './translations';
import { useManualRuleGapsFillingConfirmation } from '../manual_rule_fill_gaps/use_manual_rule_gaps_filling_confirmation';
import { ManualRuleGapsFillingModal } from '../manual_rule_fill_gaps';
import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action';
import {
BulkActionsDryRunErrCode,
BulkActionsDryRunErrCodeEnum,
BulkActionTypeEnum,
} from '../../../../../common/api/detection_engine/rule_management';
import { useBulkActionsDryRun } from '../../../rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions_dry_run';
import { useBulkActionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions_confirmation';
import { useInvalidateFindGapsQuery } from '../../api/hooks/use_find_gaps_for_rule';
import { useInvalidateFindBackfillQuery } from '../../api/hooks/use_find_backfills_for_rules';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useKibana } from '../../../../common/lib/kibana';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
import { BULK_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
interface BulkActionRuleErrorItemProps {
errorCode: BulkActionsDryRunErrCode | undefined;
message: string;
}
const ErrorMessage = ({
errorCode,
message,
}: BulkActionRuleErrorItemProps) => {
switch (errorCode) {
case BulkActionsDryRunErrCodeEnum.RULE_FILL_GAPS_DISABLED_RULE:
return (
<EuiText>
{i18n.GAP_FILL_ALL_GAPS_ERROR_DISABLED_RULE_MESSAGE}
</EuiText>
);
default:
return (
<EuiText>
{i18n.GAP_FILL_ALL_GAPS_UNKNOWN_ERROR_MESSAGE(message)}
</EuiText>
);
}
};
type Props = {
ruleId: string
}
export const FillRuleGapsButton = ({ ruleId }: Props) => {
const {
isManualRuleGapsFillingConfirmationVisible,
showManualRuleGapsFillingConfirmation,
cancelManualRuleGapsFilling,
confirmManualRuleGapsFilling,
} = useManualRuleGapsFillingConfirmation()
const { executeBulkAction } = useExecuteBulkAction();
const { isBulkActionsDryRunLoading, executeBulkActionsDryRun } = useBulkActionsDryRun();
const {
bulkActionsDryRunResult,
bulkAction,
isBulkActionConfirmationVisible,
showBulkActionConfirmation,
cancelBulkActionConfirmation,
} = useBulkActionsConfirmation();
const [isBulkActionExecuteLoading, setIsBulkActionExecuteLoading] = useState(false)
const invalidateFindGapsQuery = useInvalidateFindGapsQuery();
const invalidateFindBackfillsQuery = useInvalidateFindBackfillQuery();
const toasts = useAppToasts()
const { services: startServices } = useKibana();
const { startTransaction } = useStartTransaction();
const onFillGapsClick = useCallback(async () => {
if (isBulkActionsDryRunLoading || isBulkActionExecuteLoading) {
return
}
let longTimeWarningToast: Toast;
let isBulkFillGapsFinished = false;
startTransaction({ name: BULK_RULE_ACTIONS.FILL_GAPS });
setIsBulkActionExecuteLoading(true);
const dryRunResult = await executeBulkActionsDryRun({
type: BulkActionTypeEnum.fill_gaps,
ids: [ruleId],
fillGapsPayload: {
start_date: new Date(Date.now() - 1000).toISOString(),
end_date: new Date().toISOString(),
},
});
setIsBulkActionExecuteLoading(false);
const hasActionBeenConfirmed = await showBulkActionConfirmation(
dryRunResult,
BulkActionTypeEnum.fill_gaps
);
if (hasActionBeenConfirmed === false) {
return;
}
const modalManualGapsFillingConfirmationResult = await showManualRuleGapsFillingConfirmation();
// startServices.telemetry.reportEvent(ManualRuleRunEventTypes.FillGaps, {
// type: 'bulk',
// });
if (modalManualGapsFillingConfirmationResult === null) {
return;
}
setIsBulkActionExecuteLoading(true)
const hideWarningToast = () => {
if (longTimeWarningToast) {
toasts.api.remove(longTimeWarningToast);
}
};
// show warning toast only if bulk fill gaps action exceeds 5s
// if bulkAction already finished, we won't show toast at all (hence flag "isBulkFillGapsFinished")
setTimeout(() => {
if (isBulkFillGapsFinished) {
return;
}
longTimeWarningToast = toasts.addWarning(
{
title: i18n.BULK_FILL_RULE_GAPS_WARNING_TOAST_TITLE,
text: toMountPoint(
<>
<p>
{i18n.BULK_FILL_RULE_GAPS_WARNING_TOAST_DESCRIPTION}
</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton color="warning" size="s" onClick={hideWarningToast}>
{i18n.BULK_FILL_RULE_GAPS_WARNING_TOAST_NOTIFY}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>,
startServices
),
iconType: undefined,
},
{ toastLifeTimeMs: 10 * 60 * 1000 }
);
}, 5 * 1000);
await executeBulkAction({
type: BulkActionTypeEnum.fill_gaps,
ids: [ruleId],
fillGapsPayload: {
start_date: modalManualGapsFillingConfirmationResult.startDate.toISOString(),
end_date: modalManualGapsFillingConfirmationResult.endDate.toISOString(),
},
});
isBulkFillGapsFinished = true
hideWarningToast();
setIsBulkActionExecuteLoading(false)
invalidateFindGapsQuery()
invalidateFindBackfillsQuery()
// startServices.telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunExecute, {
// rangeInMs: modalManualGapsFillingConfirmationResult.endDate.diff(
// modalManualGapsFillingConfirmationResult.startDate
// ),
// status: 'success',
// rulesCount: enabledIds.length,
// });
}, [isBulkActionsDryRunLoading, isBulkActionExecuteLoading, startServices, showManualRuleGapsFillingConfirmation, showBulkActionConfirmation])
return (
<>
{isManualRuleGapsFillingConfirmationVisible && (
<ManualRuleGapsFillingModal onCancel={cancelManualRuleGapsFilling} onConfirm={confirmManualRuleGapsFilling} rulesCount={1} />
)}
{isBulkActionConfirmationVisible && bulkAction && (
<EuiConfirmModal
title={i18n.GAPS_FILL_ALL_GAPS_DRY_RUN_MODAL_HEADING}
onCancel={cancelBulkActionConfirmation}
onConfirm={cancelBulkActionConfirmation}
confirmButtonText={i18n.GAPS_FILL_ALL_GAPS_DRY_RUN_FAILED_MODAL_CLOSE_BUTTON_LABEL}
defaultFocusedButton="confirm"
data-test-subj="bulkActionRejectModal"
>
<ErrorMessage errorCode={bulkActionsDryRunResult?.ruleErrors[0].errorCode} message={bulkActionsDryRunResult?.ruleErrors[0].message ?? ''} />
</EuiConfirmModal>
)}
<EuiButton
data-test-subj="fill_rule_gaps_button"
color="primary"
onClick={onFillGapsClick}
aria-label={i18n.GAPS_FILL_ALL_GAPS_BUTTON_LABEL}
fill={true}
isLoading={isBulkActionsDryRunLoading || isBulkActionExecuteLoading}
>
{i18n.GAPS_FILL_ALL_GAPS_BUTTON_LABEL}
</EuiButton>
</>)
}

View file

@ -40,6 +40,7 @@ import { getStatusLabel } from './utils';
import { GapStatusFilter } from './status_filter';
import { useFindGapsForRule } from '../../api/hooks/use_find_gaps_for_rule';
import { FillGap } from './fill_gap';
import { FillRuleGapsButton } from './fill_rule_gaps_button';
const DatePickerEuiFlexItem = styled(EuiFlexItem)`
max-width: 582px;
`;
@ -270,7 +271,7 @@ export const RuleGaps = ({ ruleId, enabled }: { ruleId: string; enabled: boolean
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<GapStatusFilter selectedItems={selectedStatuses} onChange={handleStatusChange} />
</EuiFlexItem>
@ -290,6 +291,9 @@ export const RuleGaps = ({ ruleId, enabled }: { ruleId: string; enabled: boolean
/>
</DatePickerEuiFlexItem>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FillRuleGapsButton ruleId={ruleId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -179,3 +179,61 @@ export const GAPS_TABLE_TOTAL_GAPS_LABEL = (totalItems: number, maxItems: number
values: { totalItems, maxItems },
defaultMessage: `More than {totalItems} gaps match filters provided. Showing first {maxItems}. Constrain filters further to view additional gaps.`,
});
export const GAP_FILL_ALL_GAPS_ERROR_DISABLED_RULE_MESSAGE = i18n.translate(
'xpack.securitySolution.gaps.dryRunFillAllGaps.failedModalDisabledRuleErrorLabel',
{
defaultMessage: 'Cannot schedule manual rule run for a disabled rule',
}
);
export const GAP_FILL_ALL_GAPS_UNKNOWN_ERROR_MESSAGE = (message: string) => i18n.translate(
'xpack.securitySolution.gaps.dryRunFillAllGaps.failedModalUnknownErrorLabel',
{
values: { message },
defaultMessage: 'Cannot fill gaps for this rule ({message})',
}
);
export const GAPS_FILL_ALL_GAPS_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.gaps.fillAllGapsButtonLabel',
{
defaultMessage: 'Fill gaps',
}
);
export const GAPS_FILL_ALL_GAPS_DRY_RUN_FAILED_MODAL_CLOSE_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.gaps.dryRunFillAllGaps.failedModalCloseButtonLabel',
{
defaultMessage: 'Close',
}
);
export const GAPS_FILL_ALL_GAPS_DRY_RUN_MODAL_HEADING = i18n.translate(
'xpack.securitySolution.gaps.dryRunFillAllGaps.failedModalHeading',
{
defaultMessage: 'The rule gaps cannot be filled',
}
);
export const BULK_FILL_RULE_GAPS_WARNING_TOAST_TITLE = i18n.translate(
'xpack.securitySolution.gaps.fillRuleGapsLongRunWarningToastTitle',
{
defaultMessage: 'Rule update is in progress',
}
);
export const BULK_FILL_RULE_GAPS_WARNING_TOAST_DESCRIPTION = i18n.translate(
'xpack.securitySolution.gaps.fillRuleGapsLongRunWarningToastMessage',
{
defaultMessage: 'Scheduling the filling of the gaps for this rule.',
}
);
export const BULK_FILL_RULE_GAPS_WARNING_TOAST_NOTIFY = i18n.translate(
'xpack.securitySolution.gaps.fillRuleGapsLongRunWarningToastNotifyLabel',
{
defaultMessage: `Notify me when done`,
}
);

View file

@ -42,6 +42,7 @@ import type {
CoverageOverviewResponse,
GetRuleManagementFiltersResponse,
ImportRulesResponse,
BulkManualRuleFillGaps,
} from '../../../../common/api/detection_engine/rule_management';
import {
BulkActionTypeEnum,
@ -352,6 +353,7 @@ type PlainBulkAction = {
| BulkActionTypeEnum['export']
| BulkActionTypeEnum['duplicate']
| BulkActionTypeEnum['run']
| BulkActionTypeEnum['fill_gaps']
>;
} & QueryOrIds;
@ -370,11 +372,17 @@ export type ManualRuleRunBulkAction = {
runPayload: BulkManualRuleRun['run'];
} & QueryOrIds;
export type ManualRuleFillGapsAction = {
type: BulkActionTypeEnum['fill_gaps'];
fillGapsPayload: BulkManualRuleFillGaps['fill_gaps'];
} & QueryOrIds;
export type BulkAction =
| PlainBulkAction
| EditBulkAction
| DuplicateBulkAction
| ManualRuleRunBulkAction;
| ManualRuleRunBulkAction
| ManualRuleFillGapsAction;
export interface PerformRulesBulkActionProps {
bulkAction: BulkAction;
@ -403,6 +411,7 @@ export async function performBulkAction({
run: bulkAction.type === BulkActionTypeEnum.run ? bulkAction.runPayload : undefined,
gaps_range_start: 'gapRange' in bulkAction ? bulkAction.gapRange?.start : undefined,
gaps_range_end: 'gapRange' in bulkAction ? bulkAction.gapRange?.end : undefined,
fill_gaps: bulkAction.type === BulkActionTypeEnum.fill_gaps ? bulkAction.fillGapsPayload : undefined,
};
return KibanaServices.get().http.fetch<BulkActionResponse>(DETECTION_ENGINE_RULES_BULK_ACTION, {

View file

@ -36,6 +36,9 @@ export function summarizeBulkSuccess(action: BulkActionType): string {
case BulkActionTypeEnum.run:
return i18n.RULES_BULK_MANUAL_RULE_RUN_SUCCESS;
case BulkActionTypeEnum.fill_gaps:
return i18n.RULES_BULK_MANUAL_RULE_RUN_SUCCESS;
case BulkActionTypeEnum.fill_gaps:
return i18n.RULES_BULK_FILL_GAPS_SUCCESS;
@ -115,6 +118,9 @@ export function summarizeBulkError(action: BulkActionType): string {
case BulkActionTypeEnum.run:
return i18n.RULES_BULK_MANUAL_RULE_RUN_FAILURE;
case BulkActionTypeEnum.fill_gaps:
return i18n.RULES_BULK_FILL_GAPS_FAILURE;
case BulkActionTypeEnum.fill_gaps:
return i18n.RULES_BULK_FILL_GAPS_FAILURE;
@ -150,6 +156,9 @@ export function explainBulkError(action: BulkActionType, error: HTTPError): stri
case BulkActionTypeEnum.run:
return i18n.RULES_BULK_MANUAL_RULE_RUN_FAILURE_DESCRIPTION(summary.failed);
case BulkActionTypeEnum.fill_gaps:
return i18n.RULES_BULK_FILL_GAPS_FAILURE_DESCRIPTION(summary.failed);
case BulkActionTypeEnum.fill_gaps:
return i18n.RULES_BULK_FILL_GAPS_FAILURE_DESCRIPTION(summary.failed);

View file

@ -26,6 +26,8 @@ const getActionRejectedTitle = (
return i18n.BULK_EXPORT_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
case BulkActionTypeEnum.run:
return i18n.BULK_MANUAL_RULE_RUN_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
case BulkActionTypeEnum.fill_gaps:
return i18n.BULK_FILL_RULE_GAPS_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
default:
assertUnreachable(bulkAction);
}
@ -42,6 +44,8 @@ const getActionConfirmLabel = (
return i18n.BULK_EXPORT_CONFIRMATION_CONFIRM(succeededRulesCount);
case BulkActionTypeEnum.run:
return i18n.BULK_MANUAL_RULE_RUN_CONFIRMATION_CONFIRM(succeededRulesCount);
case BulkActionTypeEnum.fill_gaps:
return i18n.BULK_FILL_RULE_GAPS_CONFIRMATION_CONFIRM(succeededRulesCount);
default:
assertUnreachable(bulkAction);
}

View file

@ -165,6 +165,35 @@ const BulkManualRuleRunErrorItem = ({
}
};
const BulkFillRuleGapsErrorItem = ({
errorCode,
message,
rulesCount,
}: BulkActionRuleErrorItemProps) => {
switch (errorCode) {
case BulkActionsDryRunErrCodeEnum.RULE_FILL_GAPS_DISABLED_RULE:
return (
<li key={message}>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.fillGapsDisabledRuleDescription"
defaultMessage="{rulesCount, plural, =1 {# rule} other {# rules}} (Cannot fill gaps for disabled rules)"
values={{ rulesCount }}
/>
</li>
);
default:
return (
<li key={message}>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.defaultScheduleRuleRunFailureDescription"
defaultMessage="Cannot fill gaps for {rulesCount, plural, =1 {# rule} other {# rules}} ({message})"
values={{ rulesCount, message }}
/>
</li>
);
}
};
interface BulkActionRuleErrorsListProps {
ruleErrors: DryRunResult['ruleErrors'];
bulkAction: BulkActionForConfirmation;
@ -215,6 +244,15 @@ const BulkActionRuleErrorsListComponent = ({
rulesCount={rulesCount}
/>
);
case BulkActionTypeEnum.fill_gaps:
return (
<BulkFillRuleGapsErrorItem
message={message}
errorCode={errorCode}
rulesCount={rulesCount}
/>
);
default:
return null;

View file

@ -11,15 +11,17 @@ import type {
} from '../../../../../../common/api/detection_engine/rule_management';
/**
* Only 3 bulk actions are supported for for confirmation dry run modal:
* Only 4 bulk actions are supported for for confirmation dry run modal:
* * export
* * edit
* * manual rule run
* * fill gaps
*/
export type BulkActionForConfirmation =
| BulkActionTypeEnum['export']
| BulkActionTypeEnum['edit']
| BulkActionTypeEnum['run'];
| BulkActionTypeEnum['run']
| BulkActionTypeEnum['fill_gaps'];
/**
* transformed results of dry run

View file

@ -58,6 +58,7 @@ interface UseBulkActionsArgs {
) => Promise<boolean>;
showBulkDuplicateConfirmation: () => Promise<string | null>;
showManualRuleRunConfirmation: () => Promise<TimeRange | null>;
showManualRuleGapsFillingConfirmation: () => Promise<TimeRange | null>;
showManualRuleRunLimitError: () => void;
completeBulkEditForm: (
bulkActionEditType: BulkActionEditType
@ -71,6 +72,7 @@ export const useBulkActions = ({
showBulkActionConfirmation,
showBulkDuplicateConfirmation,
showManualRuleRunConfirmation,
showManualRuleGapsFillingConfirmation,
showManualRuleRunLimitError,
completeBulkEditForm,
executeBulkActionsDryRun,
@ -276,6 +278,111 @@ export const useBulkActions = ({
});
};
const handleScheduleFillGapsAction = async () => {
let longTimeWarningToast: Toast;
let isBulkFillGapsFinished = false;
startTransaction({ name: BULK_RULE_ACTIONS.FILL_GAPS });
closePopover();
setIsPreflightInProgress(true);
const dryRunResult = await executeBulkActionsDryRun({
type: BulkActionTypeEnum.fill_gaps,
...(isAllSelected
? { query: convertRulesFilterToKQL(filterOptions) }
: { ids: selectedRuleIds }),
fillGapsPayload: {
start_date: new Date(Date.now() - 1000).toISOString(),
end_date: new Date().toISOString(),
},
});
setIsPreflightInProgress(false);
if ((dryRunResult?.succeededRulesCount ?? 0) > MAX_MANUAL_RULE_RUN_BULK_SIZE) {
showManualRuleRunLimitError();
return;
}
const hasActionBeenConfirmed = await showBulkActionConfirmation(
dryRunResult,
BulkActionTypeEnum.fill_gaps
);
if (hasActionBeenConfirmed === false) {
return;
}
const modalManualGapsFillingConfirmationResult = await showManualRuleGapsFillingConfirmation();
// startServices.telemetry.reportEvent(ManualRuleRunEventTypes.FillGaps, {
// type: 'bulk',
// });
if (modalManualGapsFillingConfirmationResult === null) {
return;
}
const enabledIds = selectedRules.filter(({ enabled }) => enabled).map(({ id }) => id);
const hideWarningToast = () => {
if (longTimeWarningToast) {
toasts.api.remove(longTimeWarningToast);
}
};
// show warning toast only if bulk fill gaps action exceeds 5s
// if bulkAction already finished, we won't show toast at all (hence flag "isBulkFillGapsFinished")
setTimeout(() => {
if (isBulkFillGapsFinished) {
return;
}
longTimeWarningToast = toasts.addWarning(
{
title: i18n.BULK_FILL_RULE_GAPS_WARNING_TOAST_TITLE,
text: toMountPoint(
<>
<p>
{i18n.BULK_FILL_RULE_GAPS_WARNING_TOAST_DESCRIPTION(
dryRunResult?.succeededRulesCount ?? 0
)}
</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton color="warning" size="s" onClick={hideWarningToast}>
{i18n.BULK_FILL_RULE_GAPS_WARNING_TOAST_NOTIFY}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>,
startServices
),
iconType: undefined,
},
{ toastLifeTimeMs: 10 * 60 * 1000 }
);
}, 5 * 1000);
await executeBulkAction({
type: BulkActionTypeEnum.fill_gaps,
...(isAllSelected ? { query: kql } : { ids: enabledIds }),
fillGapsPayload: {
start_date: modalManualGapsFillingConfirmationResult.startDate.toISOString(),
end_date: modalManualGapsFillingConfirmationResult.endDate.toISOString(),
},
});
isBulkFillGapsFinished = true
hideWarningToast()
// startServices.telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunExecute, {
// rangeInMs: modalManualGapsFillingConfirmationResult.endDate.diff(
// modalManualGapsFillingConfirmationResult.startDate
// ),
// status: 'success',
// rulesCount: enabledIds.length,
// });
};
const handleBulkEdit = (bulkEditActionType: BulkActionEditType) => async () => {
let longTimeWarningToast: Toast;
let isBulkEditFinished = false;
@ -469,6 +576,14 @@ export const useBulkActions = ({
onClick: handleScheduleRuleRunAction,
icon: undefined,
},
{
key: i18n.BULK_ACTION_FILL_GAPS,
name: i18n.BULK_ACTION_FILL_GAPS,
'data-test-subj': 'scheduleFillGaps',
disabled: containsLoading || (!containsEnabled && !isAllSelected),
onClick: handleScheduleFillGapsAction,
icon: undefined,
},
{
key: i18n.BULK_ACTION_DISABLE,
name: i18n.BULK_ACTION_DISABLE,

View file

@ -46,6 +46,8 @@ import { ManualRuleRunModal } from '../../../rule_gaps/components/manual_rule_ru
import { BulkManualRuleRunLimitErrorModal } from './bulk_actions/bulk_manual_rule_run_limit_error_modal';
import { RulesWithGapsOverviewPanel } from '../../../rule_gaps/components/rules_with_gaps_overview_panel';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { ManualRuleGapsFillingModal } from '../../../rule_gaps/components/manual_rule_fill_gaps';
import { useManualRuleGapsFillingConfirmation } from '../../../rule_gaps/components/manual_rule_fill_gaps/use_manual_rule_gaps_filling_confirmation';
const INITIAL_SORT_FIELD = 'enabled';
@ -126,6 +128,13 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
confirmManualRuleRun,
} = useManualRuleRunConfirmation();
const {
isManualRuleGapsFillingConfirmationVisible,
showManualRuleGapsFillingConfirmation,
cancelManualRuleGapsFilling,
confirmManualRuleGapsFilling,
} = useManualRuleGapsFillingConfirmation();
const [
isManualRuleRunLimitErrorVisible,
showManualRuleRunLimitError,
@ -148,6 +157,7 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
showBulkActionConfirmation,
showBulkDuplicateConfirmation,
showManualRuleRunConfirmation,
showManualRuleGapsFillingConfirmation,
showManualRuleRunLimitError,
completeBulkEditForm,
executeBulkActionsDryRun,
@ -295,6 +305,9 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
{isManualRuleRunConfirmationVisible && (
<ManualRuleRunModal onCancel={cancelManualRuleRun} onConfirm={confirmManualRuleRun} />
)}
{isManualRuleGapsFillingConfirmationVisible && (
<ManualRuleGapsFillingModal onCancel={cancelManualRuleGapsFilling} onConfirm={confirmManualRuleGapsFilling} rulesCount={selectedRuleIds.length} />
)}
{isManualRuleRunLimitErrorVisible && (
<BulkManualRuleRunLimitErrorModal onClose={hideManualRuleRunLimitError} />
)}

View file

@ -62,6 +62,8 @@ interface ActionColumnsProps {
confirmDeletion: () => Promise<boolean>;
}
const loadingActionsSet = new Set(['disable', 'enable', 'edit', 'delete', 'run', 'fill_gaps'])
export const useEnabledColumn = ({
hasCRUDPermissions,
startMlJobs,
@ -72,7 +74,7 @@ export const useEnabledColumn = ({
const loadingIds = useMemo(
() =>
['disable', 'enable', 'edit', 'delete', 'run'].includes(loadingRulesAction ?? '')
loadingActionsSet.has(loadingRulesAction ?? '')
? loadingRuleIds
: [],
[loadingRuleIds, loadingRulesAction]

View file

@ -0,0 +1,95 @@
/*
* 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 { enableSelectedRules, scheduleBulkFillGapsForSelectedRules } from '../../../../tasks/rules_bulk_actions';
import { MODAL_ERROR_BODY, TOASTER_BODY } from '../../../../screens/alerts_detection_rules';
import { visitRulesManagementTable } from '../../../../tasks/rules_management';
import {
disableAutoRefresh,
clickErrorToastBtn,
selectAllRules,
selectRulesByName,
} from '../../../../tasks/alerts_detection_rules';
import { getNewRule } from '../../../../objects/rule';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import { createRule } from '../../../../tasks/api_calls/rules';
import { login } from '../../../../tasks/login';
describe('bulk fill rule gaps', {
tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'storeGapsInEventLogEnabled',
])}`,
],
},
},
}, () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
const defaultValues = { enabled: false, interval: '5s', from: 'now-1s' };
Array.from({ length: 5 }).forEach((_, idx) => {
const ruleId = String(idx + 1)
createRule(getNewRule({ rule_id: ruleId, name: `Rule ${ruleId}`, ...defaultValues }));
})
// Create gas
cy.wait(10000)
visitRulesManagementTable();
disableAutoRefresh();
const enabledRules = ['Rule 1', 'Rule 2', 'Rule 4'] as const;
selectRulesByName(enabledRules);
enableSelectedRules()
cy.wait(10000)
});
it('schedule enabled rules', () => {
const enabledRules = ['Rule 1', 'Rule 2', 'Rule 4'] as const;
selectRulesByName(enabledRules);
const enabledCount = enabledRules.length;
const disabledCount = 0;
scheduleBulkFillGapsForSelectedRules(enabledCount, disabledCount);
cy.contains(TOASTER_BODY, `Successfully scheduled gaps filling for ${enabledCount} rules`);
});
it('schedule enable rules and show warning about disabled rules', () => {
const enabledRules = ['Rule 1', 'Rule 2', 'Rule 4'] as const;
const disabledRules = ['Rule 3', 'Rule 5'] as const;
selectRulesByName([...enabledRules, ...disabledRules]);
const enabledCount = enabledRules.length;
const disabledCount = disabledRules.length;
scheduleBulkFillGapsForSelectedRules(enabledCount, disabledCount);
cy.contains(TOASTER_BODY, `Successfully scheduled gaps filling for ${enabledCount} rule`);
});
it('schedule enable rules and show partial error for disabled rules when all rules are selected', () => {
selectAllRules();
const enabledCount = 3;
const disabledCount = 2;
scheduleBulkFillGapsForSelectedRules(enabledCount, disabledCount);
cy.contains(
TOASTER_BODY,
`${disabledCount} rules failed to schedule bulk fill rule gaps.See the full error`
);
// on error toast button click display error that it is not possible to schedule bulk fill rule gaps for disabled rules
clickErrorToastBtn();
cy.contains(MODAL_ERROR_BODY, 'Cannot schedule bulk fill rule gaps for a disabled rule');
});
});

View file

@ -119,4 +119,7 @@ export const BULK_EXPORT_ACTION_BTN = '[data-test-subj="exportRuleBulk"]';
// SCHEDULE MANUAL RULE RUN
export const BULK_MANUAL_RULE_RUN_BTN = '[data-test-subj="scheduleRuleRunBulk"]';
// SCHEDULE BULK FILL GAPS
export const BULK_FILL_RULE_GAPS_BTN = '[data-test-subj="scheduleFillGaps"]';
export const BULK_MANUAL_RULE_RUN_WARNING_MODAL = '[data-test-subj="bulkActionConfirmationModal"]';

View file

@ -58,6 +58,7 @@ import {
UPDATE_SCHEDULE_LOOKBACK_INPUT,
UPDATE_SCHEDULE_MENU_ITEM,
UPDATE_SCHEDULE_TIME_UNIT_SELECT,
BULK_FILL_RULE_GAPS_BTN,
} from '../screens/rules_bulk_actions';
import { SCHEDULE_DETAILS } from '../screens/rule_details';
@ -445,3 +446,20 @@ export const scheduleManualRuleRunForSelectedRules = (
}
cy.get(MODAL_CONFIRMATION_BTN).click();
};
export const scheduleBulkFillGapsForSelectedRules = (
enabledCount: number,
disabledCount: number
) => {
cy.log('Bulk fill gaps for selected rules');
cy.get(BULK_ACTIONS_BTN).click();
cy.get(BULK_FILL_RULE_GAPS_BTN).click();
if (disabledCount > 0) {
cy.get(BULK_MANUAL_RULE_RUN_WARNING_MODAL).should(
'have.text',
`This action can only be applied to ${enabledCount} rulesThis action can't be applied to the following rules in your selection:${disabledCount} rules (Cannot schedule manual rule run for disabled rules)CancelSchedule ${enabledCount} rules`
);
cy.get(CONFIRM_MANUAL_RULE_RUN_WARNING_BTN).click();
}
cy.get(MODAL_CONFIRMATION_BTN).click();
};