mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
e24c1c3ee5
commit
2473d5951a
16 changed files with 182 additions and 311 deletions
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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};
|
||||
`;
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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: {},
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
|
@ -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>
|
||||
|
|
|
@ -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[];
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue