Skip to content

Commit

Permalink
support simple rule schedule in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
maximpn committed Jan 15, 2025
1 parent 094922a commit 7149d81
Show file tree
Hide file tree
Showing 38 changed files with 1,093 additions and 485 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36652,7 +36652,6 @@
"xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "Activer",
"xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab": "Exceptions de point de terminaison",
"xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel": "Exemples de faux positifs",
"xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel": "Temps de récupération supplémentaire",
"xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel": "Taille de la fenêtre d’historique",
"xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel": "Modèles d'indexation",
"xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel": "Installer et activer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36511,7 +36511,6 @@
"xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "有効にする",
"xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab": "エンドポイント例外",
"xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel": "誤検出の例",
"xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel": "追加のルックバック時間",
"xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel": "履歴ウィンドウサイズ",
"xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel": "インデックスパターン",
"xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel": "インストールして有効化",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35967,7 +35967,6 @@
"xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "启用",
"xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab": "终端例外",
"xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel": "误报示例",
"xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel": "更多回查时间",
"xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel": "历史记录窗口大小",
"xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel": "索引模式",
"xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel": "安装并启用",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ import {
RuleEsqlQuery,
RuleKqlQuery,
RuleNameOverrideObject,
RuleSchedule,
TimelineTemplateReference,
TimestampOverrideObject,
} from './diffable_field_types';
import { RuleSchedule } from '../../../../model/rule_schema/rule_schedule';

export type DiffableCommonFields = z.infer<typeof DiffableCommonFields>;
export const DiffableCommonFields = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { validateHistoryWindowStart } from './validate_history_window_start';
const COMPONENT_PROPS = {
idAria: 'historyWindowSize',
dataTestSubj: 'historyWindowSize',
timeTypes: ['m', 'h', 'd'],
units: ['m', 'h', 'd'],
minValue: 0,
};

interface HistoryWindowStartEditProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,17 @@ describe('ScheduleItemField', () => {
it('renders correctly', () => {
const mockField = useFormFieldMock<string>();
const wrapper = shallow(
<ScheduleItemField
dataTestSubj="schedule-item"
idAria="idAria"
isDisabled={false}
field={mockField}
/>
<ScheduleItemField field={mockField} dataTestSubj="schedule-item" idAria="idAria" />
);

expect(wrapper.find('[data-test-subj="schedule-item"]')).toHaveLength(1);
});

it('accepts a large number via user input', () => {
it('accepts user input', () => {
const mockField = useFormFieldMock<string>();
const wrapper = mount(
<TestProviders>
<ScheduleItemField
dataTestSubj="schedule-item"
idAria="idAria"
isDisabled={false}
field={mockField}
/>
<ScheduleItemField field={mockField} dataTestSubj="schedule-item" idAria="idAria" />
</TestProviders>
);

Expand All @@ -47,17 +37,20 @@ describe('ScheduleItemField', () => {
expect(mockField.setValue).toHaveBeenCalledWith('5000000s');
});

it('clamps a number value greater than MAX_SAFE_INTEGER to MAX_SAFE_INTEGER', () => {
const unsafeInput = '99999999999999999999999';

it.each([
[-10, -5],
[-5, 0],
[5, 10],
[60, 90],
])('saturates a value "%s" lower than minValue', (unsafeInput, expected) => {
const mockField = useFormFieldMock<string>();
const wrapper = mount(
<TestProviders>
<ScheduleItemField
field={mockField}
minValue={expected}
dataTestSubj="schedule-item"
idAria="idAria"
isDisabled={false}
field={mockField}
/>
</TestProviders>
);
Expand All @@ -67,21 +60,23 @@ describe('ScheduleItemField', () => {
.last()
.simulate('change', { target: { value: unsafeInput } });

const expectedValue = `${Number.MAX_SAFE_INTEGER}s`;
expect(mockField.setValue).toHaveBeenCalledWith(expectedValue);
expect(mockField.setValue).toHaveBeenCalledWith(`${expected}s`);
});

it('converts a non-numeric value to 0', () => {
const unsafeInput = 'this is not a number';

it.each([
[-5, -10],
[5, 0],
[10, 5],
[90, 60],
])('saturates a value "%s" greater than maxValue', (unsafeInput, expected) => {
const mockField = useFormFieldMock<string>();
const wrapper = mount(
<TestProviders>
<ScheduleItemField
field={mockField}
maxValue={expected}
dataTestSubj="schedule-item"
idAria="idAria"
isDisabled={false}
field={mockField}
/>
</TestProviders>
);
Expand All @@ -91,6 +86,24 @@ describe('ScheduleItemField', () => {
.last()
.simulate('change', { target: { value: unsafeInput } });

expect(mockField.setValue).toHaveBeenCalledWith('0s');
expect(mockField.setValue).toHaveBeenCalledWith(`${expected}s`);
});

it('skips updating a non-numeric values', () => {
const unsafeInput = 'this is not a number';

const mockField = useFormFieldMock<string>();
const wrapper = mount(
<TestProviders>
<ScheduleItemField field={mockField} dataTestSubj="schedule-item" idAria="idAria" />
</TestProviders>
);

wrapper
.find('[data-test-subj="interval"]')
.last()
.simulate('change', { target: { value: unsafeInput } });

expect(mockField.setValue).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { EuiSelectProps, EuiFieldNumberProps } from '@elastic/eui';
import {
EuiFlexGroup,
Expand All @@ -14,8 +15,6 @@ import {
EuiSelect,
transparentize,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';

import type { FieldHook } from '../../../../shared_imports';
Expand All @@ -27,8 +26,9 @@ interface ScheduleItemProps {
field: FieldHook<string>;
dataTestSubj: string;
idAria: string;
isDisabled: boolean;
minimumValue?: number;
isDisabled?: boolean;
minValue?: number;
maxValue?: number;
timeTypes?: string[];
fullWidth?: boolean;
}
Expand Down Expand Up @@ -67,24 +67,16 @@ const MyEuiSelect = styled(EuiSelect)`
box-shadow: none;
`;

const getNumberFromUserInput = (input: string, minimumValue = 0): number => {
const number = parseInt(input, 10);
if (Number.isNaN(number)) {
return minimumValue;
} else {
return Math.max(minimumValue, Math.min(number, Number.MAX_SAFE_INTEGER));
}
};

export const ScheduleItemField = ({
dataTestSubj,
export function ScheduleItemField({
field,
idAria,
isDisabled,
minimumValue = 0,
timeTypes = ['s', 'm', 'h'],
dataTestSubj,
idAria,
minValue = Number.MIN_SAFE_INTEGER,
maxValue = Number.MAX_SAFE_INTEGER,
timeTypes = DEFAULT_TIME_DURATION_UNITS,
fullWidth = false,
}: ScheduleItemProps) => {
}: ScheduleItemProps): JSX.Element {
const [timeType, setTimeType] = useState(timeTypes[0]);
const [timeVal, setTimeVal] = useState<number>(0);
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
Expand All @@ -100,38 +92,40 @@ export const ScheduleItemField = ({

const onChangeTimeVal = useCallback<NonNullable<EuiFieldNumberProps['onChange']>>(
(e) => {
const sanitizedValue = getNumberFromUserInput(e.target.value, minimumValue);
setTimeVal(sanitizedValue);
setValue(`${sanitizedValue}${timeType}`);
const number = parseInt(e.target.value, 10);

if (Number.isNaN(number)) {
return;
}

const newTimeValue = saturate(number, minValue, maxValue);

setTimeVal(newTimeValue);
setValue(`${newTimeValue}${timeType}`);
},
[minimumValue, setValue, timeType]
[minValue, maxValue, setValue, timeType]
);

useEffect(() => {
if (value !== `${timeVal}${timeType}`) {
const filterTimeVal = value.match(/\d+/g);
const filterTimeType = value.match(/[a-zA-Z]+/g);
if (
!isEmpty(filterTimeVal) &&
filterTimeVal != null &&
!isNaN(Number(filterTimeVal[0])) &&
Number(filterTimeVal[0]) !== Number(timeVal)
) {
setTimeVal(Number(filterTimeVal[0]));
}
if (
!isEmpty(filterTimeType) &&
filterTimeType != null &&
timeTypes.includes(filterTimeType[0]) &&
filterTimeType[0] !== timeType
) {
setTimeType(filterTimeType[0]);
}
if (value === `${timeVal}${timeType}`) {
return;
}

const isNegative = value.startsWith('-');
const durationRegexp = new RegExp(`^\\-?(\\d+)(${timeTypes.join('|')})$`);
const durationMatchArray = value.match(durationRegexp);

if (!durationMatchArray) {
return;
}

const [, timeStr, unit] = durationMatchArray;
const time = parseInt(timeStr, 10) * (isNegative ? -1 : 1);

setTimeVal(time);
setTimeType(unit);
}, [timeType, timeTypes, timeVal, value]);

// EUI missing some props
const rest = { disabled: isDisabled };
const label = useMemo(
() => (
<EuiFlexGroup gutterSize="s" justifyContent="flexStart" alignItems="center">
Expand Down Expand Up @@ -161,21 +155,27 @@ export const ScheduleItemField = ({
<MyEuiSelect
fullWidth
options={timeTypeOptions.filter((type) => timeTypes.includes(type.value))}
onChange={onChangeTimeType}
value={timeType}
onChange={onChangeTimeType}
disabled={isDisabled}
aria-label={field.label}
data-test-subj="timeType"
{...rest}
/>
}
fullWidth
min={minimumValue}
max={Number.MAX_SAFE_INTEGER}
onChange={onChangeTimeVal}
min={minValue}
max={maxValue}
value={timeVal}
onChange={onChangeTimeVal}
disabled={isDisabled}
data-test-subj="interval"
{...rest}
/>
</StyledEuiFormRow>
);
};
}

const DEFAULT_TIME_DURATION_UNITS = ['s', 'm', 'h'];

function saturate(input: number, minValue: number, maxValue: number): number {
return Math.max(minValue, Math.min(input, maxValue));
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
idAria: 'detectionEngineStepScheduleRuleInterval',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepScheduleRuleInterval',
minimumValue: 1,
minValue: 1,
}}
/>
<UseField
Expand All @@ -58,7 +58,7 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
idAria: 'detectionEngineStepScheduleRuleFrom',
isDisabled: isLoading,
dataTestSubj: 'detectionEngineStepScheduleRuleFrom',
minimumValue: 1,
minValue: 0,
}}
/>
</StyledForm>
Expand Down
Loading

0 comments on commit 7149d81

Please sign in to comment.