mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.15`: - [[Integration Assistant] Implement unit tests for the UI (#187590)](https://github.com/elastic/kibana/pull/187590) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Sergi Massaneda","email":"sergi.massaneda@elastic.co"},"sourceCommit":{"committedDate":"2024-07-11T14:43:31Z","message":"[Integration Assistant] Implement unit tests for the UI (#187590)\n\n## Summary\r\n\r\nUnit tests for the Integration Assistant UI components\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"490bbdb3f61ab0435befc1bc303b40a7774a4532","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team: SecuritySolution","v8.15.0","v8.16.0"],"title":"[Integration Assistant] Implement unit tests for the UI","number":187590,"url":"https://github.com/elastic/kibana/pull/187590","mergeCommit":{"message":"[Integration Assistant] Implement unit tests for the UI (#187590)\n\n## Summary\r\n\r\nUnit tests for the Integration Assistant UI components\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"490bbdb3f61ab0435befc1bc303b40a7774a4532"}},"sourceBranch":"main","suggestedTargetBranches":["8.15"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187590","number":187590,"mergeCommit":{"message":"[Integration Assistant] Implement unit tests for the UI (#187590)\n\n## Summary\r\n\r\nUnit tests for the Integration Assistant UI components\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"490bbdb3f61ab0435befc1bc303b40a7774a4532"}}]}] BACKPORT--> Co-authored-by: Sergi Massaneda <sergi.massaneda@elastic.co>
This commit is contained in:
parent
2dc9f66822
commit
bb3c191384
39 changed files with 2747 additions and 148 deletions
|
@ -25,7 +25,11 @@ export const AuthorizationWrapper = React.memo<AuthorizationWrapperProps>(
|
|||
|
||||
if (!isAuthorized) {
|
||||
return (
|
||||
<EuiCallOut title={i18n.PRIVILEGES_MISSING_TITLE} iconType="iInCircle">
|
||||
<EuiCallOut
|
||||
title={i18n.PRIVILEGES_MISSING_TITLE}
|
||||
iconType="iInCircle"
|
||||
data-test-subj="missingPrivilegesCallOut"
|
||||
>
|
||||
<MissingPrivilegesDescription {...authRequired} />
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
|
|
@ -44,7 +44,12 @@ export const ButtonsFooter = React.memo<ButtonsFooterProps>(
|
|||
const integrationsUrl = useKibana().services.application.getUrlForApp('integrations');
|
||||
return (
|
||||
<KibanaPageTemplate.BottomBar paddingSize="s" position="sticky" css={bottomBarCss}>
|
||||
<EuiFlexGroup direction="column" alignItems="center" css={containerCss}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
css={containerCss}
|
||||
data-test-subj="buttonsFooter"
|
||||
>
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
|
@ -54,7 +59,11 @@ export const ButtonsFooter = React.memo<ButtonsFooterProps>(
|
|||
>
|
||||
<EuiFlexItem>
|
||||
{!hideCancel && (
|
||||
<EuiLink href={integrationsUrl} color="text">
|
||||
<EuiLink
|
||||
href={integrationsUrl}
|
||||
color="text"
|
||||
data-test-subj="buttonsFooter-cancelButton"
|
||||
>
|
||||
{cancelButtonText || (
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.footer.cancel"
|
||||
|
@ -73,7 +82,11 @@ export const ButtonsFooter = React.memo<ButtonsFooterProps>(
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{onBack && (
|
||||
<EuiLink onClick={onBack} color="text">
|
||||
<EuiLink
|
||||
onClick={onBack}
|
||||
color="text"
|
||||
data-test-subj="buttonsFooter-backButton"
|
||||
>
|
||||
{backButtonText || (
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.footer.back"
|
||||
|
@ -85,7 +98,13 @@ export const ButtonsFooter = React.memo<ButtonsFooterProps>(
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{onNext && (
|
||||
<EuiButton fill color="primary" onClick={onNext} isDisabled={isNextDisabled}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
onClick={onNext}
|
||||
isDisabled={isNextDisabled}
|
||||
data-test-subj="buttonsFooter-nextButton"
|
||||
>
|
||||
{nextButtonText || (
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.footer.next"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Authorization, RoutesAuthorization } from '../use_authorization';
|
||||
|
||||
export const useAuthorization = jest.fn(
|
||||
(): Authorization => ({
|
||||
canCreateIntegrations: true,
|
||||
canExecuteConnectors: true,
|
||||
canCreateConnectors: true,
|
||||
})
|
||||
);
|
||||
|
||||
export const useRoutesAuthorization = jest.fn(
|
||||
(): RoutesAuthorization => ({
|
||||
canUseIntegrationAssistant: true,
|
||||
canUseIntegrationUpload: true,
|
||||
})
|
||||
);
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Availability } from '../use_availability';
|
||||
|
||||
export const useAvailability = jest.fn((): Availability => {
|
||||
return { hasLicense: true, renderUpselling: undefined };
|
||||
});
|
||||
|
||||
export const useIsAvailable = jest.fn((): boolean => true);
|
|
@ -11,10 +11,12 @@ import { MINIMUM_LICENSE_TYPE } from '../../../common/constants';
|
|||
import { useKibana } from './use_kibana';
|
||||
import type { RenderUpselling } from '../../services';
|
||||
|
||||
export const useAvailability = (): {
|
||||
export interface Availability {
|
||||
hasLicense: boolean;
|
||||
renderUpselling: RenderUpselling | undefined;
|
||||
} => {
|
||||
}
|
||||
|
||||
export const useAvailability = (): Availability => {
|
||||
const { licensing, renderUpselling$ } = useKibana().services;
|
||||
const licenseService = useObservable(licensing.license$);
|
||||
const renderUpselling = useObservable(renderUpselling$);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EpmPackageResponse } from './api';
|
||||
import { getIntegrationNameFromResponse } from './api_parsers';
|
||||
|
||||
describe('getIntegrationNameFromResponse', () => {
|
||||
it.each([
|
||||
['audit-security.data-stream-1.0.0', 'security-1.0.0'],
|
||||
['audit-endpoint_security.data_stream-1.0.0', 'endpoint_security-1.0.0'],
|
||||
['audit-endpoint_security_2.data_stream-1.0.0', 'endpoint_security_2-1.0.0'],
|
||||
])(
|
||||
'should return the integration name from the ingest pipeline name %s',
|
||||
(ingestPipelineName, expected) => {
|
||||
const response = { response: [{ id: ingestPipelineName }] } as EpmPackageResponse;
|
||||
expect(getIntegrationNameFromResponse(response)).toEqual(expected);
|
||||
}
|
||||
);
|
||||
it('should return an empty string if the response is empty', () => {
|
||||
const response = { response: [] } as unknown as EpmPackageResponse;
|
||||
expect(getIntegrationNameFromResponse(response)).toEqual('');
|
||||
});
|
||||
it('should return an empty string if the response is undefined', () => {
|
||||
const response = {} as EpmPackageResponse;
|
||||
expect(getIntegrationNameFromResponse(response)).toEqual('');
|
||||
});
|
||||
it('should return an empty string if the response is null', () => {
|
||||
const response = { response: null } as unknown as EpmPackageResponse;
|
||||
expect(getIntegrationNameFromResponse(response)).toEqual('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProvider } from '../../mocks/test_provider';
|
||||
import { CreateIntegration } from './create_integration';
|
||||
import { mockServices } from '../../services/mocks/services';
|
||||
import { useRoutesAuthorization } from '../../common/hooks/use_authorization';
|
||||
import { useIsAvailable } from '../../common/hooks/use_availability';
|
||||
|
||||
jest.mock('../../common/hooks/use_authorization');
|
||||
jest.mock('../../common/hooks/use_availability');
|
||||
const mockUseRoutesAuthorization = useRoutesAuthorization as jest.Mock;
|
||||
const mockUseIsAvailable = useIsAvailable as jest.Mock;
|
||||
|
||||
jest.mock('./create_integration_landing', () => ({
|
||||
CreateIntegrationLanding: jest.fn(() => <div data-test-subj="landingMock" />),
|
||||
}));
|
||||
jest.mock('./create_integration_upload', () => ({
|
||||
CreateIntegrationUpload: jest.fn(() => <div data-test-subj="uploadMock" />),
|
||||
}));
|
||||
jest.mock('./create_integration_assistant', () => ({
|
||||
CreateIntegrationAssistant: jest.fn(() => <div data-test-subj="assistantMock" />),
|
||||
}));
|
||||
|
||||
const getWrapper = (pathname: string): React.FC<PropsWithChildren<{}>> =>
|
||||
function wrapper({ children }) {
|
||||
return (
|
||||
<TestProvider>
|
||||
<MemoryRouter initialEntries={[{ pathname }]}>{children}</MemoryRouter>
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const getElement = () => <CreateIntegration services={mockServices} />;
|
||||
|
||||
describe('CreateIntegration', () => {
|
||||
describe('when url is /create', () => {
|
||||
let wrapper: React.ComponentType;
|
||||
beforeEach(() => {
|
||||
wrapper = getWrapper('/create');
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('and user is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
mockUseRoutesAuthorization.mockReturnValueOnce({
|
||||
canUseIntegrationAssistant: false,
|
||||
canUseIntegrationUpload: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the product is not available', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsAvailable.mockReturnValueOnce(false);
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when url is /create/assistant', () => {
|
||||
let wrapper: React.ComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = getWrapper('/create/assistant');
|
||||
});
|
||||
|
||||
it('should render the assistant page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('assistantMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('and user is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
mockUseRoutesAuthorization.mockReturnValueOnce({
|
||||
canUseIntegrationAssistant: false,
|
||||
canUseIntegrationUpload: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('assistantMock')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the product is not available', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsAvailable.mockReturnValueOnce(false);
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('assistantMock')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when url is /create/upload', () => {
|
||||
let wrapper: React.ComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = getWrapper('/create/upload');
|
||||
});
|
||||
|
||||
it('should render the upload page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('uploadMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('and user is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
mockUseRoutesAuthorization.mockReturnValueOnce({
|
||||
canUseIntegrationAssistant: true,
|
||||
canUseIntegrationUpload: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('uploadMock')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the product is not available', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsAvailable.mockReturnValueOnce(false);
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('uploadMock')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when url is not exact', () => {
|
||||
let wrapper: React.ComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = getWrapper('/create/something_else');
|
||||
});
|
||||
|
||||
it('should render the landing page', () => {
|
||||
const result = render(getElement(), { wrapper });
|
||||
expect(result.queryByTestId('landingMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProvider } from '../../../mocks/test_provider';
|
||||
import { CreateIntegrationAssistant } from './create_integration_assistant';
|
||||
import type { State } from './state';
|
||||
|
||||
export const defaultInitialState: State = {
|
||||
step: 1,
|
||||
connector: undefined,
|
||||
integrationSettings: undefined,
|
||||
isGenerating: false,
|
||||
result: undefined,
|
||||
};
|
||||
const mockInitialState = jest.fn((): State => defaultInitialState);
|
||||
jest.mock('./state', () => ({
|
||||
...jest.requireActual('./state'),
|
||||
get initialState() {
|
||||
return mockInitialState();
|
||||
},
|
||||
}));
|
||||
|
||||
const mockConnectorStep = jest.fn(() => <div data-test-subj="connectorStepMock" />);
|
||||
const mockIntegrationStep = jest.fn(() => <div data-test-subj="integrationStepMock" />);
|
||||
const mockDataStreamStep = jest.fn(() => <div data-test-subj="dataStreamStepMock" />);
|
||||
const mockReviewStep = jest.fn(() => <div data-test-subj="reviewStepMock" />);
|
||||
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();
|
||||
|
||||
jest.mock('./steps/connector_step', () => ({
|
||||
ConnectorStep: () => mockConnectorStep(),
|
||||
isConnectorStepReady: () => mockIsConnectorStepReady(),
|
||||
}));
|
||||
jest.mock('./steps/integration_step', () => ({
|
||||
IntegrationStep: () => mockIntegrationStep(),
|
||||
isIntegrationStepReady: () => mockIsIntegrationStepReady(),
|
||||
}));
|
||||
jest.mock('./steps/data_stream_step', () => ({
|
||||
DataStreamStep: () => mockDataStreamStep(),
|
||||
isDataStreamStepReady: () => mockIsDataStreamStepReady(),
|
||||
}));
|
||||
jest.mock('./steps/review_step', () => ({
|
||||
ReviewStep: () => mockReviewStep(),
|
||||
isReviewStepReady: () => mockIsReviewStepReady(),
|
||||
}));
|
||||
jest.mock('./steps/deploy_step', () => ({ DeployStep: () => mockDeployStep() }));
|
||||
|
||||
const renderIntegrationAssistant = () =>
|
||||
render(<CreateIntegrationAssistant />, { wrapper: TestProvider });
|
||||
|
||||
describe('CreateIntegration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when step is 1', () => {
|
||||
beforeEach(() => {
|
||||
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 1 });
|
||||
});
|
||||
|
||||
it('should render connector', () => {
|
||||
const result = renderIntegrationAssistant();
|
||||
expect(result.queryByTestId('connectorStepMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call isConnectorStepReady', () => {
|
||||
renderIntegrationAssistant();
|
||||
expect(mockIsConnectorStepReady).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when step is 2', () => {
|
||||
beforeEach(() => {
|
||||
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 2 });
|
||||
});
|
||||
|
||||
it('should render integration', () => {
|
||||
const result = renderIntegrationAssistant();
|
||||
expect(result.queryByTestId('integrationStepMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call isIntegrationStepReady', () => {
|
||||
renderIntegrationAssistant();
|
||||
expect(mockIsIntegrationStepReady).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when step is 3', () => {
|
||||
beforeEach(() => {
|
||||
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 3 });
|
||||
});
|
||||
|
||||
it('should render data stream', () => {
|
||||
const result = renderIntegrationAssistant();
|
||||
expect(result.queryByTestId('dataStreamStepMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call isDataStreamStepReady', () => {
|
||||
renderIntegrationAssistant();
|
||||
expect(mockIsDataStreamStepReady).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when step is 4', () => {
|
||||
beforeEach(() => {
|
||||
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 4 });
|
||||
});
|
||||
|
||||
it('should render review', () => {
|
||||
const result = renderIntegrationAssistant();
|
||||
expect(result.queryByTestId('reviewStepMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call isReviewStepReady', () => {
|
||||
renderIntegrationAssistant();
|
||||
expect(mockIsReviewStepReady).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when step is 5', () => {
|
||||
beforeEach(() => {
|
||||
mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 5 });
|
||||
});
|
||||
|
||||
it('should render deploy', () => {
|
||||
const result = renderIntegrationAssistant();
|
||||
expect(result.queryByTestId('deployStepMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useReducer, useMemo, useCallback, useEffect } from 'react';
|
||||
import React, { useReducer, useMemo, useEffect } from 'react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { Header } from './header';
|
||||
import { Footer } from './footer';
|
||||
|
@ -59,8 +59,6 @@ export const CreateIntegrationAssistant = React.memo(() => {
|
|||
return false;
|
||||
}, [state]);
|
||||
|
||||
const onGenerate = useCallback(() => actions.setIsGenerating(true), [actions]);
|
||||
|
||||
return (
|
||||
<ActionsProvider value={actions}>
|
||||
<KibanaPageTemplate>
|
||||
|
@ -92,7 +90,6 @@ export const CreateIntegrationAssistant = React.memo(() => {
|
|||
</KibanaPageTemplate.Section>
|
||||
<Footer
|
||||
currentStep={state.step}
|
||||
onGenerate={onGenerate}
|
||||
isGenerating={state.isGenerating}
|
||||
isNextStepEnabled={isNextStepEnabled}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, 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';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('../../../../common/hooks/use_navigate', () => ({
|
||||
...jest.requireActual('../../../../common/hooks/use_navigate'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when rendered', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
result = render(<Footer currentStep={1} isGenerating={false} isNextStepEnabled />, {
|
||||
wrapper,
|
||||
});
|
||||
});
|
||||
it('should render footer buttons component', () => {
|
||||
expect(result.queryByTestId('buttonsFooter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render cancel button', () => {
|
||||
expect(result.queryByTestId('buttonsFooter-cancelButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render back button', () => {
|
||||
expect(result.queryByTestId('buttonsFooter-backButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render next button', () => {
|
||||
expect(result.queryByTestId('buttonsFooter-nextButton')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when step is 1', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
result = render(<Footer currentStep={1} isGenerating={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} 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} 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} 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} 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} />, {
|
||||
wrapper,
|
||||
});
|
||||
});
|
||||
it('should render next button disabled', () => {
|
||||
expect(result.queryByTestId('buttonsFooter-nextButton')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -20,7 +20,7 @@ const AnalyzeButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerating
|
|||
}
|
||||
return (
|
||||
<>
|
||||
<EuiLoadingSpinner size="s" />
|
||||
<EuiLoadingSpinner size="s" data-test-subj="generatingLoader" />
|
||||
{i18n.LOADING}
|
||||
</>
|
||||
);
|
||||
|
@ -30,14 +30,13 @@ AnalyzeButtonText.displayName = 'AnalyzeButtonText';
|
|||
interface FooterProps {
|
||||
currentStep: State['step'];
|
||||
isGenerating: State['isGenerating'];
|
||||
onGenerate: () => void;
|
||||
isNextStepEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const Footer = React.memo<FooterProps>(
|
||||
({ currentStep, onGenerate, isGenerating, isNextStepEnabled = false }) => {
|
||||
({ currentStep, isGenerating, isNextStepEnabled = false }) => {
|
||||
const telemetry = useTelemetry();
|
||||
const { setStep } = useActions();
|
||||
const { setStep, setIsGenerating } = useActions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
|
@ -51,11 +50,11 @@ export const Footer = React.memo<FooterProps>(
|
|||
const onNext = useCallback(() => {
|
||||
telemetry.reportAssistantStepComplete({ step: currentStep });
|
||||
if (currentStep === 3) {
|
||||
onGenerate();
|
||||
setIsGenerating(true);
|
||||
} else {
|
||||
setStep(currentStep + 1);
|
||||
}
|
||||
}, [currentStep, onGenerate, setStep, telemetry]);
|
||||
}, [currentStep, setIsGenerating, setStep, telemetry]);
|
||||
|
||||
const nextButtonText = useMemo(() => {
|
||||
if (currentStep === 3) {
|
||||
|
|
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Pipeline, Docs } from '../../../../../common';
|
||||
import type { Actions, State } from '../state';
|
||||
import type { AIConnector } from '../types';
|
||||
|
||||
const result: { pipeline: Pipeline; docs: Docs } = {
|
||||
pipeline: {
|
||||
description: 'Pipeline to process my_integration my_data_stream_title logs',
|
||||
processors: [
|
||||
{
|
||||
set: {
|
||||
field: 'ecs.version',
|
||||
tag: 'set_ecs_version',
|
||||
value: '8.11.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'message',
|
||||
target_field: 'event.original',
|
||||
tag: 'rename_message',
|
||||
ignore_missing: true,
|
||||
if: 'ctx.event?.original == null',
|
||||
},
|
||||
},
|
||||
{
|
||||
remove: {
|
||||
field: 'message',
|
||||
ignore_missing: true,
|
||||
tag: 'remove_message',
|
||||
if: 'ctx.event?.original != null',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
field: 'event.original',
|
||||
tag: 'json_original',
|
||||
target_field: 'my_integration.my_data_stream_title',
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.event',
|
||||
target_field: 'event.action',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.uid',
|
||||
target_field: 'event.id',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.code',
|
||||
target_field: 'event.code',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
date: {
|
||||
field: 'my_integration.my_data_stream_title.time',
|
||||
target_field: '@timestamp',
|
||||
formats: ['ISO8601'],
|
||||
if: 'ctx.my_integration?.my_data_stream_title?.time != null',
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.user',
|
||||
target_field: 'user.name',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.user_agent',
|
||||
target_field: 'user_agent.original',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.addr.remote',
|
||||
target_field: 'source.address',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.server_hostname',
|
||||
target_field: 'destination.domain',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'my_integration.my_data_stream_title.proto',
|
||||
target_field: 'network.transport',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
script: {
|
||||
description: 'Drops null/empty values recursively.',
|
||||
tag: 'script_drop_null_empty_values',
|
||||
lang: 'painless',
|
||||
source:
|
||||
'boolean dropEmptyFields(Object object) {\n if (object == null || object == "") {\n return true;\n } else if (object instanceof Map) {\n ((Map) object).values().removeIf(value -> dropEmptyFields(value));\n return (((Map) object).size() == 0);\n } else if (object instanceof List) {\n ((List) object).removeIf(value -> dropEmptyFields(value));\n return (((List) object).length == 0);\n }\n return false;\n}\ndropEmptyFields(ctx);\n',
|
||||
},
|
||||
},
|
||||
{
|
||||
geoip: {
|
||||
field: 'source.ip',
|
||||
tag: 'geoip_source_ip',
|
||||
target_field: 'source.geo',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
geoip: {
|
||||
ignore_missing: true,
|
||||
database_file: 'GeoLite2-ASN.mmdb',
|
||||
field: 'source.ip',
|
||||
tag: 'geoip_source_asn',
|
||||
target_field: 'source.as',
|
||||
properties: ['asn', 'organization_name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'source.as.asn',
|
||||
tag: 'rename_source_as_asn',
|
||||
target_field: 'source.as.number',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'source.as.organization_name',
|
||||
tag: 'rename_source_as_organization_name',
|
||||
target_field: 'source.as.organization.name',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
geoip: {
|
||||
field: 'destination.ip',
|
||||
tag: 'geoip_destination_ip',
|
||||
target_field: 'destination.geo',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
geoip: {
|
||||
database_file: 'GeoLite2-ASN.mmdb',
|
||||
field: 'destination.ip',
|
||||
tag: 'geoip_destination_asn',
|
||||
target_field: 'destination.as',
|
||||
properties: ['asn', 'organization_name'],
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'destination.as.asn',
|
||||
tag: 'rename_destination_as_asn',
|
||||
target_field: 'destination.as.number',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'event.type',
|
||||
value: ['start'],
|
||||
allow_duplicates: false,
|
||||
if: "ctx.event?.action == 'user.login'",
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'event.category',
|
||||
value: ['authentication'],
|
||||
allow_duplicates: false,
|
||||
if: "ctx.event?.action == 'user.login'",
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'event.type',
|
||||
value: ['start'],
|
||||
allow_duplicates: false,
|
||||
if: "ctx.event?.action == 'session.start'",
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'event.category',
|
||||
value: ['session'],
|
||||
allow_duplicates: false,
|
||||
if: "ctx.event?.action == 'session.start'",
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'event.type',
|
||||
value: ['info'],
|
||||
allow_duplicates: false,
|
||||
if: 'ctx.my_integration?.my_data_stream_title?.mfa_device != null',
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'event.category',
|
||||
value: ['authentication'],
|
||||
allow_duplicates: false,
|
||||
if: 'ctx.my_integration?.my_data_stream_title?.mfa_device != null',
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'related.ip',
|
||||
value: ['{{{my_integration.my_data_stream_title.addr.remote}}}'],
|
||||
if: 'ctx?.my_integration?.my_data_stream_title?.addr?.remote != null',
|
||||
allow_duplicates: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'related.user',
|
||||
value: ['{{{user.name}}}'],
|
||||
allow_duplicates: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'related.hosts',
|
||||
value: ['{{{destination.domain}}}'],
|
||||
if: 'ctx?.destination?.domain != null',
|
||||
allow_duplicates: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
append: {
|
||||
field: 'related.hosts',
|
||||
value: ['{{{my_integration.my_data_stream_title.server_labels.hostname}}}'],
|
||||
if: 'ctx?.my_integration?.my_data_stream_title?.server_labels?.hostname != null',
|
||||
allow_duplicates: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
rename: {
|
||||
field: 'destination.as.organization_name',
|
||||
tag: 'rename_destination_as_organization_name',
|
||||
target_field: 'destination.as.organization.name',
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
remove: {
|
||||
field: 'event.original',
|
||||
tag: 'remove_original_event',
|
||||
if: 'ctx?.tags == null || !(ctx.tags.contains("preserve_original_event"))',
|
||||
ignore_failure: true,
|
||||
ignore_missing: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
on_failure: [
|
||||
{
|
||||
append: {
|
||||
field: 'error.message',
|
||||
value:
|
||||
'Processor {{{_ingest.on_failure_processor_type}}} with tag {{{_ingest.on_failure_processor_tag}}} in pipeline {{{_ingest.on_failure_pipeline}}} failed with message: {{{_ingest.on_failure_message}}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
set: {
|
||||
field: 'event.kind',
|
||||
value: 'pipeline_error',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
docs: [
|
||||
{
|
||||
'@timestamp': '2024-02-23T18:56:50.628Z',
|
||||
ecs: {
|
||||
version: '8.11.0',
|
||||
},
|
||||
my_integration: {
|
||||
my_data_stream_title: {
|
||||
cluster_name: 'teleport.ericbeahan.com',
|
||||
'addr.remote': '136.61.214.196:50332',
|
||||
ei: 0,
|
||||
method: 'local',
|
||||
required_private_key_policy: 'none',
|
||||
success: true,
|
||||
time: '2024-02-23T18:56:50.628Z',
|
||||
mfa_device: {
|
||||
mfa_device_type: 'TOTP',
|
||||
mfa_device_uuid: 'd07bf388-af49-4ec2-b8a4-c8a9e785b70b',
|
||||
mfa_device_name: 'otp-device',
|
||||
},
|
||||
},
|
||||
},
|
||||
related: {
|
||||
user: ['teleport-admin'],
|
||||
},
|
||||
event: {
|
||||
action: 'user.login',
|
||||
code: 'T1000I',
|
||||
id: 'b675d102-fc25-4f7a-bf5d-96468cc176ea',
|
||||
type: ['start', 'info'],
|
||||
category: ['authentication'],
|
||||
},
|
||||
user: {
|
||||
name: 'teleport-admin',
|
||||
},
|
||||
user_agent: {
|
||||
original:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
||||
},
|
||||
tags: [
|
||||
'_geoip_database_unavailable_GeoLite2-City.mmdb',
|
||||
'_geoip_database_unavailable_GeoLite2-ASN.mmdb',
|
||||
'_geoip_database_unavailable_GeoLite2-City.mmdb',
|
||||
'_geoip_database_unavailable_GeoLite2-ASN.mmdb',
|
||||
],
|
||||
},
|
||||
{
|
||||
'@timestamp': '2024-02-23T18:56:57.199Z',
|
||||
ecs: {
|
||||
version: '8.11.0',
|
||||
},
|
||||
my_integration: {
|
||||
my_data_stream_title: {
|
||||
cluster_name: 'teleport.ericbeahan.com',
|
||||
'addr.remote': '136.61.214.196:50339',
|
||||
ei: 0,
|
||||
private_key_policy: 'none',
|
||||
login: 'ec2-user',
|
||||
server_id: 'face0091-2bf1-43fd-a16a-f1514b4119f4',
|
||||
sid: '293fda2d-2266-4d4d-b9d1-bd5ea9dd9fc3',
|
||||
user_kind: 1,
|
||||
server_labels: {
|
||||
'teleport.internal/resource-id': 'dccb2999-9fb8-4169-aded-ec7a1c0a26de',
|
||||
hostname: 'ip-172-31-8-163.us-east-2.compute.internal',
|
||||
},
|
||||
size: '80:25',
|
||||
namespace: 'default',
|
||||
session_recording: 'node',
|
||||
time: '2024-02-23T18:56:57.199Z',
|
||||
},
|
||||
},
|
||||
related: {
|
||||
user: ['teleport-admin'],
|
||||
hosts: ['ip-172-31-8-163.us-east-2.compute.internal'],
|
||||
},
|
||||
destination: {
|
||||
domain: 'ip-172-31-8-163.us-east-2.compute.internal',
|
||||
},
|
||||
event: {
|
||||
action: 'session.start',
|
||||
code: 'T2000I',
|
||||
id: 'fff30583-13be-49e8-b159-32952c6ea34f',
|
||||
type: ['start'],
|
||||
category: ['session'],
|
||||
},
|
||||
user: {
|
||||
name: 'teleport-admin',
|
||||
},
|
||||
network: {
|
||||
transport: 'ssh',
|
||||
},
|
||||
tags: [
|
||||
'_geoip_database_unavailable_GeoLite2-City.mmdb',
|
||||
'_geoip_database_unavailable_GeoLite2-ASN.mmdb',
|
||||
'_geoip_database_unavailable_GeoLite2-City.mmdb',
|
||||
'_geoip_database_unavailable_GeoLite2-ASN.mmdb',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const rawSamples = [
|
||||
'{"ei":0,"event":"user.login","uid":"b675d102-fc25-4f7a-bf5d-96468cc176ea","code":"T1000I","time":"2024-02-23T18:56:50.628Z","cluster_name":"teleport.ericbeahan.com","user":"teleport-admin","required_private_key_policy":"none","success":true,"method":"local","mfa_device.mfa_device_name":"otp-device","mfa_device.mfa_device_uuid":"d07bf388-af49-4ec2-b8a4-c8a9e785b70b","mfa_device.mfa_device_type":"TOTP","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36","addr.remote":"136.61.214.196:50332"}',
|
||||
'{"ei":0,"event":"cert.create","uid":"efd326fc-dd13-4df8-acef-3102c2d717d3","code":"TC000I","time":"2024-02-23T18:56:50.653Z","cluster_name":"teleport.ericbeahan.com","cert_type":"user","identity.user":"teleport-admin","identity.roles":["access","editor"],"identity.logins":["root","ubuntu","ec2-user","-teleport-internal-join"],"identity.expires":"2024-02-24T06:56:50.648137154Z","identity.route_to_cluster":"teleport.ericbeahan.com","identity.traits.aws_role_arns":null,"identity.traits.azure_identities":null,"identity.traits.db_names":null,"identity.traits.db_roles":null,"identity.traits.db_users":null,"identity.traits.gcp_service_accounts":null,"identity.traits.host_user_gid":[""],"identity.traits.host_user_uid":[""],"identity.traits.kubernetes_groups":null,"identity.traits.kubernetes_users":null,"identity.traits.logins":["root","ubuntu","ec2-user"],"identity.traits.windows_logins":null,"identity.teleport_cluster":"teleport.ericbeahan.com","identity.client_ip":"136.61.214.196","identity.prev_identity_expires":"0001-01-01T00:00:00Z","identity.private_key_policy":"none"}',
|
||||
'{"ei":0,"event":"session.start","uid":"fff30583-13be-49e8-b159-32952c6ea34f","code":"T2000I","time":"2024-02-23T18:56:57.199Z","cluster_name":"teleport.ericbeahan.com","user":"teleport-admin","login":"ec2-user","user_kind":1,"sid":"293fda2d-2266-4d4d-b9d1-bd5ea9dd9fc3","private_key_policy":"none","namespace":"default","server_id":"face0091-2bf1-43fd-a16a-f1514b4119f4","server_hostname":"ip-172-31-8-163.us-east-2.compute.internal","server_labels.hostname":"ip-172-31-8-163.us-east-2.compute.internal","server_labels.teleport.internal/resource-id":"dccb2999-9fb8-4169-aded-ec7a1c0a26de","addr.remote":"136.61.214.196:50339","proto":"ssh","size":"80:25","initial_command":[""],"session_recording":"node"}',
|
||||
];
|
||||
const logo =
|
||||
'PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzMyNjlfMjExOTMpIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMC41IDExLjk5OTdMNC40MDQgMS42ODQyMUMzLjM4Nzc1IC0wLjAzNzA0MDIgMC43NSAwLjY4NDQ2IDAuNzUgMi42ODMyMVYyMS4zMTYyQzAuNzUgMjMuMzE1IDMuMzg3NzUgMjQuMDM2NSA0LjQwNCAyMi4zMTUyTDEwLjUgMTEuOTk5N1oiIGZpbGw9IiNGMDRFOTgiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMy4xMTMzIDEyTDEyLjQzNzYgMTMuMTQ0NUw2LjM0MTU2IDIzLjQ2QzYuMjI2ODEgMjMuNjUzNSA2LjA5NzA2IDIzLjgzMDUgNS45NTgzMSAyNEgxMi44NDAzQzE0LjY1MDggMjQgMTYuMzMzMSAyMy4wNjc3IDE3LjI5MjMgMjEuNTMyNUwyMy4yNTAzIDEySDEzLjExMzNaIiBmaWxsPSIjRkE3NDRFIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTcuMjkyNCAyLjQ2NzVDMTYuMzMzMiAwLjkzMjI1IDE0LjY1MDkgMCAxMi44Mzk3IDBINS45NTg0NEM2LjA5NjQ0IDAuMTY5NSA2LjIyNjk0IDAuMzQ2NSA2LjM0MDk0IDAuNTRMMTIuNDM2OSAxMC44NTU1TDEzLjExMzQgMTJIMjMuMjQ5N0wxNy4yOTI0IDIuNDY3NVoiIGZpbGw9IiMzNDM3NDEiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8zMjY5XzIxMTkzIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=';
|
||||
|
||||
export const mockState: State = {
|
||||
step: 4,
|
||||
connector: {
|
||||
id: 'claudeV3OpusUsWest2',
|
||||
name: 'Claude 3 Opus US-WEST-2',
|
||||
actionTypeId: '.bedrock',
|
||||
config: {
|
||||
apiUrl: 'https://bedrock-runtime.us-west-2.amazonaws.com',
|
||||
defaultModel: 'anthropic.claude-3-opus-20240229-v1:0',
|
||||
},
|
||||
} as AIConnector,
|
||||
integrationSettings: {
|
||||
title: 'Mocked Integration title',
|
||||
name: 'mocked_integration',
|
||||
logo,
|
||||
description: 'Mocked Integration description',
|
||||
dataStreamTitle: 'Mocked Data Stream Title',
|
||||
dataStreamName: 'mocked_datastream_name',
|
||||
dataStreamDescription: 'Mocked Data Stream Description',
|
||||
inputType: 'filestream',
|
||||
logsSampleParsed: rawSamples,
|
||||
},
|
||||
isGenerating: false,
|
||||
result,
|
||||
};
|
||||
|
||||
export const mockActions: Actions = {
|
||||
setStep: jest.fn(),
|
||||
setConnector: jest.fn(),
|
||||
setIntegrationSettings: jest.fn(),
|
||||
setIsGenerating: jest.fn(),
|
||||
setResult: jest.fn(),
|
||||
};
|
|
@ -45,7 +45,12 @@ export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
|
|||
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
|
||||
|
@ -55,6 +60,7 @@ export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
|
|||
hasBorder
|
||||
paddingSize="l"
|
||||
css={rowCss}
|
||||
data-test-subj={`connectorSelector-${connector.id}`}
|
||||
>
|
||||
<EuiFlexGroup direction="row" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
|
@ -63,6 +69,9 @@ export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
|
|||
id={connector.id}
|
||||
checked={selectedConnectorId === connector.id}
|
||||
onChange={noop}
|
||||
data-test-subj={`connectorSelectorRadio-${connector.id}${
|
||||
selectedConnectorId === connector.id ? '-selected' : ''
|
||||
}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -74,7 +83,7 @@ export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
|
|||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -50,7 +50,6 @@ export const ConnectorSetup = React.memo<ConnectorSetupProps>(
|
|||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
|
||||
|
||||
const onModalClose = useCallback(() => {
|
||||
setSelectedActionType(null);
|
||||
onClose?.();
|
||||
|
@ -74,6 +73,7 @@ export const ConnectorSetup = React.memo<ConnectorSetupProps>(
|
|||
{compressed ? (
|
||||
<EuiListGroup
|
||||
flush
|
||||
data-test-subj="connectorSetupCompressed"
|
||||
listItems={actionTypes.map((actionType) => ({
|
||||
id: actionType.id,
|
||||
label: actionType.name,
|
||||
|
@ -90,10 +90,13 @@ export const ConnectorSetup = React.memo<ConnectorSetupProps>(
|
|||
}))}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="s" data-test-subj="connectorSetupPage">
|
||||
{actionTypes?.map((actionType: ActionType) => (
|
||||
<EuiFlexItem data-test-subj="action-option" key={actionType.id}>
|
||||
<EuiLink onClick={() => setSelectedActionType(actionType)}>
|
||||
<EuiFlexItem key={actionType.id}>
|
||||
<EuiLink
|
||||
onClick={() => setSelectedActionType(actionType)}
|
||||
data-test-subj={`actionType-${actionType.id}`}
|
||||
>
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l" css={panelCss}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, type RenderResult } from '@testing-library/react';
|
||||
import type { ActionTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { ConnectorStep } from './connector_step';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions, mockState } from '../../mocks/state';
|
||||
import { useAuthorization } from '../../../../../common/hooks/use_authorization';
|
||||
import { mockServices } from '../../../../../services/mocks/services';
|
||||
import type { AIConnector } from '../../types';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_authorization');
|
||||
const mockUseAuthorization = useAuthorization as jest.Mock;
|
||||
|
||||
const connector = mockState.connector!;
|
||||
const defaultUseMockConnectors: { data: AIConnector[]; isLoading: boolean; refetch: Function } = {
|
||||
data: [],
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
const mockUseLoadConnectors = jest.fn(() => defaultUseMockConnectors);
|
||||
jest.mock('@kbn/elastic-assistant', () => ({
|
||||
useLoadConnectors: () => mockUseLoadConnectors(),
|
||||
}));
|
||||
|
||||
const actionType = { id: '.bedrock', name: 'Bedrock', iconClass: 'logoBedrock' };
|
||||
mockServices.triggersActionsUi.actionTypeRegistry.register(
|
||||
actionType as unknown as ActionTypeModel
|
||||
);
|
||||
jest.mock('@kbn/elastic-assistant/impl/connectorland/use_load_action_types', () => ({
|
||||
useLoadActionTypes: jest.fn(() => ({ data: [actionType] })),
|
||||
}));
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants', () => ({
|
||||
ConnectorAddModal: () => <div data-test-subj="connectorAddModal" />,
|
||||
}));
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('ConnectorStep', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when no connector selected', () => {
|
||||
describe('when no connector exists', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
mockUseLoadConnectors.mockReturnValue({ ...defaultUseMockConnectors, data: [] });
|
||||
result = render(<ConnectorStep connector={undefined} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should render connector setup page', () => {
|
||||
expect(result.queryByTestId('connectorSetupPage')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('connectorSetupCompressed')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render connector selector page', () => {
|
||||
expect(result.queryByTestId('connectorSelector')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render connector setup popover', () => {
|
||||
expect(result.queryByTestId('createConnectorPopover')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when connector type selected clicked', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
result.getByTestId(`actionType-${actionType.id}`).click();
|
||||
});
|
||||
});
|
||||
it('should render connector creation modal', () => {
|
||||
expect(result.queryByTestId('connectorAddModal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when connectors exist', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
mockUseLoadConnectors.mockReturnValue({ ...defaultUseMockConnectors, data: [connector] });
|
||||
result = render(<ConnectorStep connector={undefined} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should render connector selector page', () => {
|
||||
expect(result.queryByTestId('connectorSelector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render connector setup page', () => {
|
||||
expect(result.queryByTestId('connectorSetup')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render connector setup popover', () => {
|
||||
expect(result.queryByTestId('createConnectorPopover')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when connector clicked', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
result.getByTestId(`connectorSelector-${connector.id}`).click();
|
||||
});
|
||||
});
|
||||
it('should dispatch setConnector', () => {
|
||||
expect(mockActions.setConnector).toHaveBeenCalledWith(connector);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when add connector popover is clicked', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
result.getByTestId('createConnectorPopoverButton').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render connector setup compressed', () => {
|
||||
expect(result.queryByTestId('connectorSetupCompressed')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('connectorSetupPage')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when connector selected', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
mockUseLoadConnectors.mockReturnValue({ ...defaultUseMockConnectors, data: [connector] });
|
||||
result = render(<ConnectorStep connector={connector} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should render connector selector page', () => {
|
||||
expect(result.queryByTestId('connectorSelector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render connector selected', () => {
|
||||
expect(
|
||||
result.queryByTestId(`connectorSelectorRadio-${connector.id}-selected`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when create connector privileges missing', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
mockUseAuthorization.mockReturnValue({ canCreateConnectors: false });
|
||||
});
|
||||
|
||||
describe('when no connector exists', () => {
|
||||
beforeEach(() => {
|
||||
mockUseLoadConnectors.mockReturnValue({ ...defaultUseMockConnectors, data: [] });
|
||||
result = render(<ConnectorStep connector={undefined} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should not render connector setup page', () => {
|
||||
expect(result.queryByTestId('connectorSetup')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the missing privilege callout', () => {
|
||||
expect(result.queryByTestId('missingPrivilegesCallOut')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when connectors exist', () => {
|
||||
beforeEach(() => {
|
||||
mockUseLoadConnectors.mockReturnValue({ ...defaultUseMockConnectors, data: [connector] });
|
||||
result = render(<ConnectorStep connector={undefined} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should render connector selector page', () => {
|
||||
expect(result.queryByTestId('connectorSelector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the disabled create connector link', () => {
|
||||
expect(result.queryByTestId('createConnectorPopoverButtonDisabled')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -70,9 +70,7 @@ export const ConnectorStep = React.memo<ConnectorStepProps>(({ connector }) => {
|
|||
) : (
|
||||
<>
|
||||
{hasConnectors ? (
|
||||
<EuiFlexGroup alignItems="stretch" direction="column" gutterSize="s">
|
||||
<ConnectorSelector connectors={connectors} selectedConnectorId={connector?.id} />
|
||||
</EuiFlexGroup>
|
||||
<ConnectorSelector connectors={connectors} selectedConnectorId={connector?.id} />
|
||||
) : (
|
||||
<AuthorizationWrapper canCreateConnectors>
|
||||
<ConnectorSetup
|
||||
|
@ -107,15 +105,22 @@ const CreateConnectorPopover = React.memo<CreateConnectorPopoverProps>(({ onConn
|
|||
if (!canCreateConnectors) {
|
||||
return (
|
||||
<MissingPrivilegesTooltip canCreateConnectors>
|
||||
<EuiLink disabled>{i18n.CREATE_CONNECTOR}</EuiLink>
|
||||
<EuiLink data-test-subj="createConnectorPopoverButtonDisabled" disabled>
|
||||
{i18n.CREATE_CONNECTOR}
|
||||
</EuiLink>
|
||||
</MissingPrivilegesTooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiPopover
|
||||
button={<EuiLink onClick={openPopover}>{i18n.CREATE_CONNECTOR}</EuiLink>}
|
||||
button={
|
||||
<EuiLink data-test-subj="createConnectorPopoverButton" onClick={openPopover}>
|
||||
{i18n.CREATE_CONNECTOR}
|
||||
</EuiLink>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
data-test-subj="createConnectorPopover"
|
||||
>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, type RenderResult, fireEvent } from '@testing-library/react';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { DataStreamStep } from './data_stream_step';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions, mockState } from '../../mocks/state';
|
||||
|
||||
jest.mock('./generation_modal', () => ({
|
||||
GenerationModal: jest.fn(() => <div data-test-subj="generationModal" />),
|
||||
}));
|
||||
|
||||
const duplicatePackageName = 'valid_but_duplicate_package_name';
|
||||
jest.mock('./use_load_package_names', () => {
|
||||
return {
|
||||
useLoadPackageNames: jest.fn(() => ({
|
||||
isLoading: false,
|
||||
packageNames: new Set([duplicatePackageName]),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('DataStreamStep', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when open with initial state', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
result = render(
|
||||
<DataStreamStep
|
||||
integrationSettings={undefined}
|
||||
connector={mockState.connector}
|
||||
isGenerating={false}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render data stream step page', () => {
|
||||
expect(result.queryByTestId('dataStreamStep')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when package name changes', () => {
|
||||
let input: HTMLElement;
|
||||
beforeEach(() => {
|
||||
input = result.getByTestId('nameInput');
|
||||
});
|
||||
|
||||
describe('with a valid name', () => {
|
||||
const name = 'valid_package_name_1';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: name } });
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render invalid input', () => {
|
||||
expect(input).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ name });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an invalid name', () => {
|
||||
describe.each(['package name', 'package-name', 'packageName', 'package.name'])(
|
||||
'should render error for %s',
|
||||
(name) => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: name } });
|
||||
});
|
||||
});
|
||||
it('should render invalid input', () => {
|
||||
expect(input).toHaveAttribute('aria-invalid');
|
||||
});
|
||||
it('should call setIntegrationSettings with undefined', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({
|
||||
name: undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('with a duplicate name', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: duplicatePackageName } });
|
||||
});
|
||||
});
|
||||
|
||||
it('should render invalid input', () => {
|
||||
expect(input).toHaveAttribute('aria-invalid');
|
||||
});
|
||||
it('should call setIntegrationSettings with undefined', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ name: undefined });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataStreamTitle changes', () => {
|
||||
const dataStreamTitle = 'Data stream title';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('dataStreamTitleInput'), {
|
||||
target: { value: dataStreamTitle },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ dataStreamTitle });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataStreamDescription changes', () => {
|
||||
const dataStreamDescription = 'Data stream description';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('dataStreamDescriptionInput'), {
|
||||
target: { value: dataStreamDescription },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({
|
||||
dataStreamDescription,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataStreamName changes', () => {
|
||||
let input: HTMLElement;
|
||||
beforeEach(() => {
|
||||
input = result.getByTestId('dataStreamNameInput');
|
||||
});
|
||||
|
||||
describe('with a valid name', () => {
|
||||
const dataStreamName = 'valid_data_stream_name_1';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: dataStreamName } });
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render invalid input', () => {
|
||||
expect(input).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ dataStreamName });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an invalid name', () => {
|
||||
describe.each([
|
||||
'data stream name',
|
||||
'data-stream-name',
|
||||
'dataStreamName',
|
||||
'data.stream.name',
|
||||
])('should render error for %s', (dataStreamName) => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: dataStreamName } });
|
||||
});
|
||||
});
|
||||
it('should render invalid input', () => {
|
||||
expect(input).toHaveAttribute('aria-invalid');
|
||||
});
|
||||
it('should call setIntegrationSettings with undefined', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ name: undefined });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataCollectionMethod changes', () => {
|
||||
const dataCollectionMethod = 'kafka';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('dataCollectionMethodInput'), {
|
||||
target: { value: dataCollectionMethod },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({
|
||||
inputType: dataCollectionMethod,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when is generating', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
result = render(
|
||||
<DataStreamStep
|
||||
integrationSettings={mockState.integrationSettings}
|
||||
connector={mockState.connector}
|
||||
isGenerating={true}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render generation modal', () => {
|
||||
expect(result.queryByTestId('generationModal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -136,7 +136,7 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
|
|||
}, [packageNames, name]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexGroup direction="column" gutterSize="l" data-test-subj="dataStreamStep">
|
||||
<EuiFlexItem>
|
||||
<StepContentWrapper
|
||||
title={i18n.INTEGRATION_NAME_TITLE}
|
||||
|
@ -154,6 +154,7 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
|
|||
>
|
||||
<EuiFieldText
|
||||
name="name"
|
||||
data-test-subj="nameInput"
|
||||
value={name}
|
||||
onChange={onChange.name}
|
||||
isInvalid={invalidFields.name}
|
||||
|
@ -176,6 +177,7 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
|
|||
<EuiFormRow label={i18n.DATA_STREAM_TITLE_LABEL}>
|
||||
<EuiFieldText
|
||||
name="dataStreamTitle"
|
||||
data-test-subj="dataStreamTitleInput"
|
||||
value={integrationSettings?.dataStreamTitle ?? ''}
|
||||
onChange={onChange.dataStreamTitle}
|
||||
/>
|
||||
|
@ -183,6 +185,7 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
|
|||
<EuiFormRow label={i18n.DATA_STREAM_DESCRIPTION_LABEL}>
|
||||
<EuiFieldText
|
||||
name="dataStreamDescription"
|
||||
data-test-subj="dataStreamDescriptionInput"
|
||||
value={integrationSettings?.dataStreamDescription ?? ''}
|
||||
onChange={onChange.dataStreamDescription}
|
||||
/>
|
||||
|
@ -195,6 +198,7 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
|
|||
>
|
||||
<EuiFieldText
|
||||
name="dataStreamName"
|
||||
data-test-subj="dataStreamNameInput"
|
||||
value={dataStreamName}
|
||||
onChange={onChange.dataStreamName}
|
||||
isInvalid={invalidFields.dataStreamName}
|
||||
|
@ -203,15 +207,13 @@ export const DataStreamStep = React.memo<DataStreamStepProps>(
|
|||
<EuiFormRow label={i18n.DATA_COLLECTION_METHOD_LABEL}>
|
||||
<EuiSelect
|
||||
name="dataCollectionMethod"
|
||||
data-test-subj="dataCollectionMethodInput"
|
||||
options={InputTypeOptions}
|
||||
value={integrationSettings?.inputType ?? ''}
|
||||
onChange={onChange.inputType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<SampleLogsInput
|
||||
integrationSettings={integrationSettings}
|
||||
setIntegrationSettings={setIntegrationSettings}
|
||||
/>
|
||||
<SampleLogsInput integrationSettings={integrationSettings} />
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
</StepContentWrapper>
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, type RenderResult, waitFor } from '@testing-library/react';
|
||||
import { mockReportEvent } from '../../../../../services/telemetry/mocks/service';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { GenerationModal } from './generation_modal';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions, mockState } from '../../mocks/state';
|
||||
import { TelemetryEventType } from '../../../../../services/telemetry/types';
|
||||
|
||||
const integrationSettings = mockState.integrationSettings!;
|
||||
const connector = mockState.connector!;
|
||||
|
||||
const mockEcsMappingResults = { pipeline: { test: 'ecsMappingResponse' }, docs: [] };
|
||||
const mockCategorizationResults = { pipeline: { test: 'categorizationResponse' }, docs: [] };
|
||||
const mockRelatedResults = { pipeline: { test: 'relatedResponse' }, docs: [] };
|
||||
const mockRunEcsGraph = jest.fn((_: unknown) => ({ results: mockEcsMappingResults }));
|
||||
const mockRunCategorizationGraph = jest.fn((_: unknown) => ({
|
||||
results: mockCategorizationResults,
|
||||
}));
|
||||
const mockRunRelatedGraph = jest.fn((_: unknown) => ({ results: mockRelatedResults }));
|
||||
|
||||
const defaultRequest = {
|
||||
packageName: integrationSettings.name ?? '',
|
||||
dataStreamName: integrationSettings.dataStreamName ?? '',
|
||||
rawSamples: integrationSettings.logsSampleParsed ?? [],
|
||||
connectorId: connector.id,
|
||||
};
|
||||
|
||||
jest.mock('../../../../../common/lib/api', () => ({
|
||||
runEcsGraph: (params: unknown) => mockRunEcsGraph(params),
|
||||
runCategorizationGraph: (params: unknown) => mockRunCategorizationGraph(params),
|
||||
runRelatedGraph: (params: unknown) => mockRunRelatedGraph(params),
|
||||
}));
|
||||
|
||||
const mockOnComplete = jest.fn();
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('GenerationModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when there are no errors', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<GenerationModal
|
||||
integrationSettings={integrationSettings}
|
||||
connector={connector}
|
||||
onComplete={mockOnComplete}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
await waitFor(() => expect(mockOnComplete).toBeCalled());
|
||||
});
|
||||
});
|
||||
|
||||
it('should render generation modal', () => {
|
||||
expect(result.queryByTestId('generationModal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call runEcsGraph with correct parameters', () => {
|
||||
expect(mockRunEcsGraph).toHaveBeenCalledWith(defaultRequest);
|
||||
});
|
||||
|
||||
it('should call runCategorizationGraph with correct parameters', () => {
|
||||
expect(mockRunCategorizationGraph).toHaveBeenCalledWith({
|
||||
...defaultRequest,
|
||||
currentPipeline: mockEcsMappingResults.pipeline,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call runRelatedGraph with correct parameters', () => {
|
||||
expect(mockRunRelatedGraph).toHaveBeenCalledWith({
|
||||
...defaultRequest,
|
||||
currentPipeline: mockCategorizationResults.pipeline,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onComplete with the results', () => {
|
||||
expect(mockOnComplete).toHaveBeenCalledWith(mockRelatedResults);
|
||||
});
|
||||
|
||||
it('should report telemetry for generation complete', () => {
|
||||
expect(mockReportEvent).toHaveBeenCalledWith(
|
||||
TelemetryEventType.IntegrationAssistantGenerationComplete,
|
||||
{
|
||||
sessionId: expect.any(String),
|
||||
sampleRows: integrationSettings.logsSampleParsed?.length ?? 0,
|
||||
actionTypeId: connector.actionTypeId,
|
||||
model: expect.anything(),
|
||||
provider: connector.apiProvider ?? 'unknown',
|
||||
durationMs: expect.any(Number),
|
||||
errorMessage: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are errors', () => {
|
||||
const errorMessage = 'error message';
|
||||
let result: RenderResult;
|
||||
beforeEach(async () => {
|
||||
mockRunEcsGraph.mockImplementationOnce(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<GenerationModal
|
||||
integrationSettings={integrationSettings}
|
||||
connector={connector}
|
||||
onComplete={mockOnComplete}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(result.queryByTestId('generationErrorCallout')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the error text', () => {
|
||||
expect(result.queryByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('should render the retry button', () => {
|
||||
expect(result.queryByTestId('retryButton')).toBeInTheDocument();
|
||||
});
|
||||
it('should report telemetry for generation error', () => {
|
||||
expect(mockReportEvent).toHaveBeenCalledWith(
|
||||
TelemetryEventType.IntegrationAssistantGenerationComplete,
|
||||
{
|
||||
sessionId: expect.any(String),
|
||||
sampleRows: integrationSettings.logsSampleParsed?.length ?? 0,
|
||||
actionTypeId: connector.actionTypeId,
|
||||
model: expect.anything(),
|
||||
provider: connector.apiProvider ?? 'unknown',
|
||||
durationMs: expect.any(Number),
|
||||
errorMessage,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('when the retrying successfully', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
result.getByTestId('retryButton').click();
|
||||
await waitFor(() => expect(mockOnComplete).toBeCalled());
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the error callout', () => {
|
||||
expect(result.queryByTestId('generationErrorCallout')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should not render the retry button', () => {
|
||||
expect(result.queryByTestId('retryButton')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -163,11 +163,7 @@ export const useGeneration = ({
|
|||
setIsRequesting(true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
progress,
|
||||
error,
|
||||
retry,
|
||||
};
|
||||
return { progress, error, retry };
|
||||
};
|
||||
|
||||
const useModalCss = () => {
|
||||
|
@ -205,7 +201,7 @@ export const GenerationModal = React.memo<GenerationModalProps>(
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onClose}>
|
||||
<EuiModal onClose={onClose} data-test-subj="generationModal">
|
||||
<EuiModalHeader css={headerCss}>
|
||||
<EuiModalHeaderTitle>{i18n.ANALYZING}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
@ -219,6 +215,7 @@ export const GenerationModal = React.memo<GenerationModalProps>(
|
|||
title={i18n.GENERATION_ERROR(progressText[progress])}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
data-test-subj="generationErrorCallout"
|
||||
>
|
||||
{error}
|
||||
</EuiCallOut>
|
||||
|
@ -256,7 +253,7 @@ export const GenerationModal = React.memo<GenerationModalProps>(
|
|||
{error ? (
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="refresh" onClick={retry}>
|
||||
<EuiButtonEmpty iconType="refresh" onClick={retry} data-test-subj="retryButton">
|
||||
{i18n.RETRY}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, fireEvent, render, waitFor, type RenderResult } from '@testing-library/react';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { SampleLogsInput } from './sample_logs_input';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions } from '../../mocks/state';
|
||||
import { mockServices } from '../../../../../services/mocks/services';
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
const changeFile = async (input: HTMLElement, file: File) => {
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { files: [file] } });
|
||||
await waitFor(() => expect(input).toHaveAttribute('data-loading', 'true'));
|
||||
await waitFor(() => expect(input).toHaveAttribute('data-loading', 'false'));
|
||||
});
|
||||
};
|
||||
|
||||
describe('SampleLogsInput', () => {
|
||||
let result: RenderResult;
|
||||
let input: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
result = render(<SampleLogsInput integrationSettings={undefined} />, { wrapper });
|
||||
input = result.getByTestId('logsSampleFilePicker');
|
||||
});
|
||||
|
||||
describe('when uploading a json logs sample', () => {
|
||||
const type = 'application/json';
|
||||
|
||||
describe('when the file is valid', () => {
|
||||
const logsSampleRaw = `{"message":"test message 1"},{"message":"test message 2"}`;
|
||||
beforeEach(async () => {
|
||||
await changeFile(input, new File([`[${logsSampleRaw}]`], 'test.json', { type }));
|
||||
});
|
||||
|
||||
it('should set the integrationSetting correctly', () => {
|
||||
expect(mockActions.setIntegrationSettings).toBeCalledWith({
|
||||
logsSampleParsed: logsSampleRaw.split(','),
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the file has too many rows', () => {
|
||||
const tooLargeLogsSample = Array(6).fill(logsSampleRaw).join(','); // 12 entries
|
||||
beforeEach(async () => {
|
||||
await changeFile(input, new File([`[${tooLargeLogsSample}]`], 'test.json', { type }));
|
||||
});
|
||||
|
||||
it('should truncate the logs sample', () => {
|
||||
expect(mockActions.setIntegrationSettings).toBeCalledWith({
|
||||
logsSampleParsed: tooLargeLogsSample.split(',').slice(0, 10),
|
||||
});
|
||||
});
|
||||
it('should add a notification toast', () => {
|
||||
expect(mockServices.notifications.toasts.addInfo).toBeCalledWith(
|
||||
`The logs sample has been truncated to 10 rows.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the file is invalid', () => {
|
||||
describe.each([
|
||||
['[{"message":"test message 1"}', `The logs sample file has not a valid ${type} format`],
|
||||
['["test message 1"]', 'The logs sample file contains non-object entries'],
|
||||
['{"message":"test message 1"}', 'The logs sample file is not an array'],
|
||||
['[]', 'The logs sample file is empty'],
|
||||
])('with logs content %s', (logsSample, errorMessage) => {
|
||||
beforeEach(async () => {
|
||||
await changeFile(input, new File([logsSample], 'test.json', { type }));
|
||||
});
|
||||
|
||||
it('should render error message', () => {
|
||||
expect(result.queryByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set the integrationSetting correctly', () => {
|
||||
expect(mockActions.setIntegrationSettings).toBeCalledWith({
|
||||
logsSampleParsed: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setting a ndjson logs sample', () => {
|
||||
const type = 'application/x-ndjson';
|
||||
|
||||
describe('when the file is valid', () => {
|
||||
const logsSampleRaw = `{"message":"test message 1"}\n{"message":"test message 2"}`;
|
||||
beforeEach(async () => {
|
||||
await changeFile(input, new File([logsSampleRaw], 'test.json', { type }));
|
||||
});
|
||||
|
||||
it('should set the integrationSetting correctly', () => {
|
||||
expect(mockActions.setIntegrationSettings).toBeCalledWith({
|
||||
logsSampleParsed: logsSampleRaw.split('\n'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the file has too many rows', () => {
|
||||
const tooLargeLogsSample = Array(6).fill(logsSampleRaw).join('\n'); // 12 entries
|
||||
beforeEach(async () => {
|
||||
await changeFile(input, new File([tooLargeLogsSample], 'test.json', { type }));
|
||||
});
|
||||
|
||||
it('should truncate the logs sample', () => {
|
||||
expect(mockActions.setIntegrationSettings).toBeCalledWith({
|
||||
logsSampleParsed: tooLargeLogsSample.split('\n').slice(0, 10),
|
||||
});
|
||||
});
|
||||
it('should add a notification toast', () => {
|
||||
expect(mockServices.notifications.toasts.addInfo).toBeCalledWith(
|
||||
`The logs sample has been truncated to 10 rows.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the file is invalid', () => {
|
||||
describe.each([
|
||||
['{"message":"test message 1"]', `The logs sample file has not a valid ${type} format`],
|
||||
['"test message 1"', 'The logs sample file contains non-object entries'],
|
||||
['', 'The logs sample file is empty'],
|
||||
])('with logs content %s', (logsSample, errorMessage) => {
|
||||
beforeEach(async () => {
|
||||
await changeFile(input, new File([logsSample], 'test.json', { type }));
|
||||
});
|
||||
|
||||
it('should render error message', () => {
|
||||
expect(result.queryByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set the integrationSetting correctly', () => {
|
||||
expect(mockActions.setIntegrationSettings).toBeCalledWith({
|
||||
logsSampleParsed: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,6 +11,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import { isPlainObject } from 'lodash/fp';
|
||||
import type { IntegrationSettings } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
import { useActions } from '../../state';
|
||||
|
||||
const MaxLogsSampleRows = 10;
|
||||
|
||||
|
@ -61,88 +62,88 @@ const parseLogsContent = (
|
|||
|
||||
interface SampleLogsInputProps {
|
||||
integrationSettings: IntegrationSettings | undefined;
|
||||
setIntegrationSettings: (param: IntegrationSettings) => void;
|
||||
}
|
||||
export const SampleLogsInput = React.memo<SampleLogsInputProps>(
|
||||
({ integrationSettings, setIntegrationSettings }) => {
|
||||
const { notifications } = useKibana().services;
|
||||
const [isParsing, setIsParsing] = useState(false);
|
||||
const [sampleFileError, setSampleFileError] = useState<string>();
|
||||
export const SampleLogsInput = React.memo<SampleLogsInputProps>(({ integrationSettings }) => {
|
||||
const { notifications } = useKibana().services;
|
||||
const { setIntegrationSettings } = useActions();
|
||||
const [isParsing, setIsParsing] = useState(false);
|
||||
const [sampleFileError, setSampleFileError] = useState<string>();
|
||||
|
||||
const onChangeLogsSample = useCallback(
|
||||
(files: FileList | null) => {
|
||||
const logsSampleFile = files?.[0];
|
||||
if (logsSampleFile == null) {
|
||||
setSampleFileError(undefined);
|
||||
const onChangeLogsSample = useCallback(
|
||||
(files: FileList | null) => {
|
||||
const logsSampleFile = files?.[0];
|
||||
if (logsSampleFile == null) {
|
||||
setSampleFileError(undefined);
|
||||
setIntegrationSettings({ ...integrationSettings, logsSampleParsed: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file.
|
||||
const { error, isTruncated, logsSampleParsed } = parseLogsContent(
|
||||
fileContent,
|
||||
logsSampleFile.type
|
||||
);
|
||||
setIsParsing(false);
|
||||
setSampleFileError(error);
|
||||
if (error) {
|
||||
setIntegrationSettings({ ...integrationSettings, logsSampleParsed: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file.
|
||||
const { error, isTruncated, logsSampleParsed } = parseLogsContent(
|
||||
fileContent,
|
||||
logsSampleFile.type
|
||||
);
|
||||
setIsParsing(false);
|
||||
setSampleFileError(error);
|
||||
if (error) {
|
||||
setIntegrationSettings({ ...integrationSettings, logsSampleParsed: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTruncated) {
|
||||
notifications?.toasts.addInfo(i18n.LOGS_SAMPLE_TRUNCATED(MaxLogsSampleRows));
|
||||
}
|
||||
|
||||
setIntegrationSettings({
|
||||
...integrationSettings,
|
||||
logsSampleParsed,
|
||||
});
|
||||
};
|
||||
setIsParsing(true);
|
||||
reader.readAsText(logsSampleFile);
|
||||
},
|
||||
[integrationSettings, setIntegrationSettings, notifications?.toasts, setIsParsing]
|
||||
);
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.LOGS_SAMPLE_LABEL}
|
||||
helpText={
|
||||
<EuiText color="danger" size="xs">
|
||||
{sampleFileError}
|
||||
</EuiText>
|
||||
if (isTruncated) {
|
||||
notifications?.toasts.addInfo(i18n.LOGS_SAMPLE_TRUNCATED(MaxLogsSampleRows));
|
||||
}
|
||||
isInvalid={sampleFileError != null}
|
||||
>
|
||||
<>
|
||||
<EuiCallOut iconType="iInCircle" color="warning">
|
||||
{i18n.LOGS_SAMPLE_WARNING}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFilePicker
|
||||
id="logsSampleFilePicker"
|
||||
initialPromptText={
|
||||
<>
|
||||
<EuiText size="s" textAlign="center">
|
||||
{i18n.LOGS_SAMPLE_DESCRIPTION}
|
||||
</EuiText>
|
||||
<EuiText size="xs" color="subdued" textAlign="center">
|
||||
{i18n.LOGS_SAMPLE_DESCRIPTION_2}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
onChange={onChangeLogsSample}
|
||||
display="large"
|
||||
aria-label="Upload logs sample file"
|
||||
accept="application/json,application/x-ndjson"
|
||||
isLoading={isParsing}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
);
|
||||
setIntegrationSettings({
|
||||
...integrationSettings,
|
||||
logsSampleParsed,
|
||||
});
|
||||
};
|
||||
setIsParsing(true);
|
||||
reader.readAsText(logsSampleFile);
|
||||
},
|
||||
[integrationSettings, setIntegrationSettings, notifications?.toasts, setIsParsing]
|
||||
);
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.LOGS_SAMPLE_LABEL}
|
||||
helpText={
|
||||
<EuiText color="danger" size="xs">
|
||||
{sampleFileError}
|
||||
</EuiText>
|
||||
}
|
||||
isInvalid={sampleFileError != null}
|
||||
>
|
||||
<>
|
||||
<EuiCallOut iconType="iInCircle" color="warning">
|
||||
{i18n.LOGS_SAMPLE_WARNING}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFilePicker
|
||||
id="logsSampleFilePicker"
|
||||
initialPromptText={
|
||||
<>
|
||||
<EuiText size="s" textAlign="center">
|
||||
{i18n.LOGS_SAMPLE_DESCRIPTION}
|
||||
</EuiText>
|
||||
<EuiText size="xs" color="subdued" textAlign="center">
|
||||
{i18n.LOGS_SAMPLE_DESCRIPTION_2}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
onChange={onChangeLogsSample}
|
||||
display="large"
|
||||
aria-label="Upload logs sample file"
|
||||
accept="application/json,application/x-ndjson"
|
||||
isLoading={isParsing}
|
||||
data-test-subj="logsSampleFilePicker"
|
||||
data-loading={isParsing}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
||||
SampleLogsInput.displayName = 'SampleLogsInput';
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, type RenderResult, waitFor } from '@testing-library/react';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { DeployStep } from './deploy_step';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions, mockState } from '../../mocks/state';
|
||||
import type { BuildIntegrationRequestBody } from '../../../../../../common';
|
||||
import { mockReportEvent } from '../../../../../services/telemetry/mocks/service';
|
||||
import { TelemetryEventType } from '../../../../../services/telemetry/types';
|
||||
|
||||
const integrationSettings = mockState.integrationSettings!;
|
||||
const connector = mockState.connector!;
|
||||
const results = mockState.result!;
|
||||
|
||||
const parameters: BuildIntegrationRequestBody = {
|
||||
integration: {
|
||||
title: integrationSettings.title!,
|
||||
description: integrationSettings.description!,
|
||||
name: integrationSettings.name!,
|
||||
logo: integrationSettings.logo,
|
||||
dataStreams: [
|
||||
{
|
||||
title: integrationSettings.dataStreamTitle!,
|
||||
description: integrationSettings.dataStreamDescription!,
|
||||
name: integrationSettings.dataStreamName!,
|
||||
inputTypes: [integrationSettings.inputType!],
|
||||
rawSamples: integrationSettings.logsSampleParsed!,
|
||||
docs: results.docs!,
|
||||
pipeline: results.pipeline,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const builtIntegration = new Blob();
|
||||
const mockRunBuildIntegration = jest.fn((_: unknown) => builtIntegration);
|
||||
const integrationName = 'my_integration_33-1.0.0';
|
||||
const mockRunInstallPackage = jest.fn((_: unknown) => ({
|
||||
response: [{ id: 'audit-my_integration_33.data-stream-1.0.0' }],
|
||||
}));
|
||||
jest.mock('../../../../../common/lib/api', () => ({
|
||||
runBuildIntegration: (params: unknown) => mockRunBuildIntegration(params),
|
||||
runInstallPackage: (params: unknown) => mockRunInstallPackage(params),
|
||||
}));
|
||||
|
||||
const mockSaveAs = jest.fn();
|
||||
jest.mock('@elastic/filesaver', () => ({
|
||||
saveAs: (...params: unknown[]) => mockSaveAs(...params),
|
||||
}));
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('DeployStep', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when deploy is successful', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<DeployStep
|
||||
integrationSettings={integrationSettings}
|
||||
connector={connector}
|
||||
result={results}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
await waitFor(() => expect(result.queryByTestId('deployStep-loading')).toBeInTheDocument());
|
||||
await waitFor(() =>
|
||||
expect(result.queryByTestId('deployStep-loading')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call build integration api', () => {
|
||||
expect(mockRunBuildIntegration).toHaveBeenCalledWith(parameters);
|
||||
});
|
||||
|
||||
it('should call install package api', () => {
|
||||
expect(mockRunInstallPackage).toHaveBeenCalledWith(builtIntegration);
|
||||
});
|
||||
|
||||
it('should render success deploy message', () => {
|
||||
expect(result.queryByTestId('deployStep-success')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should report telemetry for integration complete', () => {
|
||||
expect(mockReportEvent).toHaveBeenCalledWith(
|
||||
TelemetryEventType.IntegrationAssistantComplete,
|
||||
{
|
||||
sessionId: expect.any(String),
|
||||
integrationName,
|
||||
integrationDescription: integrationSettings.description,
|
||||
dataStreamName: integrationSettings.dataStreamName,
|
||||
inputType: integrationSettings.inputType,
|
||||
model: expect.any(String),
|
||||
actionTypeId: connector.actionTypeId,
|
||||
provider: connector.apiProvider ?? 'unknown',
|
||||
durationMs: expect.any(Number),
|
||||
errorMessage: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the save button', () => {
|
||||
expect(result.queryByTestId('saveZipButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when save button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
result.getByTestId('saveZipButton').click();
|
||||
});
|
||||
|
||||
it('should save file', () => {
|
||||
expect(mockSaveAs).toHaveBeenCalledWith(builtIntegration, `${integrationName}.zip`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when deploy fails', () => {
|
||||
describe('when build integration throws errors', () => {
|
||||
const errorMessage = 'build integration failed';
|
||||
let result: RenderResult;
|
||||
beforeEach(async () => {
|
||||
mockRunBuildIntegration.mockImplementationOnce(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<DeployStep
|
||||
integrationSettings={integrationSettings}
|
||||
connector={connector}
|
||||
result={results}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
await waitFor(() => expect(result.queryByTestId('deployStep-error')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render success deploy message', () => {
|
||||
expect(result.queryByTestId('deployStep-success')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the error message', () => {
|
||||
expect(result.queryByTestId('deployStep-error')).toBeInTheDocument();
|
||||
expect(result.queryByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render the save button', () => {
|
||||
expect(result.queryByTestId('saveZipButton')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should report telemetry for integration complete with error', () => {
|
||||
expect(mockReportEvent).toHaveBeenCalledWith(
|
||||
TelemetryEventType.IntegrationAssistantComplete,
|
||||
{
|
||||
sessionId: expect.any(String),
|
||||
integrationName: integrationSettings.name,
|
||||
integrationDescription: integrationSettings.description,
|
||||
dataStreamName: integrationSettings.dataStreamName,
|
||||
inputType: integrationSettings.inputType,
|
||||
model: expect.any(String),
|
||||
actionTypeId: connector.actionTypeId,
|
||||
provider: connector.apiProvider ?? 'unknown',
|
||||
durationMs: expect.any(Number),
|
||||
errorMessage,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when install integration throws errors', () => {
|
||||
const errorMessage = 'install integration failed';
|
||||
let result: RenderResult;
|
||||
beforeEach(async () => {
|
||||
mockRunInstallPackage.mockImplementationOnce(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<DeployStep
|
||||
integrationSettings={integrationSettings}
|
||||
connector={connector}
|
||||
result={results}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
await waitFor(() => expect(result.queryByTestId('deployStep-error')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render success deploy message', () => {
|
||||
expect(result.queryByTestId('deployStep-success')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the error message', () => {
|
||||
expect(result.queryByTestId('deployStep-error')).toBeInTheDocument();
|
||||
expect(result.queryByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render the save button', () => {
|
||||
expect(result.queryByTestId('saveZipButton')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should report telemetry for integration complete with error', () => {
|
||||
expect(mockReportEvent).toHaveBeenCalledWith(
|
||||
TelemetryEventType.IntegrationAssistantComplete,
|
||||
{
|
||||
sessionId: expect.any(String),
|
||||
integrationName: integrationSettings.name,
|
||||
integrationDescription: integrationSettings.description,
|
||||
dataStreamName: integrationSettings.dataStreamName,
|
||||
inputType: integrationSettings.inputType,
|
||||
model: expect.any(String),
|
||||
actionTypeId: connector.actionTypeId,
|
||||
provider: connector.apiProvider ?? 'unknown',
|
||||
durationMs: expect.any(Number),
|
||||
errorMessage,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -47,9 +47,9 @@ export const DeployStep = React.memo<DeployStepProps>(
|
|||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isLoading && <EuiLoadingSpinner size="xl" />}
|
||||
{isLoading && <EuiLoadingSpinner size="xl" data-test-subj="deployStep-loading" />}
|
||||
{error && (
|
||||
<EuiText color="danger" size="s">
|
||||
<EuiText color="danger" size="s" data-test-subj="deployStep-error">
|
||||
{error}
|
||||
</EuiText>
|
||||
)}
|
||||
|
@ -62,7 +62,7 @@ export const DeployStep = React.memo<DeployStepProps>(
|
|||
return (
|
||||
<SuccessSection integrationName={integrationName}>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l" data-test-subj="deployStep-success">
|
||||
<EuiFlexGroup direction="row" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" alignItems="flexStart" justifyContent="flexStart">
|
||||
|
@ -86,7 +86,9 @@ export const DeployStep = React.memo<DeployStepProps>(
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink onClick={onSaveZip}>{i18n.DOWNLOAD_ZIP_LINK}</EuiLink>
|
||||
<EuiLink onClick={onSaveZip} data-test-subj="saveZipButton">
|
||||
{i18n.DOWNLOAD_ZIP_LINK}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -88,7 +88,18 @@ export const useDeployIntegration = ({
|
|||
}
|
||||
} catch (e) {
|
||||
if (abortController.signal.aborted) return;
|
||||
setError(`Error: ${e.body?.message ?? e.message}`);
|
||||
const errorMessage = `${e.message}${
|
||||
e.body ? ` (${e.body.statusCode}): ${e.body.message}` : ''
|
||||
}`;
|
||||
|
||||
telemetry.reportAssistantComplete({
|
||||
integrationName: integrationSettings.name ?? '',
|
||||
integrationSettings,
|
||||
connector,
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, type RenderResult, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { IntegrationStep } from './integration_step';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions, mockState } from '../../mocks/state';
|
||||
|
||||
const integrationSettings = mockState.integrationSettings!;
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('IntegrationStep', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when settings are undefined', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
result = render(<IntegrationStep integrationSettings={undefined} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should render integration step page', () => {
|
||||
expect(result.queryByTestId('integrationStep')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when title changes', () => {
|
||||
const title = 'My Integration';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('integrationTitleInput'), {
|
||||
target: { value: title },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ title });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when description changes', () => {
|
||||
const description = 'My Integration description';
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('integrationDescriptionInput'), {
|
||||
target: { value: description },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', () => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({ description });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logo is set', () => {
|
||||
describe('when logo is valid', () => {
|
||||
const file = new File(['(⌐□_□)'], 'test.svg', { type: 'image/svg+xml' });
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('integrationLogoFilePicker'), {
|
||||
target: { files: [file] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', async () => {
|
||||
await waitFor(() => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({
|
||||
logo: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logo is too large', () => {
|
||||
const file = new File(['(⌐□_□)'], 'test.svg', { type: 'image/svg+xml' });
|
||||
Object.defineProperty(file, 'size', { value: 1024 * 1024 * 2 }); // override Blob size getter value by 2Mb
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('integrationLogoFilePicker'), {
|
||||
target: { files: [file] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render logo error', async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByText('test.svg is too large, maximum size is 1Mb.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(mockActions.setIntegrationSettings).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logo is removed', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('integrationLogoFilePicker'), {
|
||||
target: { files: [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIntegrationSettings', async () => {
|
||||
await waitFor(() => {
|
||||
expect(mockActions.setIntegrationSettings).toHaveBeenCalledWith({
|
||||
logo: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when settings are defined', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
result = render(<IntegrationStep integrationSettings={integrationSettings} />, { wrapper });
|
||||
});
|
||||
|
||||
it('should render preview integration title', () => {
|
||||
expect(result.queryByTestId('packageCardPreview')).toHaveTextContent(
|
||||
integrationSettings.title!
|
||||
);
|
||||
});
|
||||
|
||||
it('should render preview integration description', () => {
|
||||
expect(result.queryByTestId('packageCardPreview')).toHaveTextContent(
|
||||
integrationSettings.description!
|
||||
);
|
||||
});
|
||||
|
||||
it('should render preview integration logo', () => {
|
||||
expect(result.queryByTestId('packageCardPreviewIcon')).toHaveAttribute(
|
||||
'data-euiicon-type',
|
||||
`data:image/svg+xml;base64,${integrationSettings.logo}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -27,7 +27,7 @@ import { PackageCardPreview } from './package_card_preview';
|
|||
import { useActions } from '../../state';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const MaxLogoSize = 1048576; // One megabyte
|
||||
const MaxLogoSize = 1024 * 1024; // One megabyte
|
||||
|
||||
const useLayoutStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
@ -92,7 +92,7 @@ export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSe
|
|||
|
||||
return (
|
||||
<StepContentWrapper title={i18n.TITLE} subtitle={i18n.DESCRIPTION}>
|
||||
<EuiPanel paddingSize="none" hasShadow={false} hasBorder>
|
||||
<EuiPanel paddingSize="none" hasShadow={false} hasBorder data-test-subj="integrationStep">
|
||||
<EuiFlexGroup direction="row" gutterSize="none">
|
||||
<EuiFlexItem css={styles.left}>
|
||||
<EuiForm component="form" fullWidth>
|
||||
|
@ -101,6 +101,7 @@ export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSe
|
|||
name="title"
|
||||
value={integrationSettings?.title ?? ''}
|
||||
onChange={onChange.title}
|
||||
data-test-subj="integrationTitleInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={i18n.DESCRIPTION_LABEL}>
|
||||
|
@ -108,6 +109,7 @@ export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSe
|
|||
name="description"
|
||||
value={integrationSettings?.description ?? ''}
|
||||
onChange={onChange.description}
|
||||
data-test-subj="integrationDescriptionInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={i18n.LOGO_LABEL}>
|
||||
|
@ -120,6 +122,7 @@ export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSe
|
|||
aria-label="Upload an svg logo image"
|
||||
accept="image/svg+xml"
|
||||
isInvalid={logoError != null}
|
||||
data-test-subj="integrationLogoFilePicker"
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
{logoError && (
|
||||
|
|
|
@ -40,7 +40,7 @@ export const PackageCardPreview = React.memo<PackageCardPreviewProps>(({ integra
|
|||
return (
|
||||
<EuiCard
|
||||
css={cardCss}
|
||||
data-test-subj="package-card-preview"
|
||||
data-test-subj="packageCardPreview"
|
||||
layout="horizontal"
|
||||
title={integrationSettings?.title ?? ''}
|
||||
description={integrationSettings?.description ?? ''}
|
||||
|
@ -49,6 +49,7 @@ export const PackageCardPreview = React.memo<PackageCardPreviewProps>(({ integra
|
|||
icon={
|
||||
<EuiIcon
|
||||
size={'xl'}
|
||||
data-test-subj="packageCardPreviewIcon"
|
||||
type={
|
||||
integrationSettings?.logo
|
||||
? `data:image/svg+xml;base64,${integrationSettings.logo}`
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, type RenderResult, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { TestProvider } from '../../../../../mocks/test_provider';
|
||||
import { ReviewStep } from './review_step';
|
||||
import { ActionsProvider } from '../../state';
|
||||
import { mockActions, mockState } from '../../mocks/state';
|
||||
|
||||
const integrationSettings = mockState.integrationSettings!;
|
||||
const results = mockState.result!;
|
||||
|
||||
const mockResults = {
|
||||
pipeline: { test: 'checkPipelineResponse' },
|
||||
docs: [{ id: 'testDoc' }],
|
||||
};
|
||||
const customPipeline = {
|
||||
...mockResults.pipeline,
|
||||
description: 'testing',
|
||||
};
|
||||
const defaultRequest = {
|
||||
pipeline: customPipeline,
|
||||
rawSamples: integrationSettings.logsSampleParsed!,
|
||||
};
|
||||
const mockRunCheckPipelineResults = jest.fn((_: unknown) => ({ results: mockResults }));
|
||||
jest.mock('../../../../../common/lib/api', () => ({
|
||||
runCheckPipelineResults: (params: unknown) => mockRunCheckPipelineResults(params),
|
||||
}));
|
||||
jest.mock('@kbn/code-editor', () => ({
|
||||
...jest.requireActual('@kbn/code-editor'),
|
||||
CodeEditor: (props: { value: string; onChange: Function }) => (
|
||||
<input
|
||||
data-test-subj={'mockCodeEditor'}
|
||||
value={props.value}
|
||||
onChange={(e) => {
|
||||
props.onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<TestProvider>
|
||||
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
describe('ReviewStep', () => {
|
||||
let result: RenderResult;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
result = render(
|
||||
<ReviewStep
|
||||
integrationSettings={integrationSettings}
|
||||
isGenerating={false}
|
||||
result={results}
|
||||
/>,
|
||||
{ wrapper }
|
||||
);
|
||||
});
|
||||
|
||||
it('should render integration step page', () => {
|
||||
expect(result.queryByTestId('reviewStep')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when edit pipeline button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
result.getByTestId('editPipelineButton').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should open pipeline editor', () => {
|
||||
expect(result.queryByTestId('mockCodeEditor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when saving pipeline without changes', () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
result.getByTestId('savePipelineButton').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setResults', () => {
|
||||
expect(mockActions.setResult).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when saving pipeline with changes', () => {
|
||||
beforeEach(async () => {
|
||||
act(() => {
|
||||
fireEvent.change(result.getByTestId('mockCodeEditor'), {
|
||||
target: { value: JSON.stringify(customPipeline) },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when check pipeline is successful', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
result.getByTestId('savePipelineButton').click();
|
||||
await waitFor(() => expect(mockActions.setIsGenerating).toHaveBeenCalledWith(false));
|
||||
});
|
||||
});
|
||||
|
||||
it('should call setIsGenerating', () => {
|
||||
expect(mockActions.setIsGenerating).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should check pipeline', () => {
|
||||
expect(mockRunCheckPipelineResults).toHaveBeenCalledWith(defaultRequest);
|
||||
});
|
||||
|
||||
it('should call setResults', () => {
|
||||
expect(mockActions.setResult).toHaveBeenCalledWith({
|
||||
...mockResults,
|
||||
pipeline: customPipeline,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when check pipeline fails', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunCheckPipelineResults.mockImplementationOnce(() => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
await act(async () => {
|
||||
result.getByTestId('savePipelineButton').click();
|
||||
await waitFor(() => expect(mockActions.setIsGenerating).toHaveBeenCalledWith(false));
|
||||
});
|
||||
});
|
||||
|
||||
it('should check pipeline', () => {
|
||||
expect(mockRunCheckPipelineResults).toHaveBeenCalledWith(defaultRequest);
|
||||
});
|
||||
|
||||
it('should not call setResults', () => {
|
||||
expect(mockActions.setResult).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error', () => {
|
||||
expect(result.queryByText('Error: test error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when check pipeline has no docs', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunCheckPipelineResults.mockReturnValueOnce({
|
||||
results: { ...mockResults, docs: [] },
|
||||
});
|
||||
await act(async () => {
|
||||
result.getByTestId('savePipelineButton').click();
|
||||
await waitFor(() => expect(mockActions.setIsGenerating).toHaveBeenCalledWith(false));
|
||||
});
|
||||
});
|
||||
|
||||
it('should check pipeline', () => {
|
||||
expect(mockRunCheckPipelineResults).toHaveBeenCalledWith(defaultRequest);
|
||||
});
|
||||
|
||||
it('should not call setResults', () => {
|
||||
expect(mockActions.setResult).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error', () => {
|
||||
expect(result.queryByText('No results for the pipeline')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -73,12 +73,15 @@ export const ReviewStep = React.memo<ReviewStepProps>(
|
|||
title={i18n.TITLE}
|
||||
subtitle={i18n.DESCRIPTION}
|
||||
right={
|
||||
<EuiButton onClick={() => setIsPipelineEditionVisible(true)}>
|
||||
<EuiButton
|
||||
onClick={() => setIsPipelineEditionVisible(true)}
|
||||
data-test-subj="editPipelineButton"
|
||||
>
|
||||
{i18n.EDIT_PIPELINE_BUTTON}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiPanel hasShadow={false} hasBorder data-test-subj="reviewStep">
|
||||
{isGenerating ? (
|
||||
<EuiLoadingSpinner size="l" />
|
||||
) : (
|
||||
|
@ -106,7 +109,7 @@ export const ReviewStep = React.memo<ReviewStepProps>(
|
|||
responsive={false}
|
||||
css={{ height: '100%' }}
|
||||
>
|
||||
<EuiFlexItem grow={true} data-test-subj="inspectorRequestCodeViewerContainer">
|
||||
<EuiFlexItem grow={true}>
|
||||
<CodeEditor
|
||||
languageId={XJsonLang.ID}
|
||||
value={JSON.stringify(result?.pipeline, null, 2)}
|
||||
|
@ -129,7 +132,11 @@ export const ReviewStep = React.memo<ReviewStepProps>(
|
|||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup direction="row" gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={saveCustomPipeline}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={saveCustomPipeline}
|
||||
data-test-subj="savePipelineButton"
|
||||
>
|
||||
{i18n.SAVE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -42,19 +42,19 @@ export const useCheckPipeline = ({ integrationSettings, customPipeline }: CheckP
|
|||
};
|
||||
setIsGenerating(true);
|
||||
|
||||
const ecsGraphResult = await runCheckPipelineResults(parameters, deps);
|
||||
const checkPipelineResults = await runCheckPipelineResults(parameters, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
if (isEmpty(ecsGraphResult?.results.docs)) {
|
||||
if (isEmpty(checkPipelineResults?.results.docs)) {
|
||||
setError('No results for the pipeline');
|
||||
return;
|
||||
}
|
||||
setResult({
|
||||
pipeline: customPipeline,
|
||||
docs: ecsGraphResult.results.docs,
|
||||
docs: checkPipelineResults.results.docs,
|
||||
});
|
||||
} catch (e) {
|
||||
if (abortController.signal.aborted) return;
|
||||
setError(`Error: ${e.body.message ?? e.message}`);
|
||||
setError(`Error: ${e.body?.message ?? e.message}`);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ type ReportAssistantComplete = (params: {
|
|||
integrationName: string;
|
||||
integrationSettings: IntegrationSettings;
|
||||
connector: AIConnector;
|
||||
error?: string;
|
||||
}) => void;
|
||||
|
||||
interface TelemetryContextProps {
|
||||
|
@ -113,7 +114,7 @@ export const TelemetryContextProvider = React.memo<PropsWithChildren<{}>>(({ chi
|
|||
);
|
||||
|
||||
const reportAssistantComplete = useCallback<ReportAssistantComplete>(
|
||||
({ integrationName, integrationSettings, connector }) => {
|
||||
({ integrationName, integrationSettings, connector, error }) => {
|
||||
telemetry.reportEvent(TelemetryEventType.IntegrationAssistantComplete, {
|
||||
sessionId: sessionData.current.sessionId,
|
||||
integrationName,
|
||||
|
@ -124,6 +125,7 @@ export const TelemetryContextProvider = React.memo<PropsWithChildren<{}>>(({ chi
|
|||
model: getConnectorModel(connector),
|
||||
provider: connector.apiProvider ?? 'unknown',
|
||||
durationMs: Date.now() - sessionData.current.startedAt,
|
||||
errorMessage: error,
|
||||
});
|
||||
},
|
||||
[telemetry]
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProvider } from '../../mocks/test_provider';
|
||||
import { CreateIntegrationCardButton } from './create_integration_card_button';
|
||||
import { mockServices } from '../../services/mocks/services';
|
||||
|
||||
const renderOptions = { wrapper: TestProvider };
|
||||
|
||||
describe('CreateIntegrationCardButton', () => {
|
||||
describe('when not compressed (default)', () => {
|
||||
let element: React.ReactElement;
|
||||
|
||||
beforeEach(() => {
|
||||
element = <CreateIntegrationCardButton />;
|
||||
});
|
||||
|
||||
it('should render the link button', () => {
|
||||
const result = render(element, renderOptions);
|
||||
expect(result.queryByTestId('createIntegrationLink')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the sub-title', () => {
|
||||
const result = render(element, renderOptions);
|
||||
expect(
|
||||
result.queryByText('Create a custom one to fit your requirements')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate when button clicked', () => {
|
||||
const url = '/app/integrations';
|
||||
mockServices.application.getUrlForApp.mockReturnValueOnce(url);
|
||||
|
||||
const result = render(element, renderOptions);
|
||||
result.queryByTestId('createIntegrationLink')?.click();
|
||||
|
||||
expect(mockServices.application.navigateToUrl).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when compressed', () => {
|
||||
let element: React.ReactElement;
|
||||
|
||||
beforeEach(() => {
|
||||
element = <CreateIntegrationCardButton compressed />;
|
||||
});
|
||||
|
||||
it('should render the link button', () => {
|
||||
const result = render(element, renderOptions);
|
||||
expect(result.queryByTestId('createIntegrationLink')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render the sub-title', () => {
|
||||
const result = render(element, renderOptions);
|
||||
expect(
|
||||
result.queryByText('Create a custom one to fit your requirements')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate when button clicked', () => {
|
||||
const url = '/app/integrations';
|
||||
mockServices.application.getUrlForApp.mockReturnValueOnce(url);
|
||||
|
||||
const result = render(element, renderOptions);
|
||||
|
||||
result.queryByTestId('createIntegrationLink')?.click();
|
||||
expect(mockServices.application.navigateToUrl).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -93,7 +93,12 @@ export const CreateIntegrationCardButton = React.memo<CreateIntegrationCardButto
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink color="primary" href={href} onClick={navigate}>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href={href}
|
||||
onClick={navigate}
|
||||
data-test-subj="createIntegrationLink"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { mockServices } from '../services/mocks/services';
|
||||
import { TelemetryContextProvider } from '../components/create_integration/telemetry';
|
||||
|
||||
export const TestProvider: React.FC<PropsWithChildren<{}>> = ({ children }) => {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={mockServices}>
|
||||
<TelemetryContextProvider>{children}</TelemetryContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
|
@ -4,17 +4,5 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { IntegrationAssistantPluginStartDependencies } from '../types';
|
||||
import type { TelemetryService } from './telemetry/service';
|
||||
|
||||
export * from './types';
|
||||
export { Telemetry } from './telemetry/service';
|
||||
|
||||
export type RenderUpselling = React.ReactNode;
|
||||
|
||||
export type Services = CoreStart &
|
||||
IntegrationAssistantPluginStartDependencies & {
|
||||
telemetry: TelemetryService;
|
||||
renderUpselling$: Observable<RenderUpselling | undefined>;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
import { telemetryServiceMock } from '../telemetry/mocks/service';
|
||||
|
||||
export const mockServices = {
|
||||
...coreMock.createStart(),
|
||||
triggersActionsUi: triggersActionsUiMock.createStart(),
|
||||
licensing: licensingMock.createStart(),
|
||||
telemetry: telemetryServiceMock.createStart(),
|
||||
renderUpselling$: of(undefined),
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const mockReportEvent = jest.fn();
|
||||
|
||||
export const telemetryServiceMock = {
|
||||
createStart: () => ({
|
||||
reportEvent: mockReportEvent,
|
||||
}),
|
||||
};
|
|
@ -5,8 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { IntegrationAssistantPluginStartDependencies } from '../types';
|
||||
import type { TelemetryService } from './telemetry/service';
|
||||
|
||||
export type RenderUpselling = React.ReactNode;
|
||||
|
||||
export type Services = CoreStart &
|
||||
IntegrationAssistantPluginStartDependencies & { telemetry: TelemetryService };
|
||||
IntegrationAssistantPluginStartDependencies & {
|
||||
telemetry: TelemetryService;
|
||||
renderUpselling$: Observable<RenderUpselling | undefined>;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue