[Attack Discovery][Scheduling] Cases support followup 1 (#225452)

## Summary

Summarize your PR. If it involves visual changes include a screenshot or
gif.

These changes addresses the review comment from my previous PR
36ed6b38c1 (r2150081638)

Initially I broke up rendered component into multiple memoized
sub-sections. Reverting that back and adding tests coverage for the new
functionality - Case actions UI for the Attack Discovery rule type:
* Hidden `group by` component
* Hidden `time window` component
* Hidden `reopen case` component
* Disabled `template selector` component
* Tooltip explaining why we disabled the `template selector` component
This commit is contained in:
Ievgen Sorokopud 2025-06-26 21:31:35 +02:00 committed by GitHub
parent 4de6f7ca2c
commit d38801034a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 133 additions and 100 deletions

View file

@ -19,6 +19,7 @@ import { useGetAllCaseConfigurations } from '../../../containers/configure/use_g
import { useGetAllCaseConfigurationsResponse } from '../../configure_cases/__mock__';
import { templatesConfigurationMock } from '../../../containers/mock';
import * as utils from '../../../containers/configure/utils';
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
jest.mock('@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view');
jest.mock('../../../common/lib/kibana/use_application');
@ -72,6 +73,7 @@ describe('CasesParamsFields renders', () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
useApplicationMock.mockReturnValueOnce({ appId: 'management' });
useAlertsDataViewMock.mockReturnValue({
@ -397,4 +399,67 @@ describe('CasesParamsFields renders', () => {
expect(editAction.mock.calls[0][1].reopenClosedCases).toEqual(true);
});
});
describe('Attack Discovery', () => {
it('does not render `group by` component', async () => {
const newProps = {
...defaultProps,
ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
};
render(<CasesParamsFields {...newProps} />);
expect(screen.queryByTestId('group-by-alert-field-combobox')).not.toBeInTheDocument();
});
it('does not render `time window` component', async () => {
const newProps = {
...defaultProps,
ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
};
render(<CasesParamsFields {...newProps} />);
expect(screen.queryByTestId('time-window-size-input')).not.toBeInTheDocument();
expect(screen.queryByTestId('time-window-unit-select')).not.toBeInTheDocument();
});
it('does not render `reopen case` component', async () => {
const newProps = {
...defaultProps,
ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
};
render(<CasesParamsFields {...newProps} />);
expect(screen.queryByTestId('reopen-case')).not.toBeInTheDocument();
});
it('renders disabled `template selector` component', async () => {
const newProps = {
...defaultProps,
ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
};
render(<CasesParamsFields {...newProps} />);
const templateSelectorComponent = await screen.findByTestId('create-case-template-select');
expect(templateSelectorComponent).toBeInTheDocument();
expect(templateSelectorComponent).toBeDisabled();
});
it('shows attack discovery explanation tooltip', async () => {
const newProps = {
...defaultProps,
ruleTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
};
render(<CasesParamsFields {...newProps} />);
await user.hover(await screen.findByTestId('create-case-template-select'));
expect(await screen.findByTestId('case-action-attack-discovery-tooltip')).toBeTruthy();
expect(
await screen.findByText(
'Attack Discovery Schedules fully manage Case actions, automatically filling in all fields for new Cases.'
)
).toBeInTheDocument();
});
});
});

View file

