[Response Ops][Rules] Settings as Flyout instead of Modal (#216162)

## Summary

Closes https://github.com/elastic/kibana/issues/215910

<img width="597" alt="Screenshot 2025-03-28 at 12 28 08"
src="https://github.com/user-attachments/assets/6f4b5cb0-0778-4771-851a-a0a2d295f6b1"
/>

## Release note:
Moves rule settings to a flyout instead of a modal
This commit is contained in:
Julian Gernun 2025-04-01 09:59:25 +02:00 committed by GitHub
parent 74376338e4
commit 2881895b45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 165 additions and 202 deletions

View file

@ -83,7 +83,7 @@ export const RuleSettingsFlappingInputs = (props: RuleSettingsFlappingInputsProp
);
return (
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<RuleSettingsRangeInput
fullWidth

View file

@ -47379,22 +47379,6 @@
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetection": "Détection de bagotement d'alerte",
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetectionDescription": "Modifiez la fréquence à laquelle une alerte peut passer de l'état actif à l'état récupéré au cours d'une période d'exécution de règle.",
"xpack.triggersActionsUI.rulesSettings.link.title": "Paramètres",
"xpack.triggersActionsUI.rulesSettings.modal.calloutMessage": "Appliquer à toutes les règles dans l'espace actuel.",
"xpack.triggersActionsUI.rulesSettings.modal.cancelButton": "Annuler",
"xpack.triggersActionsUI.rulesSettings.modal.errorPromptBody": "Une erreur s'est produite lors du chargement de vos paramètres de règles. Contactez votre administrateur pour obtenir de l'aide",
"xpack.triggersActionsUI.rulesSettings.modal.errorPromptTitle": "Impossible de charger vos paramètres de règles",
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "Détectez les alertes qui passent rapidement de l'état actif à l'état récupéré et réduisez le bruit non souhaité de ces alertes bagotantes.",
"xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptBody": "Une erreur s'est produite lors du chargement de vos paramètres de bagotement. Contactez votre administrateur pour obtenir de l'aide",
"xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptTitle": "Impossible de charger vos paramètres de bagotement",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "Désactivé",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "Activé (recommandé)",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription": "Retarder toutes les requêtes de règles pour atténuer l'impact des intervalles d'actualisation de l'index sur la disponibilité des données.",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptBody": "Une erreur s'est produite lors du chargement des paramètres de retardement des requêtes. Contactez votre administrateur pour obtenir de l'aide",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptTitle": "Impossible de charger vos paramètres de retardement des requêtes",
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "Enregistrer",
"xpack.triggersActionsUI.rulesSettings.modal.title": "Paramètres de règle",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "Impossible de mettre à jour les paramètres des règles.",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsSuccess": "Paramètres des règles correctement mis à jour.",
"xpack.triggersActionsUI.rulesSettings.queryDelayLabel": "Durée de retard de la requête (secondes)",
"xpack.triggersActionsUI.rulesSettings.queryDelayTitle": "Retard de requête",
"xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel": "Retour",

View file

@ -47341,22 +47341,6 @@
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetection": "アラートフラップ検出",
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetectionDescription": "ルールの実行期間中、アラートがアクティブと回復済みの間を遷移できる頻度を変更します。",
"xpack.triggersActionsUI.rulesSettings.link.title": "設定",
"xpack.triggersActionsUI.rulesSettings.modal.calloutMessage": "現在のスペース内のすべてのルールに適用",
"xpack.triggersActionsUI.rulesSettings.modal.cancelButton": "キャンセル",
"xpack.triggersActionsUI.rulesSettings.modal.errorPromptBody": "ルール設定の読み込み中にエラーが発生しました。ヘルプについては、管理者にお問い合わせください",
"xpack.triggersActionsUI.rulesSettings.modal.errorPromptTitle": "ルール設定を読み込めません",
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "アクティブと回復済みの状態がすばやく切り替わるアラートを検出し、これらのフラップアラートに対する不要なノイズを低減します。",
"xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptBody": "フラップ設定の読み込み中にエラーが発生しました。ヘルプについては、管理者にお問い合わせください",
"xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptTitle": "フラップ設定を読み込めません",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "オフ",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "オン(推奨)",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription": "すべてのルールクエリを遅延させて、インデックスの更新間隔がデータの可用性に与える影響を軽減します。",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptBody": "クエリ遅延設定の読み込み中にエラーが発生しました。ヘルプについては、管理者にお問い合わせください",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptTitle": "クエリ遅延設定を読み込めません",
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "保存",
"xpack.triggersActionsUI.rulesSettings.modal.title": "ルール設定",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "ルール設定を更新できませんでした。",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsSuccess": "ルール設定は正常に更新されました。",
"xpack.triggersActionsUI.rulesSettings.queryDelayLabel": "クエリ遅延長さ(秒)",
"xpack.triggersActionsUI.rulesSettings.queryDelayTitle": "クエリの遅延",
"xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel": "戻る",

View file

@ -47416,22 +47416,6 @@
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetection": "告警摆动检测",
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetectionDescription": "修改在规则运行期间内告警可在“活动”和“已恢复”之间切换的频率。",
"xpack.triggersActionsUI.rulesSettings.link.title": "设置",
"xpack.triggersActionsUI.rulesSettings.modal.calloutMessage": "应用于当前工作区内的所有规则。",
"xpack.triggersActionsUI.rulesSettings.modal.cancelButton": "取消",
"xpack.triggersActionsUI.rulesSettings.modal.errorPromptBody": "加载规则设置时出错。请联系您的管理员寻求帮助",
"xpack.triggersActionsUI.rulesSettings.modal.errorPromptTitle": "无法加载规则设置",
"xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription": "检测在“活动”和“已恢复”状态之间快速切换的告警,并为这些摆动告警减少不必要噪音。",
"xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptBody": "加载摆动设置时出错。请联系您的管理员寻求帮助",
"xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptTitle": "无法加载摆动设置",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel": "关闭",
"xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel": "开(建议)",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription": "延迟所有规则查询以减轻索引刷新时间间隔对数据可用性的影响。",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptBody": "加载查询延迟设置时出错。请联系您的管理员寻求帮助",
"xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptTitle": "无法加载查询延迟设置",
"xpack.triggersActionsUI.rulesSettings.modal.saveButton": "保存",
"xpack.triggersActionsUI.rulesSettings.modal.title": "规则设置",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure": "无法更新规则设置。",
"xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsSuccess": "已成功更新规则设置。",
"xpack.triggersActionsUI.rulesSettings.queryDelayLabel": "查询延迟长度(秒)",
"xpack.triggersActionsUI.rulesSettings.queryDelayTitle": "查询延迟",
"xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel": "返回",

View file

@ -52,7 +52,7 @@ export const RulesSettingsFlappingFormSection = memo(
const { lookBackWindow, statusChangeThreshold } = flappingSettings;
return (
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" gutterSize="s">
{compressed && (
<>
<EuiFlexItem>

View file

@ -28,26 +28,20 @@ import {
} from './rules_settings_flapping_form_section';
const flappingDescription = i18n.translate(
'xpack.triggersActionsUI.rulesSettings.modal.flappingDetectionDescription',
'xpack.triggersActionsUI.rulesSettings.flappingDetectionDescription',
{
defaultMessage:
'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts.',
}
);
const flappingOnLabel = i18n.translate(
'xpack.triggersActionsUI.rulesSettings.modal.flappingOnLabel',
{
defaultMessage: 'On (recommended)',
}
);
const flappingOnLabel = i18n.translate('xpack.triggersActionsUI.rulesSettings.flappingOnLabel', {
defaultMessage: 'On (recommended)',
});
const flappingOffLabel = i18n.translate(
'xpack.triggersActionsUI.rulesSettings.modal.flappingOffLabel',
{
defaultMessage: 'Off',
}
);
const flappingOffLabel = i18n.translate('xpack.triggersActionsUI.rulesSettings.flappingOffLabel', {
defaultMessage: 'Off',
});
export const RulesSettingsFlappingErrorPrompt = memo(() => {
return (
@ -58,7 +52,7 @@ export const RulesSettingsFlappingErrorPrompt = memo(() => {
title={
<h4>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptTitle"
id="xpack.triggersActionsUI.rulesSettings.flappingErrorPromptTitle"
defaultMessage="Unable to load your flapping settings"
/>
</h4>
@ -66,7 +60,7 @@ export const RulesSettingsFlappingErrorPrompt = memo(() => {
body={
<p>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.flappingErrorPromptBody"
id="xpack.triggersActionsUI.rulesSettings.flappingErrorPromptBody"
defaultMessage="There was an error loading your flapping settings. Contact your administrator for help"
/>
</p>
@ -86,7 +80,7 @@ export const RulesSettingsFlappingFormLeft = memo((props: RulesSettingsFlappingF
return (
<EuiFlexItem>
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<p>{flappingDescription}</p>
@ -164,7 +158,7 @@ export const RulesSettingsFlappingSection = memo((props: RulesSettingsFlappingSe
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="flexStart">
<RulesSettingsFlappingFormLeft
isSwitchDisabled={!canWrite}
settings={settings}

View file

@ -25,7 +25,7 @@ import {
import { RuleSettingsRangeInput } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_range_input';
const queryDelayDescription = i18n.translate(
'xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription',
'xpack.triggersActionsUI.rulesSettings.queryDelayDescription',
{
defaultMessage:
'Delay all rule queries to mitigate the impact of index refresh intervals on data availability.',
@ -45,7 +45,7 @@ export const RulesSettingsQueryDelayErrorPrompt = memo(() => {
title={
<h4>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptTitle"
id="xpack.triggersActionsUI.rulesSettings.queryDelayErrorPromptTitle"
defaultMessage="Unable to load your query delay settings"
/>
</h4>
@ -53,7 +53,7 @@ export const RulesSettingsQueryDelayErrorPrompt = memo(() => {
body={
<p>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.queryDelayErrorPromptBody"
id="xpack.triggersActionsUI.rulesSettings.queryDelayErrorPromptBody"
defaultMessage="There was an error loading your query delay settings. Contact your administrator for help"
/>
</p>
@ -100,7 +100,7 @@ export const RulesSettingsQueryDelaySection = memo((props: RulesSettingsQueryDel
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiText color="subdued" size="s">
<p>{queryDelayDescription}</p>

View file

@ -13,7 +13,7 @@ import userEvent from '@testing-library/user-event';
import { coreMock } from '@kbn/core/public/mocks';
import { IToasts } from '@kbn/core/public';
import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
import { RulesSettingsModal, RulesSettingsModalProps } from './rules_settings_modal';
import { RulesSettingsFlyout, RulesSettingsFlyoutProps } from './rules_settings_flyout';
import { useKibana } from '../../../common/lib/kibana';
import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings';
import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings';
@ -80,29 +80,29 @@ const mockQueryDelaySetting: RulesSettingsQueryDelay = {
updatedAt: new Date().toISOString(),
};
const modalProps: RulesSettingsModalProps = {
const flyoutProps: RulesSettingsFlyoutProps = {
isVisible: true,
setUpdatingRulesSettings: jest.fn(),
onClose: jest.fn(),
onSave: jest.fn(),
};
const RulesSettingsModalWithProviders: React.FunctionComponent<RulesSettingsModalProps> = (
const RulesSettingsFlyoutWithProviders: React.FunctionComponent<RulesSettingsFlyoutProps> = (
props
) => (
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RulesSettingsModal {...props} />
<RulesSettingsFlyout {...props} />
</QueryClientProvider>
</IntlProvider>
);
const waitForModalLoad = async (options?: {
const waitForFlyoutLoad = async (options?: {
flappingSection?: boolean;
queryDelaySection?: boolean;
}) => {
await waitFor(() => {
expect(screen.queryByTestId('centerJustifiedSpinner')).toBe(null);
expect(screen.queryByTestId('rulesSettingsFlyout')).not.toBe(null);
});
const { flappingSection = true, queryDelaySection = true } = options || {};
@ -117,7 +117,7 @@ const waitForModalLoad = async (options?: {
});
};
describe('rules_settings_modal', () => {
describe('rules_settings_flyout', () => {
beforeEach(async () => {
jest.clearAllMocks();
@ -160,22 +160,24 @@ describe('rules_settings_modal', () => {
});
test('renders flapping settings correctly', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1);
await waitForModalLoad();
await waitForFlyoutLoad();
expect(
result.getByTestId('rulesSettingsFlappingEnableSwitch').getAttribute('aria-checked')
).toBe('true');
expect(result.getByTestId('lookBackWindowRangeInput').getAttribute('value')).toBe('10');
expect(result.getByTestId('statusChangeThresholdRangeInput').getAttribute('value')).toBe('10');
expect(result.getByTestId('rulesSettingsModalCancelButton')).toBeInTheDocument();
expect(result.getByTestId('rulesSettingsModalSaveButton').getAttribute('disabled')).toBeFalsy();
expect(result.getByTestId('rulesSettingsFlyoutCancelButton')).toBeInTheDocument();
expect(
result.getByTestId('rulesSettingsFlyoutSaveButton').getAttribute('disabled')
).toBeFalsy();
});
test('can save flapping settings', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad();
const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput');
const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput');
@ -187,12 +189,12 @@ describe('rules_settings_modal', () => {
expect(statusChangeThresholdInput.getAttribute('value')).toBe('5');
// Try saving
await userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
await userEvent.click(result.getByTestId('rulesSettingsFlyoutSaveButton'));
await waitFor(() => {
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
});
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(flyoutProps.onClose).toHaveBeenCalledTimes(1);
expect(updateFlappingSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({
flappingSettings: {
@ -203,15 +205,15 @@ describe('rules_settings_modal', () => {
})
);
expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1);
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(modalProps.onSave).toHaveBeenCalledTimes(1);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.onSave).toHaveBeenCalledTimes(1);
});
test('reset flapping settings to initial state on cancel without triggering another server reload', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1);
expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
await waitForModalLoad();
await waitForFlyoutLoad();
const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput');
const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput');
@ -223,11 +225,11 @@ describe('rules_settings_modal', () => {
expect(statusChangeThresholdInput.getAttribute('value')).toBe('3');
// Try cancelling
await userEvent.click(result.getByTestId('rulesSettingsModalCancelButton'));
await userEvent.click(result.getByTestId('rulesSettingsFlyoutCancelButton'));
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(flyoutProps.onClose).toHaveBeenCalledTimes(1);
expect(updateFlappingSettingsMock).not.toHaveBeenCalled();
expect(modalProps.onSave).not.toHaveBeenCalled();
expect(flyoutProps.onSave).not.toHaveBeenCalled();
expect(screen.queryByTestId('centerJustifiedSpinner')).toBe(null);
expect(lookBackWindowInput.getAttribute('value')).toBe('10');
@ -238,8 +240,8 @@ describe('rules_settings_modal', () => {
});
test('should prevent statusChangeThreshold from being greater than lookBackWindow', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad();
const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput');
const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput');
@ -261,8 +263,8 @@ describe('rules_settings_modal', () => {
test('handles errors when saving settings', async () => {
updateFlappingSettingsMock.mockRejectedValue('failed!');
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad();
const lookBackWindowInput = result.getByTestId('lookBackWindowRangeInput');
const statusChangeThresholdInput = result.getByTestId('statusChangeThresholdRangeInput');
@ -274,19 +276,19 @@ describe('rules_settings_modal', () => {
expect(statusChangeThresholdInput.getAttribute('value')).toBe('5');
// Try saving
await userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
await userEvent.click(result.getByTestId('rulesSettingsFlyoutSaveButton'));
await waitFor(() => {
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
});
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(flyoutProps.onClose).toHaveBeenCalledTimes(1);
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(modalProps.onSave).toHaveBeenCalledTimes(1);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.onSave).toHaveBeenCalledTimes(1);
});
test('displays flapping detection off prompt when flapping is disabled', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad();
expect(result.queryByTestId('rulesSettingsFlappingOffPrompt')).toBe(null);
await userEvent.click(result.getByTestId('rulesSettingsFlappingEnableSwitch'));
@ -308,13 +310,13 @@ describe('rules_settings_modal', () => {
readFlappingSettingsUI: true,
},
};
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad({ queryDelaySection: false });
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad({ queryDelaySection: false });
expect(result.getByTestId('rulesSettingsFlappingEnableSwitch')).toBeDisabled();
expect(result.getByTestId('lookBackWindowRangeInput')).toBeDisabled();
expect(result.getByTestId('statusChangeThresholdRangeInput')).toBeDisabled();
expect(result.getByTestId('rulesSettingsModalSaveButton')).toBeDisabled();
expect(result.getByTestId('rulesSettingsFlyoutSaveButton')).toBeDisabled();
});
test('form elements are not visible when provided with insufficient read permissions', async () => {
@ -332,41 +334,40 @@ describe('rules_settings_modal', () => {
readFlappingSettingsUI: false,
},
};
await waitForModalLoad({ flappingSection: false, queryDelaySection: false });
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitFor(() => {
expect(result.queryByTestId('centerJustifiedSpinner')).toBe(null);
});
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad({ flappingSection: false, queryDelaySection: false });
expect(result.queryByTestId('rulesSettingsFlappingSection')).toBe(null);
});
test('renders query delay settings correctly', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
await waitForModalLoad();
await waitForFlyoutLoad();
expect(result.getByTestId('queryDelayRangeInput').getAttribute('value')).toBe('10');
expect(result.getByTestId('rulesSettingsModalCancelButton')).toBeInTheDocument();
expect(result.getByTestId('rulesSettingsModalSaveButton').getAttribute('disabled')).toBeFalsy();
expect(result.getByTestId('rulesSettingsFlyoutCancelButton')).toBeInTheDocument();
expect(
result.getByTestId('rulesSettingsFlyoutSaveButton').getAttribute('disabled')
).toBeFalsy();
});
test('can save query delay settings', async () => {
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad();
const queryDelayRangeInput = result.getByTestId('queryDelayRangeInput');
fireEvent.change(queryDelayRangeInput, { target: { value: 20 } });
expect(queryDelayRangeInput.getAttribute('value')).toBe('20');
// Try saving
await userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
await userEvent.click(result.getByTestId('rulesSettingsFlyoutSaveButton'));
await waitFor(() => {
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
});
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(flyoutProps.onClose).toHaveBeenCalledTimes(1);
expect(updateQueryDelaySettingsMock).toHaveBeenCalledWith(
expect.objectContaining({
queryDelaySettings: {
@ -375,29 +376,29 @@ describe('rules_settings_modal', () => {
})
);
expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1);
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(modalProps.onSave).toHaveBeenCalledTimes(1);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.onSave).toHaveBeenCalledTimes(1);
});
test('handles errors when saving query delay settings', async () => {
updateQueryDelaySettingsMock.mockRejectedValue('failed!');
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad();
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad();
const queryDelayRangeInput = result.getByTestId('queryDelayRangeInput');
fireEvent.change(queryDelayRangeInput, { target: { value: 20 } });
expect(queryDelayRangeInput.getAttribute('value')).toBe('20');
// Try saving
await userEvent.click(result.getByTestId('rulesSettingsModalSaveButton'));
await userEvent.click(result.getByTestId('rulesSettingsFlyoutSaveButton'));
await waitFor(() => {
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
});
expect(modalProps.onClose).toHaveBeenCalledTimes(1);
expect(flyoutProps.onClose).toHaveBeenCalledTimes(1);
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
expect(modalProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(modalProps.onSave).toHaveBeenCalledTimes(1);
expect(flyoutProps.setUpdatingRulesSettings).toHaveBeenCalledWith(true);
expect(flyoutProps.onSave).toHaveBeenCalledTimes(1);
});
test('query delay form elements are disabled when provided with insufficient write permissions', async () => {
@ -415,11 +416,11 @@ describe('rules_settings_modal', () => {
readQueryDelaySettingsUI: true,
},
};
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad({ flappingSection: false });
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad({ flappingSection: false });
expect(result.getByTestId('queryDelayRangeInput')).toBeDisabled();
expect(result.getByTestId('rulesSettingsModalSaveButton')).toBeDisabled();
expect(result.getByTestId('rulesSettingsFlyoutSaveButton')).toBeDisabled();
});
test('query delay form elements are not visible when provided with insufficient read permissions', async () => {
@ -438,16 +439,16 @@ describe('rules_settings_modal', () => {
},
};
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad({ flappingSection: false, queryDelaySection: false });
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad({ flappingSection: false, queryDelaySection: false });
expect(result.queryByTestId('rulesSettingsQueryDelaySection')).toBe(null);
});
test('hides query delay settings when not serverless', async () => {
useKibanaMock().services.isServerless = false;
const result = render(<RulesSettingsModalWithProviders {...modalProps} />);
await waitForModalLoad({ queryDelaySection: false });
const result = render(<RulesSettingsFlyoutWithProviders {...flyoutProps} />);
await waitForFlyoutLoad({ queryDelaySection: false });
expect(result.queryByTestId('rulesSettingsQueryDelaySection')).not.toBeInTheDocument();
});
});

View file

@ -18,15 +18,18 @@ import {
EuiButtonEmpty,
EuiCallOut,
EuiHorizontalRule,
EuiModal,
EuiModalHeader,
EuiModalBody,
EuiModalFooter,
EuiModalHeaderTitle,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiTitle,
EuiSpacer,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings';
import { css } from '@emotion/react';
import { useKibana } from '../../../common/lib/kibana';
import { RulesSettingsFlappingSection } from './flapping/rules_settings_flapping_section';
import { RulesSettingsQueryDelaySection } from './query_delay/rules_settings_query_delay_section';
@ -44,7 +47,7 @@ export const RulesSettingsErrorPrompt = memo(() => {
title={
<h4>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.errorPromptTitle"
id="xpack.triggersActionsUI.rulesSettings.flyout.errorPromptTitle"
defaultMessage="Unable to load your rules settings"
/>
</h4>
@ -52,7 +55,7 @@ export const RulesSettingsErrorPrompt = memo(() => {
body={
<p>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.errorPromptBody"
id="xpack.triggersActionsUI.rulesSettings.flyout.errorPromptBody"
defaultMessage="There was an error loading your rules settings. Contact your administrator for help"
/>
</p>
@ -81,14 +84,14 @@ const useResettableState: <T>(
return [value, hasChanged, updateValue, reset];
};
export interface RulesSettingsModalProps {
export interface RulesSettingsFlyoutProps {
isVisible: boolean;
setUpdatingRulesSettings?: (isUpdating: boolean) => void;
onClose: () => void;
onSave?: () => void;
}
export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
export const RulesSettingsFlyout = memo((props: RulesSettingsFlyoutProps) => {
const { isVisible, onClose, setUpdatingRulesSettings, onSave } = props;
const {
@ -142,7 +145,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
},
});
const onCloseModal = useCallback(() => {
const onCloseFlyout = useCallback(() => {
resetFlappingSettings();
resetQueryDelaySettings();
onClose();
@ -252,46 +255,59 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
};
return (
<EuiModal data-test-subj="rulesSettingsModal" onClose={onCloseModal} maxWidth={880}>
<EuiModalHeader>
<EuiModalHeaderTitle component="h3">
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.title"
defaultMessage="Rule settings"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCallOut
size="s"
title={i18n.translate('xpack.triggersActionsUI.rulesSettings.modal.calloutMessage', {
defaultMessage: 'Apply to all rules within the current space.',
})}
/>
<EuiHorizontalRule />
{maybeRenderForm()}
<EuiSpacer />
<EuiHorizontalRule margin="none" />
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty data-test-subj="rulesSettingsModalCancelButton" onClick={onCloseModal}>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="rulesSettingsModalSaveButton"
onClick={handleSave}
disabled={!canWriteFlappingSettings && !canWriteQueryDelaySettings}
>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.modal.saveButton"
defaultMessage="Save"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
<EuiFlyout type="push" data-test-subj="rulesSettingsFlyout" onClose={onCloseFlyout} size="s">
<EuiFlyoutHeader>
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.flyout.title"
defaultMessage="Rule settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
</EuiFlyoutHeader>
<EuiHorizontalRule margin="none" />
<EuiCallOut
size="m"
title={i18n.translate('xpack.triggersActionsUI.rulesSettings.flyout.calloutMessage', {
defaultMessage: 'Apply to all rules within the current space.',
})}
css={css`
position: sticky;
`}
/>
<EuiFlyoutBody>{maybeRenderForm()}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="rulesSettingsFlyoutCancelButton"
onClick={onCloseFlyout}
>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.flyout.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
data-test-subj="rulesSettingsFlyoutSaveButton"
onClick={handleSave}
disabled={!canWriteFlappingSettings && !canWriteQueryDelaySettings}
>
<FormattedMessage
id="xpack.triggersActionsUI.rulesSettings.flyout.saveButton"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
});

View file

@ -107,7 +107,7 @@ describe('rules_settings_link', () => {
expect(result.getByText('Settings')).toBeInTheDocument();
});
expect(result.getByText('Settings')).not.toBeDisabled();
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
expect(result.queryByTestId('rulesSettingsFlyout')).toBe(null);
});
test('renders the rules setting link correctly (readFlappingSettingsUI = true)', async () => {
@ -133,7 +133,7 @@ describe('rules_settings_link', () => {
expect(result.getByText('Settings')).toBeInTheDocument();
});
expect(result.getByText('Settings')).not.toBeDisabled();
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
expect(result.queryByTestId('rulesSettingsFlyout')).toBe(null);
});
test('renders the rules setting link correctly (readQueryDelaySettingsUI = true)', async () => {
@ -159,19 +159,19 @@ describe('rules_settings_link', () => {
expect(result.getByText('Settings')).toBeInTheDocument();
});
expect(result.getByText('Settings')).not.toBeDisabled();
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
expect(result.queryByTestId('rulesSettingsFlyout')).toBe(null);
});
test('clicking the settings link opens the rules settings modal', async () => {
const result = render(<RulesSettingsLinkWithProviders />);
await waitFor(() => {
expect(result.queryByTestId('rulesSettingsModal')).toBe(null);
expect(result.queryByTestId('rulesSettingsFlyout')).toBe(null);
});
await userEvent.click(result.getByText('Settings'));
await waitFor(() => {
expect(result.queryByTestId('rulesSettingsModal')).not.toBe(null);
expect(result.queryByTestId('rulesSettingsFlyout')).not.toBe(null);
});
});

View file

@ -8,7 +8,7 @@
import React, { useState } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { RulesSettingsModal } from './rules_settings_modal';
import { RulesSettingsFlyout } from './rules_settings_flyout';
import { useKibana } from '../../../common/lib/kibana';
export const RulesSettingsLink = () => {
@ -35,7 +35,7 @@ export const RulesSettingsLink = () => {
defaultMessage="Settings"
/>
</EuiButtonEmpty>
<RulesSettingsModal isVisible={isVisible} onClose={() => setIsVisible(false)} />
<RulesSettingsFlyout isVisible={isVisible} onClose={() => setIsVisible(false)} />
</>
);
};

View file

@ -47,14 +47,14 @@ export const useUpdateRuleSettings = (props: UseUpdateRuleSettingsProps) => {
},
onSuccess: () => {
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsSuccess', {
i18n.translate('xpack.triggersActionsUI.rulesSettings.updateRulesSettingsSuccess', {
defaultMessage: 'Rules settings updated successfully.',
})
);
},
onError: () => {
toasts.addDanger(
i18n.translate('xpack.triggersActionsUI.rulesSettings.modal.updateRulesSettingsFailure', {
i18n.translate('xpack.triggersActionsUI.rulesSettings.updateRulesSettingsFailure', {
defaultMessage: 'Failed to update rules settings.',
})
);

View file

@ -26,7 +26,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('should be able to open the modal', async () => {
await testSubjects.click('rulesSettingsLink');
await testSubjects.waitForDeleted('centerJustifiedSpinner');
await testSubjects.existOrFail('rulesSettingsModal');
await testSubjects.existOrFail('rulesSettingsFlyout');
});
});
};

View file

@ -81,7 +81,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('should allow the user to open up the rules settings modal', async () => {
await testSubjects.click('rulesSettingsLink');
await testSubjects.existOrFail('rulesSettingsModal');
await testSubjects.existOrFail('rulesSettingsFlyout');
await testSubjects.waitForDeleted('centerJustifiedSpinner');
// Flapping enabled by default
@ -135,9 +135,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.existOrFail('rulesSettingsFlappingOffPrompt');
// Save
await testSubjects.click('rulesSettingsModalSaveButton');
await testSubjects.click('rulesSettingsFlyoutSaveButton');
await pageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('rulesSettingsModal');
await testSubjects.missingOrFail('rulesSettingsFlyout');
// Open up the modal again
await testSubjects.click('rulesSettingsLink');