[Automatic Import] Fix the enter bug (#199894)

## Release Notes

Fixes the bug where pressing Enter reloaded the Automatic Import.

## Summary

- Fixes #198238
- Adds/fixes telemetry for CEL events.
- Refactors navigation functionality.
- Adds extensive unit tests and a Cypress test for it.

## Details

When the user presses the Enter inside our input field, the expected
action is to send the form, in this case completing the step. However,
previously the form submission would instead lead to reloading the whole
Automatic Import page.

In this PR we capture the form submission event and bubble it up as
`completeStep` to the main component. We also move the implementation
from the `Footer` up to this main component
`CreateIntegrationAssistant`. This helps collect all the details about
the step order in one place and refactor this logic.

As a result, pressing `Enter` in any field now either
 - Is processed by the field itself (in case of multi-line fields);
- Leads to the same action as pressing the "Next" button (desired
result); or
- Does nothing (e.g. in the inputs in the "Define data stream and upload
logs" group – the reason for this is unclear).

We add CEL-specific telemetry identifiers so that telemetry for step 5
is not always reported as `Deploy Step`.

We also rename a bunch of stuff that was named `...StepReady` into
`...StepReadyToComplete` as the previous name was ambiguous. To
demonstrate this ambiguity we've enlisted the help of GPT 4o:

<img width="832" alt="SCR-20241125-tiaa"
src="https://github.com/user-attachments/assets/ad6bcf7c-7cb2-41c2-ac6b-38924ce990d3">


## Testing

We provide a Cypress test for Enter behavior: pressing it on the
"integration title" input should let the flow proceed to the next step.
This test fails on `main`.

We also provide unit tests for all steps of navigation functionality in
`x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx`:


Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Ilya Nikokoshev 2024-12-11 16:04:45 +01:00 committed by GitHub
parent d4194ba5eb
commit d8bb72ebfd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 976 additions and 495 deletions

View file

@ -6,11 +6,13 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import { render, act } from '@testing-library/react';
import { TestProvider } from '../../../mocks/test_provider';
import { CreateIntegrationAssistant } from './create_integration_assistant';
import type { State } from './state';
import { ExperimentalFeaturesService } from '../../../services';
import { mockReportEvent } from '../../../services/telemetry/mocks/service';
import { TelemetryEventType } from '../../../services/telemetry/types';
export const defaultInitialState: State = {
step: 1,
@ -20,6 +22,7 @@ export const defaultInitialState: State = {
hasCelInput: false,
result: undefined,
};
const mockInitialState = jest.fn((): State => defaultInitialState);
jest.mock('./state', () => ({
...jest.requireActual('./state'),
@ -39,39 +42,45 @@ const mockCelInputStep = jest.fn(() => <div data-test-subj="celInputStepMock" />
const mockReviewCelStep = jest.fn(() => <div data-test-subj="reviewCelStepMock" />);
const mockDeployStep = jest.fn(() => <div data-test-subj="deployStepMock" />);
const mockIsConnectorStepReady = jest.fn();
const mockIsIntegrationStepReady = jest.fn();
const mockIsDataStreamStepReady = jest.fn();
const mockIsReviewStepReady = jest.fn();
const mockIsCelInputStepReady = jest.fn();
const mockIsCelReviewStepReady = jest.fn();
const mockIsConnectorStepReadyToComplete = jest.fn();
const mockIsIntegrationStepReadyToComplete = jest.fn();
const mockIsDataStreamStepReadyToComplete = jest.fn();
const mockIsReviewStepReadyToComplete = jest.fn();
const mockIsCelInputStepReadyToComplete = jest.fn();
const mockIsCelReviewStepReadyToComplete = jest.fn();
jest.mock('./steps/connector_step', () => ({
ConnectorStep: () => mockConnectorStep(),
isConnectorStepReady: () => mockIsConnectorStepReady(),
isConnectorStepReadyToComplete: () => mockIsConnectorStepReadyToComplete(),
}));
jest.mock('./steps/integration_step', () => ({
IntegrationStep: () => mockIntegrationStep(),
isIntegrationStepReady: () => mockIsIntegrationStepReady(),
isIntegrationStepReadyToComplete: () => mockIsIntegrationStepReadyToComplete(),
}));
jest.mock('./steps/data_stream_step', () => ({
DataStreamStep: () => mockDataStreamStep(),
isDataStreamStepReady: () => mockIsDataStreamStepReady(),
isDataStreamStepReadyToComplete: () => mockIsDataStreamStepReadyToComplete(),
}));
jest.mock('./steps/review_step', () => ({
ReviewStep: () => mockReviewStep(),
isReviewStepReady: () => mockIsReviewStepReady(),
isReviewStepReadyToComplete: () => mockIsReviewStepReadyToComplete(),
}));
jest.mock('./steps/cel_input_step', () => ({
CelInputStep: () => mockCelInputStep(),
isCelInputStepReady: () => mockIsCelInputStepReady(),
isCelInputStepReadyToComplete: () => mockIsCelInputStepReadyToComplete(),
}));
jest.mock('./steps/review_cel_step', () => ({
ReviewCelStep: () => mockReviewCelStep(),
isCelReviewStepReady: () => mockIsCelReviewStepReady(),
isCelReviewStepReadyToComplete: () => mockIsCelReviewStepReadyToComplete(),
}));
jest.mock('./steps/deploy_step', () => ({ DeployStep: () => mockDeployStep() }));
const mockNavigate = jest.fn();
jest.mock('../../../common/hooks/use_navigate', () => ({
...jest.requireActual('../../../common/hooks/use_navigate'),
useNavigate: () => mockNavigate,
}));
const renderIntegrationAssistant = () =>
render(<CreateIntegrationAssistant />, { wrapper: TestProvider });
@ -89,19 +98,116 @@ describe('CreateIntegration', () => {
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 1 });
});
it('should render connector', () => {
it('shoud report telemetry for assistant open', () => {
renderIntegrationAssistant();
expect(mockReportEvent).toHaveBeenCalledWith(TelemetryEventType.IntegrationAssistantOpen, {
sessionId: expect.any(String),
});
});
it('should render connector step', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('connectorStepMock')).toBeInTheDocument();
});
it('should call isConnectorStepReady', () => {
it('should call isConnectorStepReadyToComplete', () => {
renderIntegrationAssistant();
expect(mockIsConnectorStepReady).toHaveBeenCalled();
expect(mockIsConnectorStepReadyToComplete).toHaveBeenCalled();
});
it('should show "Next" on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Next');
});
describe('when connector step is not done', () => {
beforeEach(() => {
mockIsConnectorStepReadyToComplete.mockReturnValue(false);
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled();
});
it('should still enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should still enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
});
describe('when connector step is done', () => {
beforeEach(() => {
mockIsConnectorStepReadyToComplete.mockReturnValue(true);
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
it('should enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
describe('when next button is clicked', () => {
beforeEach(() => {
const result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should report telemetry for connector step completion', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 1,
stepName: 'Connector Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
});
});
describe('when back button is clicked', () => {
let result: ReturnType<typeof renderIntegrationAssistant>;
beforeEach(() => {
result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should not report telemetry', () => {
expect(mockReportEvent).not.toHaveBeenCalled();
});
it('should navigate to the landing page', () => {
expect(mockNavigate).toHaveBeenCalledWith('landing');
});
});
});
describe('when step is 2', () => {
beforeEach(() => {
mockIsConnectorStepReadyToComplete.mockReturnValue(true);
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 2 });
});
@ -110,14 +216,109 @@ describe('CreateIntegration', () => {
expect(result.queryByTestId('integrationStepMock')).toBeInTheDocument();
});
it('should call isIntegrationStepReady', () => {
it('should call isIntegrationStepReadyToComplete', () => {
renderIntegrationAssistant();
expect(mockIsIntegrationStepReady).toHaveBeenCalled();
expect(mockIsIntegrationStepReadyToComplete).toHaveBeenCalled();
});
it('should show "Next" on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Next');
});
describe('when integration step is not done', () => {
beforeEach(() => {
mockIsIntegrationStepReadyToComplete.mockReturnValue(false);
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled();
});
it('should still enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should still enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
});
describe('when integration step is done', () => {
beforeEach(() => {
mockIsIntegrationStepReadyToComplete.mockReturnValue(true);
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
it('should enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
describe('when next button is clicked', () => {
beforeEach(() => {
const result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should report telemetry for integration step completion', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 2,
stepName: 'Integration Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
});
});
describe('when back button is clicked', () => {
let result: ReturnType<typeof renderIntegrationAssistant>;
beforeEach(() => {
result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should not report telemetry', () => {
expect(mockReportEvent).not.toHaveBeenCalled();
});
it('should show connector step', () => {
expect(result.queryByTestId('connectorStepMock')).toBeInTheDocument();
});
it('should enable the next button', () => {
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
});
});
describe('when step is 3', () => {
beforeEach(() => {
mockIsConnectorStepReadyToComplete.mockReturnValue(true);
mockIsIntegrationStepReadyToComplete.mockReturnValue(true);
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 3 });
});
@ -126,9 +327,116 @@ describe('CreateIntegration', () => {
expect(result.queryByTestId('dataStreamStepMock')).toBeInTheDocument();
});
it('should call isDataStreamStepReady', () => {
it('should call isDataStreamStepReadyToComplete', () => {
renderIntegrationAssistant();
expect(mockIsDataStreamStepReady).toHaveBeenCalled();
expect(mockIsDataStreamStepReadyToComplete).toHaveBeenCalled();
});
it('should show "Analyze logs" on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Analyze logs');
});
describe('when data stream step is not done', () => {
beforeEach(() => {
mockIsDataStreamStepReadyToComplete.mockReturnValue(false);
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled();
});
it('should still enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should still enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
});
describe('when data stream step is done', () => {
beforeEach(() => {
mockIsDataStreamStepReadyToComplete.mockReturnValue(true);
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
it('should enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
describe('when next button is clicked', () => {
beforeEach(() => {
const result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should report telemetry for data stream step completion', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 3,
stepName: 'DataStream Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
it('should show loader on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('generatingLoader')).toBeInTheDocument();
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
// Not sure why there are two buttons when testing.
const nextButton = result
.getAllByTestId('buttonsFooter-nextButton')
.filter((button) => button.textContent !== 'Next')[0];
expect(nextButton).toBeDisabled();
});
});
});
describe('when back button is clicked', () => {
let result: ReturnType<typeof renderIntegrationAssistant>;
beforeEach(() => {
result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should not report telemetry', () => {
expect(mockReportEvent).not.toHaveBeenCalled();
});
it('should show integration step', () => {
expect(result.queryByTestId('integrationStepMock')).toBeInTheDocument();
});
it('should enable the next button', () => {
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
});
});
@ -142,9 +450,89 @@ describe('CreateIntegration', () => {
expect(result.queryByTestId('reviewStepMock')).toBeInTheDocument();
});
it('should call isReviewStepReady', () => {
it('should call isReviewStepReadyToComplete', () => {
renderIntegrationAssistant();
expect(mockIsReviewStepReady).toHaveBeenCalled();
expect(mockIsReviewStepReadyToComplete).toHaveBeenCalled();
});
it('should show the "Add to Elastic" on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Add to Elastic');
});
describe('when review step is not done', () => {
beforeEach(() => {
mockIsReviewStepReadyToComplete.mockReturnValue(false);
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled();
});
it('should still enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should still enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
});
describe('when review step is done', () => {
beforeEach(() => {
mockIsReviewStepReadyToComplete.mockReturnValue(true);
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
it('should enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
describe('when next button is clicked', () => {
beforeEach(() => {
const result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should report telemetry for review step completion', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 4,
stepName: 'Review Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
it('should show deploy step', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('deployStepMock')).toBeInTheDocument();
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
});
});
});
@ -157,6 +545,26 @@ describe('CreateIntegration', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('deployStepMock')).toBeInTheDocument();
});
it('should hide the back button', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null);
});
it('should hide the next button', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null);
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
it('should show "Close" on the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close');
});
});
});
@ -179,9 +587,107 @@ describe('CreateIntegration with generateCel enabled', () => {
expect(result.queryByTestId('celInputStepMock')).toBeInTheDocument();
});
it('should call isCelInputStepReady', () => {
it('should call isCelInputStepReadyToComplete', () => {
renderIntegrationAssistant();
expect(mockIsCelInputStepReady).toHaveBeenCalled();
expect(mockIsCelInputStepReadyToComplete).toHaveBeenCalled();
});
it('should show "Generate CEL input configuration" on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent(
'Generate CEL input configuration'
);
});
it('should enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
describe('when cel input step is not done', () => {
beforeEach(() => {
mockIsCelInputStepReadyToComplete.mockReturnValue(false);
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
// Not sure why there are two buttons when testing.
const nextButton = result
.getAllByTestId('buttonsFooter-nextButton')
.filter((button) => button.textContent !== 'Next')[0];
expect(nextButton).toBeDisabled();
});
});
describe('when cel input step is done', () => {
beforeEach(() => {
mockIsCelInputStepReadyToComplete.mockReturnValue(true);
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
describe('when next button is clicked', () => {
beforeEach(() => {
const result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should report telemetry for cel input step completion', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 5,
stepName: 'CEL Input Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
it('should show loader on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('generatingLoader')).toBeInTheDocument();
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
// Not sure why there are two buttons when testing.
const nextButton = result
.getAllByTestId('buttonsFooter-nextButton')
.filter((button) => button.textContent !== 'Next')[0];
expect(nextButton).toBeDisabled();
});
});
});
describe('when back button is clicked', () => {
let result: ReturnType<typeof renderIntegrationAssistant>;
beforeEach(() => {
result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should not report telemetry', () => {
expect(mockReportEvent).not.toHaveBeenCalled();
});
it('should show review step', () => {
expect(result.queryByTestId('reviewStepMock')).toBeInTheDocument();
});
it('should enable the next button', () => {
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
});
});
@ -194,6 +700,26 @@ describe('CreateIntegration with generateCel enabled', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('deployStepMock')).toBeInTheDocument();
});
it('should hide the back button', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null);
});
it('should hide the next button', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null);
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
it('should show "Close" on the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close');
});
});
describe('when step is 6', () => {
@ -210,9 +736,89 @@ describe('CreateIntegration with generateCel enabled', () => {
expect(result.queryByTestId('reviewCelStepMock')).toBeInTheDocument();
});
it('should call isReviewCelStepReady', () => {
it('should call isReviewCelStepReadyToComplete', () => {
renderIntegrationAssistant();
expect(mockIsCelReviewStepReady).toHaveBeenCalled();
expect(mockIsCelReviewStepReadyToComplete).toHaveBeenCalled();
});
it('should show the "Add to Elastic" on the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toHaveTextContent('Add to Elastic');
});
describe('when cel review step is not done', () => {
beforeEach(() => {
mockIsCelReviewStepReadyToComplete.mockReturnValue(false);
});
it('should disable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeDisabled();
});
it('should still enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should still enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
});
describe('when cel review step is done', () => {
beforeEach(() => {
mockIsCelReviewStepReadyToComplete.mockReturnValue(true);
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
it('should enable the back button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-backButton')).toBeEnabled();
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
describe('when next button is clicked', () => {
beforeEach(() => {
const result = renderIntegrationAssistant();
mockReportEvent.mockClear();
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should report telemetry for review step completion', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 6,
stepName: 'CEL Review Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
it('should show deploy step', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('deployStepMock')).toBeInTheDocument();
});
it('should enable the next button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-nextButton')).toBeEnabled();
});
});
});
});
@ -225,5 +831,25 @@ describe('CreateIntegration with generateCel enabled', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('deployStepMock')).toBeInTheDocument();
});
it('should hide the back button', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null);
});
it('should hide the next button', () => {
const result = renderIntegrationAssistant();
expect(result.queryByTestId('buttonsFooter-backButton')).toBe(null);
});
it('should enable the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toBeEnabled();
});
it('should show "Close" on the cancel button', () => {
const result = renderIntegrationAssistant();
expect(result.getByTestId('buttonsFooter-cancelButton')).toHaveTextContent('Close');
});
});
});

