mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Implement Asset Inventory data grid (#206115)
## Summary Closes https://github.com/elastic/security-team/issues/11270. ### Screenshots <details><summary>Current state</summary> <img width="1486" alt="Screenshot 2025-01-15 at 17 28 42" src="https://github.com/user-attachments/assets/1a39ae67-406c-464d-849b-0fba3380e982" /> </details> <details><summary>Current state + RiskBadge + Criticality + SearchBar (implemented in separate PRs)</summary> <img width="1752" alt="Screenshot 2025-01-13 at 16 34 10" src="https://github.com/user-attachments/assets/bca30c71-dba3-4505-aba6-a3787ba7f6b1" /> </details> ### Definition of done > [!NOTE] > For now it only works with static data until backend is ready - [x] Implement DataGrid using the `<UnifiedDataTable>` component, based on [[EuiDataGrid](https://eui.elastic.co/#/tabular-content/data-grid)](https://eui.elastic.co/#/tabular-content/data-grid), ensuring consistency with Kibana standards. - [x] Configure columns as follows: - **Action column**: No label; includes a button in each row to expand the `EntityFlyout`. - **Risk**: Numerical indicators representing the asset's risk. - **Name**: The name or identifier of the asset. - **Criticality**: Displays priority or severity levels (e.g., High, Medium, Low). Field `asset.criticality` - **Source**: Represents the asset source (e.g., Host, Storage, Database). `asset.source` - **Last Seen**: Timestamp indicating the last observed data for the asset. - [x] Add static/mock data rows to display paginated asset data, with each row including: - Buttons/icons for expanding the `EntityFlyout`. - [x] Include the following interactive elements: - [x] Multi-sorting: Allow users to sort by multiple columns (e.g., Risk and Criticality). **This only works if fields are added manually to the DataView** - [x] Columns selector: Provide an option for users to show/hide specific columns. - [x] Fullscreen toggle: Allow users to expand the DataGrid to fullscreen mode for enhanced visibility. - [x] Pagination controls: Enable navigation across multiple pages of data. - [x] Rows per page dropdown: Allow users to select the number of rows displayed per page (10, 25, 50, 100, 250, 500). - [x] Enforce constraints: - Limit search results to 500 at a time using `UnifiedDataTable`'s pagination helper for loading more data once the limit is reached. ### Out of scope - Risk score colored badges (implemented in follow-up PR) - Group-by functionality or switching between grid and grouped views - Field selector implementation - Flyout rendering ### Duplicated files > [!CAUTION] > As of now, `<UnifiedDataTable>` is a complex component that needs to be fed with multiple props. For that, we need several components, hooks and utilities that currently exist within the CSP plugin and are too coupled with it. It's currently not possible to reuse all this logic unless we move that into a separate @kbn-package so I had to temporarily duplicate a bunch of files. This is the list to account them for: - `hooks/` - `use_asset_inventory_data_table/` - `index.ts` - `use_asset_inventory_data_table.ts` - `use_base_es_query.ts` - `use_page_size.ts` - `use_persisted_query.ts` - `use_url_query.ts` - `utils.ts` - `data_view_context.ts` - `use_fields_modal.ts` - `use_styles.ts` - `components/` - `additional_controls.tsx` - `empty_state.tsx` - `fields_selector_modal.tsx` - `fields_selector_table.tsx` This ticket will track progress on this task to remove duplicities and refactor code to have a single source of truth reusable in both Asset Inventory and CSP plugins: - https://github.com/elastic/security-team/issues/11584 ### How to test 1. Open the Index Management page in `http://localhost:5601/kbn/app/management/data/index_management` and click on "Create index". Then type `asset-inventory-logs` in the dialog's input. 2. Open the DataViews page in `http://localhost:5601/kbn/app/management/kibana/dataViews` and click on "Create Data View". 3. Fill in the flyout form typing the following values before clicking on the "Save data view to Kibana" button: - `asset-inventory-logs` in "name" and "index pattern" fields. - `@timestamp` is the value set on the "Timestamp field". - Click on "Show advanced settings", then type `asset-inventory-logs-default` in the "Custom data view ID" field. 4. Open the Inventory page from the Security solution in `http://localhost:5601/kbn/app/security/asset_inventory`. <details><summary>Data View Example</summary> <img width="894" alt="Screenshot 2025-01-10 at 11 09 00" src="https://github.com/user-attachments/assets/9a20f504-e602-4b67-a24e-0341f447878e" /> </details> ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [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) - [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. - [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 risks at all.
This commit is contained in:
parent
49f9724680
commit
5cc1315dd3
19 changed files with 1662 additions and 70 deletions
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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, { type FC, type PropsWithChildren } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
|
||||
import { type DataView } from '@kbn/data-views-plugin/common';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getAbbreviatedNumber } from '@kbn/cloud-security-posture-common';
|
||||
import { FieldsSelectorModal } from './fields_selector_modal';
|
||||
import { useFieldsModal } from '../hooks/use_fields_modal';
|
||||
import { useStyles } from '../hooks/use_styles';
|
||||
|
||||
const ASSET_INVENTORY_FIELDS_SELECTOR_OPEN_BUTTON = 'assetInventoryFieldsSelectorOpenButton';
|
||||
|
||||
const GroupSelectorWrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const AdditionalControls = ({
|
||||
total,
|
||||
title,
|
||||
dataView,
|
||||
columns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
groupSelectorComponent,
|
||||
onResetColumns,
|
||||
}: {
|
||||
total: number;
|
||||
title: string;
|
||||
dataView: DataView;
|
||||
columns: string[];
|
||||
onAddColumn: (column: string) => void;
|
||||
onRemoveColumn: (column: string) => void;
|
||||
groupSelectorComponent?: JSX.Element;
|
||||
onResetColumns: () => void;
|
||||
}) => {
|
||||
const { isFieldSelectorModalVisible, closeFieldsSelectorModal, openFieldsSelectorModal } =
|
||||
useFieldsModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFieldSelectorModalVisible && (
|
||||
<FieldsSelectorModal
|
||||
columns={columns}
|
||||
dataView={dataView}
|
||||
closeModal={closeFieldsSelectorModal}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onResetColumns={onResetColumns}
|
||||
/>
|
||||
)}
|
||||
<EuiFlexItem grow={0}>
|
||||
<span className="assetInventoryDataTableTotal">{`${getAbbreviatedNumber(
|
||||
total
|
||||
)} ${title}`}</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonEmpty
|
||||
className="assetInventoryDataTableFields"
|
||||
iconType="tableOfContents"
|
||||
onClick={openFieldsSelectorModal}
|
||||
size="xs"
|
||||
color="text"
|
||||
data-test-subj={ASSET_INVENTORY_FIELDS_SELECTOR_OPEN_BUTTON}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.dataTable.fieldsButton"
|
||||
defaultMessage="Fields"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{groupSelectorComponent && (
|
||||
<GroupSelectorWrapper>{groupSelectorComponent}</GroupSelectorWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,34 +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 React from 'react';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
|
||||
import { EuiPageTemplate, EuiTitle } from '@elastic/eui';
|
||||
|
||||
const AssetInventoryApp = () => {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<>
|
||||
<EuiPageTemplate restrictWidth="1000px">
|
||||
<EuiPageTemplate.Header>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.allAssets"
|
||||
defaultMessage="All Assets"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageTemplate.Header>
|
||||
<EuiPageTemplate.Section />
|
||||
</EuiPageTemplate>
|
||||
</>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// we need to use default exports to import it via React.lazy
|
||||
export default AssetInventoryApp; // eslint-disable-line import/no-default-export
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { EuiImage, EuiEmptyPrompt, EuiButton, EuiLink, useEuiTheme } from '@elastic/eui';
|
||||
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';
|
||||
|
||||
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;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
css={css`
|
||||
max-width: 734px;
|
||||
&& > .euiEmptyPrompt__main {
|
||||
gap: ${euiTheme.size.xl};
|
||||
}
|
||||
&& {
|
||||
margin-top: ${euiTheme.size.xxxl}};
|
||||
}
|
||||
`}
|
||||
data-test-subj={EMPTY_STATE_TEST_SUBJ}
|
||||
icon={
|
||||
<EuiImage
|
||||
url={illustration}
|
||||
alt={i18n.translate('xpack.securitySolution.assetInventory.emptyState.illustrationAlt', {
|
||||
defaultMessage: 'No results',
|
||||
})}
|
||||
css={css`
|
||||
width: 290px;
|
||||
`}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.title"
|
||||
defaultMessage="No results match your search criteria"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
layout="horizontal"
|
||||
color="plain"
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.description"
|
||||
defaultMessage="Try modifying your search or filter set"
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
<EuiButton color="primary" fill onClick={onResetFilters}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.resetFiltersButton"
|
||||
defaultMessage="Reset filters"
|
||||
/>
|
||||
</EuiButton>,
|
||||
<EuiLink href={docsUrl} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.readDocsLink"
|
||||
defaultMessage="Read the docs"
|
||||
/>
|
||||
</EuiLink>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { type DataView } from '@kbn/data-views-plugin/common';
|
||||
import { FieldsSelectorTable } from './fields_selector_table';
|
||||
|
||||
const ASSET_INVENTORY_FIELDS_SELECTOR_MODAL = 'assetInventoryFieldsSelectorModal';
|
||||
const ASSET_INVENTORY_FIELDS_SELECTOR_RESET_BUTTON = 'assetInventoryFieldsSelectorResetButton';
|
||||
const ASSET_INVENTORY_FIELDS_SELECTOR_CLOSE_BUTTON = 'assetInventoryFieldsSelectorCloseButton';
|
||||
|
||||
interface FieldsSelectorModalProps {
|
||||
dataView: DataView;
|
||||
columns: string[];
|
||||
onAddColumn: (column: string) => void;
|
||||
onRemoveColumn: (column: string) => void;
|
||||
closeModal: () => void;
|
||||
onResetColumns: () => void;
|
||||
}
|
||||
|
||||
const title = i18n.translate('xpack.securitySolution.assetInventory.dataTable.fieldsModalTitle', {
|
||||
defaultMessage: 'Fields',
|
||||
});
|
||||
|
||||
export const FieldsSelectorModal = ({
|
||||
closeModal,
|
||||
dataView,
|
||||
columns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
onResetColumns,
|
||||
}: FieldsSelectorModalProps) => {
|
||||
return (
|
||||
<EuiModal onClose={closeModal} data-test-subj={ASSET_INVENTORY_FIELDS_SELECTOR_MODAL}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<FieldsSelectorTable
|
||||
title={title}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
onClick={onResetColumns}
|
||||
data-test-subj={ASSET_INVENTORY_FIELDS_SELECTOR_RESET_BUTTON}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.dataTable.fieldsModalReset"
|
||||
defaultMessage="Reset Fields"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
onClick={closeModal}
|
||||
fill
|
||||
data-test-subj={ASSET_INVENTORY_FIELDS_SELECTOR_CLOSE_BUTTON}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.dataTable.fieldsModalClose"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useState } from 'react';
|
||||
import useSessionStorage from 'react-use/lib/useSessionStorage';
|
||||
import {
|
||||
type CriteriaWithPagination,
|
||||
type EuiBasicTableColumn,
|
||||
type EuiSearchBarProps,
|
||||
EuiButtonEmpty,
|
||||
EuiCheckbox,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiInMemoryTable,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
const SESSION_STORAGE_FIELDS_MODAL_SHOW_SELECTED = 'assetInventory:fieldsModal:showSelected';
|
||||
const ACTION_COLUMN_WIDTH = '24px';
|
||||
const defaultSorting = {
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const VIEW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.allAssets.fieldsModal.viewLabel',
|
||||
{
|
||||
defaultMessage: 'View',
|
||||
}
|
||||
);
|
||||
|
||||
const VIEW_VALUE_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.allAssets.fieldsModal.viewSelected',
|
||||
{
|
||||
defaultMessage: 'selected',
|
||||
}
|
||||
);
|
||||
|
||||
const VIEW_VALUE_ALL = i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.allAssets.fieldsModal.viewAll',
|
||||
{
|
||||
defaultMessage: 'all',
|
||||
}
|
||||
);
|
||||
|
||||
export interface FieldsSelectorTableProps {
|
||||
dataView: DataView;
|
||||
columns: string[];
|
||||
onAddColumn: (column: string) => void;
|
||||
onRemoveColumn: (column: string) => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const filterFieldsBySearch = (
|
||||
fields: DataViewField[],
|
||||
visibleColumns: string[] = [],
|
||||
searchQuery?: string,
|
||||
isFilterSelectedEnabled: boolean = false
|
||||
) => {
|
||||
const allowedFields = fields
|
||||
.filter((field) => field.name !== '_index' && field.visualizable)
|
||||
.map((field) => ({
|
||||
id: field.name,
|
||||
name: field.name,
|
||||
displayName: field.customLabel || '',
|
||||
}));
|
||||
|
||||
const visibleFields = !isFilterSelectedEnabled
|
||||
? allowedFields
|
||||
: allowedFields.filter((field) => visibleColumns.includes(field.id));
|
||||
|
||||
return !searchQuery
|
||||
? visibleFields
|
||||
: visibleFields.filter((field) => {
|
||||
const normalizedName = `${field.name} ${field.displayName}`.toLowerCase();
|
||||
const normalizedQuery = searchQuery.toLowerCase() || '';
|
||||
return normalizedName.indexOf(normalizedQuery) !== -1;
|
||||
});
|
||||
};
|
||||
|
||||
export const FieldsSelectorTable = ({
|
||||
title,
|
||||
dataView,
|
||||
columns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
}: FieldsSelectorTableProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState<string | undefined>();
|
||||
const [isFilterSelectedEnabled, setIsFilterSelectedEnabled] = useSessionStorage(
|
||||
SESSION_STORAGE_FIELDS_MODAL_SHOW_SELECTED,
|
||||
false
|
||||
);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0 });
|
||||
const onTableChange = ({ page: { index } }: CriteriaWithPagination<Field>) => {
|
||||
setPagination({ pageIndex: index });
|
||||
};
|
||||
const fields = useMemo<Field[]>(
|
||||
() =>
|
||||
filterFieldsBySearch(dataView.fields.getAll(), columns, searchQuery, isFilterSelectedEnabled),
|
||||
[dataView, columns, searchQuery, isFilterSelectedEnabled]
|
||||
);
|
||||
|
||||
const togglePopover = useCallback(() => {
|
||||
setIsPopoverOpen((open) => !open);
|
||||
}, []);
|
||||
const closePopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const onFilterSelectedChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
setIsFilterSelectedEnabled(enabled);
|
||||
},
|
||||
[setIsFilterSelectedEnabled]
|
||||
);
|
||||
|
||||
let debounceTimeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
const onQueryChange: EuiSearchBarProps['onChange'] = ({ query }) => {
|
||||
clearTimeout(debounceTimeoutId);
|
||||
|
||||
debounceTimeoutId = setTimeout(() => {
|
||||
setSearchQuery(query?.text);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const tableColumns: Array<EuiBasicTableColumn<Field>> = [
|
||||
{
|
||||
field: 'action',
|
||||
name: '',
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
sortable: false,
|
||||
render: (_, { id }: Field) => (
|
||||
<EuiCheckbox
|
||||
checked={columns.includes(id)}
|
||||
id={`cloud-security-fields-selector-item-${id}`}
|
||||
data-test-subj={`cloud-security-fields-selector-item-${id}`}
|
||||
onChange={(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
return isChecked ? onAddColumn(id) : onRemoveColumn(id);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.securitySolution.assetInventory.allAssets.fieldsModalName', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const error = useMemo(() => {
|
||||
if (!dataView || dataView.fields.length === 0) {
|
||||
return i18n.translate('xpack.securitySolution.assetInventory.allAssets.fieldsModalError', {
|
||||
defaultMessage: 'No fields found in the data view',
|
||||
});
|
||||
}
|
||||
return '';
|
||||
}, [dataView]);
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
onChange: onQueryChange,
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.allAssets.fieldsModalSearch',
|
||||
{
|
||||
defaultMessage: 'Search field name',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const tableHeader = useMemo(() => {
|
||||
const totalFields = fields.length;
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText data-test-subj="assetInventory:dataTable:fieldsModal:fieldsShowing" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.allAssets.fieldsModalFieldsShowing"
|
||||
defaultMessage="Showing"
|
||||
/>{' '}
|
||||
<strong data-test-subj="assetInventory:dataTable:fieldsModal:fieldsCount">
|
||||
{totalFields}
|
||||
</strong>{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.allAssets.fieldsModalFieldsCount"
|
||||
defaultMessage="{totalFields, plural, one {field} other {fields}}"
|
||||
values={{
|
||||
totalFields,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="viewSelectorButton"
|
||||
size="xs"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={togglePopover}
|
||||
>
|
||||
{`${VIEW_LABEL}: ${isFilterSelectedEnabled ? VIEW_VALUE_SELECTED : VIEW_VALUE_ALL}`}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
data-test-subj="viewSelectorMenu"
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="viewSelectorOption-all"
|
||||
key="viewAll"
|
||||
icon={isFilterSelectedEnabled ? 'empty' : 'check'}
|
||||
onClick={() => {
|
||||
onFilterSelectedChange(false);
|
||||
closePopover();
|
||||
}}
|
||||
>
|
||||
{`${VIEW_LABEL} ${VIEW_VALUE_ALL}`}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiHorizontalRule key="separator" margin="none" />,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="viewSelectorOption-selected"
|
||||
key="viewSelected"
|
||||
icon={isFilterSelectedEnabled ? 'check' : 'empty'}
|
||||
onClick={() => {
|
||||
onFilterSelectedChange(true);
|
||||
closePopover();
|
||||
}}
|
||||
>
|
||||
{`${VIEW_LABEL} ${VIEW_VALUE_SELECTED}`}
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [
|
||||
closePopover,
|
||||
fields.length,
|
||||
isFilterSelectedEnabled,
|
||||
isPopoverOpen,
|
||||
onFilterSelectedChange,
|
||||
togglePopover,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
tableCaption={title}
|
||||
items={fields}
|
||||
columns={tableColumns}
|
||||
search={search}
|
||||
pagination={pagination}
|
||||
sorting={defaultSorting}
|
||||
error={error}
|
||||
childrenBetween={tableHeader}
|
||||
onTableChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { createContext, useContext } from 'react';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
interface DataViewContextValue {
|
||||
dataView: DataView;
|
||||
dataViewRefetch?: () => void;
|
||||
dataViewIsLoading?: boolean;
|
||||
dataViewIsRefetching?: boolean;
|
||||
}
|
||||
|
||||
export const DataViewContext = createContext<DataViewContextValue | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Retrieve context's properties
|
||||
*/
|
||||
export const useDataViewContext = (): DataViewContextValue => {
|
||||
const contextValue = useContext(DataViewContext);
|
||||
|
||||
if (!contextValue) {
|
||||
throw new Error('useDataViewContext can only be used within DataViewContext provider');
|
||||
}
|
||||
|
||||
return contextValue;
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './use_asset_inventory_data_table';
|
||||
export * from './use_base_es_query';
|
||||
export * from './use_persisted_query';
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 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 { 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 URLQuery = AssetsBaseURLQuery & Record<string, unknown>;
|
||||
|
||||
type SortOrder = [string, string];
|
||||
|
||||
export interface AssetInventoryDataTableResult {
|
||||
setUrlQuery: (query: Record<string, unknown>) => void;
|
||||
sort: SortOrder[];
|
||||
filters: Filter[];
|
||||
query: { bool: BoolQuery };
|
||||
queryError?: Error;
|
||||
pageIndex: number;
|
||||
urlQuery: URLQuery;
|
||||
setTableOptions: (options: CriteriaWithPagination<object>) => void;
|
||||
handleUpdateQuery: (query: URLQuery) => void;
|
||||
pageSize: number;
|
||||
setPageSize: Dispatch<SetStateAction<number | undefined>>;
|
||||
onChangeItemsPerPage: (newPageSize: number) => void;
|
||||
onChangePage: (newPageIndex: number) => void;
|
||||
onSort: (sort: string[][]) => void;
|
||||
onResetFilters: () => void;
|
||||
columnsLocalStorageKey: string;
|
||||
getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[];
|
||||
}
|
||||
|
||||
/*
|
||||
Hook for managing common table state and methods for the Asset Inventory DataTable
|
||||
*/
|
||||
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);
|
||||
const { pageSize, setPageSize } = usePageSize(paginationLocalStorageKey);
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setUrlQuery({
|
||||
pageIndex: 0,
|
||||
pageSize: newPageSize,
|
||||
});
|
||||
},
|
||||
[setPageSize, setUrlQuery]
|
||||
);
|
||||
|
||||
const onResetFilters = useCallback(() => {
|
||||
setUrlQuery({
|
||||
pageIndex: 0,
|
||||
filters: [],
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
});
|
||||
}, [setUrlQuery]);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
(newPageIndex: number) => {
|
||||
setUrlQuery({
|
||||
pageIndex: newPageIndex,
|
||||
});
|
||||
},
|
||||
[setUrlQuery]
|
||||
);
|
||||
|
||||
const onSort = useCallback(
|
||||
(sort: string[][]) => {
|
||||
setUrlQuery({
|
||||
sort,
|
||||
});
|
||||
},
|
||||
[setUrlQuery]
|
||||
);
|
||||
|
||||
const setTableOptions = useCallback(
|
||||
({ page, sort }: CriteriaWithPagination<object>) => {
|
||||
setPageSize(page.size);
|
||||
setUrlQuery({
|
||||
sort,
|
||||
pageIndex: page.index,
|
||||
});
|
||||
},
|
||||
[setUrlQuery, setPageSize]
|
||||
);
|
||||
|
||||
/**
|
||||
* Page URL query to ES query
|
||||
*/
|
||||
const baseEsQuery = useBaseEsQuery({
|
||||
filters: urlQuery.filters,
|
||||
query: urlQuery.query,
|
||||
...(nonPersistedFilters ? { nonPersistedFilters } : {}),
|
||||
});
|
||||
|
||||
const handleUpdateQuery = useCallback(
|
||||
(query: URLQuery) => {
|
||||
setUrlQuery({ ...query, pageIndex: 0 });
|
||||
},
|
||||
[setUrlQuery]
|
||||
);
|
||||
|
||||
const getRowsFromPages = (data: Array<{ page: DataTableRecord[] }> | undefined) =>
|
||||
data
|
||||
?.map(({ page }: { page: DataTableRecord[] }) => {
|
||||
return page;
|
||||
})
|
||||
.flat() || [];
|
||||
|
||||
const queryError = baseEsQuery instanceof Error ? baseEsQuery : undefined;
|
||||
|
||||
return {
|
||||
setUrlQuery,
|
||||
sort: urlQuery.sort as SortOrder[],
|
||||
filters: urlQuery.filters || [],
|
||||
query: baseEsQuery.query
|
||||
? baseEsQuery.query
|
||||
: {
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
queryError,
|
||||
pageIndex: urlQuery.pageIndex as number,
|
||||
urlQuery,
|
||||
setTableOptions,
|
||||
handleUpdateQuery,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
onSort,
|
||||
onResetFilters,
|
||||
columnsLocalStorageKey: columnsLocalStorageKey || LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY,
|
||||
getRowsFromPages,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { 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 type { AssetsBaseURLQuery } from './use_asset_inventory_data_table';
|
||||
|
||||
interface AssetsBaseESQueryConfig {
|
||||
config: EsQueryConfig;
|
||||
}
|
||||
|
||||
const getBaseQuery = ({
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
config,
|
||||
}: AssetsBaseURLQuery &
|
||||
AssetsBaseESQueryConfig & {
|
||||
dataView: DataView | undefined;
|
||||
}) => {
|
||||
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 = ({
|
||||
filters = [],
|
||||
query,
|
||||
nonPersistedFilters,
|
||||
}: AssetsBaseURLQuery) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
data: {
|
||||
query: { filterManager, queryString },
|
||||
},
|
||||
uiSettings,
|
||||
} = useKibana().services;
|
||||
const { dataView } = useDataViewContext();
|
||||
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.securitySolution.assetInventory.allAssets.search.queryErrorToastMessage',
|
||||
{
|
||||
defaultMessage: 'Query Error',
|
||||
}
|
||||
),
|
||||
toastLifeTimeMs: 1000 * 5,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(handleMalformedQueryError, [baseEsQuery, toasts]);
|
||||
|
||||
return baseEsQuery;
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 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)
|
||||
|
||||
/**
|
||||
* @description handles persisting the users table row size selection
|
||||
*/
|
||||
export const usePageSize = (localStorageKey: string) => {
|
||||
const [persistedPageSize, setPersistedPageSize] = useLocalStorage(
|
||||
localStorageKey,
|
||||
DEFAULT_VISIBLE_ROWS_PER_PAGE
|
||||
);
|
||||
|
||||
let pageSize = DEFAULT_VISIBLE_ROWS_PER_PAGE;
|
||||
|
||||
if (persistedPageSize) {
|
||||
pageSize = persistedPageSize;
|
||||
}
|
||||
|
||||
return { pageSize, setPageSize: setPersistedPageSize };
|
||||
};
|
|
@ -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 type { AssetsBaseURLQuery } from './use_asset_inventory_data_table';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
export const usePersistedQuery = <T>(getter: ({ filters, query }: AssetsBaseURLQuery) => T) => {
|
||||
const {
|
||||
data: {
|
||||
query: { filterManager, queryString },
|
||||
},
|
||||
} = useKibana().services;
|
||||
|
||||
return useCallback(
|
||||
() =>
|
||||
getter({
|
||||
filters: filterManager.getAppFilters(),
|
||||
query: queryString.getQuery() as Query,
|
||||
}),
|
||||
[getter, filterManager, queryString]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { encodeQuery, decodeQuery } from '@kbn/cloud-security-posture';
|
||||
|
||||
/**
|
||||
* @description uses 'rison' to encode/decode a url query
|
||||
* @todo replace getDefaultQuery with schema. validate after decoded from URL, use defaultValues
|
||||
* @note shallow-merges default, current and next query
|
||||
*/
|
||||
export const useUrlQuery = <T extends object>(getDefaultQuery: () => T) => {
|
||||
const { push, replace } = useHistory();
|
||||
const { search, key } = useLocation();
|
||||
|
||||
const urlQuery = useMemo(
|
||||
() => ({ ...getDefaultQuery(), ...decodeQuery<T>(search) }),
|
||||
[getDefaultQuery, search]
|
||||
);
|
||||
|
||||
const setUrlQuery = useCallback(
|
||||
(query: Partial<T>) =>
|
||||
push({
|
||||
search: encodeQuery({ ...getDefaultQuery(), ...urlQuery, ...query }),
|
||||
}),
|
||||
[getDefaultQuery, urlQuery, push]
|
||||
);
|
||||
|
||||
// Set initial query
|
||||
useEffect(() => {
|
||||
if (search) return;
|
||||
|
||||
replace({ search: encodeQuery(getDefaultQuery()) });
|
||||
}, [getDefaultQuery, search, replace]);
|
||||
|
||||
return {
|
||||
key,
|
||||
urlQuery,
|
||||
setUrlQuery,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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,
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { useState } from 'react';
|
||||
|
||||
export const useFieldsModal = () => {
|
||||
const [isFieldSelectorModalVisible, setIsFieldSelectorModalVisible] = useState(false);
|
||||
|
||||
const closeFieldsSelectorModal = () => setIsFieldSelectorModalVisible(false);
|
||||
const openFieldsSelectorModal = () => setIsFieldSelectorModalVisible(true);
|
||||
|
||||
return {
|
||||
isFieldSelectorModalVisible,
|
||||
closeFieldsSelectorModal,
|
||||
openFieldsSelectorModal,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const gridContainer = css`
|
||||
min-height: 400px;
|
||||
`;
|
||||
|
||||
const gridStyle = css`
|
||||
& .euiDataGridHeaderCell__icon {
|
||||
display: none;
|
||||
}
|
||||
& .euiDataGrid__controls {
|
||||
border-bottom: none;
|
||||
margin-bottom: ${euiTheme.size.s};
|
||||
border-top: none;
|
||||
}
|
||||
& .euiDataGrid--headerUnderline .euiDataGridHeaderCell {
|
||||
border-bottom: ${euiTheme.border.width.thick} solid ${euiTheme.colors.fullShade};
|
||||
}
|
||||
& .euiButtonIcon[data-test-subj='docTableExpandToggleColumn'] {
|
||||
color: ${euiTheme.colors.primary};
|
||||
}
|
||||
|
||||
& .euiDataGridRowCell {
|
||||
font-size: ${euiTheme.size.m};
|
||||
|
||||
// Vertically center content
|
||||
.euiDataGridRowCell__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
& .euiDataGridRowCell.euiDataGridRowCell--numeric {
|
||||
text-align: left;
|
||||
}
|
||||
& .euiDataGridHeaderCell--numeric .euiDataGridHeaderCell__content {
|
||||
flex-grow: 0;
|
||||
text-align: left;
|
||||
}
|
||||
& .assetInventoryDataTableTotal {
|
||||
font-size: ${euiTheme.size.m};
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
border-right: ${euiTheme.border.thin};
|
||||
margin-inline: ${euiTheme.size.s};
|
||||
padding-right: ${euiTheme.size.m};
|
||||
}
|
||||
|
||||
& [data-test-subj='docTableExpandToggleColumn'] svg {
|
||||
inline-size: 16px;
|
||||
block-size: 16px;
|
||||
}
|
||||
|
||||
& .unifiedDataTable__cellValue {
|
||||
font-family: ${euiTheme.font.family};
|
||||
}
|
||||
& .unifiedDataTable__inner .euiDataGrid__controls {
|
||||
border-top: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const groupBySelector = css`
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
return {
|
||||
gridStyle,
|
||||
groupBySelector,
|
||||
gridContainer,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,431 @@
|
|||
/*
|
||||
* 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, { 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 {
|
||||
UnifiedDataTable,
|
||||
DataLoadingState,
|
||||
DataGridDensity,
|
||||
useColumns,
|
||||
type UnifiedDataTableSettings,
|
||||
type UnifiedDataTableSettingsColumn,
|
||||
} 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 DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import {
|
||||
type EuiDataGridCellValueElementProps,
|
||||
type EuiDataGridControlColumn,
|
||||
type EuiDataGridStyle,
|
||||
EuiProgress,
|
||||
EuiPageTemplate,
|
||||
EuiTitle,
|
||||
EuiButtonIcon,
|
||||
} 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 { type CriticalityLevelWithUnassigned } from '../../../common/entity_analytics/asset_criticality/types';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
import { EmptyState } from '../components/empty_state';
|
||||
import { AdditionalControls } from '../components/additional_controls';
|
||||
|
||||
import { useDataViewContext } from '../hooks/data_view_context';
|
||||
import { useStyles } from '../hooks/use_styles';
|
||||
import {
|
||||
useAssetInventoryDataTable,
|
||||
type AssetsBaseURLQuery,
|
||||
type URLQuery,
|
||||
} from '../hooks/use_asset_inventory_data_table';
|
||||
|
||||
const gridStyle: EuiDataGridStyle = {
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
header: 'underline',
|
||||
};
|
||||
|
||||
const MAX_ASSETS_TO_LOAD = 500; // equivalent to MAX_FINDINGS_TO_LOAD in @kbn/cloud-security-posture-common
|
||||
|
||||
const title = i18n.translate('xpack.securitySolution.assetInventory.allAssets.tableRowTypeLabel', {
|
||||
defaultMessage: 'assets',
|
||||
});
|
||||
|
||||
const columnsLocalStorageKey = 'assetInventoryColumns';
|
||||
const LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY = 'assetInventory:dataTable:pageSize';
|
||||
|
||||
const columnHeaders: Record<string, string> = {
|
||||
'asset.risk': i18n.translate('xpack.securitySolution.assetInventory.allAssets.risk', {
|
||||
defaultMessage: 'Risk',
|
||||
}),
|
||||
'asset.name': i18n.translate('xpack.securitySolution.assetInventory.allAssets.name', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
'asset.criticality': i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.allAssets.criticality',
|
||||
{
|
||||
defaultMessage: 'Criticality',
|
||||
}
|
||||
),
|
||||
'asset.source': i18n.translate('xpack.securitySolution.assetInventory.allAssets.source', {
|
||||
defaultMessage: 'Source',
|
||||
}),
|
||||
'@timestamp': i18n.translate('xpack.securitySolution.assetInventory.allAssets.lastSeen', {
|
||||
defaultMessage: 'Last Seen',
|
||||
}),
|
||||
} as const;
|
||||
|
||||
const customCellRenderer = (rows: DataTableRecord[]) => ({
|
||||
'asset.risk': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
|
||||
const risk = rows[rowIndex].flattened['asset.risk'] as number;
|
||||
return risk;
|
||||
},
|
||||
'asset.criticality': ({ rowIndex }: EuiDataGridCellValueElementProps) => {
|
||||
const criticality = rows[rowIndex].flattened[
|
||||
'asset.criticality'
|
||||
] as CriticalityLevelWithUnassigned;
|
||||
return criticality;
|
||||
},
|
||||
});
|
||||
|
||||
interface AssetInventoryDefaultColumn {
|
||||
id: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const defaultColumns: AssetInventoryDefaultColumn[] = [
|
||||
{ id: 'asset.risk', width: 50 },
|
||||
{ id: 'asset.name', width: 400 },
|
||||
{ id: 'asset.criticality' },
|
||||
{ id: 'asset.source' },
|
||||
{ id: '@timestamp' },
|
||||
];
|
||||
|
||||
const getDefaultQuery = ({ query, filters }: AssetsBaseURLQuery): URLQuery => ({
|
||||
query,
|
||||
filters,
|
||||
sort: [['@timestamp', 'desc']],
|
||||
});
|
||||
|
||||
export interface AllAssetsProps {
|
||||
rows: DataTableRecord[];
|
||||
isLoading: boolean;
|
||||
height?: number | string;
|
||||
loadMore: () => void;
|
||||
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;
|
||||
/**
|
||||
* This is the component that will be rendered in the flyout when a row is expanded.
|
||||
* This component will receive the row data and a function to close the flyout.
|
||||
*/
|
||||
flyoutComponent: (hit: DataTableRecord, onCloseFlyout: () => void) => JSX.Element;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const AllAssets = ({
|
||||
rows,
|
||||
isLoading,
|
||||
loadMore,
|
||||
nonPersistedFilters,
|
||||
height,
|
||||
hasDistributionBar = true,
|
||||
createFn,
|
||||
flyoutComponent,
|
||||
...rest
|
||||
}: AllAssetsProps) => {
|
||||
const assetInventoryDataTable = useAssetInventoryDataTable({
|
||||
paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY,
|
||||
columnsLocalStorageKey,
|
||||
defaultQuery: getDefaultQuery,
|
||||
nonPersistedFilters,
|
||||
});
|
||||
|
||||
const {
|
||||
// columnsLocalStorageKey,
|
||||
pageSize,
|
||||
onChangeItemsPerPage,
|
||||
setUrlQuery,
|
||||
onSort,
|
||||
onResetFilters,
|
||||
filters,
|
||||
sort,
|
||||
} = assetInventoryDataTable;
|
||||
|
||||
const [columns, setColumns] = useLocalStorage(
|
||||
columnsLocalStorageKey,
|
||||
defaultColumns.map((c) => c.id)
|
||||
);
|
||||
|
||||
const [persistedSettings, setPersistedSettings] = useLocalStorage<UnifiedDataTableSettings>(
|
||||
`${columnsLocalStorageKey}:settings`,
|
||||
{
|
||||
columns: defaultColumns.reduce((columnSettings, column) => {
|
||||
const columnDefaultSettings = column.width ? { width: column.width } : {};
|
||||
const newColumn = { [column.id]: columnDefaultSettings };
|
||||
return { ...columnSettings, ...newColumn };
|
||||
}, {} as UnifiedDataTableSettings['columns']),
|
||||
}
|
||||
);
|
||||
|
||||
const settings = useMemo(() => {
|
||||
return {
|
||||
columns: Object.keys(persistedSettings?.columns as UnifiedDataTableSettings).reduce(
|
||||
(columnSettings, columnId) => {
|
||||
const newColumn: UnifiedDataTableSettingsColumn = {
|
||||
..._.pick(persistedSettings?.columns?.[columnId], ['width']),
|
||||
display: columnHeaders?.[columnId],
|
||||
};
|
||||
|
||||
return { ...columnSettings, [columnId]: newColumn };
|
||||
},
|
||||
{} as UnifiedDataTableSettings['columns']
|
||||
),
|
||||
};
|
||||
}, [persistedSettings]);
|
||||
|
||||
const { dataView, dataViewIsLoading, dataViewIsRefetching } = useDataViewContext();
|
||||
|
||||
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
|
||||
|
||||
const renderDocumentView = (hit: DataTableRecord) =>
|
||||
flyoutComponent(hit, () => setExpandedDoc(undefined));
|
||||
|
||||
const {
|
||||
uiActions,
|
||||
uiSettings,
|
||||
dataViews,
|
||||
data,
|
||||
application,
|
||||
theme,
|
||||
fieldFormats,
|
||||
notifications,
|
||||
storage,
|
||||
} = useKibana().services;
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
const { capabilities } = application;
|
||||
const { filterManager } = data.query;
|
||||
|
||||
const services = {
|
||||
theme,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
toastNotifications: notifications.toasts,
|
||||
storage,
|
||||
data,
|
||||
};
|
||||
|
||||
const {
|
||||
columns: currentColumns,
|
||||
onSetColumns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
} = useColumns({
|
||||
capabilities,
|
||||
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
|
||||
dataView,
|
||||
dataViews,
|
||||
setAppState: (props) => setColumns(props.columns),
|
||||
columns,
|
||||
sort,
|
||||
});
|
||||
|
||||
/**
|
||||
* This object is used to determine if the table rendering will be virtualized and the virtualization wrapper height.
|
||||
* mode should be passed as a key to the UnifiedDataTable component to force a re-render when the mode changes.
|
||||
*/
|
||||
const computeDataTableRendering = useMemo(() => {
|
||||
// Enable virtualization mode when the table is set to a large page size.
|
||||
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 {
|
||||
wrapperHeight: getWrapperHeight(),
|
||||
mode: isVirtualizationEnabled ? 'virtualized' : 'standard',
|
||||
};
|
||||
}, [pageSize, height, filters?.length, hasDistributionBar]);
|
||||
|
||||
const onAddFilter: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
filterManager && dataView
|
||||
? (clickedField, values, operation) => {
|
||||
const newFilters = generateFilters(
|
||||
filterManager,
|
||||
clickedField,
|
||||
values,
|
||||
operation,
|
||||
dataView
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
setUrlQuery({
|
||||
filters: filterManager.getFilters(),
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[dataView, filterManager, setUrlQuery]
|
||||
);
|
||||
|
||||
const onResize = (colSettings: { columnId: string; width: number | undefined }) => {
|
||||
const grid = persistedSettings || {};
|
||||
const newColumns = { ...(grid.columns || {}) };
|
||||
newColumns[colSettings.columnId] = colSettings.width
|
||||
? { width: Math.round(colSettings.width) }
|
||||
: {};
|
||||
const newGrid = { ...grid, columns: newColumns };
|
||||
setPersistedSettings(newGrid);
|
||||
};
|
||||
|
||||
const externalCustomRenderers = useMemo(() => {
|
||||
if (!customCellRenderer) {
|
||||
return undefined;
|
||||
}
|
||||
return customCellRenderer(rows);
|
||||
}, [rows]);
|
||||
|
||||
const onResetColumns = () => {
|
||||
setColumns(defaultColumns.map((c) => c.id));
|
||||
};
|
||||
|
||||
const externalAdditionalControls = (
|
||||
<AdditionalControls
|
||||
total={rows.length}
|
||||
dataView={dataView}
|
||||
title={title}
|
||||
columns={currentColumns}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
// groupSelectorComponent={groupSelectorComponent}
|
||||
onResetColumns={onResetColumns}
|
||||
/>
|
||||
);
|
||||
|
||||
const externalControlColumns: EuiDataGridControlColumn[] = [
|
||||
{
|
||||
id: 'take-action',
|
||||
width: 20,
|
||||
headerCellRender: () => null,
|
||||
rowCellRender: ({ rowIndex }) => (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.flyout.moreActionsButton',
|
||||
{
|
||||
defaultMessage: 'More actions',
|
||||
}
|
||||
)}
|
||||
iconType="boxesHorizontal"
|
||||
color="primary"
|
||||
isLoading={isLoading}
|
||||
// onClick={() => createFn(rowIndex)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const loadingStyle = {
|
||||
opacity: isLoading ? 1 : 0,
|
||||
};
|
||||
|
||||
const loadingState =
|
||||
isLoading || dataViewIsLoading || dataViewIsRefetching || !dataView
|
||||
? DataLoadingState.loading
|
||||
: DataLoadingState.loaded;
|
||||
|
||||
// TODO Improve this loading - prevent race condition fetching rows and dataView
|
||||
if (loadingState === DataLoadingState.loaded && !rows.length && !!dataView) {
|
||||
return <EmptyState onResetFilters={onResetFilters} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<EuiPageTemplate.Section>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.allAssets"
|
||||
defaultMessage="All Assets"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
|
||||
<div
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
className={styles.gridContainer}
|
||||
style={{
|
||||
height: computeDataTableRendering.wrapperHeight,
|
||||
}}
|
||||
>
|
||||
<EuiProgress size="xs" color="accent" style={loadingStyle} />
|
||||
{!dataView ? null : (
|
||||
<UnifiedDataTable
|
||||
key={computeDataTableRendering.mode}
|
||||
className={styles.gridStyle}
|
||||
ariaLabelledBy={title}
|
||||
columns={currentColumns}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
loadingState={loadingState}
|
||||
onFilter={onAddFilter as DocViewFilterFn}
|
||||
onResize={onResize}
|
||||
onSetColumns={onSetColumns}
|
||||
onSort={onSort}
|
||||
rows={rows}
|
||||
sampleSizeState={MAX_ASSETS_TO_LOAD}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
renderDocumentView={renderDocumentView}
|
||||
sort={sort}
|
||||
rowsPerPageState={pageSize}
|
||||
totalHits={rows.length}
|
||||
services={services}
|
||||
onUpdateRowsPerPage={onChangeItemsPerPage}
|
||||
rowHeightState={0}
|
||||
showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
|
||||
showTimeCol={false}
|
||||
settings={settings}
|
||||
onFetchMoreRecords={loadMore}
|
||||
externalControlColumns={externalControlColumns}
|
||||
externalCustomRenderers={externalCustomRenderers}
|
||||
externalAdditionalControls={externalAdditionalControls}
|
||||
gridStyleOverride={gridStyle}
|
||||
rowLineHeightOverride="24px"
|
||||
dataGridDensityState={DataGridDensity.EXPANDED}
|
||||
showFullScreenButton
|
||||
// showKeyboardShortcuts
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CellActionsProvider>
|
||||
</EuiPageTemplate.Section>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// we need to use default exports to import it via React.lazy
|
||||
export default AllAssets; // eslint-disable-line import/no-default-export
|
|
@ -1,24 +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 React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
|
||||
|
||||
const AssetInventoryLazy = lazy(() => import('../components/app'));
|
||||
|
||||
export const AssetInventoryContainer = React.memo(() => {
|
||||
return (
|
||||
<SecuritySolutionPageWrapper noPadding>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<AssetInventoryLazy />
|
||||
</Suspense>
|
||||
</SecuritySolutionPageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
AssetInventoryContainer.displayName = 'AssetInventoryContainer';
|
|
@ -5,15 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { useDataView } from '@kbn/cloud-security-posture/src/hooks/use_data_view';
|
||||
import type { SecuritySubPluginRoutes } from '../app/types';
|
||||
import { SecurityPageName } from '../app/types';
|
||||
import { ASSET_INVENTORY_PATH } from '../../common/constants';
|
||||
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
|
||||
import { AssetInventoryContainer } from './pages';
|
||||
import { DataViewContext } from './hooks/data_view_context';
|
||||
import { mockData } from './sample_data';
|
||||
|
||||
const AllAssetsLazy = lazy(() => import('./pages/all_assets'));
|
||||
|
||||
const rows = [
|
||||
...mockData,
|
||||
...mockData,
|
||||
...mockData,
|
||||
...mockData,
|
||||
...mockData,
|
||||
...mockData,
|
||||
...mockData,
|
||||
] as typeof mockData;
|
||||
|
||||
// Initializing react-query
|
||||
const queryClient = new QueryClient({
|
||||
|
@ -26,15 +41,37 @@ const queryClient = new QueryClient({
|
|||
},
|
||||
});
|
||||
|
||||
export const AssetInventoryRoutes = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PluginTemplateWrapper>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.assetInventory}>
|
||||
<AssetInventoryContainer />
|
||||
</SecurityRoutePageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
export const AssetInventoryRoutes = () => {
|
||||
const dataViewQuery = useDataView('asset-inventory-logs');
|
||||
|
||||
const dataViewContextValue = {
|
||||
dataView: dataViewQuery.data!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
dataViewRefetch: dataViewQuery.refetch,
|
||||
dataViewIsLoading: dataViewQuery.isLoading,
|
||||
dataViewIsRefetching: dataViewQuery.isRefetching,
|
||||
};
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PluginTemplateWrapper>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.assetInventory}>
|
||||
<DataViewContext.Provider value={dataViewContextValue}>
|
||||
<SecuritySolutionPageWrapper noPadding>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<AllAssetsLazy
|
||||
rows={rows}
|
||||
isLoading={false}
|
||||
loadMore={() => {}}
|
||||
flyoutComponent={() => <></>}
|
||||
/>
|
||||
</Suspense>
|
||||
</SecuritySolutionPageWrapper>
|
||||
</DataViewContext.Provider>
|
||||
</SecurityRoutePageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const routes: SecuritySubPluginRoutes = [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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': null,
|
||||
'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': null,
|
||||
'asset.source': 'cloud-sec-dev',
|
||||
'@timestamp': '2025-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
] as DataTableRecord[];
|
Loading…
Add table
Add a link
Reference in a new issue