From 1b89bd5afaf569defeb91b460f831604b4bb4093 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 8 Jan 2025 14:58:05 -0600 Subject: [PATCH 01/14] Add new rule flyout to response-ops pkg --- .../rule_form/src/create_rule_form.tsx | 3 +- .../rule_form/src/edit_rule_form.tsx | 3 +- .../src/hooks/use_rule_form_steps.tsx | 20 ++- .../rule_form/src/rule_flyout/index.ts | 10 ++ .../rule_form/src/rule_flyout/rule_flyout.tsx | 140 ++++++++++++++++++ .../rule_flyout/rule_flyout_create_footer.tsx | 99 +++++++++++++ .../rule_flyout/rule_flyout_edit_footer.tsx | 74 +++++++++ .../src/rule_flyout/rule_flyout_edit_tabs.tsx | 37 +++++ .../rule_form/src/translations.ts | 63 ++++++++ 9 files changed, 443 insertions(+), 6 deletions(-) create mode 100644 packages/response-ops/rule_form/src/rule_flyout/index.ts create mode 100644 packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx create mode 100644 packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx create mode 100644 packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx create mode 100644 packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_tabs.tsx diff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx index 7b0a6f8fb3d6b..7d98683e08903 100644 --- a/packages/response-ops/rule_form/src/create_rule_form.tsx +++ b/packages/response-ops/rule_form/src/create_rule_form.tsx @@ -31,6 +31,7 @@ import { parseRuleCircuitBreakerErrorMessage, } from './utils'; import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations'; +import { RuleFlyout } from './rule_flyout'; export interface CreateRuleFormProps { ruleTypeId: string; @@ -198,7 +199,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - + ); diff --git a/packages/response-ops/rule_form/src/edit_rule_form.tsx b/packages/response-ops/rule_form/src/edit_rule_form.tsx index d1bdb8afab83e..ac7563c56c93e 100644 --- a/packages/response-ops/rule_form/src/edit_rule_form.tsx +++ b/packages/response-ops/rule_form/src/edit_rule_form.tsx @@ -26,6 +26,7 @@ import { import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations'; import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils'; import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; +import { RuleFlyout } from './rule_flyout'; export interface EditRuleFormProps { id: string; @@ -211,7 +212,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { showMustacheAutocompleteSwitch, }} > - + ); diff --git a/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx b/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx index d484f6d8c58c6..74b926a2bc20c 100644 --- a/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx +++ b/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx @@ -18,6 +18,8 @@ import { RULE_FORM_PAGE_RULE_ACTIONS_TITLE, RULE_FORM_PAGE_RULE_DEFINITION_TITLE, RULE_FORM_PAGE_RULE_DETAILS_TITLE, + RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT, + RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT, } from '../translations'; import { hasActionsError, hasActionsParamsErrors, hasParamsErrors } from '../validation'; import { RuleFormStepId } from '../constants'; @@ -27,6 +29,7 @@ interface UseRuleFormStepsOptions { touchedSteps: Record; /* Used to track the current step in horizontal steps, not used for vertical steps */ currentStep?: RuleFormStepId; + shortTitles?: boolean; } /** @@ -69,7 +72,11 @@ const getStepStatus = ({ }; // Create a common hook for both horizontal and vertical steps -const useCommonRuleFormSteps = ({ touchedSteps, currentStep }: UseRuleFormStepsOptions) => { +const useCommonRuleFormSteps = ({ + touchedSteps, + currentStep, + shortTitles, +}: UseRuleFormStepsOptions) => { const { plugins: { application }, baseErrors = {}, @@ -132,7 +139,9 @@ const useCommonRuleFormSteps = ({ touchedSteps, currentStep }: UseRuleFormStepsO const steps = useMemo( () => ({ [RuleFormStepId.DEFINITION]: { - title: RULE_FORM_PAGE_RULE_DEFINITION_TITLE, + title: shortTitles + ? RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT + : RULE_FORM_PAGE_RULE_DEFINITION_TITLE, status: ruleDefinitionStatus, children: , }, @@ -150,7 +159,9 @@ const useCommonRuleFormSteps = ({ touchedSteps, currentStep }: UseRuleFormStepsO } : null, [RuleFormStepId.DETAILS]: { - title: RULE_FORM_PAGE_RULE_DETAILS_TITLE, + title: shortTitles + ? RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT + : RULE_FORM_PAGE_RULE_DETAILS_TITLE, status: ruleDetailsStatus, children: ( <> @@ -161,7 +172,7 @@ const useCommonRuleFormSteps = ({ touchedSteps, currentStep }: UseRuleFormStepsO ), }, }), - [ruleDefinitionStatus, canReadConnectors, actionsStatus, ruleDetailsStatus] + [ruleDefinitionStatus, canReadConnectors, actionsStatus, ruleDetailsStatus, shortTitles] ); const stepOrder: RuleFormStepId[] = useMemo( @@ -247,6 +258,7 @@ export const useRuleFormHorizontalSteps: () => RuleFormHorizontalSteps = () => { const { steps, stepOrder } = useCommonRuleFormSteps({ touchedSteps, currentStep, + shortTitles: true, }); // Determine current navigation position diff --git a/packages/response-ops/rule_form/src/rule_flyout/index.ts b/packages/response-ops/rule_form/src/rule_flyout/index.ts new file mode 100644 index 0000000000000..bd430e32d43c3 --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/index.ts @@ -0,0 +1,10 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './rule_flyout'; diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx new file mode 100644 index 0000000000000..8f0e57df10057 --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -0,0 +1,140 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiStepsHorizontal, + EuiTitle, + useEuiBackgroundColorCSS, +} from '@elastic/eui'; +import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; +import React, { useCallback, useMemo } from 'react'; +import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks'; +import { RULE_FLYOUT_HEADER_CREATE_TITLE, RULE_FLYOUT_HEADER_EDIT_TITLE } from '../translations'; +import type { RuleFormData } from '../types'; +import { hasRuleErrors } from '../validation'; +import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; +import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer'; + +import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs'; + +export interface RuleFlyoutProps { + isEdit?: boolean; + isSaving?: boolean; + onCancel?: () => void; + onSave: (formData: RuleFormData) => void; +} + +export const RuleFlyout = (props: RuleFlyoutProps) => { + const { isEdit = false, isSaving = false, onCancel = () => {}, onSave } = props; + + const { + formData, + multiConsumerSelection, + connectorTypes, + connectors, + baseErrors = {}, + paramsErrors = {}, + actionsErrors = {}, + actionsParamsErrors = {}, + } = useRuleFormState(); + + const hasErrors = useMemo(() => { + const hasBrokenConnectors = formData.actions.some((action) => { + return !connectors.find((connector) => connector.id === action.id); + }); + + if (hasBrokenConnectors) { + return true; + } + + return hasRuleErrors({ + baseErrors, + paramsErrors, + actionsErrors, + actionsParamsErrors, + }); + }, [formData, connectors, baseErrors, paramsErrors, actionsErrors, actionsParamsErrors]); + + const { + steps, + currentStepComponent, + goToNextStep, + goToPreviousStep, + hasNextStep, + hasPreviousStep, + } = useRuleFormHorizontalSteps(); + + const { actions } = formData; + + const styles = useEuiBackgroundColorCSS().transparent; + + const onSaveInternal = useCallback(() => { + onSave({ + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }); + }, [onSave, formData, multiConsumerSelection]); + + const hasActionsDisabled = useMemo(() => { + const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); + return actions.some((action) => { + const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId); + if (!actionType) { + return false; + } + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionType, + preconfiguredConnectors + ); + return !actionType.enabled && !checkEnabledResult.isEnabled; + }); + }, [actions, connectors, connectorTypes]); + + return ( + + + + +

+ {isEdit ? RULE_FLYOUT_HEADER_EDIT_TITLE : RULE_FLYOUT_HEADER_CREATE_TITLE} +

+
+ {isEdit && } +
+ + {!isEdit && } + {currentStepComponent} + + {isEdit ? ( + + ) : ( + + )} +
+
+ ); +}; diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx new file mode 100644 index 0000000000000..88114e1b52142 --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx @@ -0,0 +1,99 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, +} from '@elastic/eui'; +import React from 'react'; +import { + RULE_FLYOUT_FOOTER_BACK_TEXT, + RULE_FLYOUT_FOOTER_CANCEL_TEXT, + RULE_FLYOUT_FOOTER_CREATE_TEXT, + RULE_FLYOUT_FOOTER_NEXT_TEXT, +} from '../translations'; + +export interface RuleFlyoutCreateFooterProps { + isSaving: boolean; + hasErrors: boolean; + onCancel: () => void; + onSave: () => void; + hasNextStep: boolean; + hasPreviousStep: boolean; + goToNextStep: () => void; + goToPreviousStep: () => void; +} +export const RuleFlyoutCreateFooter = ({ + onCancel, + onSave, + hasErrors, + isSaving, + hasNextStep, + hasPreviousStep, + goToNextStep, + goToPreviousStep, +}: RuleFlyoutCreateFooterProps) => { + return ( + + + + {hasPreviousStep ? ( + + {RULE_FLYOUT_FOOTER_BACK_TEXT} + + ) : ( + + {RULE_FLYOUT_FOOTER_CANCEL_TEXT} + + )} + + + + + {/* + + + + */} + + {hasNextStep ? ( + + {RULE_FLYOUT_FOOTER_NEXT_TEXT} + + ) : ( + + {RULE_FLYOUT_FOOTER_CREATE_TEXT} + + )} + + + + + + ); +}; diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx new file mode 100644 index 0000000000000..63e03fda3a3fe --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx @@ -0,0 +1,74 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, +} from '@elastic/eui'; +import React from 'react'; +import { RULE_FLYOUT_FOOTER_CANCEL_TEXT, RULE_FLYOUT_FOOTER_SAVE_TEXT } from '../translations'; + +export interface RuleFlyoutEditFooterProps { + isSaving: boolean; + hasErrors: boolean; + onCancel: () => void; + onSave: () => void; +} +export const RuleFlyoutEditFooter = ({ + onCancel, + onSave, + hasErrors, + isSaving, +}: RuleFlyoutEditFooterProps) => { + return ( + + + + + {RULE_FLYOUT_FOOTER_CANCEL_TEXT} + + + + + + {/* + + + + */} + + + {RULE_FLYOUT_FOOTER_SAVE_TEXT} + + + + + + + ); +}; diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_tabs.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_tabs.tsx new file mode 100644 index 0000000000000..ea560832f9514 --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_tabs.tsx @@ -0,0 +1,37 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useMemo } from 'react'; +import { EuiTabs, EuiTab, useEuiPaddingSize } from '@elastic/eui'; +import { EuiStepHorizontalProps } from '@elastic/eui/src/components/steps/step_horizontal'; + +interface RuleFlyoutEditTabsProps { + steps: Array>; +} + +export const RuleFlyoutEditTabs = ({ steps }: RuleFlyoutEditTabsProps) => { + const bottomMarginOffset = `-${useEuiPaddingSize('l')}`; + + const tabs = useMemo( + () => + steps.map((step, index) => { + return ( + + {step.title} + + ); + }), + [steps] + ); + return ( +
+ {tabs} +
+ ); +}; diff --git a/packages/response-ops/rule_form/src/translations.ts b/packages/response-ops/rule_form/src/translations.ts index 3f34baee68b3c..ca55db87090d6 100644 --- a/packages/response-ops/rule_form/src/translations.ts +++ b/packages/response-ops/rule_form/src/translations.ts @@ -307,6 +307,55 @@ export const RULE_PAGE_FOOTER_SAVE_TEXT = i18n.translate( } ); +export const RULE_FLYOUT_HEADER_CREATE_TITLE = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutHeader.createTitle', + { + defaultMessage: 'Create rule', + } +); + +export const RULE_FLYOUT_HEADER_EDIT_TITLE = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutHeader.editTitle', + { + defaultMessage: 'Edit rule', + } +); + +export const RULE_FLYOUT_FOOTER_CANCEL_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.cancelText', + { + defaultMessage: 'Cancel', + } +); + +export const RULE_FLYOUT_FOOTER_BACK_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.backText', + { + defaultMessage: 'Back', + } +); + +export const RULE_FLYOUT_FOOTER_NEXT_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.nextText', + { + defaultMessage: 'Next', + } +); + +export const RULE_FLYOUT_FOOTER_CREATE_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.createText', + { + defaultMessage: 'Create rule', + } +); + +export const RULE_FLYOUT_FOOTER_SAVE_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.saveText', + { + defaultMessage: 'Save changes', + } +); + export const HEALTH_CHECK_ALERTS_ERROR_TITLE = i18n.translate( 'responseOpsRuleForm.healthCheck.alertsErrorTitle', { @@ -490,6 +539,13 @@ export const RULE_FORM_PAGE_RULE_DEFINITION_TITLE = i18n.translate( } ); +export const RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleDefinitionTitleShort', + { + defaultMessage: 'Definition', + } +); + export const RULE_FORM_PAGE_RULE_ACTIONS_TITLE = i18n.translate( 'responseOpsRuleForm.ruleForm.ruleActionsTitle', { @@ -518,6 +574,13 @@ export const RULE_FORM_PAGE_RULE_DETAILS_TITLE = i18n.translate( } ); +export const RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleDetailsTitleShort', + { + defaultMessage: 'Details', + } +); + export const RULE_FORM_RETURN_TITLE = i18n.translate('responseOpsRuleForm.ruleForm.returnTitle', { defaultMessage: 'Return', }); From 41e6e30075dee7f8721368d1a72a9e2176fd5302 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 8 Jan 2025 15:51:42 -0600 Subject: [PATCH 02/14] Add responsive rule form layout with container query --- .../response-ops/rule_form/src/create_rule_form.tsx | 3 +-- packages/response-ops/rule_form/src/edit_rule_form.tsx | 3 +-- .../rule_form/src/rule_definition/rule_definition.tsx | 8 ++++---- .../rule_form/src/rule_flyout/rule_flyout.tsx | 10 ++++++++-- packages/response-ops/rule_form/src/rule_form.tsx | 7 ++++++- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx index 7d98683e08903..7b0a6f8fb3d6b 100644 --- a/packages/response-ops/rule_form/src/create_rule_form.tsx +++ b/packages/response-ops/rule_form/src/create_rule_form.tsx @@ -31,7 +31,6 @@ import { parseRuleCircuitBreakerErrorMessage, } from './utils'; import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations'; -import { RuleFlyout } from './rule_flyout'; export interface CreateRuleFormProps { ruleTypeId: string; @@ -199,7 +198,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - + ); diff --git a/packages/response-ops/rule_form/src/edit_rule_form.tsx b/packages/response-ops/rule_form/src/edit_rule_form.tsx index ac7563c56c93e..d1bdb8afab83e 100644 --- a/packages/response-ops/rule_form/src/edit_rule_form.tsx +++ b/packages/response-ops/rule_form/src/edit_rule_form.tsx @@ -26,7 +26,6 @@ import { import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations'; import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils'; import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; -import { RuleFlyout } from './rule_flyout'; export interface EditRuleFormProps { id: string; @@ -212,7 +211,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { showMustacheAutocompleteSwitch, }} > - + ); diff --git a/packages/response-ops/rule_form/src/rule_definition/rule_definition.tsx b/packages/response-ops/rule_form/src/rule_definition/rule_definition.tsx index 5cedd0d117527..70f63642e653b 100644 --- a/packages/response-ops/rule_form/src/rule_definition/rule_definition.tsx +++ b/packages/response-ops/rule_form/src/rule_definition/rule_definition.tsx @@ -193,20 +193,20 @@ export const RuleDefinition = () => { return ( - + - + {selectedRuleType.name} - +

{selectedRuleTypeModel.description}

{docsUrl && ( - + { return ( - +

diff --git a/packages/response-ops/rule_form/src/rule_form.tsx b/packages/response-ops/rule_form/src/rule_form.tsx index f34f811a7b488..5b3f43a5bd4ba 100644 --- a/packages/response-ops/rule_form/src/rule_form.tsx +++ b/packages/response-ops/rule_form/src/rule_form.tsx @@ -18,6 +18,7 @@ import { RULE_FORM_ROUTE_PARAMS_ERROR_TEXT, } from './translations'; import { RuleFormPlugins } from './types'; +import './rule_form.scss'; const queryClient = new QueryClient(); @@ -114,5 +115,9 @@ export const RuleForm = (props: RuleFormProps) => { onSubmit, ]); - return {ruleFormComponent}; + return ( + +
{ruleFormComponent}
+
+ ); }; From 9afa612dca2fdd0ad8089b96f008afc3a288ed1c Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 8 Jan 2025 16:29:06 -0600 Subject: [PATCH 03/14] Add empty prompt with illustration for stration when no actions --- .../rule_form/src/create_rule_form.tsx | 3 +- .../src/rule_actions/rule_actions.tsx | 85 +- .../rule_actions_illustration.svg | 1025 +++++++++++++++++ .../response-ops/rule_form/src/rule_form.scss | 27 + .../rule_form/src/translations.ts | 22 + 5 files changed, 1146 insertions(+), 16 deletions(-) create mode 100644 packages/response-ops/rule_form/src/rule_actions/rule_actions_illustration.svg create mode 100644 packages/response-ops/rule_form/src/rule_form.scss diff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx index 7b0a6f8fb3d6b..7d98683e08903 100644 --- a/packages/response-ops/rule_form/src/create_rule_form.tsx +++ b/packages/response-ops/rule_form/src/create_rule_form.tsx @@ -31,6 +31,7 @@ import { parseRuleCircuitBreakerErrorMessage, } from './utils'; import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations'; +import { RuleFlyout } from './rule_flyout'; export interface CreateRuleFormProps { ruleTypeId: string; @@ -198,7 +199,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - + ); diff --git a/packages/response-ops/rule_form/src/rule_actions/rule_actions.tsx b/packages/response-ops/rule_form/src/rule_actions/rule_actions.tsx index 2479c07edfb5f..168a7d4f5a3c3 100644 --- a/packages/response-ops/rule_form/src/rule_actions/rule_actions.tsx +++ b/packages/response-ops/rule_form/src/rule_actions/rule_actions.tsx @@ -7,22 +7,41 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useMemo, useState } from 'react'; -import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { v4 as uuidv4 } from 'uuid'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText } from '@elastic/eui'; import { RuleSystemAction } from '@kbn/alerting-types'; import { ActionConnector } from '@kbn/alerts-ui-shared'; -import { ADD_ACTION_TEXT } from '../translations'; -import { RuleActionsConnectorsModal } from './rule_actions_connectors_modal'; -import { useRuleFormDispatch, useRuleFormState } from '../hooks'; +import React, { useCallback, useMemo, useState } from 'react'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { v4 as uuidv4 } from 'uuid'; import { RuleAction, RuleFormParamsErrors } from '../common/types'; import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { useRuleFormDispatch, useRuleFormState } from '../hooks'; +import { + ADD_ACTION_DESCRIPTION_TEXT, + ADD_ACTION_HEADER, + ADD_ACTION_OPTIONAL_TEXT, + ADD_ACTION_TEXT, +} from '../translations'; +import { getDefaultParams } from '../utils'; +import { RuleActionsConnectorsModal } from './rule_actions_connectors_modal'; import { RuleActionsItem } from './rule_actions_item'; import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item'; -import { getDefaultParams } from '../utils'; + +const useRuleActionsIllustration = () => { + const [imageData, setImageData] = useState(''); + useEffectOnce(() => { + const fetchImage = async () => { + const image = await import('./rule_actions_illustration.svg'); + setImageData(image.default); + }; + fetchImage(); + }); + return imageData; +}; export const RuleActions = () => { const [isConnectorModalOpen, setIsConnectorModalOpen] = useState(false); + const ruleActionsIllustration = useRuleActionsIllustration(); const { formData: { actions, consumer }, @@ -92,6 +111,8 @@ export const RuleActions = () => { return selectedRuleType.producer; }, [consumer, multiConsumerSelection, selectedRuleType]); + const hasActions = actions.length > 0; + return ( <> @@ -120,15 +141,49 @@ export const RuleActions = () => { ); })} + {!hasActions && ( + + + + + +

{ADD_ACTION_HEADER}

+
+ + {ADD_ACTION_OPTIONAL_TEXT} + +
+ + + {ADD_ACTION_DESCRIPTION_TEXT} + + +
+
+ )} - - {ADD_ACTION_TEXT} - + + + + {ADD_ACTION_TEXT} + + + {isConnectorModalOpen && ( )} diff --git a/packages/response-ops/rule_form/src/rule_actions/rule_actions_illustration.svg b/packages/response-ops/rule_form/src/rule_actions/rule_actions_illustration.svg new file mode 100644 index 0000000000000..caab2d84dd8f0 --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_actions/rule_actions_illustration.svg @@ -0,0 +1,1025 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/response-ops/rule_form/src/rule_form.scss b/packages/response-ops/rule_form/src/rule_form.scss new file mode 100644 index 0000000000000..6631f1674c40b --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_form.scss @@ -0,0 +1,27 @@ +.ruleForm__container { + container: ruleForm / inline-size; +} + +.ruleFormFlyout__container { + container: ruleForm / inline-size; +} + +@container (max-width: 768px) { + .euiDescribedFormGroup { + flex-direction: column; + } + .euiDescribedFormGroup > .euiFlexItem { + width: 100%; + } + .ruleDefinitionHeader { + flex-direction: column; + gap: 12px; + } + .ruleDefinitionHeaderRuleTypeName { + font-size: 18px; + margin-bottom: 4px; + } + .ruleDefinitionHeaderRuleTypeDescription, .ruleDefinitionHeaderDocsLink { + font-size: 14px; + } +} \ No newline at end of file diff --git a/packages/response-ops/rule_form/src/translations.ts b/packages/response-ops/rule_form/src/translations.ts index ca55db87090d6..eebe9dd2157c3 100644 --- a/packages/response-ops/rule_form/src/translations.ts +++ b/packages/response-ops/rule_form/src/translations.ts @@ -233,6 +233,28 @@ export const ADD_ACTION_TEXT = i18n.translate( } ); +export const ADD_ACTION_HEADER = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleActions.addActionHeader', + { + defaultMessage: 'Add an action', + } +); + +export const ADD_ACTION_OPTIONAL_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleActions.addActionOptionalText', + { + defaultMessage: 'Optional', + } +); + +export const ADD_ACTION_DESCRIPTION_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleActions.addActionDescriptionText', + { + defaultMessage: + 'Select a connector and configure the actions to be performed when an alert is triggered', + } +); + export const RULE_DETAILS_TITLE = i18n.translate('responseOpsRuleForm.ruleForm.ruleDetails.title', { defaultMessage: 'Rule name and tags', }); From 4f54a09d79944cf501fe29abf8401910c64020fb Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 8 Jan 2025 16:32:49 -0600 Subject: [PATCH 04/14] Restore original create page --- packages/response-ops/rule_form/src/create_rule_form.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx index 7d98683e08903..7b0a6f8fb3d6b 100644 --- a/packages/response-ops/rule_form/src/create_rule_form.tsx +++ b/packages/response-ops/rule_form/src/create_rule_form.tsx @@ -31,7 +31,6 @@ import { parseRuleCircuitBreakerErrorMessage, } from './utils'; import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations'; -import { RuleFlyout } from './rule_flyout'; export interface CreateRuleFormProps { ruleTypeId: string; @@ -199,7 +198,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - + ); From 91cc28cd5a12ba494897003fff1a0fe69cdae654 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 9 Jan 2025 10:38:50 -0600 Subject: [PATCH 05/14] Fix stylelint --- packages/response-ops/rule_form/src/rule_form.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/response-ops/rule_form/src/rule_form.scss b/packages/response-ops/rule_form/src/rule_form.scss index 6631f1674c40b..f49760300c0f2 100644 --- a/packages/response-ops/rule_form/src/rule_form.scss +++ b/packages/response-ops/rule_form/src/rule_form.scss @@ -6,7 +6,9 @@ container: ruleForm / inline-size; } -@container (max-width: 768px) { +/* TODO: Remove stylelint disable once we upgrade stylelint past 14.12.0 */ +/* https://github.com/stylelint/stylelint/issues/6304 */ +@container (max-width: 768px) { /* stylelint-disable-line */ .euiDescribedFormGroup { flex-direction: column; } From 441b089763ea52aa8660208a818b82cc066ae945 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 9 Jan 2025 10:41:01 -0600 Subject: [PATCH 06/14] Add missing disabled actions warning --- .../rule_form/src/rule_flyout/rule_flyout.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 551fa566d0b6d..7002dc1f852b4 100644 --- a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -14,12 +14,17 @@ import { EuiPortal, EuiStepsHorizontal, EuiTitle, - useEuiBackgroundColorCSS, + EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; import React, { useCallback, useMemo } from 'react'; import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks'; -import { RULE_FLYOUT_HEADER_CREATE_TITLE, RULE_FLYOUT_HEADER_EDIT_TITLE } from '../translations'; +import { + RULE_FLYOUT_HEADER_CREATE_TITLE, + RULE_FLYOUT_HEADER_EDIT_TITLE, + DISABLED_ACTIONS_WARNING_TITLE, +} from '../translations'; import type { RuleFormData } from '../types'; import { hasRuleErrors } from '../validation'; import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; @@ -75,8 +80,6 @@ export const RuleFlyout = (props: RuleFlyoutProps) => { const { actions } = formData; - const styles = useEuiBackgroundColorCSS().transparent; - const onSaveInternal = useCallback(() => { onSave({ ...formData, @@ -119,6 +122,18 @@ export const RuleFlyout = (props: RuleFlyoutProps) => { {!isEdit && } + {hasActionsDisabled && ( + <> + + + + )} {currentStepComponent} {isEdit ? ( From 9696f532f7ce97b47ab974098cd7e87755961a25 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 9 Jan 2025 10:44:35 -0600 Subject: [PATCH 07/14] Add missing svg typedef --- packages/response-ops/rule_form/tsconfig.json | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/response-ops/rule_form/tsconfig.json b/packages/response-ops/rule_form/tsconfig.json index cea1478df1ef8..5bf3091524e90 100644 --- a/packages/response-ops/rule_form/tsconfig.json +++ b/packages/response-ops/rule_form/tsconfig.json @@ -2,19 +2,10 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "types": [ - "jest", - "node", - "react" - ] + "types": ["jest", "node", "react", "@kbn/ambient-ui-types"] }, - "include": [ - "**/*.ts", - "**/*.tsx", - ], - "exclude": [ - "target/**/*" - ], + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["target/**/*"], "kbn_references": [ "@kbn/alerting-types", "@kbn/i18n", @@ -39,6 +30,6 @@ "@kbn/kibana-react-plugin", "@kbn/core-i18n-browser", "@kbn/core-theme-browser", - "@kbn/core-user-profile-browser", + "@kbn/core-user-profile-browser" ] } From 740a9b76d88721859cbfe4ba5cd88e56d255f1ff Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 9 Jan 2025 11:10:03 -0600 Subject: [PATCH 08/14] Add rule flyout tests --- .../src/rule_flyout/rule_flyout.test.tsx | 151 ++++++++++++++++++ .../rule_flyout/rule_flyout_create_footer.tsx | 15 +- 2 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 packages/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx new file mode 100644 index 0000000000000..ec8f85d025fb8 --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx @@ -0,0 +1,151 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { RuleFlyout } from './rule_flyout'; +import { + RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT, + RULE_FORM_PAGE_RULE_ACTIONS_TITLE, + RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT, +} from '../translations'; +import { RuleFormData } from '../types'; + +jest.mock('../rule_definition', () => ({ + RuleDefinition: () =>
, +})); + +jest.mock('../rule_actions', () => ({ + RuleActions: () =>
, +})); + +jest.mock('../rule_details', () => ({ + RuleDetails: () =>
, +})); + +jest.mock('../hooks/use_rule_form_state', () => ({ + useRuleFormState: jest.fn(), +})); + +jest.mock('../hooks/use_rule_form_dispatch', () => ({ + useRuleFormDispatch: jest.fn(), +})); + +const { useRuleFormState } = jest.requireMock('../hooks/use_rule_form_state'); + +const navigateToUrl = jest.fn(); + +const formDataMock: RuleFormData = { + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'alert.executionStatus.lastExecutionDate', + }, + actions: [], + consumer: 'stackAlerts', + schedule: { interval: '1m' }, + tags: [], + name: 'test', + notifyWhen: 'onActionGroupChange', + alertDelay: { + active: 10, + }, +}; + +const onCancel = jest.fn(); + +useRuleFormState.mockReturnValue({ + plugins: { + application: { + navigateToUrl, + capabilities: { + actions: { + show: true, + save: true, + execute: true, + }, + }, + }, + }, + baseErrors: {}, + paramsErrors: {}, + multiConsumerSelection: 'logs', + formData: formDataMock, + connectors: [], + connectorTypes: [], + aadTemplateFields: [], +}); + +const onSave = jest.fn(); + +describe('ruleFlyout', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders correctly', () => { + render(); + + expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT)).toBeInTheDocument(); + expect(screen.getByText(RULE_FORM_PAGE_RULE_ACTIONS_TITLE)).toBeInTheDocument(); + expect(screen.getByText(RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT)).toBeInTheDocument(); + + expect(screen.getByTestId('ruleFlyoutFooterCancelButton')).toBeInTheDocument(); + expect(screen.getByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument(); + }); + + test('should navigate back and forth through steps correctly', async () => { + render(); + + fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); + await waitFor(() => + expect(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument() + ); + fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); + await waitFor(() => + expect(screen.getByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument() + ); + fireEvent.click(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')); + await waitFor(() => + expect(screen.getByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument() + ); + }); + + test('should call onSave when save button is pressed', async () => { + render(); + + fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); + await waitFor(() => + expect(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument() + ); + fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); + await waitFor(() => + expect(screen.getByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument() + ); + fireEvent.click(screen.getByTestId('ruleFlyoutFooterSaveButton')); + + expect(onSave).toHaveBeenCalledWith({ + ...formDataMock, + consumer: 'logs', + }); + }); + + test('should call onCancel when the cancel button is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('ruleFlyoutFooterCancelButton')); + expect(onCancel).toHaveBeenCalled(); + }); +}); diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx index 88114e1b52142..8b1c091dc22bc 100644 --- a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx @@ -47,11 +47,14 @@ export const RuleFlyoutCreateFooter = ({ {hasPreviousStep ? ( - + {RULE_FLYOUT_FOOTER_BACK_TEXT} ) : ( - + {RULE_FLYOUT_FOOTER_CANCEL_TEXT} )} @@ -75,13 +78,17 @@ export const RuleFlyoutCreateFooter = ({ */} {hasNextStep ? ( - + {RULE_FLYOUT_FOOTER_NEXT_TEXT} ) : ( Date: Thu, 9 Jan 2025 11:21:36 -0600 Subject: [PATCH 09/14] Add show request buttons --- .../rule_form/src/rule_flyout/rule_flyout.tsx | 2 ++ .../rule_flyout/rule_flyout_create_footer.tsx | 29 ++++++++++--------- .../rule_flyout/rule_flyout_edit_footer.tsx | 23 ++++++++------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 7002dc1f852b4..bc43c173b5e61 100644 --- a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -140,6 +140,7 @@ export const RuleFlyout = (props: RuleFlyoutProps) => { {} /* TODO */} isSaving={isSaving} hasErrors={hasErrors} /> @@ -147,6 +148,7 @@ export const RuleFlyout = (props: RuleFlyoutProps) => { {} /* TODO */} goToNextStep={goToNextStep} goToPreviousStep={goToPreviousStep} isSaving={isSaving} diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx index 8b1c091dc22bc..caacacb9240dc 100644 --- a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_create_footer.tsx @@ -20,6 +20,7 @@ import { RULE_FLYOUT_FOOTER_CANCEL_TEXT, RULE_FLYOUT_FOOTER_CREATE_TEXT, RULE_FLYOUT_FOOTER_NEXT_TEXT, + RULE_PAGE_FOOTER_SHOW_REQUEST_TEXT, } from '../translations'; export interface RuleFlyoutCreateFooterProps { @@ -27,6 +28,7 @@ export interface RuleFlyoutCreateFooterProps { hasErrors: boolean; onCancel: () => void; onSave: () => void; + onShowRequest: () => void; hasNextStep: boolean; hasPreviousStep: boolean; goToNextStep: () => void; @@ -35,6 +37,7 @@ export interface RuleFlyoutCreateFooterProps { export const RuleFlyoutCreateFooter = ({ onCancel, onSave, + onShowRequest, hasErrors, isSaving, hasNextStep, @@ -62,20 +65,18 @@ export const RuleFlyoutCreateFooter = ({ - {/* - - - - */} + {!hasNextStep && ( + + + {RULE_PAGE_FOOTER_SHOW_REQUEST_TEXT} + + + )} {hasNextStep ? ( void; onSave: () => void; + onShowRequest: () => void; } export const RuleFlyoutEditFooter = ({ onCancel, onSave, + onShowRequest, hasErrors, isSaving, }: RuleFlyoutEditFooterProps) => { @@ -40,20 +46,17 @@ export const RuleFlyoutEditFooter = ({ - {/* + - + {RULE_PAGE_FOOTER_SHOW_REQUEST_TEXT} - */} + + Date: Thu, 9 Jan 2025 13:57:33 -0600 Subject: [PATCH 10/14] Fix Jest --- .../rule_form/src/hooks/use_rule_form_steps.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.test.tsx b/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.test.tsx index 1bcfcd6f954c5..14ef59bec057e 100644 --- a/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.test.tsx +++ b/packages/response-ops/rule_form/src/hooks/use_rule_form_steps.test.tsx @@ -14,6 +14,8 @@ import { RULE_FORM_PAGE_RULE_DEFINITION_TITLE, RULE_FORM_PAGE_RULE_ACTIONS_TITLE, RULE_FORM_PAGE_RULE_DETAILS_TITLE, + RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT, + RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT, } from '../translations'; import { RuleFormData } from '../types'; import { EuiSteps, EuiStepsHorizontal } from '@elastic/eui'; @@ -145,9 +147,9 @@ describe('useRuleFormHorizontalSteps', () => { render(); - expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE)).toBeInTheDocument(); + expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT)).toBeInTheDocument(); expect(screen.getByText(RULE_FORM_PAGE_RULE_ACTIONS_TITLE)).toBeInTheDocument(); - expect(screen.getByText(RULE_FORM_PAGE_RULE_DETAILS_TITLE)).toBeInTheDocument(); + expect(screen.getByText(RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT)).toBeInTheDocument(); }); test('tracks current step successfully', async () => { From 366896aacdc09ed56eba4b7f48929ae05dc734ce Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 9 Jan 2025 15:50:55 -0600 Subject: [PATCH 11/14] Move rule flyout body to own component --- .../rule_form/src/rule_flyout/rule_flyout.tsx | 141 +--------------- .../src/rule_flyout/rule_flyout_body.tsx | 155 ++++++++++++++++++ 2 files changed, 164 insertions(+), 132 deletions(-) create mode 100644 packages/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index bc43c173b5e61..f1a873302b823 100644 --- a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -7,101 +7,22 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPortal, - EuiStepsHorizontal, - EuiTitle, - EuiSpacer, - EuiCallOut, -} from '@elastic/eui'; -import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; -import React, { useCallback, useMemo } from 'react'; -import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks'; -import { - RULE_FLYOUT_HEADER_CREATE_TITLE, - RULE_FLYOUT_HEADER_EDIT_TITLE, - DISABLED_ACTIONS_WARNING_TITLE, -} from '../translations'; +import { EuiFlyout, EuiPortal } from '@elastic/eui'; +import React from 'react'; import type { RuleFormData } from '../types'; -import { hasRuleErrors } from '../validation'; -import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; -import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer'; -import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs'; +import { RuleFlyoutBody } from './rule_flyout_body'; -export interface RuleFlyoutProps { +interface RuleFlyoutProps { isEdit?: boolean; isSaving?: boolean; onCancel?: () => void; onSave: (formData: RuleFormData) => void; } -export const RuleFlyout = (props: RuleFlyoutProps) => { - const { isEdit = false, isSaving = false, onCancel = () => {}, onSave } = props; - - const { - formData, - multiConsumerSelection, - connectorTypes, - connectors, - baseErrors = {}, - paramsErrors = {}, - actionsErrors = {}, - actionsParamsErrors = {}, - } = useRuleFormState(); - - const hasErrors = useMemo(() => { - const hasBrokenConnectors = formData.actions.some((action) => { - return !connectors.find((connector) => connector.id === action.id); - }); - - if (hasBrokenConnectors) { - return true; - } - - return hasRuleErrors({ - baseErrors, - paramsErrors, - actionsErrors, - actionsParamsErrors, - }); - }, [formData, connectors, baseErrors, paramsErrors, actionsErrors, actionsParamsErrors]); - - const { - steps, - currentStepComponent, - goToNextStep, - goToPreviousStep, - hasNextStep, - hasPreviousStep, - } = useRuleFormHorizontalSteps(); - - const { actions } = formData; - - const onSaveInternal = useCallback(() => { - onSave({ - ...formData, - ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), - }); - }, [onSave, formData, multiConsumerSelection]); - - const hasActionsDisabled = useMemo(() => { - const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); - return actions.some((action) => { - const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId); - if (!actionType) { - return false; - } - const checkEnabledResult = checkActionFormActionTypeEnabled( - actionType, - preconfiguredConnectors - ); - return !actionType.enabled && !checkEnabledResult.isEnabled; - }); - }, [actions, connectors, connectorTypes]); - +// Wrapper component for the rule flyout. Currently only displays RuleFlyoutBody, but will be extended to conditionally +// display the Show Request UI or the Action Connector UI. These UIs take over the entire flyout, so we need to swap out +// their body elements entirely to avoid adding another EuiFlyout element to the DOM +export const RuleFlyout = ({ onSave, isEdit, isSaving, onCancel = () => {} }: RuleFlyoutProps) => { return ( { maxWidth={500} className="ruleFormFlyout__container" > - - -

- {isEdit ? RULE_FLYOUT_HEADER_EDIT_TITLE : RULE_FLYOUT_HEADER_CREATE_TITLE} -

-
- {isEdit && } -
- - {!isEdit && } - {hasActionsDisabled && ( - <> - - - - )} - {currentStepComponent} - - {isEdit ? ( - {} /* TODO */} - isSaving={isSaving} - hasErrors={hasErrors} - /> - ) : ( - {} /* TODO */} - goToNextStep={goToNextStep} - goToPreviousStep={goToPreviousStep} - isSaving={isSaving} - hasNextStep={hasNextStep} - hasPreviousStep={hasPreviousStep} - hasErrors={hasErrors} - /> - )} +
); diff --git a/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx new file mode 100644 index 0000000000000..ec5590b3a587b --- /dev/null +++ b/packages/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx @@ -0,0 +1,155 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiFlyoutBody, + EuiFlyoutHeader, + EuiStepsHorizontal, + EuiTitle, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; +import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; +import React, { useCallback, useMemo } from 'react'; +import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks'; +import { + RULE_FLYOUT_HEADER_CREATE_TITLE, + RULE_FLYOUT_HEADER_EDIT_TITLE, + DISABLED_ACTIONS_WARNING_TITLE, +} from '../translations'; +import type { RuleFormData } from '../types'; +import { hasRuleErrors } from '../validation'; +import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; +import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer'; +import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs'; + +interface RuleFlyoutBodyProps { + isEdit?: boolean; + isSaving?: boolean; + onCancel: () => void; + onSave: (formData: RuleFormData) => void; +} + +export const RuleFlyoutBody = ({ + isEdit = false, + isSaving = false, + onCancel, + onSave, +}: RuleFlyoutBodyProps) => { + const { + formData, + multiConsumerSelection, + connectorTypes, + connectors, + baseErrors = {}, + paramsErrors = {}, + actionsErrors = {}, + actionsParamsErrors = {}, + } = useRuleFormState(); + + const hasErrors = useMemo(() => { + const hasBrokenConnectors = formData.actions.some((action) => { + return !connectors.find((connector) => connector.id === action.id); + }); + + if (hasBrokenConnectors) { + return true; + } + + return hasRuleErrors({ + baseErrors, + paramsErrors, + actionsErrors, + actionsParamsErrors, + }); + }, [formData, connectors, baseErrors, paramsErrors, actionsErrors, actionsParamsErrors]); + + const { + steps, + currentStepComponent, + goToNextStep, + goToPreviousStep, + hasNextStep, + hasPreviousStep, + } = useRuleFormHorizontalSteps(); + + const { actions } = formData; + + const onSaveInternal = useCallback(() => { + onSave({ + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }); + }, [onSave, formData, multiConsumerSelection]); + + const hasActionsDisabled = useMemo(() => { + const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); + return actions.some((action) => { + const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId); + if (!actionType) { + return false; + } + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionType, + preconfiguredConnectors + ); + return !actionType.enabled && !checkEnabledResult.isEnabled; + }); + }, [actions, connectors, connectorTypes]); + + return ( + <> + + +

+ {isEdit ? RULE_FLYOUT_HEADER_EDIT_TITLE : RULE_FLYOUT_HEADER_CREATE_TITLE} +

+
+ {isEdit && } +
+ + {!isEdit && } + {hasActionsDisabled && ( + <> + + + + )} + {currentStepComponent} + + {isEdit ? ( + {} /* TODO */} + isSaving={isSaving} + hasErrors={hasErrors} + /> + ) : ( + {} /* TODO */} + goToNextStep={goToNextStep} + goToPreviousStep={goToPreviousStep} + isSaving={isSaving} + hasNextStep={hasNextStep} + hasPreviousStep={hasPreviousStep} + hasErrors={hasErrors} + /> + )} + + ); +}; From fd856f364f10dd95fab98860b6ef0f8cf89149c8 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 10 Jan 2025 14:05:44 -0600 Subject: [PATCH 12/14] Remove stylelint disable --- packages/response-ops/rule_form/src/rule_form.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/response-ops/rule_form/src/rule_form.scss b/packages/response-ops/rule_form/src/rule_form.scss index f49760300c0f2..6631f1674c40b 100644 --- a/packages/response-ops/rule_form/src/rule_form.scss +++ b/packages/response-ops/rule_form/src/rule_form.scss @@ -6,9 +6,7 @@ container: ruleForm / inline-size; } -/* TODO: Remove stylelint disable once we upgrade stylelint past 14.12.0 */ -/* https://github.com/stylelint/stylelint/issues/6304 */ -@container (max-width: 768px) { /* stylelint-disable-line */ +@container (max-width: 768px) { .euiDescribedFormGroup { flex-direction: column; } From 1804f7d3278be6caea10afb2ba93154b484aa507 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 10 Jan 2025 15:25:17 -0600 Subject: [PATCH 13/14] Remove unnecessary container names --- packages/response-ops/rule_form/src/rule_form.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/response-ops/rule_form/src/rule_form.scss b/packages/response-ops/rule_form/src/rule_form.scss index 6631f1674c40b..dc06b4e6f5cba 100644 --- a/packages/response-ops/rule_form/src/rule_form.scss +++ b/packages/response-ops/rule_form/src/rule_form.scss @@ -1,9 +1,9 @@ .ruleForm__container { - container: ruleForm / inline-size; + container-type: inline-size; } .ruleFormFlyout__container { - container: ruleForm / inline-size; + container-type: inline-size; } @container (max-width: 768px) { From 19b68b5acaa0f12ac36b664fae18917109b22a84 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 11:29:41 -0600 Subject: [PATCH 14/14] Use euiSize vars in scss --- .../shared/response-ops/rule_form/src/rule_form.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss index dc06b4e6f5cba..fd905ed8f9d04 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss @@ -15,13 +15,13 @@ } .ruleDefinitionHeader { flex-direction: column; - gap: 12px; + gap: $euiSizeM; } .ruleDefinitionHeaderRuleTypeName { - font-size: 18px; - margin-bottom: 4px; + font-size: $euiFontSizeM; + margin-bottom: $euiSizeXS; } .ruleDefinitionHeaderRuleTypeDescription, .ruleDefinitionHeaderDocsLink { - font-size: 14px; + font-size: $euiFontSizeS; } } \ No newline at end of file