mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.4] [Kubernetes Security] Tree nav group by both cluster id and name (#137858)
* Tree nav group by both cluster id and name * Update and render cluster name correctly while only using cluster id in filters * Add more tests to tree view breadcrumb * Add multi terms aggregate route tests
This commit is contained in:
parent
d84fa4406b
commit
ccdb68a90a
21 changed files with 783 additions and 58 deletions
|
@ -11,6 +11,7 @@ export const LOCAL_STORAGE_HIDE_WIDGETS_KEY = 'kubernetesSecurity:shouldHideWidg
|
|||
|
||||
export const AGGREGATE_ROUTE = '/internal/kubernetes_security/aggregate';
|
||||
export const COUNT_ROUTE = '/internal/kubernetes_security/count';
|
||||
export const MULTI_TERMS_AGGREGATE_ROUTE = '/internal/kubernetes_security/multi_terms_aggregate';
|
||||
export const AGGREGATE_PAGE_SIZE = 10;
|
||||
|
||||
// so, bucket sort can only page through what we request at the top level agg, which means there is a ceiling to how many aggs we can page through.
|
||||
|
@ -28,11 +29,12 @@ export const ENTRY_LEADER_INTERACTIVE = 'process.entry_leader.interactive';
|
|||
export const ENTRY_LEADER_USER_ID = 'process.entry_leader.user.id';
|
||||
export const ENTRY_LEADER_ENTITY_ID = 'process.entry_leader.entity_id';
|
||||
|
||||
export const ORCHESTRATOR_CLUSTER_ID = 'orchestrator.cluster.name';
|
||||
export const ORCHESTRATOR_CLUSTER_ID = 'orchestrator.cluster.id';
|
||||
export const ORCHESTRATOR_CLUSTER_NAME = 'orchestrator.cluster.name';
|
||||
export const ORCHESTRATOR_NAMESPACE = 'orchestrator.namespace';
|
||||
export const CLOUD_INSTANCE_NAME = 'cloud.instance.name';
|
||||
export const ORCHESTRATOR_RESOURCE_ID = 'orchestrator.resource.name';
|
||||
export const CONTAINER_IMAGE_NAME = 'container.image.name';
|
||||
export const CLOUD_INSTANCE_NAME = 'cloud.instance.name';
|
||||
|
||||
export const COUNT_WIDGET_KEY_CLUSTERS = 'CountClustersWidget';
|
||||
export const COUNT_WIDGET_KEY_NAMESPACE = 'CountNamespaceWidgets';
|
||||
|
|
|
@ -10,18 +10,19 @@ interface Aggregate {
|
|||
doc_count: number;
|
||||
}
|
||||
|
||||
interface Buckets extends Aggregate {
|
||||
interface Bucket extends Aggregate {
|
||||
key_as_string?: string;
|
||||
count_by_aggs: {
|
||||
count_by_aggs?: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AggregateResult {
|
||||
buckets: Buckets[];
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface AggregateBucketPaginationResult {
|
||||
buckets: Aggregate[];
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface MultiTermsAggregateGroupBy {
|
||||
field: string;
|
||||
maybe?: string;
|
||||
}
|
||||
|
||||
interface MultiTermsAggregate {
|
||||
key: Array<string | number | boolean>;
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
export interface Bucket extends MultiTermsAggregate {
|
||||
key_as_string?: string;
|
||||
count_by_aggs?: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MultiTermsAggregateResult {
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface MultiTermsAggregateBucketPaginationResult {
|
||||
buckets: Bucket[];
|
||||
hasNextPage: boolean;
|
||||
}
|
|
@ -171,7 +171,7 @@ export const ContainerNameWidget = ({
|
|||
return aggsData?.buckets.map((aggData) => {
|
||||
return {
|
||||
name: aggData.key as string,
|
||||
count: addCommasToNumber(aggData.count_by_aggs.value),
|
||||
count: addCommasToNumber(aggData.count_by_aggs?.value ?? 0),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
|
|
@ -71,7 +71,7 @@ const KubernetesSecurityRoutesComponent = ({
|
|||
(result: AggregateResult): Record<string, number> =>
|
||||
result.buckets.reduce((groupedByKeyValue, aggregate) => {
|
||||
groupedByKeyValue[aggregate.key_as_string || (aggregate.key.toString() as string)] =
|
||||
aggregate.count_by_aggs.value;
|
||||
aggregate.count_by_aggs?.value ?? 0;
|
||||
return groupedByKeyValue;
|
||||
}, {} as Record<string, number>),
|
||||
[]
|
||||
|
@ -81,10 +81,10 @@ const KubernetesSecurityRoutesComponent = ({
|
|||
(result: AggregateResult): Record<string, number> =>
|
||||
result.buckets.reduce((groupedByKeyValue, aggregate) => {
|
||||
if (aggregate.key.toString() === '0') {
|
||||
groupedByKeyValue[aggregate.key] = aggregate.count_by_aggs.value;
|
||||
groupedByKeyValue[aggregate.key] = aggregate.count_by_aggs?.value ?? 0;
|
||||
} else {
|
||||
groupedByKeyValue.nonRoot =
|
||||
(groupedByKeyValue.nonRoot || 0) + aggregate.count_by_aggs.value;
|
||||
(groupedByKeyValue.nonRoot || 0) + (aggregate.count_by_aggs?.value ?? 0);
|
||||
}
|
||||
return groupedByKeyValue;
|
||||
}, {} as Record<string, number>),
|
||||
|
|
|
@ -22,7 +22,7 @@ Object {
|
|||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected cluster
|
||||
selected cluster name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
@ -113,7 +113,7 @@ Object {
|
|||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected cluster
|
||||
selected cluster name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
@ -261,7 +261,7 @@ Object {
|
|||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected cluster
|
||||
selected cluster name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
@ -330,7 +330,7 @@ Object {
|
|||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected cluster
|
||||
selected cluster name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
@ -433,3 +433,242 @@ Object {
|
|||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Tree view Breadcrumb component When Breadcrumb is mounted returns cluster id when no cluster name is provided 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
color="success"
|
||||
data-euiicon-type="heatmap"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected cluster id
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
color="primary"
|
||||
data-euiicon-type="nested"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected namespace
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
color="warning"
|
||||
data-euiicon-type="package"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected pod
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
color="danger"
|
||||
data-euiicon-type="image"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected image
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
css="[object Object]"
|
||||
>
|
||||
<span
|
||||
color="success"
|
||||
data-euiicon-type="heatmap"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected cluster id
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
color="primary"
|
||||
data-euiicon-type="nested"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected namespace
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
color="warning"
|
||||
data-euiicon-type="package"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected pod
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
class="emotion-EuiIcon"
|
||||
data-euiicon-type="arrowRight"
|
||||
/>
|
||||
<span
|
||||
color="danger"
|
||||
data-euiicon-type="image"
|
||||
/>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--text emotion-EuiButtonEmpty"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
selected image
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -11,7 +11,8 @@ import { KubernetesCollection, TreeNavSelection } from '../../../types';
|
|||
import { Breadcrumb } from '.';
|
||||
|
||||
const MOCK_TREE_SELECTION: TreeNavSelection = {
|
||||
[KubernetesCollection.cluster]: 'selected cluster',
|
||||
[KubernetesCollection.clusterId]: 'selected cluster id',
|
||||
[KubernetesCollection.clusterName]: 'selected cluster name',
|
||||
[KubernetesCollection.namespace]: 'selected namespace',
|
||||
[KubernetesCollection.node]: 'selected node',
|
||||
[KubernetesCollection.pod]: 'selected pod',
|
||||
|
@ -39,7 +40,7 @@ describe('Tree view Breadcrumb component', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.cluster]!)
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.clusterName]!)
|
||||
).toBeVisible();
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.namespace]!)
|
||||
|
@ -60,10 +61,38 @@ describe('Tree view Breadcrumb component', () => {
|
|||
expect(renderResult.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('returns cluster id when no cluster name is provided', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{
|
||||
...MOCK_TREE_SELECTION,
|
||||
[KubernetesCollection.clusterName]: undefined,
|
||||
[KubernetesCollection.node]: undefined,
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.clusterId]!)
|
||||
).toBeVisible();
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.namespace]!)
|
||||
).toBeVisible();
|
||||
expect(renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.node]!)).toBeFalsy();
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.pod]!)
|
||||
).toBeVisible();
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.containerImage]!)
|
||||
).toBeVisible();
|
||||
expect(renderResult).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('returns null when no cluster in selection', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{ ...MOCK_TREE_SELECTION, [KubernetesCollection.cluster]: undefined }}
|
||||
treeNavSelection={{ ...MOCK_TREE_SELECTION, [KubernetesCollection.clusterId]: undefined }}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
@ -71,11 +100,25 @@ describe('Tree view Breadcrumb component', () => {
|
|||
expect(renderResult.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('clicking on breadcrumb item triggers onSelect', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{ ...MOCK_TREE_SELECTION, [KubernetesCollection.node]: undefined }}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
renderResult.getByText(MOCK_TREE_SELECTION[KubernetesCollection.clusterName]!).click();
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders provided collections only', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<Breadcrumb
|
||||
treeNavSelection={{
|
||||
[KubernetesCollection.cluster]: MOCK_TREE_SELECTION[KubernetesCollection.cluster],
|
||||
[KubernetesCollection.clusterId]: MOCK_TREE_SELECTION[KubernetesCollection.clusterId],
|
||||
[KubernetesCollection.clusterName]:
|
||||
MOCK_TREE_SELECTION[KubernetesCollection.clusterName],
|
||||
[KubernetesCollection.node]: MOCK_TREE_SELECTION[KubernetesCollection.node],
|
||||
[KubernetesCollection.containerImage]:
|
||||
MOCK_TREE_SELECTION[KubernetesCollection.containerImage],
|
||||
|
@ -85,7 +128,7 @@ describe('Tree view Breadcrumb component', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.cluster]!)
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.clusterName]!)
|
||||
).toBeVisible();
|
||||
expect(
|
||||
renderResult.queryByText(MOCK_TREE_SELECTION[KubernetesCollection.namespace]!)
|
||||
|
|
|
@ -21,9 +21,10 @@ export const Breadcrumb = ({ treeNavSelection, onSelect }: BreadcrumbDeps) => {
|
|||
(collectionType: string) => {
|
||||
const selectionCopy = { ...treeNavSelection };
|
||||
switch (collectionType) {
|
||||
case KubernetesCollection.cluster: {
|
||||
case KubernetesCollection.clusterId: {
|
||||
onSelect({
|
||||
[KubernetesCollection.cluster]: treeNavSelection[KubernetesCollection.cluster],
|
||||
[KubernetesCollection.clusterId]: treeNavSelection[KubernetesCollection.clusterId],
|
||||
[KubernetesCollection.clusterName]: treeNavSelection[KubernetesCollection.clusterName],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -59,7 +60,10 @@ export const Breadcrumb = ({ treeNavSelection, onSelect }: BreadcrumbDeps) => {
|
|||
color="text"
|
||||
onClick={() => onBreadCrumbClick(collectionType)}
|
||||
>
|
||||
{treeNavSelection[collectionType]}
|
||||
{collectionType === KubernetesCollection.clusterId
|
||||
? treeNavSelection[KubernetesCollection.clusterName] ||
|
||||
treeNavSelection[KubernetesCollection.clusterId]
|
||||
: treeNavSelection[collectionType]}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
|
@ -72,14 +76,14 @@ export const Breadcrumb = ({ treeNavSelection, onSelect }: BreadcrumbDeps) => {
|
|||
]
|
||||
);
|
||||
|
||||
if (!treeNavSelection[KubernetesCollection.cluster]) {
|
||||
if (!treeNavSelection[KubernetesCollection.clusterId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={styles.breadcrumb}>
|
||||
{renderBreadcrumbLink(
|
||||
KubernetesCollection.cluster,
|
||||
KubernetesCollection.clusterId,
|
||||
<EuiIcon type="heatmap" color="success" />,
|
||||
!(
|
||||
treeNavSelection[KubernetesCollection.namespace] ||
|
||||
|
|
|
@ -8,8 +8,15 @@ import { useInfiniteQuery } from 'react-query';
|
|||
import { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { QueryDslQueryContainerBool } from '../../../types';
|
||||
import { QUERY_KEY_PROCESS_EVENTS, AGGREGATE_ROUTE } from '../../../../common/constants';
|
||||
import {
|
||||
QUERY_KEY_PROCESS_EVENTS,
|
||||
AGGREGATE_ROUTE,
|
||||
MULTI_TERMS_AGGREGATE_ROUTE,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
} from '../../../../common/constants';
|
||||
import { AggregateBucketPaginationResult } from '../../../../common/types/aggregate';
|
||||
import { Bucket } from '../../../../common/types/multi_terms_aggregate';
|
||||
import { KUBERNETES_COLLECTION_FIELDS } from '../helpers';
|
||||
|
||||
export const useFetchDynamicTreeView = (
|
||||
query: QueryDslQueryContainerBool,
|
||||
|
@ -22,8 +29,36 @@ export const useFetchDynamicTreeView = (
|
|||
|
||||
return useInfiniteQuery<AggregateBucketPaginationResult>(
|
||||
cachingKeys,
|
||||
async ({ pageParam = 0 }) =>
|
||||
await http.get<any>(AGGREGATE_ROUTE, {
|
||||
async ({ pageParam = 0 }) => {
|
||||
if (groupBy === KUBERNETES_COLLECTION_FIELDS.clusterId) {
|
||||
const { buckets } = await http.get<any>(MULTI_TERMS_AGGREGATE_ROUTE, {
|
||||
query: {
|
||||
query: JSON.stringify(query),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: groupBy,
|
||||
},
|
||||
{
|
||||
field: ORCHESTRATOR_CLUSTER_NAME,
|
||||
missing: '',
|
||||
},
|
||||
]),
|
||||
page: pageParam,
|
||||
perPage: 50,
|
||||
index,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
buckets: buckets.map((bucket: Bucket) => ({
|
||||
...bucket,
|
||||
key_as_string: bucket.key[1],
|
||||
key: bucket.key[0],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return await http.get<any>(AGGREGATE_ROUTE, {
|
||||
query: {
|
||||
query: JSON.stringify(query),
|
||||
groupBy,
|
||||
|
@ -31,7 +66,8 @@ export const useFetchDynamicTreeView = (
|
|||
perPage: 50,
|
||||
index,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled,
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.hasNextPage ? pages.length : undefined),
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
keys,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { KubernetesCollection } from '../../../types';
|
||||
import {
|
||||
TREE_NAVIGATION_LOADING,
|
||||
TREE_NAVIGATION_SHOW_MORE,
|
||||
|
@ -101,7 +102,12 @@ export const DynamicTreeView = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (!hasSelection && !depth && data && data.pages?.[0].buckets?.[0]?.key) {
|
||||
onSelect({}, data.pages[0].buckets[0].key, tree[depth].type);
|
||||
onSelect(
|
||||
{},
|
||||
tree[depth].type,
|
||||
data.pages[0].buckets[0].key,
|
||||
data.pages[0].buckets[0].key_as_string
|
||||
);
|
||||
}
|
||||
}, [data, depth, hasSelection, onSelect, tree]);
|
||||
|
||||
|
@ -221,18 +227,26 @@ const DynamicTreeViewItem = ({
|
|||
const styles = useStyles(depth);
|
||||
const buttonRef = useRef<Record<string, any>>({});
|
||||
|
||||
const handleSelect = () => {
|
||||
if (tree[depth].type === KubernetesCollection.clusterId) {
|
||||
onSelect(selectionDepth, tree[depth].type, aggData.key, aggData.key_as_string);
|
||||
} else {
|
||||
onSelect(selectionDepth, tree[depth].type, aggData.key);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyboardToggle = () => {
|
||||
if (!isLastNode) {
|
||||
onToggleExpand();
|
||||
}
|
||||
onSelect(selectionDepth, aggData.key, tree[depth].type);
|
||||
handleSelect();
|
||||
};
|
||||
|
||||
const onButtonToggle = () => {
|
||||
if (!isLastNode && !isExpanded) {
|
||||
onToggleExpand();
|
||||
}
|
||||
onSelect(selectionDepth, aggData.key, tree[depth].type);
|
||||
handleSelect();
|
||||
};
|
||||
|
||||
const onArrowToggle = (event: MouseEvent<SVGElement>) => {
|
||||
|
@ -309,7 +323,7 @@ const DynamicTreeViewItem = ({
|
|||
/>
|
||||
)}
|
||||
<EuiIcon {...tree[depth].iconProps} css={styles.labelIcon} />
|
||||
<span className="euiTreeView__nodeLabel">{aggData.key}</span>
|
||||
<span className="euiTreeView__nodeLabel">{aggData.key_as_string || aggData.key}</span>
|
||||
</button>
|
||||
<div
|
||||
onKeyDown={(event: React.KeyboardEvent) => onChildrenKeydown(event, aggData.key.toString())}
|
||||
|
@ -322,6 +336,9 @@ const DynamicTreeViewItem = ({
|
|||
selectionDepth={{
|
||||
...selectionDepth,
|
||||
[tree[depth].type]: aggData.key,
|
||||
...(tree[depth].type === KubernetesCollection.clusterId && {
|
||||
[KubernetesCollection.clusterName]: aggData.key_as_string,
|
||||
}),
|
||||
}}
|
||||
tree={tree}
|
||||
onSelect={onSelect}
|
||||
|
|
|
@ -12,7 +12,12 @@ export type DynamicTreeViewProps = {
|
|||
depth?: number;
|
||||
selectionDepth?: TreeNavSelection;
|
||||
query: QueryDslQueryContainerBool;
|
||||
onSelect: (selectionDepth: TreeNavSelection, key: string | number, type: string) => void;
|
||||
onSelect: (
|
||||
selectionDepth: TreeNavSelection,
|
||||
type: string,
|
||||
key: string | number,
|
||||
clusterName?: string
|
||||
) => void;
|
||||
hasSelection?: boolean;
|
||||
'aria-label': string;
|
||||
selected?: string;
|
||||
|
|
|
@ -5,15 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_QUERY } from '../../../common/constants';
|
||||
import {
|
||||
CLOUD_INSTANCE_NAME,
|
||||
CONTAINER_IMAGE_NAME,
|
||||
DEFAULT_QUERY,
|
||||
ORCHESTRATOR_CLUSTER_ID,
|
||||
ORCHESTRATOR_CLUSTER_NAME,
|
||||
ORCHESTRATOR_NAMESPACE,
|
||||
ORCHESTRATOR_RESOURCE_ID,
|
||||
} from '../../../common/constants';
|
||||
import { KubernetesCollection, QueryDslQueryContainerBool, TreeNavSelection } from '../../types';
|
||||
|
||||
export const KUBERNETES_COLLECTION_FIELDS = {
|
||||
[KubernetesCollection.cluster]: 'orchestrator.cluster.name',
|
||||
[KubernetesCollection.namespace]: 'orchestrator.namespace',
|
||||
[KubernetesCollection.node]: 'cloud.instance.name',
|
||||
[KubernetesCollection.pod]: 'orchestrator.resource.name',
|
||||
[KubernetesCollection.containerImage]: 'container.image.name',
|
||||
[KubernetesCollection.clusterId]: ORCHESTRATOR_CLUSTER_ID,
|
||||
[KubernetesCollection.clusterName]: ORCHESTRATOR_CLUSTER_NAME,
|
||||
[KubernetesCollection.namespace]: ORCHESTRATOR_NAMESPACE,
|
||||
[KubernetesCollection.node]: CLOUD_INSTANCE_NAME,
|
||||
[KubernetesCollection.pod]: ORCHESTRATOR_RESOURCE_ID,
|
||||
[KubernetesCollection.containerImage]: CONTAINER_IMAGE_NAME,
|
||||
};
|
||||
|
||||
export const addTreeNavSelectionToFilterQuery = (
|
||||
|
@ -28,20 +37,22 @@ export const addTreeNavSelectionToFilterQuery = (
|
|||
throw new Error('Invalid filter query');
|
||||
}
|
||||
parsedFilterQuery.bool.filter.push(
|
||||
...Object.keys(treeNavSelection).map((collectionKey) => {
|
||||
const collection = collectionKey as KubernetesCollection;
|
||||
return {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
[KUBERNETES_COLLECTION_FIELDS[collection]]: treeNavSelection[collection],
|
||||
...Object.keys(treeNavSelection)
|
||||
.filter((key) => key !== KubernetesCollection.clusterName)
|
||||
.map((collectionKey) => {
|
||||
const collection = collectionKey as KubernetesCollection;
|
||||
return {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
[KUBERNETES_COLLECTION_FIELDS[collection]]: treeNavSelection[collection],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
})
|
||||
],
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
validFilterQuery = JSON.stringify(parsedFilterQuery);
|
||||
} catch {
|
||||
|
|
|
@ -48,7 +48,7 @@ export const useTreeView = ({ globalFilter, indexPattern }: UseTreeViewProps) =>
|
|||
}, [filterQueryWithTimeRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!treeNavSelection[KubernetesCollection.cluster]) {
|
||||
if (!!treeNavSelection[KubernetesCollection.clusterId]) {
|
||||
setHasSelection(true);
|
||||
setTreeNavSelection(treeNavSelection);
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ import { translations } from './translations';
|
|||
|
||||
const LOGICAL_TREE_VIEW: DynamicTree[] = [
|
||||
{
|
||||
key: KUBERNETES_COLLECTION_FIELDS.cluster,
|
||||
key: KUBERNETES_COLLECTION_FIELDS.clusterId,
|
||||
iconProps: { type: 'heatmap', color: 'success' },
|
||||
type: KubernetesCollection.cluster,
|
||||
type: KubernetesCollection.clusterId,
|
||||
name: translations.cluster(),
|
||||
namePlural: translations.cluster(true),
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { KubernetesCollection } from '../../../types';
|
||||
import {
|
||||
TREE_VIEW_INFRASTRUCTURE_VIEW,
|
||||
TREE_VIEW_LOGICAL_VIEW,
|
||||
|
@ -117,10 +118,13 @@ export const TreeNav = () => {
|
|||
tree={tree}
|
||||
aria-label={selectedLabel}
|
||||
selected={selected}
|
||||
onSelect={(selectionDepth, key, type) => {
|
||||
onSelect={(selectionDepth, type, key, clusterName) => {
|
||||
const newSelectionDepth = {
|
||||
...selectionDepth,
|
||||
[type]: key,
|
||||
...(clusterName && {
|
||||
[KubernetesCollection.clusterName]: clusterName,
|
||||
}),
|
||||
};
|
||||
setSelected(
|
||||
Object.entries(newSelectionDepth)
|
||||
|
|
|
@ -48,7 +48,8 @@ export type QueryDslQueryContainerBool = {
|
|||
};
|
||||
|
||||
export enum KubernetesCollection {
|
||||
cluster = 'cluster',
|
||||
clusterId = 'clusterId',
|
||||
clusterName = 'clusterName',
|
||||
namespace = 'namespace',
|
||||
node = 'node',
|
||||
pod = 'pod',
|
||||
|
@ -56,7 +57,8 @@ export enum KubernetesCollection {
|
|||
}
|
||||
|
||||
export interface TreeNavSelection {
|
||||
[KubernetesCollection.cluster]?: string;
|
||||
[KubernetesCollection.clusterId]?: string;
|
||||
[KubernetesCollection.clusterName]?: string;
|
||||
[KubernetesCollection.namespace]?: string;
|
||||
[KubernetesCollection.node]?: string;
|
||||
[KubernetesCollection.pod]?: string;
|
||||
|
|
|
@ -8,8 +8,10 @@ import { IRouter } from '@kbn/core/server';
|
|||
import { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||
import { registerAggregateRoute } from './aggregate';
|
||||
import { registerCountRoute } from './count';
|
||||
import { registerMultiTermsAggregateRoute } from './multi_terms_aggregate';
|
||||
|
||||
export const registerRoutes = (router: IRouter, ruleRegistry: RuleRegistryPluginStartContract) => {
|
||||
registerAggregateRoute(router);
|
||||
registerCountRoute(router);
|
||||
registerMultiTermsAggregateRoute(router);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { PROCESS_EVENTS_INDEX } from '@kbn/session-view-plugin/common/constants';
|
||||
import { MULTI_TERMS_AGGREGATE_ROUTE, AGGREGATE_PAGE_SIZE } from '../../common/constants';
|
||||
import {
|
||||
MultiTermsAggregateGroupBy,
|
||||
MultiTermsAggregateBucketPaginationResult,
|
||||
} from '../../common/types/multi_terms_aggregate';
|
||||
|
||||
export const registerMultiTermsAggregateRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
{
|
||||
path: MULTI_TERMS_AGGREGATE_ROUTE,
|
||||
validate: {
|
||||
query: schema.object({
|
||||
query: schema.string(),
|
||||
countBy: schema.maybe(schema.string()),
|
||||
groupBys: schema.arrayOf(
|
||||
schema.object({
|
||||
field: schema.string(),
|
||||
missing: schema.maybe(schema.string()),
|
||||
}),
|
||||
{ defaultValue: [] }
|
||||
),
|
||||
page: schema.number(),
|
||||
perPage: schema.maybe(schema.number()),
|
||||
index: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const client = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const { query, countBy, groupBys, page, perPage, index } = request.query;
|
||||
|
||||
try {
|
||||
const body = await doSearch(client, query, groupBys, page, perPage, index, countBy);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (err) {
|
||||
return response.badRequest(err.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const doSearch = async (
|
||||
client: ElasticsearchClient,
|
||||
query: string,
|
||||
groupBys: MultiTermsAggregateGroupBy[],
|
||||
page: number, // zero based
|
||||
perPage = AGGREGATE_PAGE_SIZE,
|
||||
index?: string,
|
||||
countBy?: string
|
||||
): Promise<MultiTermsAggregateBucketPaginationResult> => {
|
||||
const queryDSL = JSON.parse(query);
|
||||
|
||||
const countByAggs = countBy
|
||||
? {
|
||||
count_by_aggs: {
|
||||
cardinality: {
|
||||
field: countBy,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const search = await client.search({
|
||||
index: [index || PROCESS_EVENTS_INDEX],
|
||||
body: {
|
||||
query: queryDSL,
|
||||
size: 0,
|
||||
aggs: {
|
||||
custom_agg: {
|
||||
multi_terms: {
|
||||
terms: groupBys,
|
||||
},
|
||||
aggs: {
|
||||
...countByAggs,
|
||||
bucket_sort: {
|
||||
bucket_sort: {
|
||||
size: perPage + 1, // check if there's a "next page"
|
||||
from: perPage * page,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const agg: any = search.aggregations?.custom_agg;
|
||||
const buckets = agg?.buckets || [];
|
||||
|
||||
const hasNextPage = buckets.length > perPage;
|
||||
|
||||
if (hasNextPage) {
|
||||
buckets.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
buckets,
|
||||
hasNextPage,
|
||||
};
|
||||
};
|
|
@ -212,3 +212,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": "kubernetes-test-index",
|
||||
"id": "15",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:31.570Z",
|
||||
"message": "hello world security",
|
||||
"orchestrator.namespace": "namespace09",
|
||||
"process.entry_leader.entity_id": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,5 +14,6 @@ export default function kubernetesSecurityApiIntegrationTests({
|
|||
describe('Kubernetes security API (basic)', function () {
|
||||
loadTestFile(require.resolve('./aggregate'));
|
||||
loadTestFile(require.resolve('./count'));
|
||||
loadTestFile(require.resolve('./multi_terms_aggregate'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { MULTI_TERMS_AGGREGATE_ROUTE } from '@kbn/kubernetes-security-plugin/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
const MOCK_INDEX = 'kubernetes-test-index';
|
||||
const ORCHESTRATOR_NAMESPACE_PROPERTY = 'orchestrator.namespace';
|
||||
const CONTAINER_IMAGE_NAME_PROPERTY = 'container.image.name';
|
||||
const ENTRY_LEADER_ENTITY_ID = 'process.entry_leader.entity_id';
|
||||
const TIMESTAMP_PROPERTY = '@timestamp';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function aggregateTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const namespaces = new Set([
|
||||
'namespace',
|
||||
'namespace02',
|
||||
'namespace03',
|
||||
'namespace04',
|
||||
'namespace05',
|
||||
'namespace06',
|
||||
'namespace07',
|
||||
'namespace08',
|
||||
'namespace09',
|
||||
'namespace10',
|
||||
]);
|
||||
|
||||
describe('Kubernetes security with a basic license', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'x-pack/test/functional/es_archives/kubernetes_security/process_events'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/functional/es_archives/kubernetes_security/process_events'
|
||||
);
|
||||
});
|
||||
|
||||
it(`${MULTI_TERMS_AGGREGATE_ROUTE} returns aggregates on process events`, async () => {
|
||||
const response = await supertest
|
||||
.get(MULTI_TERMS_AGGREGATE_ROUTE)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.query({
|
||||
query: JSON.stringify({ match: { [ENTRY_LEADER_ENTITY_ID]: '1' } }),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_IMAGE_NAME_PROPERTY,
|
||||
},
|
||||
]),
|
||||
page: 0,
|
||||
index: MOCK_INDEX,
|
||||
perPage: 10,
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.buckets.length).to.be(10);
|
||||
|
||||
response.body.buckets.forEach((bucket: { key: [string, string] }) => {
|
||||
expect(namespaces.has(bucket.key[0])).to.be(true);
|
||||
expect(bucket.key[1]).to.be('debian11');
|
||||
});
|
||||
});
|
||||
|
||||
it(`${MULTI_TERMS_AGGREGATE_ROUTE} allows pagination`, async () => {
|
||||
const response = await supertest
|
||||
.get(MULTI_TERMS_AGGREGATE_ROUTE)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.query({
|
||||
query: JSON.stringify({ match: { [ENTRY_LEADER_ENTITY_ID]: '1' } }),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_IMAGE_NAME_PROPERTY,
|
||||
missing: 'default',
|
||||
},
|
||||
]),
|
||||
page: 1,
|
||||
index: MOCK_INDEX,
|
||||
perPage: 5,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.buckets.length).to.be(5);
|
||||
expect(response.body.buckets[0].key[0]).to.be('namespace06');
|
||||
expect(response.body.buckets[0].key[1]).to.be('debian11');
|
||||
});
|
||||
|
||||
it(`${MULTI_TERMS_AGGREGATE_ROUTE} allows missing field`, async () => {
|
||||
const response = await supertest
|
||||
.get(MULTI_TERMS_AGGREGATE_ROUTE)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.query({
|
||||
query: JSON.stringify({ match: { [ENTRY_LEADER_ENTITY_ID]: '1' } }),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_IMAGE_NAME_PROPERTY,
|
||||
missing: 'default',
|
||||
},
|
||||
]),
|
||||
page: 2,
|
||||
index: MOCK_INDEX,
|
||||
perPage: 4,
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.buckets.length).to.be(2);
|
||||
expect(response.body.buckets[1].key[0]).to.be('namespace09');
|
||||
expect(response.body.buckets[1].key[1]).to.be('default');
|
||||
});
|
||||
|
||||
it(`${MULTI_TERMS_AGGREGATE_ROUTE} return countBy value for each aggregation`, async () => {
|
||||
const response = await supertest
|
||||
.get(MULTI_TERMS_AGGREGATE_ROUTE)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.query({
|
||||
query: JSON.stringify({ match: { [ENTRY_LEADER_ENTITY_ID]: '1' } }),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_IMAGE_NAME_PROPERTY,
|
||||
},
|
||||
]),
|
||||
countBy: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
page: 0,
|
||||
index: MOCK_INDEX,
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.buckets.length).to.be(10);
|
||||
|
||||
// when groupBy and countBy use the same field, count_by_aggs.value will always be 1
|
||||
response.body.buckets.forEach((agg: any) => {
|
||||
expect(agg.count_by_aggs.value).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
it(`${MULTI_TERMS_AGGREGATE_ROUTE} allows a range query`, async () => {
|
||||
const response = await supertest
|
||||
.get(MULTI_TERMS_AGGREGATE_ROUTE)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.query({
|
||||
query: JSON.stringify({
|
||||
range: {
|
||||
[TIMESTAMP_PROPERTY]: {
|
||||
gte: '2020-12-16T15:16:28.570Z',
|
||||
lte: '2020-12-16T15:16:30.570Z',
|
||||
},
|
||||
},
|
||||
}),
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_IMAGE_NAME_PROPERTY,
|
||||
},
|
||||
]),
|
||||
page: 0,
|
||||
index: MOCK_INDEX,
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.buckets.length).to.be(3);
|
||||
});
|
||||
|
||||
it(`${MULTI_TERMS_AGGREGATE_ROUTE} handles a bad request`, async () => {
|
||||
const response = await supertest
|
||||
.get(MULTI_TERMS_AGGREGATE_ROUTE)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.query({
|
||||
query: 'asdf',
|
||||
groupBys: JSON.stringify([
|
||||
{
|
||||
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_IMAGE_NAME_PROPERTY,
|
||||
},
|
||||
]),
|
||||
page: 0,
|
||||
index: MOCK_INDEX,
|
||||
});
|
||||
expect(response.status).to.be(400);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue