CSP Viewing states (#128215)

This commit is contained in:
Jordan 2022-03-29 12:32:19 +03:00 committed by GitHub
parent 7cb72014aa
commit 23586d2e4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 646 additions and 412 deletions

View file

@ -35,7 +35,7 @@ export interface Cluster {
resourcesTypes: ResourceType[];
}
export interface CloudPostureStats {
export interface ComplianceDashboardData {
stats: Stats;
resourcesTypes: ResourceType[];
clusters: Cluster[];

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export * from './use_cloud_posture_stats_api';
export * from './use_compliance_dashboard_data_api';

View file

@ -0,0 +1,31 @@
/*
* 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 { useQuery } from 'react-query';
import {
epmRouteService,
type GetInfoResponse,
type DefaultPackagesInstallationError,
} from '../../../../fleet/common';
import { CIS_KUBERNETES_PACKAGE_NAME } from '../../../common/constants';
import { useKibana } from '../hooks/use_kibana';
export const CIS_KUBERNETES_INTEGRATION_VERSION = '0.0.1';
/**
* This hook will find our cis integration and return its PackageInfo
* */
export const useCisKubernetesIntegration = () => {
const { http } = useKibana().services;
return useQuery<GetInfoResponse, DefaultPackagesInstallationError>(['integrations'], () =>
http.get<GetInfoResponse>(
epmRouteService.getInfoPath(CIS_KUBERNETES_PACKAGE_NAME, CIS_KUBERNETES_INTEGRATION_VERSION),
{ query: { experimental: true } }
)
);
};

View file

@ -7,12 +7,12 @@
import { useQuery } from 'react-query';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { CloudPostureStats } from '../../../common/types';
import { ComplianceDashboardData } from '../../../common/types';
import { STATS_ROUTE_PATH } from '../../../common/constants';
const getStatsKey = 'csp_dashboard_stats';
export const useCloudPostureStatsApi = () => {
export const useComplianceDashboardDataApi = () => {
const { http } = useKibana().services;
return useQuery([getStatsKey], () => http!.get<CloudPostureStats>(STATS_ROUTE_PATH));
return useQuery([getStatsKey], () => http!.get<ComplianceDashboardData>(STATS_ROUTE_PATH));
};

View file

@ -19,8 +19,8 @@ import { CHART_PANEL_TEST_SUBJECTS } from './constants';
interface ChartPanelProps {
title?: string;
hasBorder?: boolean;
isLoading: boolean;
isError: boolean;
isLoading?: boolean;
isError?: boolean;
}
const Loading = () => (

View file

@ -0,0 +1,258 @@
/*
* 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, { type ComponentProps } from 'react';
import { render, screen } from '@testing-library/react';
import Chance from 'chance';
import { coreMock } from '../../../../../src/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, getSideNavItems, isCommonError } from './csp_page_template';
import { LOADING, PACKAGE_NOT_INSTALLED_TEXT, DEFAULT_NO_DATA_TEXT } from './translations';
import { useCisKubernetesIntegration } from '../common/api/use_cis_kubernetes_integration';
import { UseQueryResult } from 'react-query';
const chance = new Chance();
// Synchronized to the error message in the formatted message in `csp_page_template.tsx`
const ERROR_LOADING_DATA_DEFAULT_MESSAGE = "We couldn't fetch your cloud security posture data";
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();
const id = chance.word();
const sideNavItems = getSideNavItems({ [id]: navigationItem });
expect(sideNavItems).toHaveLength(1);
expect(sideNavItems[0]).toMatchObject({
id,
name: navigationItem.name,
renderItem: expect.any(Function),
});
});
it('does not map disabled navigation items to side navigation items', () => {
const navigationItem = createNavigationItemFixture({ disabled: true });
const id = chance.word();
const sideNavItems = getSideNavItems({ [id]: navigationItem });
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(() => ({
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.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).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,
data: { item: { status: 'not_installed' } },
}));
const children = chance.sentence();
renderCspPageTemplate({ children });
Object.values(PACKAGE_NOT_INSTALLED_TEXT).forEach((text) =>
expect(screen.getByText(text)).toBeInTheDocument()
);
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).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.getByText(LOADING)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).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_LOADING_DATA_DEFAULT_MESSAGE, error, message, statusCode].forEach((text) =>
expect(screen.getByText(text, { exact: false })).toBeInTheDocument()
);
expect(screen.queryByText(LOADING)).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_LOADING_DATA_DEFAULT_MESSAGE, error, statusCode].forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
expect(screen.queryByText(LOADING)).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.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).not.toBeInTheDocument();
expect(screen.queryByText(LOADING)).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.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).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.getByText(solution, { exact: false })).toBeInTheDocument();
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).not.toBeInTheDocument();
packageNotInstalledUniqueTexts.forEach((text) =>
expect(screen.queryByText(text)).not.toBeInTheDocument()
);
});
});

View file

@ -0,0 +1,202 @@
/*
* 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 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 '../../../../../src/plugins/kibana_react/public';
import { allNavigationItems } from '../common/navigation/constants';
import type { CspNavigationItem } from '../common/navigation/types';
import { CLOUD_SECURITY_POSTURE } from '../common/translations';
import { CspLoadingState } from './csp_loading_state';
import { DEFAULT_NO_DATA_TEXT, LOADING, 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'] =>
Object.entries(navigationItems)
.filter(([_, navigationItem]) => !navigationItem.disabled)
.map(([id, navigationItem]) => ({
id,
name: navigationItem.name,
renderItem: () => (
<NavLink to={navigationItem.path} activeStyle={activeItemStyle}>
{navigationItem.name}
</NavLink>
),
}));
const DEFAULT_PAGE_PROPS: KibanaPageTemplateProps = {
solutionNav: {
name: CLOUD_SECURITY_POSTURE,
items: getSideNavItems({
dashboard: allNavigationItems.dashboard,
findings: allNavigationItems.findings,
benchmark: allNavigationItems.benchmarks,
}),
},
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: {},
};
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,
title: PACKAGE_NOT_INSTALLED_TEXT.BUTTON_TITLE,
description: PACKAGE_NOT_INSTALLED_TEXT.DESCRIPTION,
},
},
});
const DefaultLoading = () => <CspLoadingState>{LOADING}</CspLoadingState>;
const DefaultError = (error: unknown) => (
<EuiEmptyPrompt
color="danger"
iconType="alert"
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?.isSuccess &&
cisKubernetesPackageInfo.data.item.status === 'not_installed'
) {
return getPackageNotInstalledNoDataConfig(cisIntegrationLink);
}
// when query was successful, but data is undefined
if (query?.isSuccess && !query?.data) {
return kibanaPageTemplateProps.noDataConfig || DEFAULT_NO_DATA_CONFIG;
}
// when the consumer didn't pass a query, most likely to handle the render on his own
if (!query) return kibanaPageTemplateProps.noDataConfig;
};
const getTemplate = (): KibanaPageTemplateProps['template'] => {
if (query?.isLoading || query?.isError) return 'centeredContent';
return kibanaPageTemplateProps.template || 'default';
};
const render = () => {
if (query?.isLoading) return loadingRender();
if (query?.isError) return errorRender(query.error);
if (query?.isSuccess) return children;
return children;
};
return (
<KibanaPageTemplate
{...DEFAULT_PAGE_PROPS}
{...kibanaPageTemplateProps}
template={getTemplate()}
noDataConfig={getNoDataConfig()}
>
<EuiErrorBoundary>
{cisKubernetesPackageInfo?.data?.item.status === 'installed' && render()}
</EuiErrorBoundary>
</KibanaPageTemplate>
);
};

View file

@ -1,158 +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 React, { type ComponentProps } from 'react';
import { render, screen } from '@testing-library/react';
import Chance from 'chance';
import { coreMock } from '../../../../../src/core/public/mocks';
import { createStubDataView } from '../../../../../src/plugins/data_views/public/data_views/data_view.stub';
import { CSP_KUBEBEAT_INDEX_PATTERN } from '../../common/constants';
import { useKubebeatDataView } from '../common/api/use_kubebeat_data_view';
import { createNavigationItemFixture } from '../test/fixtures/navigation_item';
import { createReactQueryResponse } from '../test/fixtures/react_query';
import { TestProvider } from '../test/test_provider';
import { CspPageTemplate, getSideNavItems } from './page_template';
import {
LOADING,
NO_DATA_CONFIG_BUTTON,
NO_DATA_CONFIG_DESCRIPTION,
NO_DATA_CONFIG_TITLE,
} from './translations';
const chance = new Chance();
const BLANK_PAGE_GRAPHIC_TEXTS = [
NO_DATA_CONFIG_TITLE,
NO_DATA_CONFIG_DESCRIPTION,
NO_DATA_CONFIG_BUTTON,
];
// Synchronized to the error message in the formatted message in `page_template.tsx`
const ERROR_LOADING_DATA_DEFAULT_MESSAGE = "We couldn't fetch your cloud security posture data";
jest.mock('../common/api/use_kubebeat_data_view');
describe('getSideNavItems', () => {
it('maps navigation items to side navigation items', () => {
const navigationItem = createNavigationItemFixture();
const id = chance.word();
const sideNavItems = getSideNavItems({ [id]: navigationItem });
expect(sideNavItems).toHaveLength(1);
expect(sideNavItems[0]).toMatchObject({
id,
name: navigationItem.name,
renderItem: expect.any(Function),
});
});
it('does not map disabled navigation items to side navigation items', () => {
const navigationItem = createNavigationItemFixture({ disabled: true });
const id = chance.word();
const sideNavItems = getSideNavItems({ [id]: navigationItem });
expect(sideNavItems).toHaveLength(0);
});
});
describe('<CspPageTemplate />', () => {
beforeEach(() => {
jest.resetAllMocks();
});
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 when data view is found', () => {
(useKubebeatDataView as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: createStubDataView({
spec: {
id: CSP_KUBEBEAT_INDEX_PATTERN,
},
}),
})
);
const children = chance.sentence();
renderCspPageTemplate({ children });
expect(screen.getByText(children)).toBeInTheDocument();
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).not.toBeInTheDocument();
BLANK_PAGE_GRAPHIC_TEXTS.forEach((blankPageGraphicText) =>
expect(screen.queryByText(blankPageGraphicText)).not.toBeInTheDocument()
);
});
it('renders loading text when data view is loading', () => {
(useKubebeatDataView as jest.Mock).mockImplementation(() =>
createReactQueryResponse({ status: 'loading' })
);
const children = chance.sentence();
renderCspPageTemplate({ children });
expect(screen.getByText(LOADING)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).not.toBeInTheDocument();
BLANK_PAGE_GRAPHIC_TEXTS.forEach((blankPageGraphicText) =>
expect(screen.queryByText(blankPageGraphicText)).not.toBeInTheDocument()
);
});
it('renders an error view when data view fetching has an error', () => {
(useKubebeatDataView as jest.Mock).mockImplementation(() =>
createReactQueryResponse({ status: 'error', error: new Error('') })
);
const children = chance.sentence();
renderCspPageTemplate({ children });
expect(screen.getByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).toBeInTheDocument();
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
BLANK_PAGE_GRAPHIC_TEXTS.forEach((blankPageGraphicText) =>
expect(screen.queryByText(blankPageGraphicText)).not.toBeInTheDocument()
);
});
it('renders the blank page graphic when data view is missing', () => {
(useKubebeatDataView as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: undefined,
})
);
const children = chance.sentence();
renderCspPageTemplate({ children });
BLANK_PAGE_GRAPHIC_TEXTS.forEach((text) => expect(screen.getByText(text)).toBeInTheDocument());
expect(screen.queryByText(ERROR_LOADING_DATA_DEFAULT_MESSAGE)).not.toBeInTheDocument();
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
});
});

View file

@ -1,117 +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 React from 'react';
import { NavLink } from 'react-router-dom';
import { EuiEmptyPrompt, EuiErrorBoundary, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
KibanaPageTemplate,
type KibanaPageTemplateProps,
} from '../../../../../src/plugins/kibana_react/public';
import { useKubebeatDataView } from '../common/api/use_kubebeat_data_view';
import { allNavigationItems } from '../common/navigation/constants';
import type { CspNavigationItem } from '../common/navigation/types';
import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration';
import { CLOUD_SECURITY_POSTURE } from '../common/translations';
import { CspLoadingState } from './csp_loading_state';
import {
LOADING,
NO_DATA_CONFIG_BUTTON,
NO_DATA_CONFIG_DESCRIPTION,
NO_DATA_CONFIG_SOLUTION_NAME,
NO_DATA_CONFIG_TITLE,
} from './translations';
const activeItemStyle = { fontWeight: 700 };
export const getSideNavItems = (
navigationItems: Record<string, CspNavigationItem>
): NonNullable<KibanaPageTemplateProps['solutionNav']>['items'] =>
Object.entries(navigationItems)
.filter(([_, navigationItem]) => !navigationItem.disabled)
.map(([id, navigationItem]) => ({
id,
name: navigationItem.name,
renderItem: () => (
<NavLink to={navigationItem.path} activeStyle={activeItemStyle}>
{navigationItem.name}
</NavLink>
),
}));
const DEFAULT_PROPS: KibanaPageTemplateProps = {
solutionNav: {
name: CLOUD_SECURITY_POSTURE,
items: getSideNavItems({
dashboard: allNavigationItems.dashboard,
findings: allNavigationItems.findings,
benchmark: allNavigationItems.benchmarks,
}),
},
restrictWidth: false,
};
const getNoDataConfig = (cisIntegrationLink: string): KibanaPageTemplateProps['noDataConfig'] => ({
pageTitle: NO_DATA_CONFIG_TITLE,
solution: NO_DATA_CONFIG_SOLUTION_NAME,
// TODO: Add real docs link once we have it
docsLink: 'https://www.elastic.co/guide/index.html',
logo: 'logoSecurity',
actions: {
elasticAgent: {
href: cisIntegrationLink,
title: NO_DATA_CONFIG_BUTTON,
description: NO_DATA_CONFIG_DESCRIPTION,
},
},
});
export const CspPageTemplate: React.FC<KibanaPageTemplateProps> = ({ children, ...props }) => {
// TODO: Consider using more sophisticated logic to find out if our integration is installed
const kubeBeatQuery = useKubebeatDataView();
const cisIntegrationLink = useCISIntegrationLink();
let noDataConfig: KibanaPageTemplateProps['noDataConfig'];
if (kubeBeatQuery.status === 'success' && !kubeBeatQuery.data) {
noDataConfig = getNoDataConfig(cisIntegrationLink);
}
let template: KibanaPageTemplateProps['template'] = 'default';
if (kubeBeatQuery.status === 'error' || kubeBeatQuery.status === 'loading') {
template = 'centeredContent';
}
return (
<KibanaPageTemplate
template={template}
{...DEFAULT_PROPS}
{...props}
noDataConfig={noDataConfig}
>
<EuiErrorBoundary>
{kubeBeatQuery.status === 'loading' && <CspLoadingState>{LOADING}</CspLoadingState>}
{kubeBeatQuery.status === 'error' && (
<EuiEmptyPrompt
color="danger"
iconType="alert"
title={
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.csp.pageTemplate.loadErrorMessage"
defaultMessage="We couldn't fetch your cloud security posture data"
/>
</h2>
</EuiTitle>
}
/>
)}
{kubeBeatQuery.status === 'success' && children}
</EuiErrorBoundary>
</KibanaPageTemplate>
);
};

View file

@ -40,28 +40,27 @@ export const CSP_EVALUATION_BADGE_PASSED = i18n.translate(
}
);
export const NO_DATA_CONFIG_TITLE = i18n.translate('xpack.csp.pageTemplate.noDataConfigTitle', {
defaultMessage: 'Understand your cloud security posture',
});
export const NO_DATA_CONFIG_SOLUTION_NAME = i18n.translate(
'xpack.csp.pageTemplate.noDataConfig.solutionNameLabel',
{
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',
}
);
export const NO_DATA_CONFIG_DESCRIPTION = i18n.translate(
'xpack.csp.pageTemplate.noDataConfigDescription',
{
}),
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 NO_DATA_CONFIG_BUTTON = i18n.translate(
'xpack.csp.pageTemplate.noDataConfigButtonLabel',
{
defaultMessage: 'Add a CIS integration',
}
);
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',
}),
};

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { CspPageTemplate } from './page_template';
import { CspPageTemplate } from './csp_page_template';
import * as TEXT from './translations';
export const UnknownRoute = React.memo(() => (

View file

@ -21,13 +21,20 @@ import {
TABLE_COLUMN_HEADERS,
} from './translations';
import { useCspBenchmarkIntegrations } from './use_csp_benchmark_integrations';
import { useCisKubernetesIntegration } from '../../common/api/use_cis_kubernetes_integration';
jest.mock('./use_csp_benchmark_integrations');
jest.mock('../../common/api/use_kubebeat_data_view');
jest.mock('../../common/api/use_cis_kubernetes_integration');
describe('<Benchmarks />', () => {
beforeEach(() => {
jest.resetAllMocks();
// if package installation status is 'not_installed', CspPageTemplate will render a noDataConfig prompt
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
data: { item: { status: 'installed' } },
}));
// Required for the page template to render the benchmarks page
(useKubebeatDataView as jest.Mock).mockImplementation(() =>
createReactQueryResponse({

View file

@ -4,6 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import {
EuiFieldSearch,
EuiFieldSearchProps,
@ -15,13 +17,12 @@ import {
EuiTextColor,
EuiText,
} from '@elastic/eui';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import useDebounce from 'react-use/lib/useDebounce';
import { allNavigationItems } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
import { CspPageTemplate } from '../../components/page_template';
import { CspPageTemplate } from '../../components/csp_page_template';
import { BenchmarksTable } from './benchmarks_table';
import { ADD_A_CIS_INTEGRATION, BENCHMARK_INTEGRATIONS } from './translations';
import {

View file

@ -14,7 +14,7 @@ import {
EuiLink,
EuiText,
} from '@elastic/eui';
import { CloudPostureStats, ResourceType } from '../../../../common/types';
import { ComplianceDashboardData, ResourceType } from '../../../../common/types';
import { CompactFormattedNumber } from '../../../components/compact_formatted_number';
import * as TEXT from '../translations';
import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants';
@ -59,14 +59,14 @@ const mockData = [
];
export interface RisksTableProps {
data: CloudPostureStats['resourcesTypes'];
data: ComplianceDashboardData['resourcesTypes'];
maxItems: number;
onCellClick: (resourceTypeName: string) => void;
onViewAllClick: () => void;
}
export const getTopRisks = (
resourcesTypes: CloudPostureStats['resourcesTypes'],
resourcesTypes: ComplianceDashboardData['resourcesTypes'],
maxItems: number
) => {
const filtered = resourcesTypes.filter((x) => x.totalFailed > 0);

View file

@ -6,40 +6,51 @@
*/
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { EuiSpacer, EuiIcon } from '@elastic/eui';
import { allNavigationItems } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { SummarySection } from './dashboard_sections/summary_section';
import { BenchmarksSection } from './dashboard_sections/benchmarks_section';
import { useCloudPostureStatsApi } from '../../common/api';
import { CspPageTemplate } from '../../components/page_template';
import * as TEXT from './translations';
import { useComplianceDashboardDataApi } from '../../common/api';
import { CspPageTemplate } from '../../components/csp_page_template';
import { type KibanaPageTemplateProps } from '../../../../../../src/plugins/kibana_react/public';
import { CLOUD_POSTURE, NO_DATA_CONFIG_TEXT } from './translations';
const CompliancePage = () => {
const getStats = useCloudPostureStatsApi();
if (getStats.isLoading) return null;
return (
<>
<SummarySection />
<EuiSpacer />
<BenchmarksSection />
<EuiSpacer />
</>
);
};
const getNoDataConfig = (onClick: () => void): KibanaPageTemplateProps['noDataConfig'] => ({
pageTitle: NO_DATA_CONFIG_TEXT.PAGE_TITLE,
solution: NO_DATA_CONFIG_TEXT.SOLUTION,
// 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: NO_DATA_CONFIG_TEXT.BUTTON_TITLE,
description: NO_DATA_CONFIG_TEXT.DESCRIPTION,
},
},
});
export const ComplianceDashboard = () => {
const getDashboardDataQuery = useComplianceDashboardDataApi();
useCspBreadcrumbs([allNavigationItems.dashboard]);
return (
<CspPageTemplate
pageHeader={{
pageTitle: TEXT.CLOUD_POSTURE,
}}
pageHeader={{ pageTitle: CLOUD_POSTURE }}
restrictWidth={1600}
query={getDashboardDataQuery}
noDataConfig={getNoDataConfig(getDashboardDataQuery.refetch)}
>
<CompliancePage />
{getDashboardDataQuery.data && (
<>
<SummarySection complianceData={getDashboardDataQuery.data} />
<EuiSpacer />
<BenchmarksSection complianceData={getDashboardDataQuery.data} />
<EuiSpacer />
</>
)}
</CspPageTemplate>
);
};

View file

@ -17,34 +17,25 @@ import {
useEuiTheme,
} from '@elastic/eui';
import moment from 'moment';
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
import { PartitionElementEvent } from '@elastic/charts';
import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types';
import { CloudPostureScoreChart } from '../compliance_charts/cloud_posture_score_chart';
import { useCloudPostureStatsApi } from '../../../common/api/use_cloud_posture_stats_api';
import { ChartPanel } from '../../../components/chart_panel';
import * as TEXT from '../translations';
import { Evaluation } from '../../../../common/types';
import type { ComplianceDashboardData, Evaluation } from '../../../../common/types';
import { RisksTable } from '../compliance_charts/risks_table';
import { INTERNAL_FEATURE_FLAGS, RULE_FAILED } from '../../../../common/constants';
import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings';
const logoMap: ReadonlyMap<string, EuiIconType> = new Map([['CIS Kubernetes', 'logoKubernetes']]);
const getBenchmarkLogo = (benchmarkName: string): EuiIconType => {
return logoMap.get(benchmarkName) ?? 'logoElastic';
};
const mockClusterId = '2468540';
const cardHeight = 300;
export const BenchmarksSection = () => {
export const BenchmarksSection = ({
complianceData,
}: {
complianceData: ComplianceDashboardData;
}) => {
const { euiTheme } = useEuiTheme();
const navToFindings = useNavigateFindings();
const getStats = useCloudPostureStatsApi();
const clusters = getStats.isSuccess && getStats.data.clusters;
if (!clusters) return null;
const handleElementClick = (clusterId: string, elements: PartitionElementEvent[]) => {
const [element] = elements;
@ -68,7 +59,7 @@ export const BenchmarksSection = () => {
return (
<>
{clusters.map((cluster) => {
{complianceData.clusters.map((cluster) => {
const shortId = cluster.meta.clusterId.slice(0, 6);
return (
@ -82,7 +73,7 @@ export const BenchmarksSection = () => {
<h4>{cluster.meta.benchmarkName}</h4>
</EuiText>
<EuiText style={{ textAlign: 'center' }}>
<h4>{`Cluster ID ${shortId || mockClusterId}`}</h4>
<h4>{`Cluster ID ${shortId}`}</h4>
</EuiText>
<EuiSpacer size="xs" />
<EuiText size="xs" color="subdued" style={{ textAlign: 'center' }}>
@ -91,7 +82,8 @@ export const BenchmarksSection = () => {
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type={getBenchmarkLogo(cluster.meta.benchmarkName)} size="xxl" />
{/* TODO: change default k8s logo to use a getBenchmarkLogo function */}
<EuiIcon type="logoKubernetes" size="xxl" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{INTERNAL_FEATURE_FLAGS.showManageRulesMock && (
@ -104,12 +96,7 @@ export const BenchmarksSection = () => {
grow={4}
style={{ borderRight: `1px solid ${euiTheme.colors.lightShade}` }}
>
<ChartPanel
title={TEXT.COMPLIANCE_SCORE}
hasBorder={false}
isLoading={getStats.isLoading}
isError={getStats.isError}
>
<ChartPanel title={TEXT.COMPLIANCE_SCORE} hasBorder={false}>
<CloudPostureScoreChart
id={`${cluster.meta.clusterId}_score_chart`}
data={cluster.stats}
@ -120,12 +107,7 @@ export const BenchmarksSection = () => {
</ChartPanel>
</EuiFlexItem>
<EuiFlexItem grow={4}>
<ChartPanel
title={TEXT.RISKS}
hasBorder={false}
isLoading={getStats.isLoading}
isError={getStats.isError}
>
<ChartPanel title={TEXT.RISKS} hasBorder={false}>
<RisksTable
data={cluster.resourcesTypes}
maxItems={3}

View file

@ -9,10 +9,9 @@ import React from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { PartitionElementEvent } from '@elastic/charts';
import { ChartPanel } from '../../../components/chart_panel';
import { useCloudPostureStatsApi } from '../../../common/api';
import * as TEXT from '../translations';
import { CloudPostureScoreChart } from '../compliance_charts/cloud_posture_score_chart';
import { Evaluation } from '../../../../common/types';
import type { ComplianceDashboardData, Evaluation } from '../../../../common/types';
import { RisksTable } from '../compliance_charts/risks_table';
import { CasesTable } from '../compliance_charts/cases_table';
import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings';
@ -25,10 +24,8 @@ const summarySectionWrapperStyle = {
height: defaultHeight,
};
export const SummarySection = () => {
export const SummarySection = ({ complianceData }: { complianceData: ComplianceDashboardData }) => {
const navToFindings = useNavigateFindings();
const getStats = useCloudPostureStatsApi();
if (!getStats.isSuccess) return null;
const handleElementClick = (elements: PartitionElementEvent[]) => {
const [element] = elements;
@ -49,22 +46,18 @@ export const SummarySection = () => {
return (
<EuiFlexGrid columns={3} style={summarySectionWrapperStyle}>
<EuiFlexItem>
<ChartPanel
title={TEXT.CLOUD_POSTURE_SCORE}
isLoading={getStats.isLoading}
isError={getStats.isError}
>
<ChartPanel title={TEXT.CLOUD_POSTURE_SCORE}>
<CloudPostureScoreChart
id="cloud_posture_score_chart"
data={getStats.data.stats}
data={complianceData.stats}
partitionOnElementClick={handleElementClick}
/>
</ChartPanel>
</EuiFlexItem>
<EuiFlexItem>
<ChartPanel title={TEXT.RISKS} isLoading={getStats.isLoading} isError={getStats.isError}>
<ChartPanel title={TEXT.RISKS}>
<RisksTable
data={getStats.data.resourcesTypes}
data={complianceData.resourcesTypes}
maxItems={5}
onCellClick={handleCellClick}
onViewAllClick={handleViewAllClick}
@ -72,11 +65,7 @@ export const SummarySection = () => {
</ChartPanel>
</EuiFlexItem>
<EuiFlexItem>
<ChartPanel
title={TEXT.OPEN_CASES}
isLoading={getStats.isLoading}
isError={getStats.isError}
>
<ChartPanel title={TEXT.OPEN_CASES}>
<CasesTable />
</ChartPanel>
</EuiFlexItem>

View file

@ -82,3 +82,18 @@ export const RESOURCE_TYPE = i18n.translate('xpack.csp.resource_type', {
export const FINDINGS = i18n.translate('xpack.csp.findings', {
defaultMessage: 'Findings',
});
export const NO_DATA_CONFIG_TEXT = {
PAGE_TITLE: i18n.translate('xpack.csp.complianceDashboard.noDataConfig.pageTitle', {
defaultMessage: 'Cloud Security Compliance Dashboard',
}),
SOLUTION: i18n.translate('xpack.csp.complianceDashboard.noDataConfig.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
}),
BUTTON_TITLE: i18n.translate('xpack.csp.complianceDashboard.noDataConfig.actionTitle', {
defaultMessage: 'Try Again',
}),
DESCRIPTION: i18n.translate('xpack.csp.complianceDashboard.noDataConfig.actionDescription', {
defaultMessage: 'You can try to refetch your data',
}),
};

View file

@ -17,9 +17,11 @@ import {
import { createStubDataView } from '../../../../../../src/plugins/data_views/public/data_views/data_view.stub';
import { CSP_KUBEBEAT_INDEX_PATTERN } from '../../../common/constants';
import * as TEST_SUBJECTS from './test_subjects';
import { useCisKubernetesIntegration } from '../../common/api/use_cis_kubernetes_integration';
import type { DataView } from '../../../../../../src/plugins/data/common';
jest.mock('../../common/api/use_kubebeat_data_view');
jest.mock('../../common/api/use_cis_kubernetes_integration');
beforeEach(() => {
jest.restoreAllMocks();
@ -36,6 +38,9 @@ describe('<Findings />', () => {
const data = dataPluginMock.createStartContract();
const source = await data.search.searchSource.create();
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
data: { item: { status: 'installed' } },
}));
(source.fetch$ as jest.Mock).mockReturnValue({
toPromise: () => Promise.resolve({ rawResponse: { hits: { hits: [] } } }),
});

View file

@ -10,7 +10,7 @@ import { useKubebeatDataView } from '../../common/api/use_kubebeat_data_view';
import { allNavigationItems } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { FindingsContainer } from './findings_container';
import { CspPageTemplate } from '../../components/page_template';
import { CspPageTemplate } from '../../components/csp_page_template';
import { FINDINGS } from './translations';
const pageHeader: EuiPageHeaderProps = {
@ -18,15 +18,12 @@ const pageHeader: EuiPageHeaderProps = {
};
export const Findings = () => {
const dataView = useKubebeatDataView();
const dataViewQuery = useKubebeatDataView();
useCspBreadcrumbs([allNavigationItems.findings]);
return (
// `CspPageTemplate` takes care of loading and error states for the kubebeat data view, no need to handle them here
<CspPageTemplate pageHeader={pageHeader}>
{dataView.status === 'success' && dataView.data && (
<FindingsContainer dataView={dataView.data} />
)}
<CspPageTemplate pageHeader={pageHeader} query={dataViewQuery}>
{dataViewQuery.data && <FindingsContainer dataView={dataViewQuery.data} />}
</CspPageTemplate>
);
};

View file

@ -4,19 +4,19 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiTextColor, EuiEmptyPrompt } from '@elastic/eui';
import * as t from 'io-ts';
import { CspPageTemplate } from '../../components/page_template';
import { RulesContainer, type PageUrlParams } from './rules_container';
import { allNavigationItems } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import type { KibanaPageTemplateProps } from '../../../../../../src/plugins/kibana_react/public';
import { CspLoadingState } from '../../components/csp_loading_state';
import { CspNavigationItem } from '../../common/navigation/types';
import { extractErrorMessage } from '../../../common/utils/helpers';
import { useCspIntegration } from './use_csp_integration';
import { CspPageTemplate } from '../../components/csp_page_template';
const getRulesBreadcrumbs = (name?: string): CspNavigationItem[] =>
[allNavigationItems.benchmarks, { ...allNavigationItems.rules, name }].filter(
@ -35,7 +35,6 @@ export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>)
const pageProps: KibanaPageTemplateProps = useMemo(
() => ({
template: integrationInfo.status !== 'success' ? 'centeredContent' : undefined,
pageHeader: {
bottomBorder: false, // TODO: border still shows.
pageTitle: 'Rules',
@ -46,16 +45,16 @@ export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>)
),
},
}),
[integrationInfo.data, integrationInfo.status]
[integrationInfo.data]
);
return (
<CspPageTemplate {...pageProps}>
<CspPageTemplate
{...pageProps}
query={integrationInfo}
errorRender={(error) => <RulesErrorPrompt error={extractErrorBodyMessage(error)} />}
>
{integrationInfo.status === 'success' && <RulesContainer />}
{integrationInfo.status === 'error' && (
<RulesErrorPrompt error={extractErrorBodyMessage(integrationInfo.error)} />
)}
{integrationInfo.status === 'loading' && <CspLoadingState />}
</CspPageTemplate>
);
};

View file

@ -15,10 +15,12 @@ import { type RouteComponentProps } from 'react-router-dom';
import { cspLoadingStateTestId } from '../../components/csp_loading_state';
import type { PageUrlParams } from './rules_container';
import * as TEST_SUBJECTS from './test_subjects';
import { useCisKubernetesIntegration } from '../../common/api/use_cis_kubernetes_integration';
jest.mock('./use_csp_integration', () => ({
useCspIntegration: jest.fn(),
}));
jest.mock('../../common/api/use_cis_kubernetes_integration');
const queryClient = new QueryClient({
defaultOptions: {
@ -43,6 +45,9 @@ describe('<Rules />', () => {
beforeEach(() => {
queryClient.clear();
jest.clearAllMocks();
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
data: { item: { status: 'installed' } },
}));
});
it('calls API with URL params', async () => {
@ -63,6 +68,7 @@ describe('<Rules />', () => {
const Component = getTestComponent({ packagePolicyId: '1', policyId: '2' });
const request = {
status: 'error',
isError: true,
data: null,
error: new Error('some error message'),
};
@ -78,6 +84,7 @@ describe('<Rules />', () => {
const Component = getTestComponent({ packagePolicyId: '21', policyId: '22' });
const request = {
status: 'loading',
isLoading: true,
};
(useCspIntegration as jest.Mock).mockReturnValue(request);

View file

@ -22,11 +22,15 @@ export const createReactQueryResponse = <TData = unknown, TError = unknown>({
data = undefined,
}: CreateReactQueryResponseInput<TData, TError> = {}): Partial<UseQueryResult<TData, TError>> => {
if (status === 'success') {
return { status, data };
return { status, data, isSuccess: true, isLoading: false, isError: false };
}
if (status === 'error') {
return { status, error };
return { status, error, isSuccess: false, isLoading: false, isError: true };
}
if (status === 'loading') {
return { status, data: undefined, isSuccess: false, isLoading: true, isError: false };
}
return { status };

View file

@ -13,7 +13,7 @@ import type {
QueryDslQueryContainer,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types';
import type { CloudPostureStats } from '../../../common/types';
import type { ComplianceDashboardData } from '../../../common/types';
import { CSP_KUBEBEAT_INDEX_PATTERN, STATS_ROUTE_PATH } from '../../../common/constants';
import { CspAppContext } from '../../plugin';
import { getResourcesTypes } from './get_resources_types';
@ -102,7 +102,7 @@ export const defineGetComplianceDashboardRoute = (
getClusters(esClient, query),
]);
const body: CloudPostureStats = {
const body: ComplianceDashboardData = {
stats,
resourcesTypes,
clusters,

View file

@ -51,7 +51,7 @@ const mockClusterBuckets: ClusterBucket[] = [
];
describe('getClustersFromAggs', () => {
it('should return value matching CloudPostureStats["clusters"]', async () => {
it('should return value matching ComplianceDashboardData["clusters"]', async () => {
const clusters = getClustersFromAggs(mockClusterBuckets);
expect(clusters).toEqual([
{

View file

@ -11,7 +11,7 @@ import type {
QueryDslQueryContainer,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { CloudPostureStats } from '../../../common/types';
import { ComplianceDashboardData } from '../../../common/types';
import { getResourceTypeFromAggs, resourceTypeAggQuery } from './get_resources_types';
import type { ResourceTypeQueryResult } from './get_resources_types';
import { CSP_KUBEBEAT_INDEX_PATTERN } from '../../../common/constants';
@ -66,7 +66,9 @@ export const getClustersQuery = (query: QueryDslQueryContainer): SearchRequest =
},
});
export const getClustersFromAggs = (clusters: ClusterBucket[]): CloudPostureStats['clusters'] =>
export const getClustersFromAggs = (
clusters: ClusterBucket[]
): ComplianceDashboardData['clusters'] =>
clusters.map((cluster) => {
// get cluster's meta data
const benchmarks = cluster.benchmarks.buckets;
@ -101,7 +103,7 @@ export const getClustersFromAggs = (clusters: ClusterBucket[]): CloudPostureStat
export const getClusters = async (
esClient: ElasticsearchClient,
query: QueryDslQueryContainer
): Promise<CloudPostureStats['clusters']> => {
): Promise<ComplianceDashboardData['clusters']> => {
const queryResult = await esClient.search<unknown, ClustersQueryResult>(getClustersQuery(query), {
meta: true,
});

View file

@ -31,7 +31,7 @@ const resourceTypeBuckets: ResourceTypeBucket[] = [
];
describe('getResourceTypeFromAggs', () => {
it('should return value matching CloudPostureStats["resourcesTypes"]', async () => {
it('should return value matching ComplianceDashboardData["resourcesTypes"]', async () => {
const resourceTypes = getResourceTypeFromAggs(resourceTypeBuckets);
expect(resourceTypes).toEqual([
{

View file

@ -6,12 +6,12 @@
*/
import { ElasticsearchClient } from 'kibana/server';
import {
import type {
AggregationsMultiBucketAggregateBase as Aggregation,
QueryDslQueryContainer,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { CloudPostureStats } from '../../../common/types';
import type { ComplianceDashboardData } from '../../../common/types';
import { KeyDocCount } from './compliance_dashboard';
import { CSP_KUBEBEAT_INDEX_PATTERN } from '../../../common/constants';
@ -53,7 +53,7 @@ export const getRisksEsQuery = (query: QueryDslQueryContainer): SearchRequest =>
export const getResourceTypeFromAggs = (
queryResult: ResourceTypeBucket[]
): CloudPostureStats['resourcesTypes'] =>
): ComplianceDashboardData['resourcesTypes'] =>
queryResult.map((bucket) => ({
name: bucket.key,
totalFindings: bucket.doc_count,
@ -64,7 +64,7 @@ export const getResourceTypeFromAggs = (
export const getResourcesTypes = async (
esClient: ElasticsearchClient,
query: QueryDslQueryContainer
): Promise<CloudPostureStats['resourcesTypes']> => {
): Promise<ComplianceDashboardData['resourcesTypes']> => {
const resourceTypesQueryResult = await esClient.search<unknown, ResourceTypeQueryResult>(
getRisksEsQuery(query),
{ meta: true }

View file

@ -59,7 +59,7 @@ describe('getStatsFromFindingsEvaluationsAggs', () => {
expect(score).toEqual(36.4);
});
it('should return value matching CloudPostureStats["stats"]', async () => {
it('should return value matching ComplianceDashboardData["stats"]', async () => {
const stats = getStatsFromFindingsEvaluationsAggs(standardQueryResult);
expect(stats).toEqual({
totalFailed: 30,

View file

@ -6,9 +6,9 @@
*/
import { ElasticsearchClient } from 'kibana/server';
import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import type { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import { CSP_KUBEBEAT_INDEX_PATTERN } from '../../../common/constants';
import { CloudPostureStats, Score } from '../../../common/types';
import type { ComplianceDashboardData, Score } from '../../../common/types';
/**
* @param value value is [0, 1] range
@ -44,7 +44,7 @@ export const getEvaluationsQuery = (query: QueryDslQueryContainer): SearchReques
export const getStatsFromFindingsEvaluationsAggs = (
findingsEvaluationsAggs: FindingsEvaluationsQueryResult
): CloudPostureStats['stats'] => {
): ComplianceDashboardData['stats'] => {
const failedFindings = findingsEvaluationsAggs.failed_findings.doc_count || 0;
const passedFindings = findingsEvaluationsAggs.passed_findings.doc_count || 0;
const totalFindings = failedFindings + passedFindings;
@ -62,7 +62,7 @@ export const getStatsFromFindingsEvaluationsAggs = (
export const getStats = async (
esClient: ElasticsearchClient,
query: QueryDslQueryContainer
): Promise<CloudPostureStats['stats']> => {
): Promise<ComplianceDashboardData['stats']> => {
const evaluationsQueryResult = await esClient.search<unknown, FindingsEvaluationsQueryResult>(
getEvaluationsQuery(query),
{ meta: true }