@ -188,8 +188,26 @@ export const CasesParamsFieldsComponent: React.FunctionComponent<
[editSubActionProperty]
);
const groupByComponent = useMemo(() => {
if (isAttackDiscoveryRuleType) {
return (
<EuiToolTip
data-test-subj="case-action-attack-discovery-tooltip"
content={i18n.ATTACK_DISCOVERY_TEMPLATE_TOOLTIP}
>
<TemplateSelector
key={currentConfiguration.id}
isLoading={isLoadingCaseConfiguration}
templates={[defaultTemplate, ...currentConfiguration.templates]}
onTemplateChange={onTemplateChange}
initialTemplate={selectedTemplate}
isDisabled={true}
/>
</EuiToolTip>
);
}
return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<EuiFormRow fullWidth label={i18n.GROUP_BY_ALERT} labelAppend={OptionalFieldLabel}>
@ -207,71 +225,54 @@ export const CasesParamsFieldsComponent: React.FunctionComponent<
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
);
}, [loadingAlertDataViews, onChangeComboBox, options, selectedOptions]);
const timeWindowComponent = useMemo(() => {
return (
<>
<EuiFormRow
fullWidth
id="timeWindow"
error={errors.timeWindow as string[]}
isInvalid={
errors.timeWindow !== undefined &&
Number(errors.timeWindow.length) > 0 &&
timeWindow !== undefined
}
>
<EuiFlexGroup alignItems="flexEnd" gutterSize="s">
<EuiFlexItem grow={4}>
<EuiFieldNumber
prepend={i18n.TIME_WINDOW}
data-test-subj="time-window-size-input"
value={timeWindowSize}
min={1}
step={1}
onChange={(e) => {
handleTimeWindowChange('timeWindowSize', e.target.value);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSelect
fullWidth
data-test-subj="time-window-unit-select"
value={timeWindowUnit}
onChange={(e) => {
handleTimeWindowChange('timeWindowUnit', e.target.value);
}}
options={getTimeUnitOptions(timeWindowSize)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer size="s" />
{showTimeWindowWarning && (
<EuiCallOut
data-test-subj="show-time-window-warning"
title={i18n.TIME_WINDOW_WARNING}
color="warning"
iconType="alert"
size="s"
/>
)}
</>
);
}, [
errors.timeWindow,
handleTimeWindowChange,
showTimeWindowWarning,
timeWindow,
timeWindowSize,
timeWindowUnit,
]);
const templateSelectorComponent = useMemo(() => {
return (
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
id="timeWindow"
error={errors.timeWindow as string[]}
isInvalid={
errors.timeWindow !== undefined &&
Number(errors.timeWindow.length) > 0 &&
timeWindow !== undefined
}
>
<EuiFlexGroup alignItems="flexEnd" gutterSize="s">
<EuiFlexItem grow={4}>
<EuiFieldNumber
prepend={i18n.TIME_WINDOW}
data-test-subj="time-window-size-input"
value={timeWindowSize}
min={1}
step={1}
onChange={(e) => {
handleTimeWindowChange('timeWindowSize', e.target.value);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSelect
fullWidth
data-test-subj="time-window-unit-select"
value={timeWindowUnit}
onChange={(e) => {
handleTimeWindowChange('timeWindowUnit', e.target.value);
}}
options={getTimeUnitOptions(timeWindowSize)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer size="s" />
{showTimeWindowWarning && (
<EuiCallOut
data-test-subj="show-time-window-warning"
title={i18n.TIME_WINDOW_WARNING}
color="warning"
iconType="alert"
size="s"
/>
)}
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<TemplateSelector
@ -280,23 +281,10 @@ export const CasesParamsFieldsComponent: React.FunctionComponent<
templates={[defaultTemplate, ...currentConfiguration.templates]}
onTemplateChange={onTemplateChange}
initialTemplate={selectedTemplate}
isDisabled={isAttackDiscoveryRuleType}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}, [
currentConfiguration.id,
currentConfiguration.templates,
defaultTemplate,
isAttackDiscoveryRuleType,
isLoadingCaseConfiguration,
onTemplateChange,
selectedTemplate,
]);
const reopenClosedCasesComponent = useMemo(() => {
return (
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiCheckbox
@ -310,26 +298,6 @@ export const CasesParamsFieldsComponent: React.FunctionComponent<
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}, [editSubActionProperty, index, reopenClosedCases]);
if (isAttackDiscoveryRuleType) {
return (
<EuiToolTip content={i18n.ATTACK_DISCOVERY_TEMPLATE_TOOLTIP}>
{templateSelectorComponent}
</EuiToolTip>
);
}
return (
<>
{groupByComponent}
<EuiSpacer size="m" />
{timeWindowComponent}
<EuiSpacer size="m" />
{templateSelectorComponent}
<EuiSpacer size="m" />
{reopenClosedCasesComponent}
</>
);
};