mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Asset Inventory] Split URL State Management for Page Filters and Grouping fixes (#220455)
## Summary It closes #219580 and #219654 This PR adds separate URL state management to filters in the asset inventory page, so it doesn't conflict with the filter used in the Searchbar. It also fixes an issue with the null grouping not filtering for the missing group field. ### Screenshot https://github.com/user-attachments/assets/0c022d41-f68f-46df-bbe9-2c5918e0af44 **Null group fix** <img width="1481" alt="image" src="https://github.com/user-attachments/assets/60010d40-f314-4823-bddc-17f828f175c0" /> **Asset Criticality Grouping** <img width="1493" alt="image" src="https://github.com/user-attachments/assets/7a47742f-0d14-4643-91ac-ce3dd847b576" />
This commit is contained in:
parent
4f2a06d358
commit
e528a30fa8
8 changed files with 168 additions and 16 deletions
|
@ -219,7 +219,7 @@ const DataTableWithLocalPagination = ({
|
|||
...currentGroupFilters,
|
||||
...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []),
|
||||
]
|
||||
.map(({ query }) => (query.match_phrase ? query : null))
|
||||
.map(({ query }) => (query?.match_phrase || query?.bool?.should ? query : null))
|
||||
.filter(Boolean);
|
||||
|
||||
const newState: AssetInventoryURLStateResult = {
|
||||
|
|
|
@ -64,8 +64,8 @@ export const AssetInventoryFilters = ({ setQuery }: AssetInventoryFiltersProps)
|
|||
return (
|
||||
<FilterGroup
|
||||
dataViewId={dataView.id || null}
|
||||
onFiltersChange={(filters: Filter[]) => {
|
||||
setQuery({ filters });
|
||||
onFiltersChange={(pageFilters: Filter[]) => {
|
||||
setQuery({ pageFilters });
|
||||
}}
|
||||
ruleTypeIds={ASSET_INVENTORY_RULE_TYPE_IDS}
|
||||
Storage={Storage}
|
||||
|
|
|
@ -56,11 +56,13 @@ const defaultGroupingOptions: GroupOption[] = [
|
|||
export const getDefaultQuery = ({
|
||||
query,
|
||||
filters,
|
||||
pageFilters,
|
||||
}: AssetsBaseURLQuery): AssetsBaseURLQuery & {
|
||||
sort: string[][];
|
||||
} => ({
|
||||
query,
|
||||
filters,
|
||||
pageFilters: [],
|
||||
sort: [[]],
|
||||
});
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { getAbbreviatedNumber } from '@kbn/cloud-security-posture-common';
|
||||
import { CloudProviderIcon, type CloudProvider } from '@kbn/custom-icons';
|
||||
import type { CriticalityLevelWithUnassigned } from '../../../../../common/entity_analytics/asset_criticality/types';
|
||||
import { AssetCriticalityBadge } from '../../../../entity_analytics/components/asset_criticality';
|
||||
import { ASSET_GROUPING_OPTIONS, TEST_SUBJ_GROUPING_COUNTER } from '../../../constants';
|
||||
import { firstNonNullValue } from './first_non_null_value';
|
||||
import { NullGroup } from './null_group';
|
||||
|
@ -68,19 +70,27 @@ export const groupPanelRenderer: GroupPanelRenderer<AssetsGroupingAggregation> =
|
|||
switch (selectedGroup) {
|
||||
case ASSET_GROUPING_OPTIONS.ASSET_CRITICALITY:
|
||||
return nullGroupMessage ? (
|
||||
renderNullGroup(NULL_GROUPING_MESSAGES.ASSET_CRITICALITY)
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<AssetCriticalityBadge criticalityLevel="unassigned" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s"> {getGroupPanelTitle()}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{firstNonNullValue(bucket.assetCriticality?.buckets?.[0]?.key)}{' '}
|
||||
{firstNonNullValue(bucket.assetCriticality?.buckets?.[0]?.key)}
|
||||
</EuiText>
|
||||
<AssetCriticalityBadge
|
||||
criticalityLevel={
|
||||
firstNonNullValue(
|
||||
bucket.assetCriticality?.buckets?.[0]?.key
|
||||
) as CriticalityLevelWithUnassigned
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -17,13 +17,14 @@ import { usePersistedQuery } from './use_persisted_query';
|
|||
export interface AssetsBaseURLQuery {
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
pageFilters?: Filter[];
|
||||
/**
|
||||
* Grouping component selection
|
||||
*/
|
||||
groupBy?: string[];
|
||||
}
|
||||
|
||||
export type AssetsURLQuery = Pick<AssetsBaseURLQuery, 'query' | 'filters'>;
|
||||
export type AssetsURLQuery = Pick<AssetsBaseURLQuery, 'query' | 'filters' | 'pageFilters'>;
|
||||
|
||||
export type URLQuery = AssetsBaseURLQuery & Record<string, unknown>;
|
||||
|
||||
|
@ -33,6 +34,7 @@ export interface AssetInventoryURLStateResult {
|
|||
setUrlQuery: (query: Record<string, unknown>) => void;
|
||||
sort: SortOrder[];
|
||||
filters: Filter[];
|
||||
pageFilters: Filter[];
|
||||
query: { bool: BoolQuery };
|
||||
queryError?: Error;
|
||||
pageIndex: number;
|
||||
|
@ -52,6 +54,7 @@ export interface AssetInventoryURLStateResult {
|
|||
const getDefaultQuery = ({ query, filters }: AssetsBaseURLQuery) => ({
|
||||
query,
|
||||
filters,
|
||||
pageFilters: [],
|
||||
sort: { field: '@timestamp', direction: 'desc' },
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
@ -128,6 +131,7 @@ export const useAssetInventoryURLState = ({
|
|||
*/
|
||||
const baseEsQuery = useBaseEsQuery({
|
||||
filters: urlQuery.filters,
|
||||
pageFilters: urlQuery.pageFilters,
|
||||
query: urlQuery.query,
|
||||
});
|
||||
|
||||
|
@ -151,6 +155,7 @@ export const useAssetInventoryURLState = ({
|
|||
setUrlQuery,
|
||||
sort: urlQuery.sort as SortOrder[],
|
||||
filters: urlQuery.filters || [],
|
||||
pageFilters: urlQuery.pageFilters || [],
|
||||
query: baseEsQuery.query
|
||||
? baseEsQuery.query
|
||||
: {
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react';
|
||||
import { useBaseEsQuery } from './use_base_es_query';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useDataViewContext } from '../data_view_context';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../data_view_context');
|
||||
|
||||
const mockDataView = {
|
||||
id: 'test-data-view',
|
||||
title: 'test-*',
|
||||
getIndexPattern: () => 'test-*',
|
||||
fields: [
|
||||
{
|
||||
name: 'entity.name',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'entity.type',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
} as unknown as DataView;
|
||||
|
||||
const uiSettings = coreMock.createStart().uiSettings;
|
||||
const notifications = coreMock.createStart().notifications;
|
||||
const filterManager = {
|
||||
setAppFilters: jest.fn(),
|
||||
};
|
||||
const queryString = {
|
||||
setQuery: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
notifications,
|
||||
uiSettings,
|
||||
data: {
|
||||
query: {
|
||||
filterManager,
|
||||
queryString,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
(useDataViewContext as jest.Mock).mockReturnValue({
|
||||
dataView: mockDataView,
|
||||
});
|
||||
|
||||
uiSettings.get = jest.fn().mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('useBaseEsQuery', () => {
|
||||
it('should build and return a valid ES query', () => {
|
||||
const query = { query: 'entity.sub_type: "Compute"', language: 'kuery' };
|
||||
|
||||
const filters = [
|
||||
{
|
||||
meta: { disabled: false, key: 'entity.type', negate: false },
|
||||
query: {
|
||||
match: { 'entity.type': 'host' },
|
||||
},
|
||||
},
|
||||
];
|
||||
const pageFilters: Filter[] = [
|
||||
{
|
||||
meta: { disabled: false, key: 'entity.name', negate: false },
|
||||
query: {
|
||||
match: { 'entity.name': 'test-host' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { result } = renderHook(() => useBaseEsQuery({ query, filters, pageFilters }));
|
||||
|
||||
expect(result.current.query).toBeDefined();
|
||||
expect(result.current.query).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'entity.sub_type': 'Compute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { 'entity.type': 'host' },
|
||||
},
|
||||
{
|
||||
match: { 'entity.name': 'test-host' },
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterManager.setAppFilters).toHaveBeenCalledWith(filters);
|
||||
expect(filterManager.setAppFilters).not.toHaveBeenCalledWith(pageFilters);
|
||||
expect(queryString.setQuery).toHaveBeenCalledWith(query);
|
||||
expect(notifications.toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -21,14 +21,17 @@ const getBaseQuery = ({
|
|||
dataView,
|
||||
query,
|
||||
filters,
|
||||
pageFilters,
|
||||
config,
|
||||
}: AssetsBaseURLQuery &
|
||||
AssetsBaseESQueryConfig & {
|
||||
dataView: DataView | undefined;
|
||||
}) => {
|
||||
try {
|
||||
const mergedFilters = [...filters, ...(pageFilters ?? [])];
|
||||
|
||||
return {
|
||||
query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query
|
||||
query: buildEsQuery(dataView, query, mergedFilters, config), // will throw for malformed query
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
@ -38,7 +41,7 @@ const getBaseQuery = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const useBaseEsQuery = ({ filters = [], query }: AssetsBaseURLQuery) => {
|
||||
export const useBaseEsQuery = ({ filters = [], query, pageFilters = [] }: AssetsBaseURLQuery) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
data: {
|
||||
|
@ -54,10 +57,11 @@ export const useBaseEsQuery = ({ filters = [], query }: AssetsBaseURLQuery) => {
|
|||
getBaseQuery({
|
||||
dataView,
|
||||
filters,
|
||||
pageFilters,
|
||||
query,
|
||||
config,
|
||||
}),
|
||||
[dataView, filters, query, config]
|
||||
[dataView, filters, pageFilters, query, config]
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,9 +31,10 @@ import {
|
|||
} from '../constants';
|
||||
import { OnboardingSuccessCallout } from '../components/onboarding/onboarding_success_callout';
|
||||
|
||||
const getDefaultQuery = ({ query, filters }: AssetsBaseURLQuery): URLQuery => ({
|
||||
const getDefaultQuery = ({ query, filters, pageFilters }: AssetsBaseURLQuery): URLQuery => ({
|
||||
query,
|
||||
filters,
|
||||
pageFilters,
|
||||
sort: [['@timestamp', 'desc']],
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue