mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
WIP FE
This commit is contained in:
parent
f5c2bb3d3a
commit
aeef8f9eff
22 changed files with 1013 additions and 6 deletions
|
@ -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_LOOKBACK_WINDOW_DAYS = 90;
|
||||||
export const MAX_MANUAL_RULE_RUN_BULK_SIZE = 100;
|
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
|
* Whether it is a Jest environment
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const BULK_RULE_ACTIONS = {
|
||||||
DUPLICATE: `${APP_UI_ID} bulkRuleActions duplicate`,
|
DUPLICATE: `${APP_UI_ID} bulkRuleActions duplicate`,
|
||||||
EXPORT: `${APP_UI_ID} bulkRuleActions export`,
|
EXPORT: `${APP_UI_ID} bulkRuleActions export`,
|
||||||
MANUAL_RULE_RUN: `${APP_UI_ID} bulkRuleActions manual rule run`,
|
MANUAL_RULE_RUN: `${APP_UI_ID} bulkRuleActions manual rule run`,
|
||||||
|
FILL_GAPS: `${APP_UI_ID} bulkRuleActions fill gaps`,
|
||||||
DELETE: `${APP_UI_ID} bulkRuleActions delete`,
|
DELETE: `${APP_UI_ID} bulkRuleActions delete`,
|
||||||
EDIT: `${APP_UI_ID} bulkRuleActions edit`,
|
EDIT: `${APP_UI_ID} bulkRuleActions edit`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(
|
export const BULK_ACTION_DUPLICATE = i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle',
|
'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) =>
|
export const BULK_EXPORT_CONFIRMATION_REJECTED_TITLE = (rulesCount: number) =>
|
||||||
i18n.translate(
|
i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkExportConfirmationDeniedTitle',
|
'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) =>
|
export const BULK_EDIT_CONFIRMATION_REJECTED_TITLE = (rulesCount: number) =>
|
||||||
i18n.translate(
|
i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationDeniedTitle',
|
'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(
|
export const BULK_MANUAL_RULE_RUN_LIMIT_ERROR_TITLE = i18n.translate(
|
||||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkManualRuleRunLimitErrorMessage',
|
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkManualRuleRunLimitErrorMessage',
|
||||||
{
|
{
|
||||||
|
|
|
@ -862,7 +862,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
||||||
</>
|
</>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`/rules/id/:detailName/:tabName(${RuleDetailTabs.executionEvents})`}>
|
<Route path={`/rules/id/:detailName/:tabName(${RuleDetailTabs.executionEvents})`}>
|
||||||
<ExecutionEventsTable ruleId={ruleId} />
|
<ExecutionEventsTable ruleId={ruleId} shouldRefetch={false} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</StyledMinHeightTabContainer>
|
</StyledMinHeightTabContainer>
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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';
|
|
@ -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.',
|
||||||
|
}
|
||||||
|
);
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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>
|
||||||
|
</>)
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import { getStatusLabel } from './utils';
|
||||||
import { GapStatusFilter } from './status_filter';
|
import { GapStatusFilter } from './status_filter';
|
||||||
import { useFindGapsForRule } from '../../api/hooks/use_find_gaps_for_rule';
|
import { useFindGapsForRule } from '../../api/hooks/use_find_gaps_for_rule';
|
||||||
import { FillGap } from './fill_gap';
|
import { FillGap } from './fill_gap';
|
||||||
|
import { FillRuleGapsButton } from './fill_rule_gaps_button';
|
||||||
const DatePickerEuiFlexItem = styled(EuiFlexItem)`
|
const DatePickerEuiFlexItem = styled(EuiFlexItem)`
|
||||||
max-width: 582px;
|
max-width: 582px;
|
||||||
`;
|
`;
|
||||||
|
@ -270,7 +271,7 @@ export const RuleGaps = ({ ruleId, enabled }: { ruleId: string; enabled: boolean
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
<EuiFlexItem grow={true}>
|
<EuiFlexItem grow={true}>
|
||||||
<EuiFlexGroup justifyContent="flexEnd">
|
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<GapStatusFilter selectedItems={selectedStatuses} onChange={handleStatusChange} />
|
<GapStatusFilter selectedItems={selectedStatuses} onChange={handleStatusChange} />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
@ -290,6 +291,9 @@ export const RuleGaps = ({ ruleId, enabled }: { ruleId: string; enabled: boolean
|
||||||
/>
|
/>
|
||||||
</DatePickerEuiFlexItem>
|
</DatePickerEuiFlexItem>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<FillRuleGapsButton ruleId={ruleId} />
|
||||||
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
|
@ -179,3 +179,61 @@ export const GAPS_TABLE_TOTAL_GAPS_LABEL = (totalItems: number, maxItems: number
|
||||||
values: { totalItems, maxItems },
|
values: { totalItems, maxItems },
|
||||||
defaultMessage: `More than {totalItems} gaps match filters provided. Showing first {maxItems}. Constrain filters further to view additional gaps.`,
|
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`,
|
||||||
|
}
|
||||||
|
);
|
|
@ -42,6 +42,7 @@ import type {
|
||||||
CoverageOverviewResponse,
|
CoverageOverviewResponse,
|
||||||
GetRuleManagementFiltersResponse,
|
GetRuleManagementFiltersResponse,
|
||||||
ImportRulesResponse,
|
ImportRulesResponse,
|
||||||
|
BulkManualRuleFillGaps,
|
||||||
} from '../../../../common/api/detection_engine/rule_management';
|
} from '../../../../common/api/detection_engine/rule_management';
|
||||||
import {
|
import {
|
||||||
BulkActionTypeEnum,
|
BulkActionTypeEnum,
|
||||||
|
@ -352,6 +353,7 @@ type PlainBulkAction = {
|
||||||
| BulkActionTypeEnum['export']
|
| BulkActionTypeEnum['export']
|
||||||
| BulkActionTypeEnum['duplicate']
|
| BulkActionTypeEnum['duplicate']
|
||||||
| BulkActionTypeEnum['run']
|
| BulkActionTypeEnum['run']
|
||||||
|
| BulkActionTypeEnum['fill_gaps']
|
||||||
>;
|
>;
|
||||||
} & QueryOrIds;
|
} & QueryOrIds;
|
||||||
|
|
||||||
|
@ -370,11 +372,17 @@ export type ManualRuleRunBulkAction = {
|
||||||
runPayload: BulkManualRuleRun['run'];
|
runPayload: BulkManualRuleRun['run'];
|
||||||
} & QueryOrIds;
|
} & QueryOrIds;
|
||||||
|
|
||||||
|
export type ManualRuleFillGapsAction = {
|
||||||
|
type: BulkActionTypeEnum['fill_gaps'];
|
||||||
|
fillGapsPayload: BulkManualRuleFillGaps['fill_gaps'];
|
||||||
|
} & QueryOrIds;
|
||||||
|
|
||||||
export type BulkAction =
|
export type BulkAction =
|
||||||
| PlainBulkAction
|
| PlainBulkAction
|
||||||
| EditBulkAction
|
| EditBulkAction
|
||||||
| DuplicateBulkAction
|
| DuplicateBulkAction
|
||||||
| ManualRuleRunBulkAction;
|
| ManualRuleRunBulkAction
|
||||||
|
| ManualRuleFillGapsAction;
|
||||||
|
|
||||||
export interface PerformRulesBulkActionProps {
|
export interface PerformRulesBulkActionProps {
|
||||||
bulkAction: BulkAction;
|
bulkAction: BulkAction;
|
||||||
|
@ -403,6 +411,7 @@ export async function performBulkAction({
|
||||||
run: bulkAction.type === BulkActionTypeEnum.run ? bulkAction.runPayload : undefined,
|
run: bulkAction.type === BulkActionTypeEnum.run ? bulkAction.runPayload : undefined,
|
||||||
gaps_range_start: 'gapRange' in bulkAction ? bulkAction.gapRange?.start : undefined,
|
gaps_range_start: 'gapRange' in bulkAction ? bulkAction.gapRange?.start : undefined,
|
||||||
gaps_range_end: 'gapRange' in bulkAction ? bulkAction.gapRange?.end : 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, {
|
return KibanaServices.get().http.fetch<BulkActionResponse>(DETECTION_ENGINE_RULES_BULK_ACTION, {
|
||||||
|
|
|
@ -36,6 +36,9 @@ export function summarizeBulkSuccess(action: BulkActionType): string {
|
||||||
|
|
||||||
case BulkActionTypeEnum.run:
|
case BulkActionTypeEnum.run:
|
||||||
return i18n.RULES_BULK_MANUAL_RULE_RUN_SUCCESS;
|
return i18n.RULES_BULK_MANUAL_RULE_RUN_SUCCESS;
|
||||||
|
|
||||||
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
|
return i18n.RULES_BULK_MANUAL_RULE_RUN_SUCCESS;
|
||||||
|
|
||||||
case BulkActionTypeEnum.fill_gaps:
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
return i18n.RULES_BULK_FILL_GAPS_SUCCESS;
|
return i18n.RULES_BULK_FILL_GAPS_SUCCESS;
|
||||||
|
@ -115,6 +118,9 @@ export function summarizeBulkError(action: BulkActionType): string {
|
||||||
|
|
||||||
case BulkActionTypeEnum.run:
|
case BulkActionTypeEnum.run:
|
||||||
return i18n.RULES_BULK_MANUAL_RULE_RUN_FAILURE;
|
return i18n.RULES_BULK_MANUAL_RULE_RUN_FAILURE;
|
||||||
|
|
||||||
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
|
return i18n.RULES_BULK_FILL_GAPS_FAILURE;
|
||||||
|
|
||||||
case BulkActionTypeEnum.fill_gaps:
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
return i18n.RULES_BULK_FILL_GAPS_FAILURE;
|
return i18n.RULES_BULK_FILL_GAPS_FAILURE;
|
||||||
|
@ -150,6 +156,9 @@ export function explainBulkError(action: BulkActionType, error: HTTPError): stri
|
||||||
|
|
||||||
case BulkActionTypeEnum.run:
|
case BulkActionTypeEnum.run:
|
||||||
return i18n.RULES_BULK_MANUAL_RULE_RUN_FAILURE_DESCRIPTION(summary.failed);
|
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:
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
return i18n.RULES_BULK_FILL_GAPS_FAILURE_DESCRIPTION(summary.failed);
|
return i18n.RULES_BULK_FILL_GAPS_FAILURE_DESCRIPTION(summary.failed);
|
||||||
|
|
|
@ -26,6 +26,8 @@ const getActionRejectedTitle = (
|
||||||
return i18n.BULK_EXPORT_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
|
return i18n.BULK_EXPORT_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
|
||||||
case BulkActionTypeEnum.run:
|
case BulkActionTypeEnum.run:
|
||||||
return i18n.BULK_MANUAL_RULE_RUN_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
|
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:
|
default:
|
||||||
assertUnreachable(bulkAction);
|
assertUnreachable(bulkAction);
|
||||||
}
|
}
|
||||||
|
@ -42,6 +44,8 @@ const getActionConfirmLabel = (
|
||||||
return i18n.BULK_EXPORT_CONFIRMATION_CONFIRM(succeededRulesCount);
|
return i18n.BULK_EXPORT_CONFIRMATION_CONFIRM(succeededRulesCount);
|
||||||
case BulkActionTypeEnum.run:
|
case BulkActionTypeEnum.run:
|
||||||
return i18n.BULK_MANUAL_RULE_RUN_CONFIRMATION_CONFIRM(succeededRulesCount);
|
return i18n.BULK_MANUAL_RULE_RUN_CONFIRMATION_CONFIRM(succeededRulesCount);
|
||||||
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
|
return i18n.BULK_FILL_RULE_GAPS_CONFIRMATION_CONFIRM(succeededRulesCount);
|
||||||
default:
|
default:
|
||||||
assertUnreachable(bulkAction);
|
assertUnreachable(bulkAction);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
interface BulkActionRuleErrorsListProps {
|
||||||
ruleErrors: DryRunResult['ruleErrors'];
|
ruleErrors: DryRunResult['ruleErrors'];
|
||||||
bulkAction: BulkActionForConfirmation;
|
bulkAction: BulkActionForConfirmation;
|
||||||
|
@ -215,6 +244,15 @@ const BulkActionRuleErrorsListComponent = ({
|
||||||
rulesCount={rulesCount}
|
rulesCount={rulesCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case BulkActionTypeEnum.fill_gaps:
|
||||||
|
return (
|
||||||
|
<BulkFillRuleGapsErrorItem
|
||||||
|
message={message}
|
||||||
|
errorCode={errorCode}
|
||||||
|
rulesCount={rulesCount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -11,15 +11,17 @@ import type {
|
||||||
} from '../../../../../../common/api/detection_engine/rule_management';
|
} 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
|
* * export
|
||||||
* * edit
|
* * edit
|
||||||
* * manual rule run
|
* * manual rule run
|
||||||
|
* * fill gaps
|
||||||
*/
|
*/
|
||||||
export type BulkActionForConfirmation =
|
export type BulkActionForConfirmation =
|
||||||
| BulkActionTypeEnum['export']
|
| BulkActionTypeEnum['export']
|
||||||
| BulkActionTypeEnum['edit']
|
| BulkActionTypeEnum['edit']
|
||||||
| BulkActionTypeEnum['run'];
|
| BulkActionTypeEnum['run']
|
||||||
|
| BulkActionTypeEnum['fill_gaps'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* transformed results of dry run
|
* transformed results of dry run
|
||||||
|
|
|
@ -58,6 +58,7 @@ interface UseBulkActionsArgs {
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
showBulkDuplicateConfirmation: () => Promise<string | null>;
|
showBulkDuplicateConfirmation: () => Promise<string | null>;
|
||||||
showManualRuleRunConfirmation: () => Promise<TimeRange | null>;
|
showManualRuleRunConfirmation: () => Promise<TimeRange | null>;
|
||||||
|
showManualRuleGapsFillingConfirmation: () => Promise<TimeRange | null>;
|
||||||
showManualRuleRunLimitError: () => void;
|
showManualRuleRunLimitError: () => void;
|
||||||
completeBulkEditForm: (
|
completeBulkEditForm: (
|
||||||
bulkActionEditType: BulkActionEditType
|
bulkActionEditType: BulkActionEditType
|
||||||
|
@ -71,6 +72,7 @@ export const useBulkActions = ({
|
||||||
showBulkActionConfirmation,
|
showBulkActionConfirmation,
|
||||||
showBulkDuplicateConfirmation,
|
showBulkDuplicateConfirmation,
|
||||||
showManualRuleRunConfirmation,
|
showManualRuleRunConfirmation,
|
||||||
|
showManualRuleGapsFillingConfirmation,
|
||||||
showManualRuleRunLimitError,
|
showManualRuleRunLimitError,
|
||||||
completeBulkEditForm,
|
completeBulkEditForm,
|
||||||
executeBulkActionsDryRun,
|
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 () => {
|
const handleBulkEdit = (bulkEditActionType: BulkActionEditType) => async () => {
|
||||||
let longTimeWarningToast: Toast;
|
let longTimeWarningToast: Toast;
|
||||||
let isBulkEditFinished = false;
|
let isBulkEditFinished = false;
|
||||||
|
@ -469,6 +576,14 @@ export const useBulkActions = ({
|
||||||
onClick: handleScheduleRuleRunAction,
|
onClick: handleScheduleRuleRunAction,
|
||||||
icon: undefined,
|
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,
|
key: i18n.BULK_ACTION_DISABLE,
|
||||||
name: i18n.BULK_ACTION_DISABLE,
|
name: i18n.BULK_ACTION_DISABLE,
|
||||||
|
|
|
@ -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 { BulkManualRuleRunLimitErrorModal } from './bulk_actions/bulk_manual_rule_run_limit_error_modal';
|
||||||
import { RulesWithGapsOverviewPanel } from '../../../rule_gaps/components/rules_with_gaps_overview_panel';
|
import { RulesWithGapsOverviewPanel } from '../../../rule_gaps/components/rules_with_gaps_overview_panel';
|
||||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
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';
|
const INITIAL_SORT_FIELD = 'enabled';
|
||||||
|
|
||||||
|
@ -126,6 +128,13 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
||||||
confirmManualRuleRun,
|
confirmManualRuleRun,
|
||||||
} = useManualRuleRunConfirmation();
|
} = useManualRuleRunConfirmation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isManualRuleGapsFillingConfirmationVisible,
|
||||||
|
showManualRuleGapsFillingConfirmation,
|
||||||
|
cancelManualRuleGapsFilling,
|
||||||
|
confirmManualRuleGapsFilling,
|
||||||
|
} = useManualRuleGapsFillingConfirmation();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isManualRuleRunLimitErrorVisible,
|
isManualRuleRunLimitErrorVisible,
|
||||||
showManualRuleRunLimitError,
|
showManualRuleRunLimitError,
|
||||||
|
@ -148,6 +157,7 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
||||||
showBulkActionConfirmation,
|
showBulkActionConfirmation,
|
||||||
showBulkDuplicateConfirmation,
|
showBulkDuplicateConfirmation,
|
||||||
showManualRuleRunConfirmation,
|
showManualRuleRunConfirmation,
|
||||||
|
showManualRuleGapsFillingConfirmation,
|
||||||
showManualRuleRunLimitError,
|
showManualRuleRunLimitError,
|
||||||
completeBulkEditForm,
|
completeBulkEditForm,
|
||||||
executeBulkActionsDryRun,
|
executeBulkActionsDryRun,
|
||||||
|
@ -295,6 +305,9 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
||||||
{isManualRuleRunConfirmationVisible && (
|
{isManualRuleRunConfirmationVisible && (
|
||||||
<ManualRuleRunModal onCancel={cancelManualRuleRun} onConfirm={confirmManualRuleRun} />
|
<ManualRuleRunModal onCancel={cancelManualRuleRun} onConfirm={confirmManualRuleRun} />
|
||||||
)}
|
)}
|
||||||
|
{isManualRuleGapsFillingConfirmationVisible && (
|
||||||
|
<ManualRuleGapsFillingModal onCancel={cancelManualRuleGapsFilling} onConfirm={confirmManualRuleGapsFilling} rulesCount={selectedRuleIds.length} />
|
||||||
|
)}
|
||||||
{isManualRuleRunLimitErrorVisible && (
|
{isManualRuleRunLimitErrorVisible && (
|
||||||
<BulkManualRuleRunLimitErrorModal onClose={hideManualRuleRunLimitError} />
|
<BulkManualRuleRunLimitErrorModal onClose={hideManualRuleRunLimitError} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -62,6 +62,8 @@ interface ActionColumnsProps {
|
||||||
confirmDeletion: () => Promise<boolean>;
|
confirmDeletion: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadingActionsSet = new Set(['disable', 'enable', 'edit', 'delete', 'run', 'fill_gaps'])
|
||||||
|
|
||||||
export const useEnabledColumn = ({
|
export const useEnabledColumn = ({
|
||||||
hasCRUDPermissions,
|
hasCRUDPermissions,
|
||||||
startMlJobs,
|
startMlJobs,
|
||||||
|
@ -72,7 +74,7 @@ export const useEnabledColumn = ({
|
||||||
|
|
||||||
const loadingIds = useMemo(
|
const loadingIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
['disable', 'enable', 'edit', 'delete', 'run'].includes(loadingRulesAction ?? '')
|
loadingActionsSet.has(loadingRulesAction ?? '')
|
||||||
? loadingRuleIds
|
? loadingRuleIds
|
||||||
: [],
|
: [],
|
||||||
[loadingRuleIds, loadingRulesAction]
|
[loadingRuleIds, loadingRulesAction]
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -119,4 +119,7 @@ export const BULK_EXPORT_ACTION_BTN = '[data-test-subj="exportRuleBulk"]';
|
||||||
// SCHEDULE MANUAL RULE RUN
|
// SCHEDULE MANUAL RULE RUN
|
||||||
export const BULK_MANUAL_RULE_RUN_BTN = '[data-test-subj="scheduleRuleRunBulk"]';
|
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"]';
|
export const BULK_MANUAL_RULE_RUN_WARNING_MODAL = '[data-test-subj="bulkActionConfirmationModal"]';
|
||||||
|
|
|
@ -58,6 +58,7 @@ import {
|
||||||
UPDATE_SCHEDULE_LOOKBACK_INPUT,
|
UPDATE_SCHEDULE_LOOKBACK_INPUT,
|
||||||
UPDATE_SCHEDULE_MENU_ITEM,
|
UPDATE_SCHEDULE_MENU_ITEM,
|
||||||
UPDATE_SCHEDULE_TIME_UNIT_SELECT,
|
UPDATE_SCHEDULE_TIME_UNIT_SELECT,
|
||||||
|
BULK_FILL_RULE_GAPS_BTN,
|
||||||
} from '../screens/rules_bulk_actions';
|
} from '../screens/rules_bulk_actions';
|
||||||
import { SCHEDULE_DETAILS } from '../screens/rule_details';
|
import { SCHEDULE_DETAILS } from '../screens/rule_details';
|
||||||
|
|
||||||
|
@ -445,3 +446,20 @@ export const scheduleManualRuleRunForSelectedRules = (
|
||||||
}
|
}
|
||||||
cy.get(MODAL_CONFIRMATION_BTN).click();
|
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();
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue