forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ResponseOps] [Rule Form] Move rule form steps to hook with progress …
…tracking (elastic#205944) ## Summary Part of elastic#195211 In preparation for the horizontal rule form layout, move the generation of the rule form steps into three hooks: - `useCommonRuleFormSteps`: private hook that generates a series of objects specifying the rule form steps, how to display them, and what order to display them in - `useRuleFormSteps`: hook that calls `useCommonRuleFormSteps` and transforms them into data for the standard vertical `EuiSteps`, along with progress tracking based on `onBlur` events - `useRuleFormHorizontalSteps`: hook that calls hook that calls `useCommonRuleFormSteps` and transforms them into data for `EuiStepsHorizontal`, plus navigation functions. ***These will be used in the smaller rule form flyout in a second PR*** Because `EuiStepsHorizontal` rely more heavily on the `EuiSteps` `status` property, I took this opportunity to improve progress tracking in the standard vertical steps. Most rule types will load the create page with Step 1: Rule Definition already being in a `danger` state, because an incomplete rule definition component immediately sends errors, and the error API doesn't distinguish between invalid data or incomplete data. This PR wraps each step in a `reportOnBlur` higher-order component, which will report the first time a step triggers an `onBlur` event. Steps with errors will now report `incomplete` until they first trigger an `onBlur`. The result: 1. The user loads the Create Rule page. Rule Definition is marked `incomplete` 2. The user interacts with Rule Definition, but does not yet complete the definition. 3. The user interacts with the Actions step, the Rule Details step, or another part of the page. The Rule Definition is now marked `danger`. This is inelegant compared to an error API that can actually distinguish between an incomplete form and an invalid form, but it's an improvement for now. --------- Co-authored-by: Elastic Machine <[email protected]>
- Loading branch information
1 parent
2687f4f
commit d8b0b6e
Showing
6 changed files
with
534 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
packages/response-ops/rule_form/src/hooks/use_rule_form_steps.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.