mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Posture] Support pagination in benchmarks page (#128486)
This commit is contained in:
parent
74a00fad20
commit
51e0845146
7 changed files with 213 additions and 78 deletions
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { type TypeOf, schema } from '@kbn/config-schema';
|
||||
|
||||
export const DEFAULT_BENCHMARKS_PER_PAGE = 20;
|
||||
export const BENCHMARK_PACKAGE_POLICY_PREFIX = 'package_policy.';
|
||||
export const benchmarksInputSchema = schema.object({
|
||||
/**
|
||||
* The page of objects to return
|
||||
*/
|
||||
page: schema.number({ defaultValue: 1, min: 1 }),
|
||||
/**
|
||||
* The number of objects to include in each page
|
||||
*/
|
||||
per_page: schema.number({ defaultValue: DEFAULT_BENCHMARKS_PER_PAGE, min: 0 }),
|
||||
/**
|
||||
* Once of PackagePolicy fields for sorting the found objects.
|
||||
* Sortable fields:
|
||||
* - package_policy.id
|
||||
* - package_policy.name
|
||||
* - package_policy.policy_id
|
||||
* - package_policy.namespace
|
||||
* - package_policy.updated_at
|
||||
* - package_policy.updated_by
|
||||
* - package_policy.created_at
|
||||
* - package_policy.created_by,
|
||||
* - package_policy.package.name
|
||||
* - package_policy.package.title
|
||||
* - package_policy.package.version
|
||||
*/
|
||||
sort_field: schema.oneOf(
|
||||
[
|
||||
schema.literal('package_policy.id'),
|
||||
schema.literal('package_policy.name'),
|
||||
schema.literal('package_policy.policy_id'),
|
||||
schema.literal('package_policy.namespace'),
|
||||
schema.literal('package_policy.updated_at'),
|
||||
schema.literal('package_policy.updated_by'),
|
||||
schema.literal('package_policy.created_at'),
|
||||
schema.literal('package_policy.created_by'),
|
||||
schema.literal('package_policy.package.name'),
|
||||
schema.literal('package_policy.package.title'),
|
||||
],
|
||||
{ defaultValue: 'package_policy.name' }
|
||||
),
|
||||
/**
|
||||
* The order to sort by
|
||||
*/
|
||||
sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], {
|
||||
defaultValue: 'asc',
|
||||
}),
|
||||
/**
|
||||
* Benchmark filter
|
||||
*/
|
||||
benchmark_name: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export type BenchmarksQuerySchema = TypeOf<typeof benchmarksInputSchema>;
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { UseQueryResult } from 'react-query/types/react/types';
|
||||
import { createStubDataView } from '../../../../../../src/plugins/data_views/public/data_views/data_view.stub';
|
||||
import { CSP_KUBEBEAT_INDEX_PATTERN } from '../../../common/constants';
|
||||
|
@ -14,7 +15,11 @@ import { createCspBenchmarkIntegrationFixture } from '../../test/fixtures/csp_be
|
|||
import { createReactQueryResponse } from '../../test/fixtures/react_query';
|
||||
import { TestProvider } from '../../test/test_provider';
|
||||
import { Benchmarks, BENCHMARKS_TABLE_DATA_TEST_SUBJ } from './benchmarks';
|
||||
import { ADD_A_CIS_INTEGRATION, BENCHMARK_INTEGRATIONS } from './translations';
|
||||
import {
|
||||
ADD_A_CIS_INTEGRATION,
|
||||
BENCHMARK_INTEGRATIONS,
|
||||
TABLE_COLUMN_HEADERS,
|
||||
} from './translations';
|
||||
import { useCspBenchmarkIntegrations } from './use_csp_benchmark_integrations';
|
||||
|
||||
jest.mock('./use_csp_benchmark_integrations');
|
||||
|
@ -77,4 +82,68 @@ describe('<Benchmarks />', () => {
|
|||
|
||||
expect(screen.getByTestId(BENCHMARKS_TABLE_DATA_TEST_SUBJ)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports sorting the table by integrations', () => {
|
||||
renderBenchmarks(
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: [createCspBenchmarkIntegrationFixture()],
|
||||
})
|
||||
);
|
||||
|
||||
// The table is sorted by integrations ascending by default, asserting that
|
||||
const sortedHeaderAscending = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((element) => element.getAttribute('aria-sort') === 'ascending');
|
||||
|
||||
expect(sortedHeaderAscending).toBeInTheDocument();
|
||||
expect(
|
||||
within(sortedHeaderAscending!).getByText(TABLE_COLUMN_HEADERS.INTEGRATION)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// A click should now sort it by descending
|
||||
userEvent.click(screen.getByText(TABLE_COLUMN_HEADERS.INTEGRATION));
|
||||
|
||||
const sortedHeaderDescending = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((element) => element.getAttribute('aria-sort') === 'descending');
|
||||
expect(sortedHeaderDescending).toBeInTheDocument();
|
||||
expect(
|
||||
within(sortedHeaderDescending!).getByText(TABLE_COLUMN_HEADERS.INTEGRATION)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports sorting the table by integration type, created by, and created at columns', () => {
|
||||
renderBenchmarks(
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: [createCspBenchmarkIntegrationFixture()],
|
||||
})
|
||||
);
|
||||
|
||||
[
|
||||
TABLE_COLUMN_HEADERS.INTEGRATION_TYPE,
|
||||
TABLE_COLUMN_HEADERS.CREATED_AT,
|
||||
TABLE_COLUMN_HEADERS.CREATED_AT,
|
||||
].forEach((columnHeader) => {
|
||||
const headerTextElement = screen.getByText(columnHeader);
|
||||
expect(headerTextElement).toBeInTheDocument();
|
||||
|
||||
// Click on the header element to sort the column in ascending order
|
||||
userEvent.click(headerTextElement!);
|
||||
|
||||
const sortedHeaderAscending = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((element) => element.getAttribute('aria-sort') === 'ascending');
|
||||
expect(within(sortedHeaderAscending!).getByText(columnHeader)).toBeInTheDocument();
|
||||
|
||||
// Click on the header element again to sort the column in descending order
|
||||
userEvent.click(headerTextElement!);
|
||||
|
||||
const sortedHeaderDescending = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((element) => element.getAttribute('aria-sort') === 'descending');
|
||||
expect(within(sortedHeaderDescending!).getByText(columnHeader)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,10 @@ import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_c
|
|||
import { CspPageTemplate } from '../../components/page_template';
|
||||
import { BenchmarksTable } from './benchmarks_table';
|
||||
import { ADD_A_CIS_INTEGRATION, BENCHMARK_INTEGRATIONS } from './translations';
|
||||
import { useCspBenchmarkIntegrations } from './use_csp_benchmark_integrations';
|
||||
import {
|
||||
useCspBenchmarkIntegrations,
|
||||
UseCspBenchmarkIntegrationsProps,
|
||||
} from './use_csp_benchmark_integrations';
|
||||
import { extractErrorMessage } from '../../../common/utils/helpers';
|
||||
import { SEARCH_PLACEHOLDER } from './translations';
|
||||
|
||||
|
@ -118,7 +121,13 @@ const PAGE_HEADER: EuiPageHeaderProps = {
|
|||
};
|
||||
|
||||
export const Benchmarks = () => {
|
||||
const [query, setQuery] = useState({ name: '', page: 1, perPage: 5 });
|
||||
const [query, setQuery] = useState<UseCspBenchmarkIntegrationsProps>({
|
||||
name: '',
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
sortField: 'package_policy.name',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
const queryResult = useCspBenchmarkIntegrations(query);
|
||||
|
||||
|
@ -129,7 +138,7 @@ export const Benchmarks = () => {
|
|||
return (
|
||||
<CspPageTemplate pageHeader={PAGE_HEADER}>
|
||||
<BenchmarkSearchField
|
||||
isLoading={queryResult.isLoading}
|
||||
isLoading={queryResult.isFetching}
|
||||
onSearch={(name) => setQuery((current) => ({ ...current, name }))}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
|
@ -142,13 +151,25 @@ export const Benchmarks = () => {
|
|||
benchmarks={queryResult.data?.items || []}
|
||||
data-test-subj={BENCHMARKS_TABLE_DATA_TEST_SUBJ}
|
||||
error={queryResult.error ? extractErrorMessage(queryResult.error) : undefined}
|
||||
loading={queryResult.isLoading}
|
||||
loading={queryResult.isFetching}
|
||||
pageIndex={query.page}
|
||||
pageSize={query.perPage}
|
||||
sorting={{
|
||||
// @ts-expect-error - EUI types currently do not support sorting by nested fields
|
||||
sort: { field: query.sortField, direction: query.sortOrder },
|
||||
allowNeutralSort: false,
|
||||
}}
|
||||
totalItemCount={totalItemCount}
|
||||
setQuery={({ page }) =>
|
||||
setQuery((current) => ({ ...current, page: page.index, perPage: page.size }))
|
||||
}
|
||||
setQuery={({ page, sort }) => {
|
||||
setQuery((current) => ({
|
||||
...current,
|
||||
page: page.index,
|
||||
perPage: page.size,
|
||||
sortField:
|
||||
(sort?.field as UseCspBenchmarkIntegrationsProps['sortField']) || current.sortField,
|
||||
sortOrder: sort?.direction || current.sortOrder,
|
||||
}));
|
||||
}}
|
||||
noItemsMessage={
|
||||
queryResult.isSuccess && !queryResult.data.total ? (
|
||||
<BenchmarkEmptyState name={query.name} />
|
||||
|
|
|
@ -22,7 +22,7 @@ import { useKibana } from '../../common/hooks/use_kibana';
|
|||
import { allNavigationItems } from '../../common/navigation/constants';
|
||||
|
||||
interface BenchmarksTableProps
|
||||
extends Pick<EuiBasicTableProps<Benchmark>, 'loading' | 'error' | 'noItemsMessage'>,
|
||||
extends Pick<EuiBasicTableProps<Benchmark>, 'loading' | 'error' | 'noItemsMessage' | 'sorting'>,
|
||||
Pagination {
|
||||
benchmarks: Benchmark[];
|
||||
setQuery(pagination: CriteriaWithPagination<Benchmark>): void;
|
||||
|
@ -66,12 +66,14 @@ const BENCHMARKS_TABLE_COLUMNS: Array<EuiBasicTableColumn<Benchmark>> = [
|
|||
</Link>
|
||||
),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'package_policy.package.title',
|
||||
name: TABLE_COLUMN_HEADERS.INTEGRATION_TYPE,
|
||||
dataType: 'string',
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'agent_policy.name',
|
||||
|
@ -91,6 +93,7 @@ const BENCHMARKS_TABLE_COLUMNS: Array<EuiBasicTableColumn<Benchmark>> = [
|
|||
name: TABLE_COLUMN_HEADERS.CREATED_BY,
|
||||
dataType: 'string',
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'package_policy.created_at',
|
||||
|
@ -98,6 +101,7 @@ const BENCHMARKS_TABLE_COLUMNS: Array<EuiBasicTableColumn<Benchmark>> = [
|
|||
dataType: 'date',
|
||||
truncateText: true,
|
||||
render: (date: Benchmark['package_policy']['created_at']) => moment(date).fromNow(),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'package_policy.rules', // TODO: add fields
|
||||
|
@ -117,6 +121,7 @@ export const BenchmarksTable = ({
|
|||
error,
|
||||
setQuery,
|
||||
noItemsMessage,
|
||||
sorting,
|
||||
...rest
|
||||
}: BenchmarksTableProps) => {
|
||||
const history = useHistory();
|
||||
|
@ -137,9 +142,8 @@ export const BenchmarksTable = ({
|
|||
totalItemCount,
|
||||
};
|
||||
|
||||
const onChange = ({ page }: CriteriaWithPagination<Benchmark>) => {
|
||||
if (!page) return;
|
||||
setQuery({ page: { ...page, index: page.index + 1 } });
|
||||
const onChange = ({ page, sort }: CriteriaWithPagination<Benchmark>) => {
|
||||
setQuery({ page: { ...page, index: page.index + 1 }, sort });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -155,6 +159,7 @@ export const BenchmarksTable = ({
|
|||
loading={loading}
|
||||
noItemsMessage={noItemsMessage}
|
||||
error={error}
|
||||
sorting={sorting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,28 +8,39 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import type { ListResult } from '../../../../fleet/common';
|
||||
import { BENCHMARKS_ROUTE_PATH } from '../../../common/constants';
|
||||
import { BenchmarksQuerySchema } from '../../../common/schemas/benchmark';
|
||||
import { useKibana } from '../../common/hooks/use_kibana';
|
||||
import type { Benchmark } from '../../../common/types';
|
||||
|
||||
const QUERY_KEY = 'csp_benchmark_integrations';
|
||||
|
||||
interface Props {
|
||||
export interface UseCspBenchmarkIntegrationsProps {
|
||||
name: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
sortField: BenchmarksQuerySchema['sort_field'];
|
||||
sortOrder: BenchmarksQuerySchema['sort_order'];
|
||||
}
|
||||
|
||||
export const useCspBenchmarkIntegrations = ({ name, perPage, page }: Props) => {
|
||||
export const useCspBenchmarkIntegrations = ({
|
||||
name,
|
||||
perPage,
|
||||
page,
|
||||
sortField,
|
||||
sortOrder,
|
||||
}: UseCspBenchmarkIntegrationsProps) => {
|
||||
const { http } = useKibana().services;
|
||||
return useQuery([QUERY_KEY, { name, perPage, page }], () =>
|
||||
http.get<ListResult<Benchmark>>(BENCHMARKS_ROUTE_PATH, {
|
||||
query: {
|
||||
benchmark_name: name,
|
||||
per_page: perPage,
|
||||
page,
|
||||
sort_field: 'name',
|
||||
sort_order: 'asc',
|
||||
},
|
||||
})
|
||||
const query: BenchmarksQuerySchema = {
|
||||
benchmark_name: name,
|
||||
per_page: perPage,
|
||||
page,
|
||||
sort_field: sortField,
|
||||
sort_order: sortOrder,
|
||||
};
|
||||
|
||||
return useQuery(
|
||||
[QUERY_KEY, query],
|
||||
() => http.get<ListResult<Benchmark>>(BENCHMARKS_ROUTE_PATH, { query }),
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,9 +17,11 @@ import {
|
|||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { KibanaRequest } from 'src/core/server/http/router/request';
|
||||
import {
|
||||
defineGetBenchmarksRoute,
|
||||
benchmarksInputSchema,
|
||||
DEFAULT_BENCHMARKS_PER_PAGE,
|
||||
} from '../../../common/schemas/benchmark';
|
||||
import {
|
||||
defineGetBenchmarksRoute,
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
getPackagePolicies,
|
||||
getAgentPolicies,
|
||||
|
@ -84,7 +86,7 @@ describe('benchmarks API', () => {
|
|||
};
|
||||
defineGetBenchmarksRoute(router, cspContext);
|
||||
|
||||
const [config, _] = router.get.mock.calls[0];
|
||||
const [config] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toEqual('/api/csp/benchmarks');
|
||||
});
|
||||
|
@ -180,7 +182,7 @@ describe('benchmarks API', () => {
|
|||
|
||||
it('should not throw when sort_field is a string', async () => {
|
||||
expect(() => {
|
||||
benchmarksInputSchema.validate({ sort_field: 'name' });
|
||||
benchmarksInputSchema.validate({ sort_field: 'package_policy.name' });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
|
@ -204,7 +206,7 @@ describe('benchmarks API', () => {
|
|||
|
||||
it('should not throw when fields is a known string literal', async () => {
|
||||
expect(() => {
|
||||
benchmarksInputSchema.validate({ sort_field: 'name' });
|
||||
benchmarksInputSchema.validate({ sort_field: 'package_policy.name' });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
|
@ -240,7 +242,7 @@ describe('benchmarks API', () => {
|
|||
await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', {
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
sort_field: 'name',
|
||||
sort_field: 'package_policy.name',
|
||||
sort_order: 'desc',
|
||||
});
|
||||
|
||||
|
@ -261,7 +263,7 @@ describe('benchmarks API', () => {
|
|||
await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', {
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
sort_field: 'name',
|
||||
sort_field: 'package_policy.name',
|
||||
sort_order: 'asc',
|
||||
});
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { uniq, map } from 'lodash';
|
||||
import type { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { schema as rt, TypeOf } from '@kbn/config-schema';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type {
|
||||
PackagePolicyServiceInterface,
|
||||
|
@ -20,14 +20,16 @@ import type {
|
|||
ListResult,
|
||||
} from '../../../../fleet/common';
|
||||
import { BENCHMARKS_ROUTE_PATH, CIS_KUBERNETES_PACKAGE_NAME } from '../../../common/constants';
|
||||
import {
|
||||
BENCHMARK_PACKAGE_POLICY_PREFIX,
|
||||
benchmarksInputSchema,
|
||||
BenchmarksQuerySchema,
|
||||
} from '../../../common/schemas/benchmark';
|
||||
import { CspAppContext } from '../../plugin';
|
||||
import type { Benchmark } from '../../../common/types';
|
||||
import { isNonNullable } from '../../../common/utils/helpers';
|
||||
import { CspRouter } from '../../types';
|
||||
|
||||
type BenchmarksQuerySchema = TypeOf<typeof benchmarksInputSchema>;
|
||||
|
||||
export const DEFAULT_BENCHMARKS_PER_PAGE = 20;
|
||||
export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies';
|
||||
|
||||
const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => {
|
||||
|
@ -43,17 +45,21 @@ export const getPackagePolicies = (
|
|||
soClient: SavedObjectsClientContract,
|
||||
packagePolicyService: PackagePolicyServiceInterface,
|
||||
packageName: string,
|
||||
queryParams: BenchmarksQuerySchema
|
||||
queryParams: Partial<BenchmarksQuerySchema>
|
||||
): Promise<ListResult<PackagePolicy>> => {
|
||||
if (!packagePolicyService) {
|
||||
throw new Error('packagePolicyService is undefined');
|
||||
}
|
||||
|
||||
const sortField = queryParams.sort_field?.startsWith(BENCHMARK_PACKAGE_POLICY_PREFIX)
|
||||
? queryParams.sort_field.substring(BENCHMARK_PACKAGE_POLICY_PREFIX.length)
|
||||
: queryParams.sort_field;
|
||||
|
||||
return packagePolicyService?.list(soClient, {
|
||||
kuery: getPackageNameQuery(packageName, queryParams.benchmark_name),
|
||||
page: queryParams.page,
|
||||
perPage: queryParams.per_page,
|
||||
sortField: queryParams.sort_field,
|
||||
sortField,
|
||||
sortOrder: queryParams.sort_order,
|
||||
});
|
||||
};
|
||||
|
@ -187,44 +193,3 @@ export const defineGetBenchmarksRoute = (router: CspRouter, cspContext: CspAppCo
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const benchmarksInputSchema = rt.object({
|
||||
/**
|
||||
* The page of objects to return
|
||||
*/
|
||||
page: rt.number({ defaultValue: 1, min: 1 }),
|
||||
/**
|
||||
* The number of objects to include in each page
|
||||
*/
|
||||
per_page: rt.number({ defaultValue: DEFAULT_BENCHMARKS_PER_PAGE, min: 0 }),
|
||||
/**
|
||||
* Once of PackagePolicy fields for sorting the found objects.
|
||||
* Sortable fields: id, name, policy_id, namespace, updated_at, updated_by, created_at, created_by,
|
||||
* package.name, package.title, package.version
|
||||
*/
|
||||
sort_field: rt.maybe(
|
||||
rt.oneOf(
|
||||
[
|
||||
rt.literal('id'),
|
||||
rt.literal('name'),
|
||||
rt.literal('policy_id'),
|
||||
rt.literal('namespace'),
|
||||
rt.literal('updated_at'),
|
||||
rt.literal('updated_by'),
|
||||
rt.literal('created_at'),
|
||||
rt.literal('created_by'),
|
||||
rt.literal('package.name'),
|
||||
rt.literal('package.title'),
|
||||
],
|
||||
{ defaultValue: 'name' }
|
||||
)
|
||||
),
|
||||
/**
|
||||
* The order to sort by
|
||||
*/
|
||||
sort_order: rt.oneOf([rt.literal('asc'), rt.literal('desc')], { defaultValue: 'desc' }),
|
||||
/**
|
||||
* Benchmark filter
|
||||
*/
|
||||
benchmark_name: rt.maybe(rt.string()),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue