diff --git a/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx
new file mode 100644
index 0000000000000..cda2f0bcb42d3
--- /dev/null
+++ b/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 { render, fireEvent, screen } from '@testing-library/react';
+import { BadgeFilterWithPopover } from '.';
+import { EuiThemeProvider, copyToClipboard } from '@elastic/eui';
+import { ENTITY_TYPE } from '../../../common/es_fields/entities';
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ copyToClipboard: jest.fn(),
+}));
+
+describe('BadgeFilterWithPopover', () => {
+ const mockOnFilter = jest.fn();
+ const field = ENTITY_TYPE;
+ const value = 'host';
+ const label = 'Host';
+ const popoverContentDataTestId = 'inventoryBadgeFilterWithPopoverContent';
+ const popoverContentTitleTestId = 'inventoryBadgeFilterWithPopoverTitle';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the badge with the correct label', () => {
+ render(
+ ,
+ { wrapper: EuiThemeProvider }
+ );
+ expect(screen.queryByText(label)).toBeInTheDocument();
+ expect(screen.getByText(label).textContent).toBe(label);
+ });
+
+ it('opens the popover when the badge is clicked', () => {
+ render();
+ expect(screen.queryByTestId(popoverContentDataTestId)).not.toBeInTheDocument();
+ fireEvent.click(screen.getByText(value));
+ expect(screen.queryByTestId(popoverContentDataTestId)).toBeInTheDocument();
+ expect(screen.queryByTestId(popoverContentTitleTestId)?.textContent).toBe(`${field}:${value}`);
+ });
+
+ it('calls onFilter when the "Filter for" button is clicked', () => {
+ render();
+ fireEvent.click(screen.getByText(value));
+ fireEvent.click(screen.getByTestId('inventoryBadgeFilterWithPopoverFilterForButton'));
+ expect(mockOnFilter).toHaveBeenCalled();
+ });
+
+ it('copies value to clipboard when the "Copy value" button is clicked', () => {
+ render();
+ fireEvent.click(screen.getByText(value));
+ fireEvent.click(screen.getByTestId('inventoryBadgeFilterWithPopoverCopyValueButton'));
+ expect(copyToClipboard).toHaveBeenCalledWith(value);
+ });
+});
diff --git a/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/index.tsx
new file mode 100644
index 0000000000000..d1e952e189d6e
--- /dev/null
+++ b/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/index.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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 {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiFlexGrid,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPopover,
+ EuiPopoverFooter,
+ copyToClipboard,
+ useEuiTheme,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import React, { useState } from 'react';
+
+interface Props {
+ field: string;
+ value: string;
+ label?: string;
+ onFilter: () => void;
+}
+
+export function BadgeFilterWithPopover({ field, value, onFilter, label }: Props) {
+ const [isOpen, setIsOpen] = useState(false);
+ const theme = useEuiTheme();
+
+ return (
+ setIsOpen((state) => !state)}
+ onClickAriaLabel={i18n.translate(
+ 'xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel',
+ { defaultMessage: 'Open popover' }
+ )}
+ >
+ {label || value}
+
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ >
+
+
+
+
+ {field}:
+
+
+
+ {value}
+
+
+
+
+
+
+
+ {i18n.translate('xpack.inventory.badgeFilterWithPopover.filterForButtonEmptyLabel', {
+ defaultMessage: 'Filter for',
+ })}
+
+
+
+ copyToClipboard(value)}
+ >
+ {i18n.translate('xpack.inventory.badgeFilterWithPopover.copyValueButtonEmptyLabel', {
+ defaultMessage: 'Copy value',
+ })}
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx
index b0e6c2fcc5ee4..996f0ec951581 100644
--- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx
+++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx
@@ -5,12 +5,13 @@
* 2.0.
*/
-import { EuiDataGridSorting } from '@elastic/eui';
+import { EuiDataGridSorting, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import { Meta, Story } from '@storybook/react';
-import React, { useMemo, useState } from 'react';
import { orderBy } from 'lodash';
+import React, { useMemo, useState } from 'react';
import { EntitiesGrid } from '.';
-import { ENTITY_LAST_SEEN } from '../../../common/es_fields/entities';
+import { EntityType } from '../../../common/entities';
+import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '../../../common/es_fields/entities';
import { entitiesMock } from './mock/entities_mock';
const stories: Meta<{}> = {
@@ -25,22 +26,44 @@ export const Example: Story<{}> = () => {
id: ENTITY_LAST_SEEN,
direction: 'desc',
});
-
- const sortedItems = useMemo(
- () => orderBy(entitiesMock, sort.id, sort.direction),
- [sort.direction, sort.id]
+ const [selectedEntityType, setSelectedEntityType] = useState();
+ const filteredAndSortedItems = useMemo(
+ () =>
+ orderBy(
+ selectedEntityType
+ ? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === selectedEntityType)
+ : entitiesMock,
+ sort.id,
+ sort.direction
+ ),
+ [selectedEntityType, sort.direction, sort.id]
);
return (
-
+
+
+ {`Entity filter: ${selectedEntityType || 'N/A'}`}
+ setSelectedEntityType(undefined)}
+ >
+ Clear filter
+
+
+
+
+
+
);
};
@@ -60,6 +83,7 @@ export const EmptyGridExample: Story<{}> = () => {
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
+ onFilterByType={() => {}}
/>
);
};
diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx
index d5ab911605109..6e26548520a43 100644
--- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx
+++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx
@@ -5,7 +5,6 @@
* 2.0.
*/
import {
- EuiBadge,
EuiButtonIcon,
EuiDataGrid,
EuiDataGridCellValueElementProps,
@@ -20,6 +19,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
import { last } from 'lodash';
import React, { useCallback, useState } from 'react';
+import { EntityType } from '../../../common/entities';
import {
ENTITY_DISPLAY_NAME,
ENTITY_LAST_SEEN,
@@ -27,7 +27,7 @@ import {
} from '../../../common/es_fields/entities';
import { APIReturnType } from '../../api';
import { getEntityTypeLabel } from '../../utils/get_entity_type_label';
-import { EntityType } from '../../../common/entities';
+import { BadgeFilterWithPopover } from '../badge_filter_with_popover';
type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>;
@@ -106,6 +106,7 @@ interface Props {
pageIndex: number;
onChangeSort: (sorting: EuiDataGridSorting['columns'][0]) => void;
onChangePage: (nextPage: number) => void;
+ onFilterByType: (entityType: EntityType) => void;
}
const PAGE_SIZE = 20;
@@ -118,6 +119,7 @@ export function EntitiesGrid({
pageIndex,
onChangePage,
onChangeSort,
+ onFilterByType,
}: Props) {
const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id));
@@ -141,10 +143,14 @@ export function EntitiesGrid({
const columnEntityTableId = columnId as EntityColumnIds;
switch (columnEntityTableId) {
case ENTITY_TYPE:
+ const entityType = entity[columnEntityTableId] as EntityType;
return (
-
- {getEntityTypeLabel(entity[columnEntityTableId] as EntityType)}
-
+ onFilterByType(entityType)}
+ />
);
case ENTITY_LAST_SEEN:
return (
@@ -183,7 +189,7 @@ export function EntitiesGrid({
return entity[columnId as EntityColumnIds] || '';
}
},
- [entities]
+ [entities, onFilterByType]
);
if (loading) {
diff --git a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx
index 2fcb147cbe61f..7af9a9fc21acc 100644
--- a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx
+++ b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx
@@ -7,6 +7,7 @@
import { EuiDataGridSorting } from '@elastic/eui';
import React from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
+import { EntityType } from '../../../common/entities';
import { EntitiesGrid } from '../../components/entities_grid';
import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider';
import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async';
@@ -81,6 +82,17 @@ export function InventoryPage() {
});
}
+ function handleTypeFilter(entityType: EntityType) {
+ inventoryRoute.push('/', {
+ path: {},
+ query: {
+ ...query,
+ // Override the current entity types
+ entityTypes: [entityType],
+ },
+ });
+ }
+
return (
);
}