mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Infrastructure UI] Hosts View: Filtering by metadata functionality (#155170)
Closes #151010 ## Summary This PR adds filtering functionality to the metadata table. After some discussions there are changes in the design - see the [last comments](https://github.com/elastic/kibana/issues/151010#issuecomment-1513005095) ## Additional changes As the filters will change the order I changed the id to not depend on the table index anymore (used os instead) inside [hosts table ](https://github.com/elastic/kibana/pull/155170/files#diff-168ba138bc6696100078e3e9cbc438ed7646d6336e9ef85a9c88553c9d8956f5) ## Testing - Open the single host view for one of the hosts available in the table - Inside the flyout select the Metadata tab - Click on any of the available filter icons to add a filter - Check if the filter is applied and a toast is displayed - Click on the same filter icon to remove it and check that it is removed - If there are other filters applied they should combined with the metadata filters (so if filter A is added and then a metadata filter is applied both filters should be visible) https://user-images.githubusercontent.com/14139027/232837814-7292c7ec-8b63-4172-bcd9-2d49daf3b145.mov
This commit is contained in:
parent
ec39987b99
commit
25df1feee3
7 changed files with 217 additions and 6 deletions
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
|
||||
import { buildMetadataFilter } from './build_metadata_filter';
|
||||
import { useMetricsDataViewContext } from '../../../hooks/use_data_view';
|
||||
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
|
||||
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
|
||||
|
||||
interface AddMetadataFilterButtonProps {
|
||||
item: {
|
||||
name: string;
|
||||
value: string | string[] | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const filterAddedToastTitle = i18n.translate(
|
||||
'xpack.infra.hostsViewPage.flyout.metadata.filterAdded',
|
||||
{
|
||||
defaultMessage: 'Filter was added',
|
||||
}
|
||||
);
|
||||
|
||||
export const AddMetadataFilterButton = ({ item }: AddMetadataFilterButtonProps) => {
|
||||
const { dataView } = useMetricsDataViewContext();
|
||||
const { searchCriteria } = useUnifiedSearchContext();
|
||||
|
||||
const {
|
||||
services: {
|
||||
data: {
|
||||
query: { filterManager: filterManagerService },
|
||||
},
|
||||
notifications: { toasts: toastsService },
|
||||
},
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const existingFilter = useMemo(
|
||||
() => searchCriteria.filters.find((filter) => filter.meta.key === item.name),
|
||||
[item.name, searchCriteria.filters]
|
||||
);
|
||||
|
||||
const handleAddFilter = () => {
|
||||
const newFilter = buildMetadataFilter({
|
||||
field: item.name,
|
||||
value: item.value ?? '',
|
||||
dataView,
|
||||
negate: false,
|
||||
});
|
||||
if (newFilter) {
|
||||
filterManagerService.addFilters(newFilter);
|
||||
toastsService.addSuccess({
|
||||
title: filterAddedToastTitle,
|
||||
toastLifeTimeMs: 10000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (existingFilter) {
|
||||
return (
|
||||
<span>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.infra.hostsViewPage.flyout.metadata.setRemoveFilterTooltip',
|
||||
{
|
||||
defaultMessage: 'Remove filter',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
color="text"
|
||||
iconType="filter"
|
||||
display="base"
|
||||
data-test-subj="hostsView-flyout-metadata-remove-filter"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.hostsViewPage.flyout.metadata.filterAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Filter',
|
||||
}
|
||||
)}
|
||||
onClick={() => filterManagerService.removeFilter(existingFilter)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.infra.hostsViewPage.flyout.metadata.setFilterByValueTooltip',
|
||||
{
|
||||
defaultMessage: 'Filter by value',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
size="s"
|
||||
iconType="filter"
|
||||
data-test-subj="hostsView-flyout-metadata-add-filter"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.hostsViewPage.flyout.metadata.AddFilterAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Add Filter',
|
||||
}
|
||||
)}
|
||||
onClick={handleAddFilter}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { buildPhrasesFilter, buildPhraseFilter, FilterStateStore } from '@kbn/es-query';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
export function buildMetadataFilter({
|
||||
field,
|
||||
value,
|
||||
dataView,
|
||||
negate = false,
|
||||
}: {
|
||||
value: string | Array<string | number>;
|
||||
negate: boolean;
|
||||
field: string;
|
||||
dataView: DataView | undefined;
|
||||
}) {
|
||||
if (!dataView) {
|
||||
return null;
|
||||
}
|
||||
const indexField = dataView.getFieldByName(field)!;
|
||||
const areMultipleValues = Array.isArray(value) && value.length > 1;
|
||||
const filter = areMultipleValues
|
||||
? buildPhrasesFilter(indexField, value, dataView)
|
||||
: buildPhraseFilter(indexField, Array.isArray(value) ? value[0] : value, dataView);
|
||||
|
||||
filter.meta.type = areMultipleValues ? 'phrases' : 'phrase';
|
||||
filter.$state = { store: 'appState' as FilterStateStore.APP_STATE };
|
||||
|
||||
filter.meta.value = Array.isArray(value)
|
||||
? !areMultipleValues
|
||||
? `${value[0]}`
|
||||
: undefined
|
||||
: value;
|
||||
|
||||
filter.meta.key = field;
|
||||
filter.meta.alias = null;
|
||||
filter.meta.negate = negate;
|
||||
filter.meta.disabled = false;
|
||||
|
||||
return filter;
|
||||
}
|
|
@ -20,6 +20,7 @@ import useToggle from 'react-use/lib/useToggle';
|
|||
import { debounce } from 'lodash';
|
||||
import { Query } from '@elastic/eui';
|
||||
import { useHostFlyoutOpen } from '../../../hooks/use_host_flyout_open_url_state';
|
||||
import { AddMetadataFilterButton } from './add_metadata_filter_button';
|
||||
|
||||
interface Row {
|
||||
name: string;
|
||||
|
@ -117,10 +118,19 @@ export const Table = (props: Props) => {
|
|||
{
|
||||
field: 'value',
|
||||
name: VALUE_LABEL,
|
||||
width: '65%',
|
||||
width: '55%',
|
||||
sortable: false,
|
||||
render: (_name: string, item: Row) => <ExpandableContent values={item.value} />,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: 'Actions',
|
||||
sortable: false,
|
||||
showOnHover: true,
|
||||
render: (_name: string, item: Row) => {
|
||||
return <AddMetadataFilterButton item={item} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
@ -132,6 +142,7 @@ export const Table = (props: Props) => {
|
|||
responsive={false}
|
||||
columns={columns}
|
||||
items={rows}
|
||||
rowProps={{ className: 'euiTableRow-hasActions' }}
|
||||
search={search}
|
||||
loading={loading}
|
||||
error={searchError ? `${searchError.message}` : ''}
|
||||
|
|
|
@ -74,7 +74,7 @@ describe('useHostTable hook', () => {
|
|||
name: 'host-0',
|
||||
os: '-',
|
||||
ip: '',
|
||||
id: 'host-0-0',
|
||||
id: 'host-0--',
|
||||
title: {
|
||||
cloudProvider: 'aws',
|
||||
name: 'host-0',
|
||||
|
@ -105,7 +105,7 @@ describe('useHostTable hook', () => {
|
|||
name: 'host-1',
|
||||
os: 'macOS',
|
||||
ip: '243.86.94.22',
|
||||
id: 'host-1-1',
|
||||
id: 'host-1-macOS',
|
||||
title: {
|
||||
cloudProvider: null,
|
||||
name: 'host-1',
|
||||
|
|
|
@ -50,8 +50,8 @@ const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefin
|
|||
};
|
||||
|
||||
const buildItemsList = (nodes: SnapshotNode[]) => {
|
||||
return nodes.map(({ metrics, path, name }, index) => ({
|
||||
id: `${name}-${index}`,
|
||||
return nodes.map(({ metrics, path, name }) => ({
|
||||
id: `${name}-${path.at(-1)?.os ?? '-'}`,
|
||||
name,
|
||||
os: path.at(-1)?.os ?? '-',
|
||||
ip: path.at(-1)?.ip ?? '',
|
||||
|
|
|
@ -239,9 +239,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await logoutAndDeleteReadOnlyUser();
|
||||
});
|
||||
|
||||
it('should render metadata tab', async () => {
|
||||
it('should render metadata tab, add and remove filter', async () => {
|
||||
const metadataTab = await pageObjects.infraHostsView.getMetadataTabName();
|
||||
expect(metadataTab).to.contain('Metadata');
|
||||
|
||||
await pageObjects.infraHostsView.clickAddMetadataFilter();
|
||||
await pageObjects.infraHome.waitForLoading();
|
||||
|
||||
// Add Filter
|
||||
const addedFilter = await pageObjects.infraHostsView.getAppliedFilter();
|
||||
expect(addedFilter).to.contain('host.architecture: arm64');
|
||||
const removeFilterExists = await pageObjects.infraHostsView.getRemoveFilterExist();
|
||||
expect(removeFilterExists).to.be(true);
|
||||
|
||||
// Remove filter
|
||||
await pageObjects.infraHostsView.clickRemoveMetadataFilter();
|
||||
await pageObjects.infraHome.waitForLoading();
|
||||
const removeFilterShouldNotExist = await pageObjects.infraHostsView.getRemoveFilterExist();
|
||||
expect(removeFilterShouldNotExist).to.be(false);
|
||||
});
|
||||
|
||||
it('should navigate to Uptime after click', async () => {
|
||||
|
|
|
@ -44,6 +44,14 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
|
|||
return testSubjects.click('hostsView-flyout-apm-services-link');
|
||||
},
|
||||
|
||||
async clickAddMetadataFilter() {
|
||||
return testSubjects.click('hostsView-flyout-metadata-add-filter');
|
||||
},
|
||||
|
||||
async clickRemoveMetadataFilter() {
|
||||
return testSubjects.click('hostsView-flyout-metadata-remove-filter');
|
||||
},
|
||||
|
||||
async getHostsLandingPageDisabled() {
|
||||
const container = await testSubjects.find('hostView-no-enable-access');
|
||||
const containerText = await container.getVisibleText();
|
||||
|
@ -142,6 +150,17 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
|
|||
return tabTitle.getVisibleText();
|
||||
},
|
||||
|
||||
async getAppliedFilter() {
|
||||
const filter = await testSubjects.find(
|
||||
"filter-badge-'host.architecture: arm64' filter filter-enabled filter-key-host.architecture filter-value-arm64 filter-unpinned filter-id-0"
|
||||
);
|
||||
return filter.getVisibleText();
|
||||
},
|
||||
|
||||
async getRemoveFilterExist() {
|
||||
return testSubjects.exists('hostsView-flyout-metadata-remove-filter');
|
||||
},
|
||||
|
||||
async getProcessesTabContentTitle(index: number) {
|
||||
const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem');
|
||||
return processesListElements[index].findByCssSelector('dt');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue