Skip to content

Commit

Permalink
[Cloud Security] [Misconfiguration] Add Grouping custom renderers (#1…
Browse files Browse the repository at this point in the history
…72256)

## Summary

This PR adds custom rendering for each of the default Grouping
visualizations:

- #168543
- #169043
- #169044
- #169045

**It also adds:**

- Fix error handling (follow up from [this
comment](#169884 (comment)))
- Change the Findings page to have the Misconfiguration tab in the first
position.
- Added `size` property to the `ComplianceScoreBar` component
- Custom message for groups that don't have value (ex. No Cloud
accounts)
- Changed the sort order of grouping components to be based on the
compliance score
- Added compliance score for custom renderers

### Screenshot

Resource

<img width="1492" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/596f8bdb-abcc-4325-8512-23c919c727a9">

Rule name

<img width="1489" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/787138e3-b3b2-4e15-811a-84c583831469">

Cloud account

<img width="1490" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/9a48145d-dba5-4eda-bd7d-a97ed8f78a2d">

<img width="1492" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/399d0be0-4bc0-4090-ac20-e4b016cc4be5">



Kubernetes

<img width="1499" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/3745498a-969a-4769-b4ae-3c932511a5a9">

Custom field:

<img width="1488" alt="image"
src="https://github.com/elastic/kibana/assets/19270322/8c75535d-2248-4cf9-b1cb-9b0d318114e9">

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
opauloh and kibanamachine authored Dec 1, 2023
1 parent d3a8699 commit 0d17a94
Show file tree
Hide file tree
Showing 29 changed files with 1,150 additions and 253 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
*/

export * from './use_cloud_posture_data_table';
export * from './use_base_es_query';
export * from './use_persisted_query';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 { buildEsQuery, EsQueryConfig } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { useEffect, useMemo } from 'react';
import { FindingsBaseESQueryConfig, FindingsBaseProps, FindingsBaseURLQuery } from '../../types';
import { useKibana } from '../use_kibana';

const getBaseQuery = ({
dataView,
query,
filters,
config,
}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => {
try {
return {
query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query
};
} catch (error) {
return {
query: undefined,
error: error instanceof Error ? error : new Error('Unknown Error'),
};
}
};

export const useBaseEsQuery = ({
dataView,
filters = [],
query,
nonPersistedFilters,
}: FindingsBaseURLQuery & FindingsBaseProps) => {
const {
notifications: { toasts },
data: {
query: { filterManager, queryString },
},
uiSettings,
} = useKibana().services;
const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards');
const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]);
const baseEsQuery = useMemo(
() =>
getBaseQuery({
dataView,
filters: filters.concat(nonPersistedFilters ?? []).flat(),
query,
config,
}),
[dataView, filters, nonPersistedFilters, query, config]
);

/**
* Sync filters with the URL query
*/
useEffect(() => {
filterManager.setAppFilters(filters);
queryString.setQuery(query);
}, [filters, filterManager, queryString, query]);

const handleMalformedQueryError = () => {
const error = baseEsQuery instanceof Error ? baseEsQuery : undefined;
if (error) {
toasts.addError(error, {
title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', {
defaultMessage: 'Query Error',
}),
toastLifeTimeMs: 1000 * 5,
});
}
};

useEffect(handleMalformedQueryError, [baseEsQuery, toasts]);

return baseEsQuery;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import { CriteriaWithPagination } from '@elastic/eui';
import { DataTableRecord } from '@kbn/discover-utils/types';
import { useUrlQuery } from '../use_url_query';
import { usePageSize } from '../use_page_size';
import { getDefaultQuery, useBaseEsQuery, usePersistedQuery } from './utils';
import { getDefaultQuery } from './utils';
import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants';
import { FindingsBaseURLQuery } from '../../types';
import { useBaseEsQuery } from './use_base_es_query';
import { usePersistedQuery } from './use_persisted_query';

type URLQuery = FindingsBaseURLQuery & Record<string, any>;

