Skip to content

Commit

Permalink
[Inventory][ECO] filter by type on grid (elastic#193875)
Browse files Browse the repository at this point in the history
  • Loading branch information
cauemarcondes committed Sep 25, 2024
1 parent 22f451b commit 475fd6c
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -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(
<BadgeFilterWithPopover field={field} value={value} onFilter={mockOnFilter} label={label} />,
{ wrapper: EuiThemeProvider }
);
expect(screen.queryByText(label)).toBeInTheDocument();
expect(screen.getByText(label).textContent).toBe(label);
});

it('opens the popover when the badge is clicked', () => {
render(<BadgeFilterWithPopover field={field} value={value} onFilter={mockOnFilter} />);
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(<BadgeFilterWithPopover field={field} value={value} onFilter={mockOnFilter} />);
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(<BadgeFilterWithPopover field={field} value={value} onFilter={mockOnFilter} />);
fireEvent.click(screen.getByText(value));
fireEvent.click(screen.getByTestId('inventoryBadgeFilterWithPopoverCopyValueButton'));
expect(copyToClipboard).toHaveBeenCalledWith(value);
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<EuiPopover
button={
<EuiBadge
data-test-subj="inventoryBadgeFilterWithPopoverButton"
color="hollow"
onClick={() => setIsOpen((state) => !state)}
onClickAriaLabel={i18n.translate(
'xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel',
{ defaultMessage: 'Open popover' }
)}
>
{label || value}
</EuiBadge>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
>
<span data-test-subj="inventoryBadgeFilterWithPopoverTitle">
<EuiFlexGroup
data-test-subj="inventoryBadgeFilterWithPopoverContent"
responsive={false}
gutterSize="xs"
css={css`
font-family: ${theme.euiTheme.font.familyCode};
`}
>
<EuiFlexItem grow={false}>
<span
css={css`
font-weight: bold;
`}
>
{field}:
</span>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span className="eui-textBreakWord">{value}</span>
</EuiFlexItem>
</EuiFlexGroup>
</span>
<EuiPopoverFooter>
<EuiFlexGrid responsive={false} columns={2}>
<EuiFlexItem>
<EuiButtonEmpty
data-test-subj="inventoryBadgeFilterWithPopoverFilterForButton"
iconType="plusInCircle"
onClick={onFilter}
>
{i18n.translate('xpack.inventory.badgeFilterWithPopover.filterForButtonEmptyLabel', {
defaultMessage: 'Filter for',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonEmpty
data-test-subj="inventoryBadgeFilterWithPopoverCopyValueButton"
iconType="copyClipboard"
onClick={() => copyToClipboard(value)}
>
{i18n.translate('xpack.inventory.badgeFilterWithPopover.copyValueButtonEmptyLabel', {
defaultMessage: 'Copy value',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGrid>
</EuiPopoverFooter>
</EuiPopover>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<{}> = {
Expand All @@ -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<EntityType | undefined>();
const filteredAndSortedItems = useMemo(
() =>
orderBy(
selectedEntityType
? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === selectedEntityType)
: entitiesMock,
sort.id,
sort.direction
),
[selectedEntityType, sort.direction, sort.id]
);

return (
<EntitiesGrid
entities={sortedItems}
loading={false}
sortDirection={sort.direction}
sortField={sort.id}
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
/>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{`Entity filter: ${selectedEntityType || 'N/A'}`}
<EuiLink
disabled={!selectedEntityType}
data-test-subj="inventoryExampleClearFilterButton"
onClick={() => setSelectedEntityType(undefined)}
>
Clear filter
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EntitiesGrid
entities={filteredAndSortedItems}
loading={false}
sortDirection={sort.direction}
sortField={sort.id}
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
onFilterByType={setSelectedEntityType}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

Expand All @@ -60,6 +83,7 @@ export const EmptyGridExample: Story<{}> = () => {
onChangePage={setPageIndex}
onChangeSort={setSort}
pageIndex={pageIndex}
onFilterByType={() => {}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/
import {
EuiBadge,
EuiButtonIcon,
EuiDataGrid,
EuiDataGridCellValueElementProps,
Expand All @@ -20,14 +19,15 @@ 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,
ENTITY_TYPE,
} 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'>;

Expand Down Expand Up @@ -109,6 +109,7 @@ interface Props {
pageIndex: number;
onChangeSort: (sorting: EuiDataGridSorting['columns'][0]) => void;
onChangePage: (nextPage: number) => void;
onFilterByType: (entityType: EntityType) => void;
}

const PAGE_SIZE = 20;
Expand All @@ -121,6 +122,7 @@ export function EntitiesGrid({
pageIndex,
onChangePage,
onChangeSort,
onFilterByType,
}: Props) {
const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id));

Expand All @@ -144,10 +146,14 @@ export function EntitiesGrid({
const columnEntityTableId = columnId as EntityColumnIds;
switch (columnEntityTableId) {
case ENTITY_TYPE:
const entityType = entity[columnEntityTableId] as EntityType;
return (
<EuiBadge color="hollow">
{getEntityTypeLabel(entity[columnEntityTableId] as EntityType)}
</EuiBadge>
<BadgeFilterWithPopover
field={ENTITY_TYPE}
value={entityType}
label={getEntityTypeLabel(entityType)}
onFilter={() => onFilterByType(entityType)}
/>
);
case ENTITY_LAST_SEEN:
return (
Expand Down Expand Up @@ -186,7 +192,7 @@ export function EntitiesGrid({
return entity[columnId as EntityColumnIds] || '';
}
},
[entities]
[entities, onFilterByType]
);

if (loading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,6 +82,17 @@ export function InventoryPage() {
});
}

function handleTypeFilter(entityType: EntityType) {
inventoryRoute.push('/', {
path: {},
query: {
...query,
// Override the current entity types
entityTypes: [entityType],
},
});
}

return (
<EntitiesGrid
entities={value.entities}
Expand All @@ -90,6 +102,7 @@ export function InventoryPage() {
onChangePage={handlePageChange}
onChangeSort={handleSortChange}
pageIndex={pageIndex}
onFilterByType={handleTypeFilter}
/>
);
}

0 comments on commit 475fd6c

Please sign in to comment.