[Cloud Posture] Refactor status and query logic out of CspPageTemplate (#136104)

This commit is contained in:
Ari Aviran 2022-07-14 13:35:35 +03:00 committed by GitHub
parent ac434b3433
commit d5ba9cc098
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 590 additions and 629 deletions

View file

@ -0,0 +1,276 @@
/*
* 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 Chance from 'chance';
import { useCisKubernetesIntegration } from '../common/api/use_cis_kubernetes_integration';
import {
DEFAULT_NO_DATA_TEST_SUBJECT,
ERROR_STATE_TEST_SUBJECT,
isCommonError,
LOADING_STATE_TEST_SUBJECT,
PACKAGE_NOT_INSTALLED_TEST_SUBJECT,
} from './cloud_posture_page';
import { createReactQueryResponse } from '../test/fixtures/react_query';
import { TestProvider } from '../test/test_provider';
import { coreMock } from '@kbn/core/public/mocks';
import { render, screen } from '@testing-library/react';
import React, { ComponentProps } from 'react';
import { UseQueryResult } from 'react-query';
import { CloudPosturePage } from './cloud_posture_page';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
const chance = new Chance();
jest.mock('../common/api/use_cis_kubernetes_integration');
describe('<CloudPosturePage />', () => {
beforeEach(() => {
jest.resetAllMocks();
// if package installation status is 'not_installed', CloudPosturePage will render a noDataConfig prompt
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: { item: { status: 'installed' } },
}));
});
const renderCloudPosturePage = (
props: ComponentProps<typeof CloudPosturePage> = { children: null }
) => {
const mockCore = coreMock.createStart();
render(
<TestProvider
core={{
...mockCore,
application: {
...mockCore.application,
capabilities: {
...mockCore.application.capabilities,
// This is required so that the `noDataConfig` view will show the action button
navLinks: { integrations: true },
},
},
}}
>
<CloudPosturePage {...props} />
</TestProvider>
);
};
it('renders children if integration is installed', () => {
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByText(children)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders integrations installation prompt if integration is not installed', () => {
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: { item: { status: 'not_installed' } },
}));
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading state when the integration query is loading', () => {
(useCisKubernetesIntegration as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default error state when the integration query has an error', () => {
(useCisKubernetesIntegration as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'error',
error: new Error('error'),
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudPosturePage({ children });
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading text when query isLoading', () => {
const query = createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading text when query is idle', () => {
const query = createReactQueryResponse({
status: 'idle',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default error texts when query isError', () => {
const error = chance.sentence();
const message = chance.sentence();
const statusCode = chance.integer();
const query = createReactQueryResponse({
status: 'error',
error: {
body: {
error,
message,
statusCode,
},
},
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({ children, query });
[error, message, statusCode].forEach((text) =>
expect(screen.getByText(text, { exact: false })).toBeInTheDocument()
);
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('prefers custom error render', () => {
const error = chance.sentence();
const message = chance.sentence();
const statusCode = chance.integer();
const query = createReactQueryResponse({
status: 'error',
error: {
body: {
error,
message,
statusCode,
},
},
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({
children,
query,
errorRender: (err) => <div>{isCommonError(err) && err.body.message}</div>,
});
expect(screen.getByText(message)).toBeInTheDocument();
[error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument());
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('prefers custom loading render', () => {
const loading = chance.sentence();
const query = createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({
children,
query,
loadingRender: () => <div>{loading}</div>,
});
expect(screen.getByText(loading)).toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders no data prompt when query data is undefined', () => {
const query = createReactQueryResponse({
status: 'success',
data: undefined,
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({ children, query });
expect(screen.getByTestId(DEFAULT_NO_DATA_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('prefers custom no data prompt', () => {
const pageTitle = chance.sentence();
const solution = chance.sentence();
const docsLink = chance.sentence();
const noDataRenderer = () => (
<NoDataPage pageTitle={pageTitle} solution={solution} docsLink={docsLink} actions={{}} />
);
const query = createReactQueryResponse({
status: 'success',
data: undefined,
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudPosturePage({
children,
query,
noDataRenderer,
});
expect(screen.getByText(pageTitle)).toBeInTheDocument();
expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,193 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { UseQueryResult } from 'react-query';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
import { css } from '@emotion/react';
import { CspLoadingState } from './csp_loading_state';
import { useCisKubernetesIntegration } from '../common/api/use_cis_kubernetes_integration';
import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration';
export const LOADING_STATE_TEST_SUBJECT = 'cloud_posture_page_loading';
export const ERROR_STATE_TEST_SUBJECT = 'cloud_posture_page_error';
export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_package_not_installed';
export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data';
interface CommonError {
body: {
error: string;
message: string;
statusCode: number;
};
}
export const isCommonError = (error: unknown): error is CommonError => {
if (
!(error as any)?.body ||
!(error as any)?.body?.error ||
!(error as any)?.body?.message ||
!(error as any)?.body?.statusCode
) {
return false;
}
return true;
};
const packageNotInstalledRenderer = (cisIntegrationLink?: string) => (
<NoDataPage
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
css={css`
max-width: 950px;
margin-top: 50px;
margin-left: auto;
margin-right: auto;
`}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle', {
defaultMessage: 'Install Integration to get started',
})}
solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{
elasticAgent: {
href: cisIntegrationLink,
isDisabled: !cisIntegrationLink,
title: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel', {
defaultMessage: 'Add a CIS integration',
}),
description: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.description', {
defaultMessage:
'Use our CIS Kubernetes Benchmark integration to measure your Kubernetes cluster setup against the CIS recommendations.',
}),
},
}}
/>
);
const defaultLoadingRenderer = () => (
<CspLoadingState data-test-subj={LOADING_STATE_TEST_SUBJECT}>
<FormattedMessage
id="xpack.csp.cloudPosturePage.loadingDescription"
defaultMessage="Loading..."
/>
</CspLoadingState>
);
const defaultErrorRenderer = (error: unknown) => (
<EuiEmptyPrompt
css={css`
margin-top: 50px;
`}
color="danger"
iconType="alert"
data-test-subj={ERROR_STATE_TEST_SUBJECT}
title={
<h2>
<FormattedMessage
id="xpack.csp.cloudPosturePage.errorRenderer.errorTitle"
defaultMessage="We couldn't fetch your cloud security posture data"
/>
</h2>
}
body={
isCommonError(error) ? (
<p>
<FormattedMessage
id="xpack.csp.cloudPosturePage.errorRenderer.errorDescription"
defaultMessage="{error} {statusCode}: {body}"
values={{
error: error.body.error,
statusCode: error.body.statusCode,
body: error.body.message,
}}
/>
</p>
) : undefined
}
/>
);
const defaultNoDataRenderer = () => {
return (
<NoDataPage
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
css={css`
margin-top: 50px;
`}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.pageTitle', {
defaultMessage: 'No data found',
})}
solution={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{}}
/>
);
};
interface CloudPosturePageProps<TData, TError> {
children: React.ReactNode;
query?: UseQueryResult<TData, TError>;
loadingRender?: () => React.ReactNode;
errorRender?: (error: TError) => React.ReactNode;
noDataRenderer?: () => React.ReactNode;
}
export const CloudPosturePage = <TData, TError>({
children,
query,
loadingRender = defaultLoadingRenderer,
errorRender = defaultErrorRenderer,
noDataRenderer = defaultNoDataRenderer,
}: CloudPosturePageProps<TData, TError>) => {
const cisKubernetesPackageInfo = useCisKubernetesIntegration();
const cisIntegrationLink = useCISIntegrationLink();
const render = () => {
if (cisKubernetesPackageInfo.isError) {
return defaultErrorRenderer(cisKubernetesPackageInfo.error);
}
if (cisKubernetesPackageInfo.isLoading || cisKubernetesPackageInfo.isIdle) {
return defaultLoadingRenderer();
}
if (cisKubernetesPackageInfo.data.item.status !== 'installed') {
return packageNotInstalledRenderer(cisIntegrationLink);
}
if (!query) {
return children;
}
if (query.isError) {
return errorRender(query.error);
}
if (query.isLoading || query.isIdle) {
return loadingRender();
}
if (!query.data) {
return noDataRenderer();
}
return children;
};
return <>{render()}</>;
};

View file

@ -18,6 +18,7 @@ export const CspLoadingState: React.FunctionComponent<{ ['data-test-subj']?: str
<EuiFlexGroup
css={css`
padding: ${euiTheme.size.l};
margin-top: 50px;
`}
direction="column"
alignItems="center"

View file

@ -4,33 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { type ComponentProps } from 'react';
import { render, screen } from '@testing-library/react';
import Chance from 'chance';
import { coreMock } from '@kbn/core/public/mocks';
import { createNavigationItemFixture } from '../test/fixtures/navigation_item';
import { createReactQueryResponse } from '../test/fixtures/react_query';
import { TestProvider } from '../test/test_provider';
import {
CspPageTemplate,
ERROR_STATE_TEST_SUBJECT,
getSideNavItems,
isCommonError,
LOADING_STATE_TEST_SUBJECT,
} from './csp_page_template';
import { PACKAGE_NOT_INSTALLED_TEXT, DEFAULT_NO_DATA_TEXT } from './translations';
import { useCisKubernetesIntegration } from '../common/api/use_cis_kubernetes_integration';
import { UseQueryResult } from 'react-query';
import { getSideNavItems } from './csp_page_template';
const chance = new Chance();
const packageNotInstalledUniqueTexts = [
PACKAGE_NOT_INSTALLED_TEXT.PAGE_TITLE,
PACKAGE_NOT_INSTALLED_TEXT.DESCRIPTION,
];
jest.mock('../common/api/use_cis_kubernetes_integration');
describe('getSideNavItems', () => {
it('maps navigation items to side navigation items', () => {
const navigationItem = createNavigationItemFixture();
@ -52,230 +31,3 @@ describe('getSideNavItems', () => {
expect(sideNavItems).toHaveLength(0);
});
});
describe('<CspPageTemplate />', () => {
beforeEach(() => {
jest.resetAllMocks();
// if package installation status is 'not_installed', CspPageTemplate will render a noDataConfig prompt
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: { item: { status: 'installed' } },
}));
});
const renderCspPageTemplate = (props: ComponentProps<typeof CspPageTemplate> = {}) => {
const mockCore = coreMock.createStart();
render(
<TestProvider
core={{
...mockCore,
application: {
...mockCore.application,
capabilities: {
...mockCore.application.capabilities,
// This is required so that the `noDataConfig` view will show the action button
navLinks: { integrations: true },
},
},
}}
>
<CspPageTemplate {...props} />
</TestProvider>
);
};
it('renders children if integration is installed', () => {
const children = chance.sentence();
renderCspPageTemplate({ children });
expect(screen.getByText(children)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('renders integrations installation prompt if integration is not installed', () => {
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
isSuccess: true,
isLoading: false,
data: { item: { status: 'not_installed' } },
}));
const children = chance.sentence();
renderCspPageTemplate({ children });
Object.values(PACKAGE_NOT_INSTALLED_TEXT).forEach((text) =>
expect(screen.getAllByText(text)[0]).toBeInTheDocument()
);
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading text when query isLoading', () => {
const query = createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('renders default loading text when query is idle', () => {
const query = createReactQueryResponse({
status: 'idle',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('renders default error texts when query isError', () => {
const error = chance.sentence();
const message = chance.sentence();
const statusCode = chance.integer();
const query = createReactQueryResponse({
status: 'error',
error: {
body: {
error,
message,
statusCode,
},
},
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({ children, query });
[error, message, statusCode].forEach((text) =>
expect(screen.getByText(text, { exact: false })).toBeInTheDocument()
);
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('prefers custom error render', () => {
const error = chance.sentence();
const message = chance.sentence();
const statusCode = chance.integer();
const query = createReactQueryResponse({
status: 'error',
error: {
body: {
error,
message,
statusCode,
},
},
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({
children,
query,
errorRender: (err) => <div>{isCommonError(err) && err.body.message}</div>,
});
expect(screen.getByText(message)).toBeInTheDocument();
[error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument());
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('prefers custom loading render', () => {
const loading = chance.sentence();
const query = createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({
children,
query,
loadingRender: () => <div>{loading}</div>,
});
expect(screen.getByText(loading)).toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('renders noDataConfig prompt when query data is undefined', () => {
const query = createReactQueryResponse({
status: 'success',
data: undefined,
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({ children, query });
expect(screen.getByText(DEFAULT_NO_DATA_TEXT.PAGE_TITLE)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
it('prefers custom noDataConfig prompt', () => {
const pageTitle = chance.sentence();
const solution = chance.sentence();
const docsLink = chance.sentence();
const query = createReactQueryResponse({
status: 'success',
data: undefined,
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCspPageTemplate({
children,
query,
noDataConfig: { pageTitle, solution, docsLink, actions: {} },
});
expect(screen.getByText(pageTitle)).toBeInTheDocument();
expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
});

View file

@ -5,45 +5,18 @@
* 2.0.
*/
import React from 'react';
import type { UseQueryResult } from 'react-query';
import { NavLink } from 'react-router-dom';
import { EuiEmptyPrompt, EuiErrorBoundary, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { KibanaPageTemplate, type KibanaPageTemplateProps } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import { EuiErrorBoundary } from '@elastic/eui';
import { KibanaPageTemplate, type KibanaPageTemplateProps } from '@kbn/shared-ux-components';
import { allNavigationItems } from '../common/navigation/constants';
import type { CspNavigationItem } from '../common/navigation/types';
import { CspLoadingState } from './csp_loading_state';
import {
CLOUD_SECURITY_POSTURE,
DEFAULT_NO_DATA_TEXT,
PACKAGE_NOT_INSTALLED_TEXT,
} from './translations';
import { useCisKubernetesIntegration } from '../common/api/use_cis_kubernetes_integration';
import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration';
export interface CommonError {
body: {
error: string;
message: string;
statusCode: number;
};
}
export const isCommonError = (x: any): x is CommonError => {
if (!('body' in x)) return false;
const {
body: { error, message, statusCode },
} = x;
return !!(error && message && statusCode);
};
const activeItemStyle = { fontWeight: 700 };
export const getSideNavItems = (
navigationItems: Record<string, CspNavigationItem>
): NonNullable<KibanaPageTemplateProps['solutionNav']>['items'] =>
): NonNullable<NonNullable<KibanaPageTemplateProps['solutionNav']>['items']> =>
Object.entries(navigationItems)
.filter(([_, navigationItem]) => !navigationItem.disabled)
.map(([id, navigationItem]) => ({
@ -58,7 +31,9 @@ export const getSideNavItems = (
const DEFAULT_PAGE_PROPS: KibanaPageTemplateProps = {
solutionNav: {
name: CLOUD_SECURITY_POSTURE,
name: i18n.translate('xpack.csp.cspPageTemplate.navigationTitle', {
defaultMessage: 'Cloud Security Posture',
}),
items: getSideNavItems({
dashboard: allNavigationItems.dashboard,
findings: allNavigationItems.findings,
@ -68,146 +43,13 @@ const DEFAULT_PAGE_PROPS: KibanaPageTemplateProps = {
restrictWidth: false,
};
export const DEFAULT_NO_DATA_CONFIG: KibanaPageTemplateProps['noDataConfig'] = {
pageTitle: DEFAULT_NO_DATA_TEXT.PAGE_TITLE,
solution: DEFAULT_NO_DATA_TEXT.SOLUTION,
// TODO: Add real docs link once we have it
docsLink: 'https://www.elastic.co/guide/index.html',
logo: 'logoSecurity',
actions: {},
};
export const LOADING_STATE_TEST_SUBJECT = 'csp_page_template_loading';
export const ERROR_STATE_TEST_SUBJECT = 'csp_page_template_error';
const getPackageNotInstalledNoDataConfig = (
cisIntegrationLink?: string
): KibanaPageTemplateProps['noDataConfig'] => ({
pageTitle: PACKAGE_NOT_INSTALLED_TEXT.PAGE_TITLE,
solution: PACKAGE_NOT_INSTALLED_TEXT.SOLUTION,
// TODO: Add real docs link once we have it
docsLink: 'https://www.elastic.co/guide/index.html',
logo: 'logoSecurity',
actions: {
elasticAgent: {
href: cisIntegrationLink,
isDisabled: !cisIntegrationLink,
title: PACKAGE_NOT_INSTALLED_TEXT.BUTTON_TITLE,
description: PACKAGE_NOT_INSTALLED_TEXT.DESCRIPTION,
},
},
});
const DefaultLoading = () => (
<CspLoadingState data-test-subj={LOADING_STATE_TEST_SUBJECT}>
<FormattedMessage
id="xpack.csp.cspPageTemplate.loadingDescription"
defaultMessage="Loading..."
/>
</CspLoadingState>
);
const DefaultError = (error: unknown) => (
<EuiEmptyPrompt
color="danger"
iconType="alert"
data-test-subj={ERROR_STATE_TEST_SUBJECT}
title={
<>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.csp.pageTemplate.loadErrorMessage"
defaultMessage="We couldn't fetch your cloud security posture data"
/>
</h2>
</EuiTitle>
{isCommonError(error) && (
<>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.csp.pageTemplate.errorDetails.errorCodeTitle"
defaultMessage="{error} {statusCode}"
values={{
error: error.body.error,
statusCode: error.body.statusCode,
}}
/>
</h5>
</EuiTitle>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.csp.pageTemplate.errorDetails.errorBodyTitle"
defaultMessage="{body}"
values={{
body: error.body.message,
}}
/>
</h5>
</EuiTitle>
</>
)}
</>
}
/>
);
export const CspPageTemplate = <TData, TError>({
query,
children,
loadingRender = DefaultLoading,
errorRender = DefaultError,
...kibanaPageTemplateProps
}: KibanaPageTemplateProps & {
loadingRender?: () => React.ReactNode;
errorRender?: (error: TError) => React.ReactNode;
query?: UseQueryResult<TData, TError>;
}) => {
const cisKubernetesPackageInfo = useCisKubernetesIntegration();
const cisIntegrationLink = useCISIntegrationLink();
const getNoDataConfig = (): KibanaPageTemplateProps['noDataConfig'] => {
if (cisKubernetesPackageInfo.data?.item.status !== 'installed') {
return getPackageNotInstalledNoDataConfig(cisIntegrationLink);
}
// when query was successful, but data is undefined
if (query?.isSuccess && !query?.data) {
return kibanaPageTemplateProps.noDataConfig || DEFAULT_NO_DATA_CONFIG;
}
return kibanaPageTemplateProps.noDataConfig;
};
const getTemplate = (): KibanaPageTemplateProps['template'] => {
if (query?.isLoading || query?.isError || cisKubernetesPackageInfo.isLoading)
return 'centeredContent';
return kibanaPageTemplateProps.template || 'default';
};
const render = () => {
if (query?.isLoading || query?.isIdle || cisKubernetesPackageInfo.isLoading) {
return loadingRender();
}
if (query?.isError) return errorRender(query.error);
if (query?.isSuccess) return children;
return children;
};
}: KibanaPageTemplateProps) => {
return (
<KibanaPageTemplate
{...DEFAULT_PAGE_PROPS}
{...kibanaPageTemplateProps}
template={getTemplate()}
noDataConfig={cisKubernetesPackageInfo.isSuccess ? getNoDataConfig() : undefined}
>
<EuiErrorBoundary>
<>{render()}</>
</EuiErrorBoundary>
<KibanaPageTemplate {...DEFAULT_PAGE_PROPS} {...kibanaPageTemplateProps}>
<EuiErrorBoundary>{children}</EuiErrorBoundary>
</KibanaPageTemplate>
);
};

View file

@ -1,36 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const PACKAGE_NOT_INSTALLED_TEXT = {
PAGE_TITLE: i18n.translate('xpack.csp.cspPageTemplate.packageNotInstalled.pageTitle', {
defaultMessage: 'Install Integration to get started',
}),
SOLUTION: i18n.translate('xpack.csp.cspPageTemplate.packageNotInstalled.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
}),
BUTTON_TITLE: i18n.translate('xpack.csp.cspPageTemplate.packageNotInstalled.buttonLabel', {
defaultMessage: 'Add a CIS integration',
}),
DESCRIPTION: i18n.translate('xpack.csp.cspPageTemplate.packageNotInstalled.description', {
defaultMessage:
'Use our CIS Kubernetes Benchmark integration to measure your Kubernetes cluster setup against the CIS recommendations.',
}),
};
export const DEFAULT_NO_DATA_TEXT = {
PAGE_TITLE: i18n.translate('xpack.csp.cspPageTemplate.defaultNoDataConfig.pageTitle', {
defaultMessage: 'No data found',
}),
SOLUTION: i18n.translate('xpack.csp.cspPageTemplate.defaultNoDataConfig.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
}),
};
export const CLOUD_SECURITY_POSTURE = i18n.translate('xpack.csp.cspPageTemplate.navigationTitle', {
defaultMessage: 'Cloud Security Posture',
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import React from 'react';
import { coreMock } from '@kbn/core/public/mocks';
import { render, screen } from '@testing-library/react';
@ -13,7 +14,7 @@ import { ComplianceDashboard } from '..';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useCisKubernetesIntegration } from '../../common/api/use_cis_kubernetes_integration';
import { useComplianceDashboardDataApi } from '../../common/api/use_compliance_dashboard_data_api';
import { DASHBOARD_PAGE_HEADER, MISSING_FINDINGS_NO_DATA_CONFIG } from './test_subjects';
import { DASHBOARD_CONTAINER, MISSING_FINDINGS_NO_DATA_CONFIG } from './test_subjects';
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/api/use_cis_kubernetes_integration');
@ -196,17 +197,17 @@ describe('<ComplianceDashboard />', () => {
};
it('shows noDataConfig when latestFindingsIndexStatus is inapplicable', () => {
(useCspSetupStatusApi as jest.Mock).mockImplementation(() => ({
data: { latestFindingsIndexStatus: 'inapplicable' },
}));
(useComplianceDashboardDataApi as jest.Mock).mockImplementation(() => ({
data: undefined,
}));
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({ status: 'success', data: 'inapplicable' })
);
(useComplianceDashboardDataApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({ status: 'success', data: undefined })
);
renderComplianceDashboardPage();
expect(screen.queryByTestId(MISSING_FINDINGS_NO_DATA_CONFIG)).toBeInTheDocument();
expect(screen.queryByTestId(DASHBOARD_PAGE_HEADER)).not.toBeInTheDocument();
expect(screen.queryByTestId(DASHBOARD_CONTAINER)).not.toBeInTheDocument();
});
it('shows dashboard when latestFindingsIndexStatus is applicable', () => {
@ -225,6 +226,6 @@ describe('<ComplianceDashboard />', () => {
renderComplianceDashboardPage();
expect(screen.queryByTestId(MISSING_FINDINGS_NO_DATA_CONFIG)).not.toBeInTheDocument();
expect(screen.getByTestId(DASHBOARD_PAGE_HEADER)).toBeInTheDocument();
expect(screen.getByTestId(DASHBOARD_CONTAINER)).toBeInTheDocument();
});
});

View file

@ -7,10 +7,12 @@
import React from 'react';
import { EuiSpacer, EuiIcon } from '@elastic/eui';
import { type KibanaPageTemplateProps } from '@kbn/kibana-react-plugin/public';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
import { UseQueryResult } from 'react-query';
import { i18n } from '@kbn/i18n';
import { DASHBOARD_PAGE_HEADER, MISSING_FINDINGS_NO_DATA_CONFIG } from './test_subjects';
import { css } from '@emotion/react';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { DASHBOARD_CONTAINER, MISSING_FINDINGS_NO_DATA_CONFIG } from './test_subjects';
import { allNavigationItems } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { SummarySection } from './dashboard_sections/summary_section';
@ -19,31 +21,39 @@ import { useComplianceDashboardDataApi } from '../../common/api';
import { CspPageTemplate } from '../../components/csp_page_template';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
const getNoDataConfig = (onClick: () => void): KibanaPageTemplateProps['noDataConfig'] => ({
'data-test-subj': MISSING_FINDINGS_NO_DATA_CONFIG,
pageTitle: i18n.translate('xpack.csp.dashboard.noDataConfig.pageTitle', {
defaultMessage: 'Cloud Posture Dashboard',
}),
solution: i18n.translate('xpack.csp.dashboard.noDataConfig.solutionNameTitle', {
defaultMessage: 'Cloud Security Posture',
}),
// TODO: Add real docs link once we have it
docsLink: 'https://www.elastic.co/guide/index.html',
logo: 'logoSecurity',
actions: {
dashboardNoDataCard: {
icon: <EuiIcon type="refresh" size="xxl" />,
onClick,
title: i18n.translate('xpack.csp.dashboard.noDataConfig.actionTitle', {
defaultMessage: 'Try Again',
}),
description: i18n.translate('xpack.csp.dashboard.noDataConfig.actionDescription', {
defaultMessage:
"The cloud posture dashboard can't be presented since there are no findings. This can happen due to the agent not being installed yet, or since data is still being processed.",
}),
},
},
});
const NoData = ({ onClick }: { onClick: () => void }) => (
<NoDataPage
css={css`
margin-top: 50px;
margin-left: auto;
margin-right: auto;
max-width: 950px;
`}
data-test-subj={MISSING_FINDINGS_NO_DATA_CONFIG}
pageTitle={i18n.translate('xpack.csp.dashboard.noDataConfig.pageTitle', {
defaultMessage: 'Cloud Posture Dashboard',
})}
solution={i18n.translate('xpack.csp.dashboard.noDataConfig.solutionNameTitle', {
defaultMessage: 'Cloud Security Posture',
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{
dashboardNoDataCard: {
icon: <EuiIcon type="refresh" size="xxl" />,
onClick,
title: i18n.translate('xpack.csp.dashboard.noDataConfig.actionTitle', {
defaultMessage: 'Try Again',
}),
description: i18n.translate('xpack.csp.dashboard.noDataConfig.actionDescription', {
defaultMessage:
"The cloud posture dashboard can't be presented since there are no findings. This can happen due to the agent not being installed yet, or since data is still being processed.",
}),
},
}}
/>
);
export const ComplianceDashboard = () => {
const getInfo = useCspSetupStatusApi();
@ -51,6 +61,7 @@ export const ComplianceDashboard = () => {
const getDashboardData = useComplianceDashboardDataApi({
enabled: isFindingsIndexApplicable,
});
useCspBreadcrumbs([allNavigationItems.dashboard]);
const pageQuery: UseQueryResult = isFindingsIndexApplicable ? getDashboardData : getInfo;
@ -58,23 +69,24 @@ export const ComplianceDashboard = () => {
return (
<CspPageTemplate
pageHeader={{
'data-test-subj': DASHBOARD_PAGE_HEADER,
pageTitle: i18n.translate('xpack.csp.dashboard.cspPageTemplate.pageTitle', {
defaultMessage: 'Cloud Posture',
}),
}}
restrictWidth={1600}
query={pageQuery}
noDataConfig={!isFindingsIndexApplicable ? getNoDataConfig(getInfo.refetch) : undefined}
>
{getDashboardData.data && (
<>
<SummarySection complianceData={getDashboardData.data} />
<EuiSpacer />
<BenchmarksSection complianceData={getDashboardData.data} />
<EuiSpacer />
</>
)}
<CloudPosturePage query={pageQuery}>
{isFindingsIndexApplicable ? (
<div data-test-subj={DASHBOARD_CONTAINER}>
<SummarySection complianceData={getDashboardData.data!} />
<EuiSpacer />
<BenchmarksSection complianceData={getDashboardData.data!} />
<EuiSpacer />
</div>
) : (
<NoData onClick={getInfo.refetch} />
)}
</CloudPosturePage>
</CspPageTemplate>
);
};

View file

@ -6,4 +6,4 @@
*/
export const MISSING_FINDINGS_NO_DATA_CONFIG = 'missing-findings-no-data-config';
export const DASHBOARD_PAGE_HEADER = 'dashboard-page-header';
export const DASHBOARD_CONTAINER = 'dashboard-container';

View file

@ -7,6 +7,7 @@
import React from 'react';
import type { UseQueryResult } from 'react-query';
import { Redirect, Switch, Route, useLocation } from 'react-router-dom';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { useFindingsEsPit } from './es_pit/use_findings_es_pit';
import { FindingsEsPitContext } from './es_pit/findings_es_pit_context';
import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_data_view';
@ -21,48 +22,50 @@ export const Findings = () => {
// TODO: Consider splitting the PIT window so that each "group by" view has its own PIT
const { pitQuery, pitIdRef, setPitId } = useFindingsEsPit('findings');
let queryForPageTemplate: UseQueryResult = dataViewQuery;
let queryForSetupStatus: UseQueryResult = dataViewQuery;
if (pitQuery.isError || pitQuery.isLoading || pitQuery.isIdle) {
queryForPageTemplate = pitQuery;
queryForSetupStatus = pitQuery;
}
return (
<CspPageTemplate paddingSize="none" query={queryForPageTemplate}>
<FindingsEsPitContext.Provider
value={{
pitQuery,
// Asserting the ref as a string value since at this point the query was necessarily successful
pitIdRef: pitIdRef as React.MutableRefObject<string>,
setPitId,
}}
>
<Switch>
<Route
exact
path={allNavigationItems.findings.path}
component={() => (
<Redirect
to={{
pathname: findingsNavigation.findings_default.path,
search: location.search,
}}
/>
)}
/>
<Route
path={findingsNavigation.findings_default.path}
render={() => <LatestFindingsContainer dataView={dataViewQuery.data!} />}
/>
<Route
path={findingsNavigation.findings_by_resource.path}
render={() => <FindingsByResourceContainer dataView={dataViewQuery.data!} />}
/>
<Route
path={'*'}
component={() => <Redirect to={findingsNavigation.findings_default.path} />}
/>
</Switch>
</FindingsEsPitContext.Provider>
<CspPageTemplate paddingSize="none">
<CloudPosturePage query={queryForSetupStatus}>
<FindingsEsPitContext.Provider
value={{
pitQuery,
// Asserting the ref as a string value since at this point the query was necessarily successful
pitIdRef: pitIdRef as React.MutableRefObject<string>,
setPitId,
}}
>
<Switch>
<Route
exact
path={allNavigationItems.findings.path}
component={() => (
<Redirect
to={{
pathname: findingsNavigation.findings_default.path,
search: location.search,
}}
/>
)}
/>
<Route
path={findingsNavigation.findings_default.path}
render={() => <LatestFindingsContainer dataView={dataViewQuery.data!} />}
/>
<Route
path={findingsNavigation.findings_by_resource.path}
render={() => <FindingsByResourceContainer dataView={dataViewQuery.data!} />}
/>
<Route
path={'*'}
component={() => <Redirect to={findingsNavigation.findings_default.path} />}
/>
</Switch>
</FindingsEsPitContext.Provider>
</CloudPosturePage>
</CspPageTemplate>
);
};

View file

@ -7,19 +7,18 @@
import React, { useMemo } from 'react';
import { generatePath, Link, RouteComponentProps } from 'react-router-dom';
import { EuiTextColor, EuiEmptyPrompt, EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
import * as t from 'io-ts';
import type { KibanaPageTemplateProps } from '@kbn/kibana-react-plugin/public';
import { EuiTextColor, EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-components';
import { FormattedMessage } from '@kbn/i18n-react';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import { RulesContainer, type PageUrlParams } from './rules_container';
import { allNavigationItems } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { CspNavigationItem } from '../../common/navigation/types';
import { extractErrorMessage } from '../../../common/utils/helpers';
import { useCspIntegrationInfo } from './use_csp_integration';
import { CspPageTemplate } from '../../components/csp_page_template';
import { useKibana } from '../../common/hooks/use_kibana';
import { CloudPosturePage } from '../../components/cloud_posture_page';
const getRulesBreadcrumbs = (name?: string): CspNavigationItem[] =>
[allNavigationItems.benchmarks, { ...allNavigationItems.rules, name }].filter(
@ -93,34 +92,10 @@ export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>)
);
return (
<CspPageTemplate
{...pageProps}
query={integrationInfo}
errorRender={(error) => <RulesErrorPrompt error={extractErrorBodyMessage(error)} />}
>
<RulesContainer />
<CspPageTemplate {...pageProps}>
<CloudPosturePage query={integrationInfo}>
<RulesContainer />
</CloudPosturePage>
</CspPageTemplate>
);
};
// react-query puts the response data on the 'error' object
const bodyError = t.type({
body: t.type({
message: t.string,
}),
});
const extractErrorBodyMessage = (err: unknown) => {
if (bodyError.is(err)) return err.body.message;
return extractErrorMessage(err);
};
const RulesErrorPrompt = ({ error }: { error: string }) => (
<EuiEmptyPrompt
{...{
color: 'danger',
iconType: 'alert',
title: <h2>{error}</h2>,
}}
/>
);

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import { LOADING_STATE_TEST_SUBJECT } from '../../components/csp_page_template';
import { Rules } from '.';
import { render, screen } from '@testing-library/react';
import { QueryClient } from 'react-query';
@ -79,33 +78,6 @@ describe('<Rules />', () => {
expect(useCspIntegrationInfo).toHaveBeenCalledWith(params);
});
it('displays error state when request had an error', async () => {
const Component = getTestComponent({ packagePolicyId: '1', policyId: '2' });
const request = createReactQueryResponse({
status: 'error',
error: new Error('some error message'),
});
(useCspIntegrationInfo as jest.Mock).mockReturnValue(request);
render(<Component />);
expect(await screen.findByText(request.error?.message!)).toBeInTheDocument();
});
it('displays loading state when request is pending', () => {
const Component = getTestComponent({ packagePolicyId: '21', policyId: '22' });
const request = createReactQueryResponse({
status: 'loading',
});
(useCspIntegrationInfo as jest.Mock).mockReturnValue(request);
render(<Component />);
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
});
it('displays success state when result request is resolved', async () => {
const Component = getTestComponent({ packagePolicyId: '21', policyId: '22' });
const request = createReactQueryResponse({

View file

@ -10439,14 +10439,7 @@
"xpack.csp.cspHealthBadge.criticalLabel": "Critique",
"xpack.csp.cspHealthBadge.healthyLabel": "Intègre",
"xpack.csp.cspHealthBadge.warningLabel": "Avertissement",
"xpack.csp.cspPageTemplate.defaultNoDataConfig.pageTitle": "Aucune donnée trouvée",
"xpack.csp.cspPageTemplate.defaultNoDataConfig.solutionNameLabel": "Niveau de sécurité du cloud",
"xpack.csp.cspPageTemplate.loadingDescription": "Chargement...",
"xpack.csp.cspPageTemplate.navigationTitle": "Niveau de sécurité du cloud",
"xpack.csp.cspPageTemplate.packageNotInstalled.buttonLabel": "Ajouter une intégration CIS",
"xpack.csp.cspPageTemplate.packageNotInstalled.description": "Utilisez notre intégration CIS Kubernetes Benchmark pour mesurer votre configuration de cluster Kubernetes par rapport aux recommandations du CIS.",
"xpack.csp.cspPageTemplate.packageNotInstalled.pageTitle": "Installer l'intégration pour commencer",
"xpack.csp.cspPageTemplate.packageNotInstalled.solutionNameLabel": "Niveau de sécurité du cloud",
"xpack.csp.cspSettings.rules": "Règles de sécurité du CSP - ",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "Section CIS",
"xpack.csp.expandColumnDescriptionLabel": "Développer",
@ -10466,9 +10459,6 @@
"xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "Retour à la vue de regroupement par ressource",
"xpack.csp.findings.searchBar.searchPlaceholder": "Rechercher dans les résultats (par ex. rule.section.keyword : \"serveur d'API\")",
"xpack.csp.navigation.cloudPostureBreadcrumbLabel": "Niveau du cloud",
"xpack.csp.pageTemplate.errorDetails.errorBodyTitle": "{body}",
"xpack.csp.pageTemplate.errorDetails.errorCodeTitle": "{error} {statusCode}",
"xpack.csp.pageTemplate.loadErrorMessage": "Nous n'avons pas pu récupérer vos données sur le niveau de sécurité du cloud.",
"xpack.csp.rules.activateAllButtonLabel": "Activer {count, plural, one {# règle} other {# règles}}",
"xpack.csp.rules.clearSelectionButtonLabel": "Effacer la sélection",
"xpack.csp.rules.deactivateAllButtonLabel": "Désactiver {count, plural, one {# règle} other {# règles}}",

View file

@ -10431,14 +10431,7 @@
"xpack.csp.cspHealthBadge.criticalLabel": "重大",
"xpack.csp.cspHealthBadge.healthyLabel": "正常",
"xpack.csp.cspHealthBadge.warningLabel": "警告",
"xpack.csp.cspPageTemplate.defaultNoDataConfig.pageTitle": "データが見つかりません",
"xpack.csp.cspPageTemplate.defaultNoDataConfig.solutionNameLabel": "クラウドセキュリティ態勢",
"xpack.csp.cspPageTemplate.loadingDescription": "読み込み中...",
"xpack.csp.cspPageTemplate.navigationTitle": "クラウドセキュリティ態勢",
"xpack.csp.cspPageTemplate.packageNotInstalled.buttonLabel": "CIS統合を追加",
"xpack.csp.cspPageTemplate.packageNotInstalled.description": "CIS Kubernetes Benchmark統合は、CISの推奨事項に照らしてKubernetesクラスター設定を測定します。",
"xpack.csp.cspPageTemplate.packageNotInstalled.pageTitle": "開始するには統合をインストールしてください",
"xpack.csp.cspPageTemplate.packageNotInstalled.solutionNameLabel": "クラウドセキュリティ態勢",
"xpack.csp.cspSettings.rules": "CSPセキュリティルール - ",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CISセクション",
"xpack.csp.expandColumnDescriptionLabel": "拡張",
@ -10458,9 +10451,6 @@
"xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "リソース別グループビューに戻る",
"xpack.csp.findings.searchBar.searchPlaceholder": "検索結果rule.section.keyword\"API Server\"",
"xpack.csp.navigation.cloudPostureBreadcrumbLabel": "クラウド態勢",
"xpack.csp.pageTemplate.errorDetails.errorBodyTitle": "{body}",
"xpack.csp.pageTemplate.errorDetails.errorCodeTitle": "{error} {statusCode}",
"xpack.csp.pageTemplate.loadErrorMessage": "クラウドセキュリティ態勢データを取得できませんでした",
"xpack.csp.rules.activateAllButtonLabel": "{count, plural, other {#個のルール}}をアクティブ化",
"xpack.csp.rules.clearSelectionButtonLabel": "選択した項目をクリア",
"xpack.csp.rules.deactivateAllButtonLabel": "{count, plural, other {#個のルール}}を非アクティブ化",

View file

@ -10446,14 +10446,7 @@
"xpack.csp.cspHealthBadge.criticalLabel": "紧急",
"xpack.csp.cspHealthBadge.healthyLabel": "运行正常",
"xpack.csp.cspHealthBadge.warningLabel": "警告",
"xpack.csp.cspPageTemplate.defaultNoDataConfig.pageTitle": "未找到任何数据",
"xpack.csp.cspPageTemplate.defaultNoDataConfig.solutionNameLabel": "云安全态势",
"xpack.csp.cspPageTemplate.loadingDescription": "正在加载……",
"xpack.csp.cspPageTemplate.navigationTitle": "云安全态势",
"xpack.csp.cspPageTemplate.packageNotInstalled.buttonLabel": "添加 CIS 集成",
"xpack.csp.cspPageTemplate.packageNotInstalled.description": "使用我们的 CIS Kubernetes 基准集成根据 CIS 建议衡量 Kubernetes 集群设置。",
"xpack.csp.cspPageTemplate.packageNotInstalled.pageTitle": "安装集成以开始",
"xpack.csp.cspPageTemplate.packageNotInstalled.solutionNameLabel": "云安全态势",
"xpack.csp.cspSettings.rules": "CSP 安全规则 - ",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CIS 部分",
"xpack.csp.expandColumnDescriptionLabel": "展开",
@ -10473,9 +10466,6 @@
"xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "返回到按资源视图分组",
"xpack.csp.findings.searchBar.searchPlaceholder": "搜索结果例如rule.section.keyword“APM 服务器”)",
"xpack.csp.navigation.cloudPostureBreadcrumbLabel": "云态势",
"xpack.csp.pageTemplate.errorDetails.errorBodyTitle": "{body}",
"xpack.csp.pageTemplate.errorDetails.errorCodeTitle": "{error} {statusCode}",
"xpack.csp.pageTemplate.loadErrorMessage": "我们无法提取您的云安全态势数据",
"xpack.csp.rules.activateAllButtonLabel": "激活 {count, plural, other {# 个规则}}",
"xpack.csp.rules.clearSelectionButtonLabel": "清除所选内容",
"xpack.csp.rules.deactivateAllButtonLabel": "停用 {count, plural, other {# 个规则}}",