Skip to content

Commit

Permalink
[8.x] [ResponseOps] [Rule Form] Move rule form steps to hook with pro…
Browse files Browse the repository at this point in the history
…gress tracking (#205944) (#206346)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ResponseOps] [Rule Form] Move rule form steps to hook with progress
tracking (#205944)](#205944)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Zacqary Adam
Xeper","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-10T21:08:14Z","message":"[ResponseOps]
[Rule Form] Move rule form steps to hook with progress tracking
(#205944)\n\n## Summary\r\n\r\nPart of #195211 \r\n\r\nIn preparation
for the horizontal rule form layout, move the generation\r\nof the rule
form steps into three hooks:\r\n\r\n- `useCommonRuleFormSteps`: private
hook that generates a series of\r\nobjects specifying the rule form
steps, how to display them, and what\r\norder to display them in\r\n-
`useRuleFormSteps`: hook that calls `useCommonRuleFormSteps`
and\r\ntransforms them into data for the standard vertical `EuiSteps`,
along\r\nwith progress tracking based on `onBlur` events\r\n-
`useRuleFormHorizontalSteps`: hook that calls hook that
calls\r\n`useCommonRuleFormSteps` and transforms them into data
for\r\n`EuiStepsHorizontal`, plus navigation functions. ***These will be
used\r\nin the smaller rule form flyout in a second PR***\r\n\r\nBecause
`EuiStepsHorizontal` rely more heavily on the `EuiSteps`\r\n`status`
property, I took this opportunity to improve progress tracking\r\nin the
standard vertical steps. Most rule types will load the create\r\npage
with Step 1: Rule Definition already being in a `danger`
state,\r\nbecause an incomplete rule definition component immediately
sends\r\nerrors, and the error API doesn't distinguish between invalid
data or\r\nincomplete data.\r\n\r\nThis PR wraps each step in a
`reportOnBlur` higher-order component,\r\nwhich will report the first
time a step triggers an `onBlur` event.\r\nSteps with errors will now
report `incomplete` until they first trigger\r\nan `onBlur`. The
result:\r\n\r\n1. The user loads the Create Rule page. Rule Definition
is marked\r\n`incomplete`\r\n2. The user interacts with Rule Definition,
but does not yet complete\r\nthe definition.\r\n3. The user interacts
with the Actions step, the Rule Details step, or\r\nanother part of the
page. The Rule Definition is now marked `danger`.\r\n\r\nThis is
inelegant compared to an error API that can actually
distinguish\r\nbetween an incomplete form and an invalid form, but it's
an improvement\r\nfor now.\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"d8b0b6e926f0198dd654cf5115af9660cb8ef663","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","Feature:Alerting/RulesManagement","backport:version","v8.18.0"],"title":"[ResponseOps]
[Rule Form] Move rule form steps to hook with progress
tracking","number":205944,"url":"https://github.com/elastic/kibana/pull/205944","mergeCommit":{"message":"[ResponseOps]
[Rule Form] Move rule form steps to hook with progress tracking
(#205944)\n\n## Summary\r\n\r\nPart of #195211 \r\n\r\nIn preparation
for the horizontal rule form layout, move the generation\r\nof the rule
form steps into three hooks:\r\n\r\n- `useCommonRuleFormSteps`: private
hook that generates a series of\r\nobjects specifying the rule form
steps, how to display them, and what\r\norder to display them in\r\n-
`useRuleFormSteps`: hook that calls `useCommonRuleFormSteps`
and\r\ntransforms them into data for the standard vertical `EuiSteps`,
along\r\nwith progress tracking based on `onBlur` events\r\n-
`useRuleFormHorizontalSteps`: hook that calls hook that
calls\r\n`useCommonRuleFormSteps` and transforms them into data
for\r\n`EuiStepsHorizontal`, plus navigation functions. ***These will be
used\r\nin the smaller rule form flyout in a second PR***\r\n\r\nBecause
`EuiStepsHorizontal` rely more heavily on the `EuiSteps`\r\n`status`
property, I took this opportunity to improve progress tracking\r\nin the
standard vertical steps. Most rule types will load the create\r\npage
with Step 1: Rule Definition already being in a `danger`
state,\r\nbecause an incomplete rule definition component immediately
sends\r\nerrors, and the error API doesn't distinguish between invalid
data or\r\nincomplete data.\r\n\r\nThis PR wraps each step in a
`reportOnBlur` higher-order component,\r\nwhich will report the first
time a step triggers an `onBlur` event.\r\nSteps with errors will now
report `incomplete` until they first trigger\r\nan `onBlur`. The
result:\r\n\r\n1. The user loads the Create Rule page. Rule Definition
is marked\r\n`incomplete`\r\n2. The user interacts with Rule Definition,
but does not yet complete\r\nthe definition.\r\n3. The user interacts
with the Actions step, the Rule Details step, or\r\nanother part of the
page. The Rule Definition is now marked `danger`.\r\n\r\nThis is
inelegant compared to an error API that can actually
distinguish\r\nbetween an incomplete form and an invalid form, but it's
an improvement\r\nfor now.\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"d8b0b6e926f0198dd654cf5115af9660cb8ef663"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/205944","number":205944,"mergeCommit":{"message":"[ResponseOps]
[Rule Form] Move rule form steps to hook with progress tracking
(#205944)\n\n## Summary\r\n\r\nPart of #195211 \r\n\r\nIn preparation
for the horizontal rule form layout, move the generation\r\nof the rule
form steps into three hooks:\r\n\r\n- `useCommonRuleFormSteps`: private
hook that generates a series of\r\nobjects specifying the rule form
steps, how to display them, and what\r\norder to display them in\r\n-
`useRuleFormSteps`: hook that calls `useCommonRuleFormSteps`
and\r\ntransforms them into data for the standard vertical `EuiSteps`,
along\r\nwith progress tracking based on `onBlur` events\r\n-
`useRuleFormHorizontalSteps`: hook that calls hook that
calls\r\n`useCommonRuleFormSteps` and transforms them into data
for\r\n`EuiStepsHorizontal`, plus navigation functions. ***These will be
used\r\nin the smaller rule form flyout in a second PR***\r\n\r\nBecause
`EuiStepsHorizontal` rely more heavily on the `EuiSteps`\r\n`status`
property, I took this opportunity to improve progress tracking\r\nin the
standard vertical steps. Most rule types will load the create\r\npage
with Step 1: Rule Definition already being in a `danger`
state,\r\nbecause an incomplete rule definition component immediately
sends\r\nerrors, and the error API doesn't distinguish between invalid
data or\r\nincomplete data.\r\n\r\nThis PR wraps each step in a
`reportOnBlur` higher-order component,\r\nwhich will report the first
time a step triggers an `onBlur` event.\r\nSteps with errors will now
report `incomplete` until they first trigger\r\nan `onBlur`. The
result:\r\n\r\n1. The user loads the Create Rule page. Rule Definition
is marked\r\n`incomplete`\r\n2. The user interacts with Rule Definition,
but does not yet complete\r\nthe definition.\r\n3. The user interacts
with the Actions step, the Rule Details step, or\r\nanother part of the
page. The Rule Definition is now marked `danger`.\r\n\r\nThis is
inelegant compared to an error API that can actually
distinguish\r\nbetween an incomplete form and an invalid form, but it's
an improvement\r\nfor now.\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"d8b0b6e926f0198dd654cf5115af9660cb8ef663"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Zacqary Adam Xeper <[email protected]>
  • Loading branch information
kibanamachine and Zacqary authored Jan 10, 2025
1 parent b305ce8 commit 399a186
Show file tree
Hide file tree
Showing 6 changed files with 534 additions and 86 deletions.
6 changes: 6 additions & 0 deletions packages/response-ops/rule_form/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,9 @@ export const DEFAULT_VALID_CONSUMERS: RuleCreationValidConsumer[] = [

export const CREATE_RULE_ROUTE = '/rule/create/:ruleTypeId' as const;
export const EDIT_RULE_ROUTE = '/rule/edit/:id' as const;

export enum RuleFormStepId {
DEFINITION = 'rule-definition',
ACTIONS = 'rule-actions',
DETAILS = 'rule-details',
}
1 change: 1 addition & 0 deletions packages/response-ops/rule_form/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

export * from './use_rule_form_dispatch';
export * from './use_rule_form_state';
export * from './use_rule_form_steps';
196 changes: 196 additions & 0 deletions packages/response-ops/rule_form/src/hooks/use_rule_form_steps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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 { render, screen, fireEvent } from '@testing-library/react';
import { useRuleFormHorizontalSteps, useRuleFormSteps } from './use_rule_form_steps';
import {
RULE_FORM_PAGE_RULE_DEFINITION_TITLE,
RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
RULE_FORM_PAGE_RULE_DETAILS_TITLE,
} from '../translations';
import { RuleFormData } from '../types';
import { EuiSteps, EuiStepsHorizontal } from '@elastic/eui';

jest.mock('../rule_definition', () => ({
RuleDefinition: () => <div />,
}));

jest.mock('../rule_actions', () => ({
RuleActions: () => <div />,
}));

jest.mock('../rule_details', () => ({
RuleDetails: () => <div />,
}));

jest.mock('./use_rule_form_state', () => ({
useRuleFormState: jest.fn(),
}));

const { useRuleFormState } = jest.requireMock('./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 ruleFormStateMock = {
plugins: {
application: {
navigateToUrl,
capabilities: {
actions: {
show: true,
save: true,
execute: true,
},
},
},
},
baseErrors: {},
paramsErrors: {},
multiConsumerSelection: 'logs',
formData: formDataMock,
connectors: [],
connectorTypes: [],
aadTemplateFields: [],
};

describe('useRuleFormSteps', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('renders correctly', () => {
useRuleFormState.mockReturnValue(ruleFormStateMock);

const TestComponent = () => {
const { steps } = useRuleFormSteps();

return <EuiSteps steps={steps} />;
};

render(<TestComponent />);

expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE)).toBeInTheDocument();
expect(screen.getByText(RULE_FORM_PAGE_RULE_ACTIONS_TITLE)).toBeInTheDocument();
expect(screen.getByText(RULE_FORM_PAGE_RULE_DETAILS_TITLE)).toBeInTheDocument();
});

test('renders initial errors as incomplete, then danger when the corresponding step blurs', async () => {
useRuleFormState.mockReturnValue({
...ruleFormStateMock,
baseErrors: {
interval: ['Interval is required'],
alertDelay: ['Alert delay is required'],
},
});

const TestComponent = () => {
const { steps } = useRuleFormSteps();

return <EuiSteps steps={steps} />;
};

render(<TestComponent />);

// Use screen reader text for testing
expect(await screen.getByText('Step 1 is incomplete')).toBeInTheDocument();
const step1 = screen.getByTestId('ruleFormStep-rule-definition-reportOnBlur');
await fireEvent.blur(step1!);
expect(await screen.getByText('Step 1 has errors')).toBeInTheDocument();
});
});

describe('useRuleFormHorizontalSteps', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('renders correctly', () => {
useRuleFormState.mockReturnValue(ruleFormStateMock);

const TestComponent = () => {
const { steps } = useRuleFormHorizontalSteps();

return <EuiStepsHorizontal steps={steps} />;
};

render(<TestComponent />);

expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE)).toBeInTheDocument();
expect(screen.getByText(RULE_FORM_PAGE_RULE_ACTIONS_TITLE)).toBeInTheDocument();
expect(screen.getByText(RULE_FORM_PAGE_RULE_DETAILS_TITLE)).toBeInTheDocument();
});

test('tracks current step successfully', async () => {
useRuleFormState.mockReturnValue(ruleFormStateMock);

const TestComponent = () => {
const { steps, goToNextStep, goToPreviousStep } = useRuleFormHorizontalSteps();

return (
<>
<EuiStepsHorizontal steps={steps} />
<button onClick={goToNextStep}>Next</button>
<button onClick={goToPreviousStep}>Previous</button>
</>
);
};

render(<TestComponent />);

expect(await screen.getByText('Current step is 1')).toBeInTheDocument();

const nextButton = screen.getByText('Next');
const previousButton = screen.getByText('Previous');

fireEvent.click(nextButton);
fireEvent.click(nextButton);

expect(await screen.getByText('Current step is 3')).toBeInTheDocument();

fireEvent.click(nextButton);

expect(await screen.getByText('Current step is 3')).toBeInTheDocument();

fireEvent.click(previousButton);

expect(await screen.getByText('Current step is 2')).toBeInTheDocument();

fireEvent.click(previousButton);

expect(await screen.getByText('Current step is 1')).toBeInTheDocument();

fireEvent.click(previousButton);

expect(await screen.getByText('Current step is 1')).toBeInTheDocument();
});
});
Loading

0 comments on commit 399a186

Please sign in to comment.