Expand Down Expand Up @@ -140,7 +142,16 @@ export const useCloudPostureDataTable = ({
setUrlQuery,
sort: urlQuery.sort,
filters: urlQuery.filters,
query: baseEsQuery.query,
query: baseEsQuery.query
? baseEsQuery.query
: {
bool: {
must: [],
filter: [],
should: [],
must_not: [],
},
},
queryError,
pageIndex: urlQuery.pageIndex,
urlQuery,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { useCallback } from 'react';
import { type Query } from '@kbn/es-query';
import { FindingsBaseURLQuery } from '../../types';
import { useKibana } from '../use_kibana';

export const usePersistedQuery = <T>(getter: ({ filters, query }: FindingsBaseURLQuery) => T) => {
const {
data: {
query: { filterManager, queryString },
},
} = useKibana().services;

return useCallback(
() =>
getter({
filters: filterManager.getAppFilters(),
query: queryString.getQuery() as Query,
}),
[getter, filterManager, queryString]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,7 @@
* 2.0.
*/

import { useEffect, useCallback, useMemo } from 'react';
import { buildEsQuery, EsQueryConfig } from '@kbn/es-query';
import type { EuiBasicTableProps, Pagination } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type Query } from '@kbn/es-query';
import { useKibana } from '../use_kibana';
import type {
FindingsBaseESQueryConfig,
FindingsBaseProps,
FindingsBaseURLQuery,
} from '../../types';

const getBaseQuery = ({
dataView,
query,
filters,
config,
}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => {
try {
return {
query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query
};
} catch (error) {
throw new Error(error);
}
};

type TablePagination = NonNullable<EuiBasicTableProps<unknown>['pagination']>;

Expand All @@ -52,74 +27,6 @@ export const getPaginationQuery = ({
size: pageSize,
});

export const useBaseEsQuery = ({
dataView,
filters = [],
query,
nonPersistedFilters,
}: FindingsBaseURLQuery & FindingsBaseProps) => {
const {
notifications: { toasts },
data: {
query: { filterManager, queryString },
},
uiSettings,
} = useKibana().services;
const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards');
const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]);
const baseEsQuery = useMemo(
() =>
getBaseQuery({
dataView,
filters: filters.concat(nonPersistedFilters ?? []).flat(),
query,
config,
}),
[dataView, filters, nonPersistedFilters, query, config]
);

/**
* Sync filters with the URL query
*/
useEffect(() => {
filterManager.setAppFilters(filters);
queryString.setQuery(query);
}, [filters, filterManager, queryString, query]);

const handleMalformedQueryError = () => {
const error = baseEsQuery instanceof Error ? baseEsQuery : undefined;
if (error) {
toasts.addError(error, {
title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', {
defaultMessage: 'Query Error',
}),
toastLifeTimeMs: 1000 * 5,
});
}
};

useEffect(handleMalformedQueryError, [baseEsQuery, toasts]);

return baseEsQuery;
};

export const usePersistedQuery = <T>(getter: ({ filters, query }: FindingsBaseURLQuery) => T) => {
const {
data: {
query: { filterManager, queryString },
},
} = useKibana().services;

return useCallback(
() =>
getter({
filters: filterManager.getAppFilters(),
query: queryString.getQuery() as Query,
}),
[getter, filterManager, queryString]
);
};

export const getDefaultQuery = ({ query, filters }: any): any => ({
query,
filters,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { getAbbreviatedNumber } from './get_abbreviated_number';

describe('getAbbreviatedNumber', () => {
it('should return the same value if it is less than 1000', () => {
expect(getAbbreviatedNumber(0)).toBe(0);
expect(getAbbreviatedNumber(1)).toBe(1);
expect(getAbbreviatedNumber(500)).toBe(500);
expect(getAbbreviatedNumber(999)).toBe(999);
});

it('should use numeral to format the value if it is greater than or equal to 1000', () => {
expect(getAbbreviatedNumber(1000)).toBe('1.0k');

expect(getAbbreviatedNumber(1200)).toBe('1.2k');

expect(getAbbreviatedNumber(3500000)).toBe('3.5m');

expect(getAbbreviatedNumber(2800000000)).toBe('2.8b');

expect(getAbbreviatedNumber(5900000000000)).toBe('5.9t');

expect(getAbbreviatedNumber(59000000000000000)).toBe('59000.0t');
});

it('should return 0 if the value is NaN', () => {
expect(getAbbreviatedNumber(NaN)).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 numeral from '@elastic/numeral';

/**
* Returns an abbreviated number when the value is greater than or equal to 1000.
* The abbreviated number is formatted using numeral:
* - thousand: k
* - million: m
* - billion: b
* - trillion: t
* */
export const getAbbreviatedNumber = (value: number) => {
if (isNaN(value)) {
return 0;
}
return value < 1000 ? value : numeral(value).format('0.0a');
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
import { type DataView } from '@kbn/data-views-plugin/common';
import numeral from '@elastic/numeral';
import { FieldsSelectorModal } from './fields_selector';
import { useStyles } from './use_styles';

const formatNumber = (value: number) => {
return value < 1000 ? value : numeral(value).format('0.0a');
};
import { getAbbreviatedNumber } from '../../common/utils/get_abbreviated_number';

const GroupSelectorWrapper: React.FC = ({ children }) => {
const styles = useStyles();
Expand Down Expand Up @@ -60,7 +56,7 @@ export const AdditionalControls = ({
/>
)}
<EuiFlexItem grow={0}>
<span className="cspDataTableTotal">{`${formatNumber(total)} ${title}`}</span>
<span className="cspDataTableTotal">{`${getAbbreviatedNumber(total)} ${title}`}</span>
</EuiFlexItem>
<EuiFlexItem grow={0}>
<EuiButtonEmpty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export const useStyles = () => {
`;

const groupBySelector = css`
width: 188px;
margin-left: auto;
`;

Expand Down
Loading

0 comments on commit 0d17a94

Please sign in to comment.