Skip to content

Commit

Permalink
Implement Asset Inventory data grid (#206115)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic/security-team#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:
- elastic/security-team#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.
  • Loading branch information
albertoblaz authored Jan 16, 2025
1 parent 49f9724 commit 5cc1315
Show file tree
Hide file tree
Showing 19 changed files with 1,662 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -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>
)}
</>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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>,
]}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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>
);
};
Loading

0 comments on commit 5cc1315

Please sign in to comment.