View file

@ -5,31 +5,96 @@
* 2.0.
*/
import React, { useReducer, useMemo, useEffect } from 'react';
import React, { useReducer, useMemo, useEffect, useCallback } from 'react';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { Header } from './header';
import { Footer } from './footer';
import { ConnectorStep, isConnectorStepReady } from './steps/connector_step';
import { IntegrationStep, isIntegrationStepReady } from './steps/integration_step';
import { DataStreamStep, isDataStreamStepReady } from './steps/data_stream_step';
import { ReviewStep, isReviewStepReady } from './steps/review_step';
import { CelInputStep, isCelInputStepReady } from './steps/cel_input_step';
import { ReviewCelStep, isCelReviewStepReady } from './steps/review_cel_step';
import { useNavigate, Page } from '../../../common/hooks/use_navigate';
import { ConnectorStep, isConnectorStepReadyToComplete } from './steps/connector_step';
import { IntegrationStep, isIntegrationStepReadyToComplete } from './steps/integration_step';
import { DataStreamStep, isDataStreamStepReadyToComplete } from './steps/data_stream_step';
import { ReviewStep, isReviewStepReadyToComplete } from './steps/review_step';
import { CelInputStep, isCelInputStepReadyToComplete } from './steps/cel_input_step';
import { ReviewCelStep, isCelReviewStepReadyToComplete } from './steps/review_cel_step';
import { DeployStep } from './steps/deploy_step';
import { reducer, initialState, ActionsProvider, type Actions } from './state';
import { useTelemetry } from '../telemetry';
import { ExperimentalFeaturesService } from '../../../services';
const stepNames: Record<number | string, string> = {
1: 'Connector Step',
2: 'Integration Step',
3: 'DataStream Step',
4: 'Review Step',
cel_input: 'CEL Input Step',
cel_review: 'CEL Review Step',
deploy: 'Deploy Step',
};
export const CreateIntegrationAssistant = React.memo(() => {
const [state, dispatch] = useReducer(reducer, initialState);
const navigate = useNavigate();
const { generateCel: isGenerateCelEnabled } = ExperimentalFeaturesService.get();
const celInputStepIndex = isGenerateCelEnabled && state.hasCelInput ? 5 : null;
const celReviewStepIndex = isGenerateCelEnabled && state.celInputResult ? 6 : null;
const deployStepIndex =
celInputStepIndex !== null || celReviewStepIndex !== null || state.step === 7 ? 7 : 5;
const stepName =
state.step === deployStepIndex
? stepNames.deploy
: state.step === celReviewStepIndex
? stepNames.cel_review
: state.step === celInputStepIndex
? stepNames.cel_input
: state.step in stepNames
? stepNames[state.step]
: 'Unknown Step';
const telemetry = useTelemetry();
useEffect(() => {
telemetry.reportAssistantOpen();
}, [telemetry]);
const isThisStepReadyToComplete = useMemo(() => {
if (state.step === 1) {
return isConnectorStepReadyToComplete(state);
} else if (state.step === 2) {
return isIntegrationStepReadyToComplete(state);
} else if (state.step === 3) {
return isDataStreamStepReadyToComplete(state);
} else if (state.step === 4) {
return isReviewStepReadyToComplete(state);
} else if (isGenerateCelEnabled && state.step === 5) {
return isCelInputStepReadyToComplete(state);
} else if (isGenerateCelEnabled && state.step === 6) {
return isCelReviewStepReadyToComplete(state);
}
return false;
}, [state, isGenerateCelEnabled]);
const goBackStep = useCallback(() => {
if (state.step === 1) {
navigate(Page.landing);
} else {
dispatch({ type: 'SET_STEP', payload: state.step - 1 });
}
}, [navigate, dispatch, state.step]);
const completeStep = useCallback(() => {
if (!isThisStepReadyToComplete) {
// If the user tries to navigate to the next step without completing the current step.
return;
}
telemetry.reportAssistantStepComplete({ step: state.step, stepName });
if (state.step === 3 || state.step === celInputStepIndex) {
dispatch({ type: 'SET_IS_GENERATING', payload: true });
} else {
dispatch({ type: 'SET_STEP', payload: state.step + 1 });
}
}, [telemetry, state.step, stepName, celInputStepIndex, isThisStepReadyToComplete]);
const actions = useMemo<Actions>(
() => ({
setStep: (payload) => {
@ -53,27 +118,11 @@ export const CreateIntegrationAssistant = React.memo(() => {
setCelInputResult: (payload) => {
dispatch({ type: 'SET_CEL_INPUT_RESULT', payload });
},
completeStep,
}),
[]
[completeStep]
);
const isNextStepEnabled = useMemo(() => {
if (state.step === 1) {
return isConnectorStepReady(state);
} else if (state.step === 2) {
return isIntegrationStepReady(state);
} else if (state.step === 3) {
return isDataStreamStepReady(state);
} else if (state.step === 4) {
return isReviewStepReady(state);
} else if (isGenerateCelEnabled && state.step === 5) {
return isCelInputStepReady(state);
} else if (isGenerateCelEnabled && state.step === 6) {
return isCelReviewStepReady(state);
}
return false;
}, [state, isGenerateCelEnabled]);
return (
<ActionsProvider value={actions}>
<KibanaPageTemplate>
@ -95,28 +144,21 @@ export const CreateIntegrationAssistant = React.memo(() => {
result={state.result}
/>
)}
{state.step === 5 &&
(isGenerateCelEnabled && state.hasCelInput ? (
<CelInputStep
integrationSettings={state.integrationSettings}
connector={state.connector}
isGenerating={state.isGenerating}
/>
) : (
<DeployStep
integrationSettings={state.integrationSettings}
result={state.result}
connector={state.connector}
/>
))}
{isGenerateCelEnabled && state.celInputResult && state.step === 6 && (
{state.step === celInputStepIndex && (
<CelInputStep
integrationSettings={state.integrationSettings}
connector={state.connector}
isGenerating={state.isGenerating}
/>
)}
{state.step === celReviewStepIndex && (
<ReviewCelStep
isGenerating={state.isGenerating}
celInputResult={state.celInputResult}
/>
)}
{isGenerateCelEnabled && state.step === 7 && (
{state.step === deployStepIndex && (
<DeployStep
integrationSettings={state.integrationSettings}
result={state.result}
@ -126,10 +168,14 @@ export const CreateIntegrationAssistant = React.memo(() => {
)}
</KibanaPageTemplate.Section>
<Footer
currentStep={state.step}
isGenerating={state.isGenerating}
hasCelInput={state.hasCelInput}
isNextStepEnabled={isNextStepEnabled}
isAnalyzeStep={state.step === 3}
isAnalyzeCelStep={state.step === celInputStepIndex}
isLastStep={state.step === deployStepIndex}
isNextStepEnabled={isThisStepReadyToComplete && !state.isGenerating}
isNextAddingToElastic={state.step === deployStepIndex - 1}
onBack={goBackStep}
onNext={completeStep}
/>
</KibanaPageTemplate>
</ActionsProvider>

View file

@ -6,13 +6,11 @@
*/
import React from 'react';
import { render, act, type RenderResult } from '@testing-library/react';
import { render, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../mocks/test_provider';
import { Footer } from './footer';
import { ActionsProvider } from '../state';
import { mockActions } from '../mocks/state';
import { mockReportEvent } from '../../../../services/telemetry/mocks/service';
import { TelemetryEventType } from '../../../../services/telemetry/types';
import { ExperimentalFeaturesService } from '../../../../services';
const mockNavigate = jest.fn();
@ -39,15 +37,12 @@ describe('Footer', () => {
} as never);
});
describe('when rendered', () => {
describe('when rendered for the most common case', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer currentStep={1} isGenerating={false} hasCelInput={false} isNextStepEnabled />,
{
wrapper,
}
);
result = render(<Footer isNextStepEnabled />, {
wrapper,
});
});
it('should render footer buttons component', () => {
expect(result.queryByTestId('buttonsFooter')).toBeInTheDocument();
@ -65,230 +60,4 @@ describe('Footer', () => {
expect(result.queryByTestId('buttonsFooter-nextButton')).toBeInTheDocument();
});
});
describe('when step is 1', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer currentStep={1} isGenerating={false} hasCelInput={false} isNextStepEnabled />,
{
wrapper,
}
);
});
describe('when next button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should set step 2', () => {
expect(mockActions.setStep).toHaveBeenCalledWith(2);
});
it('should report telemetry', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 1,
stepName: 'Connector Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
});
describe('when back button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should navigate to landing', () => {
expect(mockNavigate).toHaveBeenCalledWith('landing');
});
});
});
describe('when step is 2', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer currentStep={2} isGenerating={false} hasCelInput={false} isNextStepEnabled />,
{
wrapper,
}
);
});
describe('when next button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should set step 3', () => {
expect(mockActions.setStep).toHaveBeenCalledWith(3);
});
it('should report telemetry', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 2,
stepName: 'Integration Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
});
describe('when back button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should set step 1', () => {
expect(mockActions.setStep).toHaveBeenCalledWith(1);
});
});
});
describe('when step is 3', () => {
describe('when it is not generating', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer currentStep={3} isGenerating={false} hasCelInput={false} isNextStepEnabled />,
{
wrapper,
}
);
});
describe('when next button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should set step 4', () => {
expect(mockActions.setIsGenerating).toHaveBeenCalledWith(true);
});
it('should report telemetry', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 3,
stepName: 'DataStream Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
});
describe('when back button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should set step 2', () => {
expect(mockActions.setStep).toHaveBeenCalledWith(2);
});
});
});
describe('when it is generating', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer currentStep={3} isGenerating={true} hasCelInput={false} isNextStepEnabled />,
{
wrapper,
}
);
});
it('should render the loader', () => {
expect(result.queryByTestId('generatingLoader')).toBeInTheDocument();
});
});
});
describe('when step is 4', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer currentStep={4} isGenerating={false} hasCelInput={false} isNextStepEnabled />,
{
wrapper,
}
);
});
describe('when next button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-nextButton').click();
});
});
it('should set step 5', () => {
expect(mockActions.setStep).toHaveBeenCalledWith(5);
});
it('should report telemetry', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantStepComplete,
{
sessionId: expect.any(String),
step: 4,
stepName: 'Review Step',
durationMs: expect.any(Number),
sessionElapsedTime: expect.any(Number),
}
);
});
});
describe('when back button is clicked', () => {
beforeEach(() => {
act(() => {
result.getByTestId('buttonsFooter-backButton').click();
});
});
it('should set step 3', () => {
expect(mockActions.setStep).toHaveBeenCalledWith(3);
});
});
});
describe('when next step is disabled', () => {
let result: RenderResult;
beforeEach(() => {
result = render(<Footer currentStep={1} isGenerating={false} hasCelInput={false} />, {
wrapper,
});
});
it('should render next button disabled', () => {
expect(result.queryByTestId('buttonsFooter-nextButton')).toBeDisabled();
});
});
});

View file

@ -6,13 +6,10 @@
*/
import { EuiLoadingSpinner } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import { ButtonsFooter } from '../../../../common/components/buttons_footer';
import { useNavigate, Page } from '../../../../common/hooks/use_navigate';
import { useTelemetry } from '../../telemetry';
import { useActions, type State } from '../state';
import { type State } from '../state';
import * as i18n from './translations';
import { ExperimentalFeaturesService } from '../../../../services';
// Generation button for Step 3
const AnalyzeButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerating }) => {
@ -43,61 +40,47 @@ const AnalyzeCelButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerati
AnalyzeCelButtonText.displayName = 'AnalyzeCelButtonText';
interface FooterProps {
currentStep: State['step'];
isGenerating: State['isGenerating'];
hasCelInput: State['hasCelInput'];
isGenerating?: State['isGenerating'];
isAnalyzeStep?: boolean;
isAnalyzeCelStep?: boolean;
isLastStep?: boolean;
isNextStepEnabled?: boolean;
isNextAddingToElastic?: boolean;
onBack?: () => void;
onNext?: () => void;
}
export const Footer = React.memo<FooterProps>(
({ currentStep, isGenerating, hasCelInput, isNextStepEnabled = false }) => {
const telemetry = useTelemetry();
const { setStep, setIsGenerating } = useActions();
const navigate = useNavigate();
({
isGenerating = false,
isAnalyzeStep = false,
isAnalyzeCelStep = false,
isLastStep = false,
isNextStepEnabled = false,
isNextAddingToElastic = false,
onBack = () => {},
onNext = () => {},
}) => {
const nextButtonText = useMemo(
() =>
isNextAddingToElastic ? (
i18n.ADD_TO_ELASTIC
) : isAnalyzeStep ? (
<AnalyzeButtonText isGenerating={isGenerating} />
) : isAnalyzeCelStep ? (
<AnalyzeCelButtonText isGenerating={isGenerating} />
) : null,
[isNextAddingToElastic, isAnalyzeStep, isGenerating, isAnalyzeCelStep]
);
const { generateCel: isGenerateCelEnabled } = ExperimentalFeaturesService.get();
const onBack = useCallback(() => {
if (currentStep === 1) {
navigate(Page.landing);
} else {
setStep(currentStep - 1);
}
}, [currentStep, navigate, setStep]);
const onNext = useCallback(() => {
telemetry.reportAssistantStepComplete({ step: currentStep });
if (currentStep === 3 || currentStep === 5) {
setIsGenerating(true);
} else {
setStep(currentStep + 1);
}
}, [currentStep, setIsGenerating, setStep, telemetry]);
const nextButtonText = useMemo(() => {
if (currentStep === 3) {
return <AnalyzeButtonText isGenerating={isGenerating} />;
}
if (currentStep === 4 && (!isGenerateCelEnabled || !hasCelInput)) {
return i18n.ADD_TO_ELASTIC;
}
if (currentStep === 5 && isGenerateCelEnabled && hasCelInput) {
return <AnalyzeCelButtonText isGenerating={isGenerating} />;
}
if (currentStep === 6 && isGenerateCelEnabled) {
return i18n.ADD_TO_ELASTIC;
}
}, [currentStep, isGenerating, hasCelInput, isGenerateCelEnabled]);
if (currentStep === 7 || (currentStep === 5 && (!isGenerateCelEnabled || !hasCelInput))) {
return <ButtonsFooter cancelButtonText={i18n.CLOSE} />;
}
return (
return isLastStep ? (
<ButtonsFooter cancelButtonText={i18n.CLOSE} />
) : (
<ButtonsFooter
isNextDisabled={!isNextStepEnabled || isGenerating}
nextButtonText={nextButtonText}
isNextDisabled={!isNextStepEnabled}
onBack={onBack}
onNext={onNext}
nextButtonText={nextButtonText}
/>
);
}

View file

@ -435,4 +435,5 @@ export const mockActions: Actions = {
setHasCelInput: jest.fn(),
setResult: jest.fn(),
setCelInputResult: jest.fn(),
completeStep: jest.fn(),
};

View file

@ -78,6 +78,7 @@ export interface Actions {
setHasCelInput: (payload: State['hasCelInput']) => void;
setResult: (payload: State['result']) => void;
setCelInputResult: (payload: State['celInputResult']) => void;
completeStep: () => void;
}
const ActionsContext = createContext<Actions | undefined>(undefined);

View file

@ -22,7 +22,7 @@ interface CelInputStepProps {
export const CelInputStep = React.memo<CelInputStepProps>(
({ integrationSettings, connector, isGenerating }) => {
const { setIsGenerating, setStep, setCelInputResult } = useActions();
const { setIsGenerating, setStep, setCelInputResult, completeStep } = useActions();
const onGenerationCompleted = useCallback<OnComplete>(
(result: State['celInputResult']) => {
@ -43,7 +43,14 @@ export const CelInputStep = React.memo<CelInputStepProps>(
<EuiFlexItem>
<StepContentWrapper title={i18n.CEL_INPUT_TITLE} subtitle={i18n.CEL_INPUT_DESCRIPTION}>
<EuiPanel hasShadow={false} hasBorder>
<EuiForm component="form" fullWidth>
<EuiForm
component="form"
fullWidth
onSubmit={(e) => {
e.preventDefault();
completeStep();
}}
>
<ApiDefinitionInput integrationSettings={integrationSettings} />
</EuiForm>
</EuiPanel>

View file

@ -6,7 +6,7 @@
*/
import type { State } from '../../state';
export const isCelInputStepReady = ({ integrationSettings }: State) =>
export const isCelInputStepReadyToComplete = ({ integrationSettings }: State) =>
Boolean(
integrationSettings?.name &&
integrationSettings?.dataStreamTitle &&

View file

@ -6,12 +6,18 @@
*/
import React from 'react';
import { useEuiTheme, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiRadio } from '@elastic/eui';
import { noop } from 'lodash/fp';
import {
useEuiTheme,
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiRadio,
EuiFormFieldset,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useKibana } from '../../../../../common/hooks/use_kibana';
import type { AIConnector } from '../../types';
import { useActions } from '../../state';
const useRowCss = () => {
const { euiTheme } = useEuiTheme();
@ -36,54 +42,60 @@ const useRowCss = () => {
interface ConnectorSelectorProps {
connectors: AIConnector[];
selectedConnectorId: string | undefined;
setConnector: (connector: AIConnector | undefined) => void;
}
export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
({ connectors, selectedConnectorId }) => {
({ connectors, setConnector, selectedConnectorId }) => {
const {
triggersActionsUi: { actionTypeRegistry },
} = useKibana().services;
const { setConnector } = useActions();
const rowCss = useRowCss();
return (
<EuiFlexGroup
alignItems="stretch"
direction="column"
gutterSize="s"
data-test-subj="connectorSelector"
>
{connectors.map((connector) => (
<EuiFlexItem key={connector.id}>
<EuiPanel
key={connector.id}
onClick={() => setConnector(connector)}
hasShadow={false}
hasBorder
paddingSize="l"
css={rowCss}
data-test-subj={`connectorSelector-${connector.id}`}
>
<EuiFlexGroup direction="row" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiRadio
label={connector.name}
id={connector.id}
checked={selectedConnectorId === connector.id}
onChange={noop}
data-test-subj={`connectorSelectorRadio-${connector.id}${
selectedConnectorId === connector.id ? '-selected' : ''
}`}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">
{actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiFormFieldset>
<EuiFlexGroup
alignItems="stretch"
direction="column"
gutterSize="s"
data-test-subj="connectorSelector"
>
{connectors.map((connector) => (
<EuiFlexItem key={connector.id}>
<EuiPanel
element="button"
type="button" // So that the enter button will not submit the form.
role="radio"
key={connector.id}
onClick={() => setConnector(connector)}
hasShadow={false}
hasBorder
paddingSize="l"
css={rowCss}
data-test-subj={`connectorSelector-${connector.id}`}
>
<EuiFlexGroup direction="row" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiRadio
label={connector.name}
id={connector.id}
value={connector.id}
checked={selectedConnectorId === connector.id}
onChange={() => setConnector(connector)}
data-test-subj={`connectorSelectorRadio-${connector.id}${
selectedConnectorId === connector.id ? '-selected' : ''
}`}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">
{actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFormFieldset>
);
}
);

View file

@ -8,6 +8,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useLoadConnectors } from '@kbn/elastic-assistant';
import {
EuiForm,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
@ -42,7 +43,8 @@ interface ConnectorStepProps {
export const ConnectorStep = React.memo<ConnectorStepProps>(({ connector }) => {
const { euiTheme } = useEuiTheme();
const { http, notifications } = useKibana().services;
const { setConnector } = useActions();
const { setConnector, completeStep } = useActions();
const [connectors, setConnectors] = useState<AIConnector[]>();
const {
isLoading,
@ -69,41 +71,56 @@ export const ConnectorStep = React.memo<ConnectorStepProps>(({ connector }) => {
const hasConnectors = !isLoading && connectors?.length;
return (
<StepContentWrapper
title={i18n.TITLE}
subtitle={i18n.DESCRIPTION}
right={hasConnectors ? <CreateConnectorPopover onConnectorSaved={onConnectorSaved} /> : null}
<EuiForm
component="form"
fullWidth
onSubmit={(e) => {
e.preventDefault();
completeStep();
}}
>
<EuiFlexGroup direction="column" alignItems="stretch">
<EuiFlexItem>
{isLoading ? (
<EuiLoadingSpinner />
) : (
<>
{hasConnectors ? (
<ConnectorSelector connectors={connectors} selectedConnectorId={connector?.id} />
) : (
<AuthorizationWrapper canCreateConnectors>
<ConnectorSetup
actionTypeIds={AllowedActionTypeIds}
onConnectorSaved={onConnectorSaved}
<StepContentWrapper
title={i18n.TITLE}
subtitle={i18n.DESCRIPTION}
right={
hasConnectors ? <CreateConnectorPopover onConnectorSaved={onConnectorSaved} /> : null
}
>
<EuiFlexGroup direction="column" alignItems="stretch">
<EuiFlexItem>
{isLoading ? (
<EuiLoadingSpinner />
) : (
<>
{hasConnectors ? (
<ConnectorSelector
connectors={connectors}
setConnector={setConnector}
selectedConnectorId={connector?.id}
/>
</AuthorizationWrapper>
)}
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="flexStart">
<EuiFlexItem grow={false} css={{ margin: euiTheme.size.xxs }}>
<EuiIcon type="iInCircle" />
) : (
<AuthorizationWrapper canCreateConnectors>
<ConnectorSetup
actionTypeIds={AllowedActionTypeIds}
onConnectorSaved={onConnectorSaved}
/>
</AuthorizationWrapper>
)}
</>
)}
</EuiFlexItem>
<EuiFlexItem>{i18n.SUPPORTED_MODELS_INFO}</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
</StepContentWrapper>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="flexStart">
<EuiFlexItem grow={false} css={{ margin: euiTheme.size.xxs }}>
<EuiIcon type="iInCircle" />
</EuiFlexItem>
<EuiFlexItem>{i18n.SUPPORTED_MODELS_INFO}</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
</StepContentWrapper>
</EuiForm>
);
});
ConnectorStep.displayName = 'ConnectorStep';

View file

@ -7,4 +7,4 @@
import type { State } from '../../state';
export const isConnectorStepReady = ({ connector }: State) => connector != null;
export const isConnectorStepReadyToComplete = ({ connector }: State) => connector != null;

View file

@ -53,8 +53,14 @@ interface DataStreamStepProps {
}
export const DataStreamStep = React.memo<DataStreamStepProps>(
({ integrationSettings, connector, isGenerating }) => {
const { setIntegrationSettings, setIsGenerating, setHasCelInput, setStep, setResult } =
useActions();
const {
setIntegrationSettings,
setIsGenerating,
setHasCelInput,
setStep,
setResult,
completeStep,
} = useActions();
const { isLoading: isLoadingPackageNames, packageNames } = useLoadPackageNames(); // this is used to avoid duplicate names
const [name, setName] = useState<string>(integrationSettings?.name ?? '');
@ -150,14 +156,21 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
);
return (
<EuiFlexGroup direction="column" gutterSize="l" data-test-subj="dataStreamStep">
<EuiFlexItem>
<StepContentWrapper
title={i18n.INTEGRATION_NAME_TITLE}
subtitle={i18n.INTEGRATION_NAME_DESCRIPTION}
>
<EuiPanel hasShadow={false} hasBorder>
<EuiForm component="form" fullWidth>
<EuiForm
component="form"
fullWidth
onSubmit={(e) => {
e.preventDefault();
completeStep();
}}
>
<EuiFlexGroup direction="column" gutterSize="l" data-test-subj="dataStreamStep">
<EuiFlexItem>
<StepContentWrapper
title={i18n.INTEGRATION_NAME_TITLE}
subtitle={i18n.INTEGRATION_NAME_DESCRIPTION}
>
<EuiPanel hasShadow={false} hasBorder>
<EuiFormRow
label={i18n.INTEGRATION_NAME_LABEL}
helpText={
@ -176,18 +189,16 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
disabled={isLoadingPackageNames}
/>
</EuiFormRow>
</EuiForm>
</EuiPanel>
</StepContentWrapper>
</EuiFlexItem>
</EuiPanel>
</StepContentWrapper>
</EuiFlexItem>
<EuiFlexItem>
<StepContentWrapper
title={i18n.DATA_STREAM_TITLE}
subtitle={i18n.DATA_STREAM_DESCRIPTION}
>
<EuiPanel hasShadow={false} hasBorder>
<EuiForm component="form" fullWidth>
<EuiFlexItem>
<StepContentWrapper
title={i18n.DATA_STREAM_TITLE}
subtitle={i18n.DATA_STREAM_DESCRIPTION}
>
<EuiPanel hasShadow={false} hasBorder>
<EuiFormRow label={i18n.DATA_STREAM_TITLE_LABEL}>
<EuiFieldText
name="dataStreamTitle"
@ -228,19 +239,19 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
/>
</EuiFormRow>
<SampleLogsInput integrationSettings={integrationSettings} />
</EuiForm>
</EuiPanel>
</StepContentWrapper>
{isGenerating && (
<GenerationModal
integrationSettings={integrationSettings}
connector={connector}
onComplete={onGenerationCompleted}
onClose={onGenerationClosed}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</StepContentWrapper>
{isGenerating && (
<GenerationModal
integrationSettings={integrationSettings}
connector={connector}
onComplete={onGenerationCompleted}
onClose={onGenerationClosed}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
);
}
);

View file

@ -6,7 +6,7 @@
*/
import type { State } from '../../state';
export const isDataStreamStepReady = ({ integrationSettings }: State) =>
export const isDataStreamStepReadyToComplete = ({ integrationSettings }: State) =>
Boolean(
integrationSettings?.name &&
integrationSettings?.dataStreamTitle &&

View file

@ -51,7 +51,7 @@ interface IntegrationStepProps {
export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSettings }) => {
const styles = useLayoutStyles();
const { setIntegrationSettings } = useActions();
const { setIntegrationSettings, completeStep } = useActions();
const [logoError, setLogoError] = React.useState<string>();
const setIntegrationValues = useCallback(
@ -95,7 +95,14 @@ export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSe
<EuiPanel paddingSize="none" hasShadow={false} hasBorder data-test-subj="integrationStep">
<EuiFlexGroup direction="row" gutterSize="none">
<EuiFlexItem css={styles.left}>
<EuiForm component="form" fullWidth>
<EuiForm
component="form"
fullWidth
onSubmit={(e) => {
e.preventDefault();
completeStep();
}}
>
<EuiFormRow label={i18n.TITLE_LABEL}>
<EuiFieldText
name="title"

View file

@ -7,5 +7,5 @@
import type { State } from '../../state';
export const isIntegrationStepReady = ({ integrationSettings }: State) =>
export const isIntegrationStepReadyToComplete = ({ integrationSettings }: State) =>
Boolean(integrationSettings?.title && integrationSettings?.description);

View file

@ -7,5 +7,5 @@
import type { State } from '../../state';
export const isCelReviewStepReady = ({ isGenerating, celInputResult }: State) =>
export const isCelReviewStepReadyToComplete = ({ isGenerating, celInputResult }: State) =>
isGenerating === false && celInputResult != null;

View file

@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
import { EuiForm, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
import React from 'react';
import type { State } from '../../state';
import { useActions, type State } from '../../state';
import { StepContentWrapper } from '../step_content_wrapper';
import * as i18n from './translations';
import { CelConfigResults } from './cel_config_results';
@ -17,15 +17,24 @@ interface ReviewCelStepProps {
}
export const ReviewCelStep = React.memo<ReviewCelStepProps>(({ isGenerating, celInputResult }) => {
const { completeStep } = useActions();
return (
<StepContentWrapper title={i18n.TITLE} subtitle={i18n.DESCRIPTION}>
<EuiPanel hasShadow={false} hasBorder data-test-subj="reviewCelStep">
{isGenerating ? (
<EuiLoadingSpinner size="l" />
) : (
<>
<EuiForm
component="form"
fullWidth
onSubmit={(e) => {
e.preventDefault();
completeStep();
}}
>
<CelConfigResults celInputResult={celInputResult} />
</>
</EuiForm>
)}
</EuiPanel>
</StepContentWrapper>

View file

@ -7,5 +7,5 @@
import type { State } from '../../state';
export const isReviewStepReady = ({ isGenerating, result }: State) =>
export const isReviewStepReadyToComplete = ({ isGenerating, result }: State) =>
isGenerating === false && result != null;

View file

@ -15,20 +15,12 @@ import type {
IntegrationSettings,
} from './create_integration_assistant/types';
const stepNames: Record<string, string> = {
'1': 'Connector Step',
'2': 'Integration Step',
'3': 'DataStream Step',
'4': 'Review Step',
'5': 'Deploy Step',
};
type ReportUploadZipIntegrationComplete = (params: {
integrationName?: string;
error?: string;
}) => void;
type ReportAssistantOpen = () => void;
type ReportAssistantStepComplete = (params: { step: number }) => void;
type ReportAssistantStepComplete = (params: { step: number; stepName: string }) => void;
type ReportGenerationComplete = (params: {
connector: AIConnector;
integrationSettings: IntegrationSettings;
@ -92,11 +84,11 @@ export const TelemetryContextProvider = React.memo<PropsWithChildren<{}>>(({ chi
}, [telemetry]);
const reportAssistantStepComplete = useCallback<ReportAssistantStepComplete>(
({ step }) => {
({ step, stepName }) => {
telemetry.reportEvent(TelemetryEventType.IntegrationAssistantStepComplete, {
sessionId: sessionData.current.sessionId,
step,
stepName: stepNames[step.toString()] ?? 'Unknown Step',
stepName,
durationMs: Date.now() - stepsData.current.startedAt,
sessionElapsedTime: Date.now() - sessionData.current.startedAt,
});