Refactor Asset Inventory page (#212436)

## Summary

Refactors code in Asset Inventory page for simplicity and consistency.

### Changes

- Centralized constants for consistency
- Simplified `<AllAssets>` page, removed unused props, renamed
variables, etc...
- Encapsulated technical preview stuff into `<TechnicalPreviewBadge>`
- Removed deprecations in EUI components and styling

Also, this PR **deletes the mocked data** that was used before
integrating the UI with the backend.

### Questions

- Do we see value in centralizing all strings in a new file such as
`localized_strings.ts`?

### Out of scope

Hooks in `hooks/use_asset_inventory_data_table` and field selector
components were all duplicated from the CSP plugin. I haven't put effort
in refactoring those since we'll need to remove the duplication and make
them reusable

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Risks

No risk since code is still hidden behind the *Enable Asset Inventory*
advanced setting and the beta *Cloud Asset* integration must be
installed.
This commit is contained in:
Alberto Blázquez 2025-03-04 12:28:12 +01:00 committed by GitHub
parent e24c1c3ee5
commit 2473d5951a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 182 additions and 311 deletions

View file

@ -11,17 +11,9 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import illustration from '../../common/images/illustration_product_no_results_magnifying_glass.svg';
import { DOCS_URL, TEST_SUBJ_EMPTY_STATE } from '../constants';
const ASSET_INVENTORY_DOCS_URL = 'https://ela.st/asset-inventory';
const EMPTY_STATE_TEST_SUBJ = 'assetInventory:empty-state';
export const EmptyState = ({
onResetFilters,
docsUrl = ASSET_INVENTORY_DOCS_URL,
}: {
onResetFilters: () => void;
docsUrl?: string;
}) => {
export const EmptyState = ({ onResetFilters }: { onResetFilters: () => void }) => {
const { euiTheme } = useEuiTheme();
return (
@ -35,7 +27,7 @@ export const EmptyState = ({
margin-top: ${euiTheme.size.xxxl}};
}
`}
data-test-subj={EMPTY_STATE_TEST_SUBJ}
data-test-subj={TEST_SUBJ_EMPTY_STATE}
icon={
<EuiImage
url={illustration}
@ -74,7 +66,7 @@ export const EmptyState = ({
defaultMessage="Reset filters"
/>
</EuiButton>,
<EuiLink href={docsUrl} target="_blank">
<EuiLink href={DOCS_URL} target="_blank">
<FormattedMessage
id="xpack.securitySolution.assetInventory.emptyState.readDocsLink"
defaultMessage="Read the docs"

View file

@ -12,8 +12,9 @@ import type { FilterControlConfig } from '@kbn/alerts-ui-shared';
import type { Filter } from '@kbn/es-query';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { useDataViewContext } from '../../hooks/data_view_context';
import { useSpaceId } from '../../../common/hooks/use_space_id';
import { useDataViewContext } from '../../hooks/data_view_context';
import type { AssetsURLQuery } from '../../hooks/use_asset_inventory_data_table';
import { ASSET_INVENTORY_INDEX_PATTERN } from '../../constants';
import { FilterGroupLoading } from './filters_loading';
import { ASSET_INVENTORY_RULE_TYPE_IDS } from './rule_type_ids';
@ -46,10 +47,10 @@ const DEFAULT_ASSET_INVENTORY_FILTERS: FilterControlConfig[] = [
];
export interface FiltersProps {
onFiltersChange: (newFilters: Filter[]) => void;
setQuery: (v: Partial<AssetsURLQuery>) => void;
}
export const Filters = ({ onFiltersChange }: FiltersProps) => {
export const Filters = ({ setQuery }: FiltersProps) => {
const { dataView, dataViewIsLoading } = useDataViewContext();
const spaceId = useSpaceId();
@ -85,7 +86,9 @@ export const Filters = ({ onFiltersChange }: FiltersProps) => {
<EuiSpacer size="l" />
<FilterGroup
dataViewId={dataViewSpec?.id || null}
onFiltersChange={onFiltersChange}
onFiltersChange={(filters: Filter[]) => {
setQuery({ filters });
}}
ruleTypeIds={ASSET_INVENTORY_RULE_TYPE_IDS}
Storage={Storage}
defaultControls={DEFAULT_ASSET_INVENTORY_FILTERS}

View file

@ -12,15 +12,13 @@ import type { Filter } from '@kbn/es-query';
import { useKibana } from '../../common/lib/kibana';
import { FiltersGlobal } from '../../common/components/filters_global/filters_global';
import { useDataViewContext } from '../hooks/data_view_context';
import type { AssetsBaseURLQuery } from '../hooks/use_asset_inventory_data_table';
type SearchBarQueryProps = Pick<AssetsBaseURLQuery, 'query' | 'filters'>;
import type { AssetsURLQuery } from '../hooks/use_asset_inventory_data_table';
interface AssetInventorySearchBarProps {
setQuery(v: Partial<SearchBarQueryProps>): void;
setQuery(v: Partial<AssetsURLQuery>): void;
loading: boolean;
placeholder?: string;
query: SearchBarQueryProps;
query: AssetsURLQuery;
}
export const AssetInventorySearchBar = ({
@ -54,7 +52,7 @@ export const AssetInventorySearchBar = ({
isLoading={loading}
indexPatterns={[dataView]}
onQuerySubmit={setQuery}
onFiltersUpdated={(value: Filter[]) => setQuery({ filters: value })}
onFiltersUpdated={(filters: Filter[]) => setQuery({ filters })}
placeholder={placeholder}
query={{
query: query?.query?.query || '',
@ -69,6 +67,6 @@ export const AssetInventorySearchBar = ({
const getContainerStyle = (theme: EuiThemeComputed) => css`
border-bottom: ${theme.border.thin};
background-color: ${theme.colors.body};
background-color: ${theme.colors.backgroundBaseSubdued};
padding: ${theme.size.base};
`;

View file

@ -0,0 +1,34 @@
/*
* 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 { css } from '@emotion/react';
import { EuiBetaBadge, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const TechnicalPreviewBadge = () => {
const { euiTheme } = useEuiTheme();
return (
<EuiBetaBadge
css={css`
margin-left: ${euiTheme.size.s};
`}
label={i18n.translate('xpack.securitySolution.assetInventory.technicalPreviewLabel', {
defaultMessage: 'Technical Preview',
})}
size="s"
color="subdued"
tooltipContent={i18n.translate(
'xpack.securitySolution.assetInventory.technicalPreviewTooltip',
{
defaultMessage:
'This functionality is experimental and not supported. It may change or be removed at any time.',
}
)}
/>
);
};

View file

@ -5,6 +5,23 @@
* 2.0.
*/
export const MAX_ASSETS_TO_LOAD = 500; // equivalent to MAX_FINDINGS_TO_LOAD in @kbn/cloud-security-posture-common
export const MAX_ASSETS_TO_LOAD = 500;
export const DEFAULT_VISIBLE_ROWS_PER_PAGE = 25;
export const ASSET_INVENTORY_INDEX_PATTERN = 'logs-cloud_asset_inventory.asset_inventory-*';
export const QUERY_KEY_GRID_DATA = 'asset_inventory_grid_data';
export const QUERY_KEY_CHART_DATA = 'asset_inventory_chart_data';
export const ASSET_INVENTORY_TABLE_ID = 'asset-inventory-table';
const LOCAL_STORAGE_PREFIX = 'assetInventory';
export const LOCAL_STORAGE_COLUMNS_KEY = `${LOCAL_STORAGE_PREFIX}:columns`;
export const LOCAL_STORAGE_COLUMNS_SETTINGS_KEY = `${LOCAL_STORAGE_COLUMNS_KEY}:settings`;
export const LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY = `${LOCAL_STORAGE_PREFIX}:dataTable:pageSize`;
export const LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY = `${LOCAL_STORAGE_PREFIX}:dataTable:columns`;
export const TEST_SUBJ_DATA_GRID = 'asset-inventory-test-subj-grid-wrapper';
export const TEST_SUBJ_PAGE_TITLE = 'asset-inventory-test-subj-page-title';
export const TEST_SUBJ_EMPTY_STATE = 'asset-inventory-empty-state';
export const DOCS_URL = 'https://ela.st/asset-inventory';

View file

@ -8,27 +8,23 @@ import { type Dispatch, type SetStateAction, useCallback } from 'react';
import type { BoolQuery, Filter, Query } from '@kbn/es-query';
import type { CriteriaWithPagination } from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants';
import { useUrlQuery } from './use_url_query';
import { usePageSize } from './use_page_size';
import { getDefaultQuery } from './utils';
import { useBaseEsQuery } from './use_base_es_query';
import { usePersistedQuery } from './use_persisted_query';
const LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY = 'assetInventory:dataTable:columns';
export interface AssetsBaseURLQuery {
query: Query;
filters: Filter[];
/**
* Filters that are part of the query but not persisted in the URL or in the Filter Manager
*/
nonPersistedFilters?: Filter[];
/**
* Grouping component selection
*/
groupBy?: string[];
}
export type AssetsURLQuery = Pick<AssetsBaseURLQuery, 'query' | 'filters'>;
export type URLQuery = AssetsBaseURLQuery & Record<string, unknown>;
type SortOrder = [string, string];
@ -53,6 +49,13 @@ export interface AssetInventoryDataTableResult {
getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[];
}
const getDefaultQuery = ({ query, filters }: AssetsBaseURLQuery) => ({
query,
filters,
sort: { field: '@timestamp', direction: 'desc' },
pageIndex: 0,
});
/*
Hook for managing common table state and methods for the Asset Inventory DataTable
*/
@ -60,12 +63,10 @@ export const useAssetInventoryDataTable = ({
defaultQuery = getDefaultQuery,
paginationLocalStorageKey,
columnsLocalStorageKey,
nonPersistedFilters,
}: {
defaultQuery?: (params: AssetsBaseURLQuery) => URLQuery;
paginationLocalStorageKey: string;
columnsLocalStorageKey?: string;
nonPersistedFilters?: Filter[];
}): AssetInventoryDataTableResult => {
const getPersistedDefaultQuery = usePersistedQuery<URLQuery>(defaultQuery);
const { urlQuery, setUrlQuery } = useUrlQuery<URLQuery>(getPersistedDefaultQuery);
@ -128,7 +129,6 @@ export const useAssetInventoryDataTable = ({
const baseEsQuery = useBaseEsQuery({
filters: urlQuery.filters,
query: urlQuery.query,
...(nonPersistedFilters ? { nonPersistedFilters } : {}),
});
const handleUpdateQuery = useCallback(

View file

@ -9,8 +9,8 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import { buildEsQuery, type EsQueryConfig } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { useEffect, useMemo } from 'react';
import { useDataViewContext } from '../data_view_context';
import { useKibana } from '../../../common/lib/kibana';
import { useDataViewContext } from '../data_view_context';
import type { AssetsBaseURLQuery } from './use_asset_inventory_data_table';
interface AssetsBaseESQueryConfig {
@ -38,11 +38,7 @@ const getBaseQuery = ({
}
};
export const useBaseEsQuery = ({
filters = [],
query,
nonPersistedFilters,
}: AssetsBaseURLQuery) => {
export const useBaseEsQuery = ({ filters = [], query }: AssetsBaseURLQuery) => {
const {
notifications: { toasts },
data: {
@ -57,11 +53,11 @@ export const useBaseEsQuery = ({
() =>
getBaseQuery({
dataView,
filters: filters.concat(nonPersistedFilters ?? []).flat(),
filters,
query,
config,
}),
[dataView, filters, nonPersistedFilters, query, config]
[dataView, filters, query, config]
);
/**

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import useLocalStorage from 'react-use/lib/useLocalStorage';
const DEFAULT_VISIBLE_ROWS_PER_PAGE = 10; // generic default # of table rows to show (currently we only have a list of policies)
import { DEFAULT_VISIBLE_ROWS_PER_PAGE } from '../../constants';
/**
* @description handles persisting the users table row size selection
@ -17,11 +16,8 @@ export const usePageSize = (localStorageKey: string) => {
DEFAULT_VISIBLE_ROWS_PER_PAGE
);
let pageSize = DEFAULT_VISIBLE_ROWS_PER_PAGE;
if (persistedPageSize) {
pageSize = persistedPageSize;
}
return { pageSize, setPageSize: setPersistedPageSize };
return {
pageSize: persistedPageSize || DEFAULT_VISIBLE_ROWS_PER_PAGE,
setPageSize: setPersistedPageSize,
};
};

View file

@ -7,8 +7,8 @@
import { useCallback } from 'react';
import type { Query } from '@kbn/es-query';
import type { AssetsBaseURLQuery } from './use_asset_inventory_data_table';
import { useKibana } from '../../../common/lib/kibana';
import type { AssetsBaseURLQuery } from './use_asset_inventory_data_table';
export const usePersistedQuery = <T>(getter: ({ filters, query }: AssetsBaseURLQuery) => T) => {
const {

View file

@ -1,14 +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 type { AssetsBaseURLQuery } from './use_asset_inventory_data_table';
export const getDefaultQuery = ({ query, filters }: AssetsBaseURLQuery) => ({
query,
filters,
sort: { field: '@timestamp', direction: 'desc' },
pageIndex: 0,
});

View file

@ -9,6 +9,10 @@ import { renderHook } from '@testing-library/react';
import { useAssetInventoryRoutes } from './use_asset_inventory_routes';
import { useKibana } from '../../common/lib/kibana';
import { API_VERSIONS } from '../../../common/constants';
import {
ASSET_INVENTORY_ENABLE_API_PATH,
ASSET_INVENTORY_STATUS_API_PATH,
} from '../../../common/api/asset_inventory/constants';
jest.mock('../../common/lib/kibana');
@ -31,7 +35,7 @@ describe('useAssetInventoryRoutes', () => {
await result.current.postEnableAssetInventory();
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith('/api/asset_inventory/enable', {
expect(mockFetch).toHaveBeenCalledWith(ASSET_INVENTORY_ENABLE_API_PATH, {
method: 'POST',
version: API_VERSIONS.public.v1,
body: JSON.stringify({}),
@ -45,7 +49,7 @@ describe('useAssetInventoryRoutes', () => {
const response = await result.current.getAssetInventoryStatus();
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith('/api/asset_inventory/status', {
expect(mockFetch).toHaveBeenCalledWith(ASSET_INVENTORY_STATUS_API_PATH, {
method: 'GET',
version: API_VERSIONS.public.v1,
query: {},

View file

@ -13,7 +13,7 @@ import { showErrorToast } from '@kbn/cloud-security-posture';
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import type { BaseEsQuery } from '@kbn/cloud-security-posture';
import { useKibana } from '../../common/lib/kibana';
import { ASSET_INVENTORY_INDEX_PATTERN } from '../constants';
import { ASSET_INVENTORY_INDEX_PATTERN, QUERY_KEY_CHART_DATA } from '../constants';
import { getMultiFieldsSort } from './fetch_utils';
interface UseTopAssetsOptions extends BaseEsQuery {
@ -159,7 +159,7 @@ export function useFetchChartData(options: UseTopAssetsOptions) {
notifications: { toasts },
} = useKibana().services;
return useQuery(
['asset_inventory_top_assets_chart', { params: options }],
[QUERY_KEY_CHART_DATA, { params: options }],
async () => {
const {
rawResponse: { aggregations },

View file

@ -15,7 +15,11 @@ import { showErrorToast } from '@kbn/cloud-security-posture';
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import type { BaseEsQuery } from '@kbn/cloud-security-posture';
import { useKibana } from '../../common/lib/kibana';
import { MAX_ASSETS_TO_LOAD, ASSET_INVENTORY_INDEX_PATTERN } from '../constants';
import {
MAX_ASSETS_TO_LOAD,
ASSET_INVENTORY_INDEX_PATTERN,
QUERY_KEY_GRID_DATA,
} from '../constants';
import { getRuntimeMappingsFromSort, getMultiFieldsSort } from './fetch_utils';
interface UseAssetsOptions extends BaseEsQuery {
@ -59,13 +63,13 @@ interface Asset {
type LatestAssetsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type LatestAssetsResponse = IKibanaSearchResponse<estypes.SearchResponse<Asset, never>>;
export function useFetchData(options: UseAssetsOptions) {
export function useFetchGridData(options: UseAssetsOptions) {
const {
data,
notifications: { toasts },
} = useKibana().services;
return useInfiniteQuery(
['asset_inventory', { params: options }],
[QUERY_KEY_GRID_DATA, { params: options }],
async ({ pageParam }) => {
const {
rawResponse: { hits },

View file

@ -7,7 +7,6 @@
import React, { useState, useMemo } from 'react';
import _ from 'lodash';
import { type Filter } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import {
@ -17,42 +16,43 @@ import {
useColumns,
type UnifiedDataTableSettings,
type UnifiedDataTableSettingsColumn,
type CustomCellRenderer,
} from '@kbn/unified-data-table';
import { CellActionsProvider } from '@kbn/cell-actions';
import { type HttpSetup } from '@kbn/core-http-browser';
import { SHOW_MULTIFIELDS, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import {
type RowControlColumn,
SHOW_MULTIFIELDS,
SORT_DEFAULT_ORDER_SETTING,
} from '@kbn/discover-utils';
import { type DataTableRecord } from '@kbn/discover-utils/types';
import {
type EuiDataGridCellValueElementProps,
type EuiDataGridControlColumn,
type EuiDataGridStyle,
EuiProgress,
EuiPageTemplate,
EuiTitle,
EuiButtonIcon,
EuiBetaBadge,
useEuiTheme,
} from '@elastic/eui';
import { type AddFieldFilterHandler } from '@kbn/unified-field-list';
import { generateFilters } from '@kbn/data-plugin/public';
import { type DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { css } from '@emotion/react';
import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity';
import { EmptyComponent } from '../../common/lib/cell_actions/helpers';
import { useDynamicEntityFlyout } from '../hooks/use_dynamic_entity_flyout';
import { type CriticalityLevelWithUnassigned } from '../../../common/entity_analytics/asset_criticality/types';
import { useKibana } from '../../common/lib/kibana';
import { AssetCriticalityBadge } from '../../entity_analytics/components/asset_criticality/asset_criticality_badge';
import { AdditionalControls } from '../components/additional_controls';
import { AssetInventorySearchBar } from '../components/search_bar';
import { RiskBadge } from '../components/risk_badge';
import { Filters } from '../components/filters/filters';
import { EmptyState } from '../components/empty_state';
import { TopAssetsBarChart } from '../components/top_assets_bar_chart';
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
import { useDynamicEntityFlyout } from '../hooks/use_dynamic_entity_flyout';
import { useDataViewContext } from '../hooks/data_view_context';
import { useStyles } from '../hooks/use_styles';
import {
@ -60,9 +60,19 @@ import {
type AssetsBaseURLQuery,
type URLQuery,
} from '../hooks/use_asset_inventory_data_table';
import { useFetchData } from '../hooks/use_fetch_data';
import { useFetchGridData } from '../hooks/use_fetch_grid_data';
import { useFetchChartData } from '../hooks/use_fetch_chart_data';
import { DEFAULT_VISIBLE_ROWS_PER_PAGE, MAX_ASSETS_TO_LOAD } from '../constants';
import {
DEFAULT_VISIBLE_ROWS_PER_PAGE,
MAX_ASSETS_TO_LOAD,
ASSET_INVENTORY_TABLE_ID,
TEST_SUBJ_DATA_GRID,
TEST_SUBJ_PAGE_TITLE,
LOCAL_STORAGE_COLUMNS_KEY,
LOCAL_STORAGE_COLUMNS_SETTINGS_KEY,
LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY,
} from '../constants';
const gridStyle: EuiDataGridStyle = {
border: 'horizontal',
@ -75,8 +85,12 @@ const title = i18n.translate('xpack.securitySolution.assetInventory.allAssets.ta
defaultMessage: 'assets',
});
const columnsLocalStorageKey = 'assetInventoryColumns';
const LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY = 'assetInventory:dataTable:pageSize';
const moreActionsLabel = i18n.translate(
'xpack.securitySolution.assetInventory.flyout.moreActionsButton',
{
defaultMessage: 'More actions',
}
);
const columnHeaders: Record<string, string> = {
'asset.risk': i18n.translate('xpack.securitySolution.assetInventory.allAssets.risk', {
@ -99,7 +113,7 @@ const columnHeaders: Record<string, string> = {
}),
} as const;
const customCellRenderer = (rows: DataTableRecord[]) => ({
const customCellRenderer = (rows: DataTableRecord[]): CustomCellRenderer => ({
'asset.risk': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
const risk = rows[rowIndex].flattened['asset.risk'] as number;
return <RiskBadge risk={risk} />;
@ -131,17 +145,6 @@ const getDefaultQuery = ({ query, filters }: AssetsBaseURLQuery): URLQuery => ({
sort: [['@timestamp', 'desc']],
});
export interface AllAssetsProps {
height?: number | string;
nonPersistedFilters?: Filter[];
hasDistributionBar?: boolean;
/**
* This function will be used in the control column to create a rule for a specific finding.
*/
createFn?: (rowIndex: number) => ((http: HttpSetup) => Promise<unknown>) | undefined;
'data-test-subj'?: string;
}
// TODO: Asset Inventory - adjust and remove type casting once we have real universal entity data
const getEntity = (row: DataTableRecord): EntityEcs => {
return {
@ -152,21 +155,22 @@ const getEntity = (row: DataTableRecord): EntityEcs => {
};
};
const ASSET_INVENTORY_TABLE_ID = 'asset-inventory-table';
export const AllAssets = ({
nonPersistedFilters,
height,
hasDistributionBar = true,
createFn,
...rest
}: AllAssetsProps) => {
const { euiTheme } = useEuiTheme();
const assetInventoryDataTable = useAssetInventoryDataTable({
export const AllAssets = () => {
const {
pageSize,
sort,
query,
queryError,
urlQuery,
getRowsFromPages,
onChangeItemsPerPage,
onResetFilters,
onSort,
setUrlQuery,
} = useAssetInventoryDataTable({
paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY,
columnsLocalStorageKey,
columnsLocalStorageKey: LOCAL_STORAGE_COLUMNS_KEY,
defaultQuery: getDefaultQuery,
nonPersistedFilters,
});
// Table Flyout Controls -------------------------------------------------------------------
@ -193,27 +197,14 @@ export const AllAssets = ({
};
// -----------------------------------------------------------------------------------------
const {
filters,
pageSize,
sort,
query,
queryError,
urlQuery,
getRowsFromPages,
onChangeItemsPerPage,
onResetFilters,
onSort,
setUrlQuery,
} = assetInventoryDataTable;
const {
data: rowsData,
// error: fetchError,
isFetching,
fetchNextPage: loadMore,
isLoading,
} = useFetchData({
isFetching: isFetchingGridData,
isLoading: isLoadingGridData,
} = useFetchGridData({
query,
sort,
enabled: !queryError,
@ -234,13 +225,13 @@ export const AllAssets = ({
const rows = getRowsFromPages(rowsData?.pages);
const totalHits = rowsData?.pages[0].total || 0;
const [columns, setColumns] = useLocalStorage(
columnsLocalStorageKey,
const [localStorageColumns, setLocalStorageColumns] = useLocalStorage(
LOCAL_STORAGE_COLUMNS_KEY,
defaultColumns.map((c) => c.id)
);
const [persistedSettings, setPersistedSettings] = useLocalStorage<UnifiedDataTableSettings>(
`${columnsLocalStorageKey}:settings`,
LOCAL_STORAGE_COLUMNS_SETTINGS_KEY,
{
columns: defaultColumns.reduce((columnSettings, column) => {
const columnDefaultSettings = column.width ? { width: column.width } : {};
@ -273,16 +264,12 @@ export const AllAssets = ({
uiSettings,
dataViews,
data,
application,
application: { capabilities },
theme,
fieldFormats,
notifications,
storage,
} = useKibana().services;
const styles = useStyles();
const { capabilities } = application;
const { filterManager } = data.query;
const services = {
@ -294,6 +281,8 @@ export const AllAssets = ({
data,
};
const styles = useStyles();
const {
columns: currentColumns,
onSetColumns,
@ -304,8 +293,8 @@ export const AllAssets = ({
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
dataView,
dataViews,
setAppState: (props) => setColumns(props.columns),
columns,
setAppState: (props) => setLocalStorageColumns(props.columns),
columns: localStorageColumns,
sort,
});
@ -318,22 +307,18 @@ export const AllAssets = ({
const isVirtualizationEnabled = pageSize >= 100;
const getWrapperHeight = () => {
if (height) return height;
// If virtualization is not needed the table will render unconstrained.
if (!isVirtualizationEnabled) return 'auto';
const baseHeight = 362; // height of Kibana Header + Findings page header and search bar
const filterBarHeight = filters?.length > 0 ? 40 : 0;
const distributionBarHeight = hasDistributionBar ? 52 : 0;
return `calc(100vh - ${baseHeight}px - ${filterBarHeight}px - ${distributionBarHeight}px)`;
return `calc(100vh - ${baseHeight}px)`;
};
return {
wrapperHeight: getWrapperHeight(),
mode: isVirtualizationEnabled ? 'virtualized' : 'standard',
};
}, [pageSize, height, filters?.length, hasDistributionBar]);
}, [pageSize]);
const onAddFilter: AddFieldFilterHandler | undefined = useMemo(
() =>
@ -365,15 +350,10 @@ export const AllAssets = ({
setPersistedSettings(newGrid);
};
const externalCustomRenderers = useMemo(() => {
if (!customCellRenderer) {
return undefined;
}
return customCellRenderer(rows);
}, [rows]);
const externalCustomRenderers = useMemo(() => customCellRenderer(rows), [rows]);
const onResetColumns = () => {
setColumns(defaultColumns.map((c) => c.id));
setLocalStorageColumns(defaultColumns.map((c) => c.id));
};
const externalAdditionalControls = (
@ -384,75 +364,47 @@ export const AllAssets = ({
columns={currentColumns}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
// groupSelectorComponent={groupSelectorComponent}
onResetColumns={onResetColumns}
/>
);
const externalControlColumns: EuiDataGridControlColumn[] = [
const externalControlColumns: RowControlColumn[] = [
{
id: 'take-action',
width: 20,
id: 'more-actions',
headerAriaLabel: moreActionsLabel,
headerCellRender: () => null,
rowCellRender: ({ rowIndex }) => (
renderControl: () => (
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.securitySolution.assetInventory.flyout.moreActionsButton',
{
defaultMessage: 'More actions',
}
)}
aria-label={moreActionsLabel}
iconType="boxesHorizontal"
color="primary"
isLoading={isLoading}
// onClick={() => createFn(rowIndex)}
isLoading={isLoadingGridData}
/>
),
},
];
const loadingState = isLoading || !dataView ? DataLoadingState.loading : DataLoadingState.loaded;
const loadingState =
isLoadingGridData || !dataView ? DataLoadingState.loading : DataLoadingState.loaded;
return (
<I18nProvider>
{!dataView ? null : (
<AssetInventorySearchBar
query={urlQuery}
setQuery={setUrlQuery}
loading={loadingState === DataLoadingState.loading}
/>
)}
<AssetInventorySearchBar
query={urlQuery}
setQuery={setUrlQuery}
loading={loadingState === DataLoadingState.loading}
/>
<EuiPageTemplate.Section>
<EuiTitle size="l" data-test-subj="all-assets-title">
<EuiTitle size="l" data-test-subj={TEST_SUBJ_PAGE_TITLE}>
<h1>
<FormattedMessage
id="xpack.securitySolution.assetInventory.allAssets"
id="xpack.securitySolution.assetInventory.allAssets.title"
defaultMessage="All Assets"
/>
<EuiBetaBadge
css={css`
margin-left: ${euiTheme.size.s};
`}
label={i18n.translate('xpack.securitySolution.assetInventory.technicalPreviewLabel', {
defaultMessage: 'Technical Preview',
})}
size="s"
color="subdued"
tooltipContent={i18n.translate(
'xpack.securitySolution.assetInventory.technicalPreviewTooltip',
{
defaultMessage:
'This functionality is experimental and not supported. It may change or be removed at any time.',
}
)}
/>
<TechnicalPreviewBadge />
</h1>
</EuiTitle>
<Filters
onFiltersChange={(newFilters: Filter[]) => {
setUrlQuery({ filters: newFilters });
}}
/>
<Filters setQuery={setUrlQuery} />
{dataView ? (
<TopAssetsBarChart
isLoading={isLoadingChartData}
@ -462,13 +414,13 @@ export const AllAssets = ({
) : null}
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
<div
data-test-subj={rest['data-test-subj']}
data-test-subj={TEST_SUBJ_DATA_GRID}
className={styles.gridContainer}
style={{
height: computeDataTableRendering.wrapperHeight,
}}
>
<EuiProgress size="xs" color="accent" style={{ opacity: isFetching ? 1 : 0 }} />
<EuiProgress size="xs" color="accent" style={{ opacity: isFetchingGridData ? 1 : 0 }} />
{!dataView ? null : loadingState === DataLoadingState.loaded && totalHits === 0 ? (
<EmptyState onResetFilters={onResetFilters} />
) : (
@ -498,14 +450,12 @@ export const AllAssets = ({
showTimeCol={false}
settings={settings}
onFetchMoreRecords={loadMore}
externalControlColumns={externalControlColumns}
rowAdditionalLeadingControls={externalControlColumns}
externalCustomRenderers={externalCustomRenderers}
externalAdditionalControls={externalAdditionalControls}
gridStyleOverride={gridStyle}
rowLineHeightOverride="24px"
dataGridDensityState={DataGridDensity.EXPANDED}
showFullScreenButton
// showKeyboardShortcuts
/>
)}
</div>

View file

@ -1,109 +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 { type DataTableRecord } from '@kbn/discover-utils/types';
export const mockData = [
{
id: '1',
raw: {},
flattened: {
'asset.risk': 89,
'asset.name': 'kube-scheduler-cspm-control',
'asset.criticality': 'high_impact',
'asset.source': 'cloud-sec-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '2',
raw: {},
flattened: {
'asset.risk': 88,
'asset.name': 'elastic-agent-LK3r',
'asset.criticality': 'low_impact',
'asset.source': 'security-ci',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '3',
raw: {},
flattened: {
'asset.risk': 89,
'asset.name': 'app-server-1',
'asset.criticality': 'high_impact',
'asset.source': 'sa-testing',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '4',
raw: {},
flattened: {
'asset.risk': 87,
'asset.name': 'database-backup-control',
'asset.criticality': 'high_impact',
'asset.source': 'elastic-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '5',
raw: {},
flattened: {
'asset.risk': 69,
'asset.name': 'elastic-agent-XyZ3',
'asset.criticality': 'low_impact',
'asset.source': 'elastic-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '6',
raw: {},
flattened: {
'asset.risk': 65,
'asset.name': 'kube-controller-cspm-monitor',
'asset.criticality': 'unassigned_impact',
'asset.source': 'cloud-sec-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '7',
raw: {},
flattened: {
'asset.risk': 89,
'asset.name': 'storage-service-AWS-EU-1',
'asset.criticality': 'medium_impact',
'asset.source': 'cloud-sec-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '8',
raw: {},
flattened: {
'asset.risk': 19,
'asset.name': 'web-server-LB2',
'asset.criticality': 'low_impact',
'asset.source': 'cloud-sec-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
{
id: '9',
raw: {},
flattened: {
'asset.risk': 85,
'asset.name': 'DNS-controller-azure-sec',
'asset.criticality': 'unassigned_impact',
'asset.source': 'cloud-sec-dev',
'@timestamp': '2025-01-01T00:00:00.000Z',
},
},
] as DataTableRecord[];

View file

@ -13,7 +13,7 @@ import { setKibanaSetting } from '../../tasks/api_calls/kibana_advanced_settings
import { ASSET_INVENTORY_URL } from '../../urls/navigation';
const NO_PRIVILEGES_BOX = getDataTestSubjectSelector('noPrivilegesPage');
const ALL_ASSETS_TITLE = getDataTestSubjectSelector('all-assets-title');
const ALL_ASSETS_TITLE = getDataTestSubjectSelector('asset-inventory-test-subj-page-title');
const disableAssetInventory = () => {
setKibanaSetting(SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING, false);