mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Infrastructure UI] Hosts view flyout metadata search (#154556)
Closes #154347 ## Summary This PR adds search functionality to the metadata tab. In order to optimize the search I changed the table to `EuiInMemoryTable` and handled the search there. One benefit is that table filtering is the responsibility of the table and the cases to handle errors/no data found are much easier. <img width="2435" alt="Screenshot 2023-04-06 at 15 36 35" src="https://user-images.githubusercontent.com/14139027/230400195-b77b7783-9c4d-48b0-85e6-cb38180a29d3.png"> <img width="2434" alt="Screenshot 2023-04-06 at 15 58 22" src="https://user-images.githubusercontent.com/14139027/230400337-1013626c-c802-4b45-88f1-bff67f0ec37e.png"> This also helped to get rid of some of the callouts condition and leave the table component to decide what to render based on the items and loading state. That way the loading looks much smoother rather than replacing the table with a loading component - also when loading and there are no results the loading indicator is inside the table.  ## Testing 1. Open the flyout for a single host 2. On the metadata tab start searching a. Try to search for field name/value - should get a result b. Do a typo with an invalid character (or just enter only ```, `+`, etc) - an error message should be displayed (and ⚠️ icon in the search bar) c. Try to search for something that it's not a field name/value - should display the `No metadata found.` message in the table. 3. Copy a URL after searching for something and paste it into a new browser tab/window - it should persist the search term. (In case of a search error the search filter is not persisted) https://user-images.githubusercontent.com/14139027/230400149-6ba4dc32-efaa-4068-8abb-24b6ae43de76.mov
This commit is contained in:
parent
57d6f413a1
commit
5a03c5d710
4 changed files with 113 additions and 43 deletions
|
@ -110,13 +110,23 @@ describe('Single Host Metadata (Hosts View)', () => {
|
|||
mockUseMetadata({ metadata: [] });
|
||||
const result = renderHostMetadata();
|
||||
|
||||
expect(result.queryByTestId('infraMetadataNoData')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('infraHostMetadataNoData')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return spinner if loading', async () => {
|
||||
it('should show the metadata table if metadata is returned', async () => {
|
||||
mockUseMetadata({ metadata: [{ name: 'host.os.name', value: 'Ubuntu' }] });
|
||||
const result = renderHostMetadata();
|
||||
|
||||
expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('infraMetadataTable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return loading text if loading', async () => {
|
||||
mockUseMetadata({ loading: true });
|
||||
const result = renderHostMetadata();
|
||||
|
||||
expect(result.queryByTestId('infraHostMetadataSearchBarInput')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('infraHostMetadataLoading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useSourceContext } from '../../../../../../containers/metrics_source';
|
||||
|
@ -31,17 +30,13 @@ export const Metadata = ({ node, currentTimeRange, nodeType }: TabProps) => {
|
|||
const { sourceId } = useSourceContext();
|
||||
const {
|
||||
loading: metadataLoading,
|
||||
error,
|
||||
error: fetchMetadataError,
|
||||
metadata,
|
||||
} = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, currentTimeRange);
|
||||
|
||||
const fields = useMemo(() => getAllFields(metadata), [metadata]);
|
||||
|
||||
if (metadataLoading) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (fetchMetadataError) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadata.errorTitle', {
|
||||
|
@ -71,33 +66,5 @@ export const Metadata = ({ node, currentTimeRange, nodeType }: TabProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
return fields.length > 0 ? (
|
||||
<Table rows={fields} />
|
||||
) : (
|
||||
<EuiCallOut
|
||||
data-test-subj="infraMetadataNoData"
|
||||
title={i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadata.noMetadataFound', {
|
||||
defaultMessage: 'Sorry, there is no metadata related to this host.',
|
||||
})}
|
||||
size="m"
|
||||
iconType="iInCircle"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiLoadingChart data-test-subj="infraHostMetadataLoading" size="xl" />
|
||||
</div>
|
||||
);
|
||||
return <Table rows={fields} loading={metadataLoading} />;
|
||||
};
|
||||
|
|
|
@ -5,11 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiLink, EuiBasicTable } from '@elastic/eui';
|
||||
import {
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiInMemoryTable,
|
||||
EuiSearchBarProps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
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';
|
||||
|
||||
interface Row {
|
||||
name: string;
|
||||
|
@ -18,6 +28,11 @@ interface Row {
|
|||
|
||||
interface Props {
|
||||
rows: Row[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
interface SearchErrorType {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,8 +46,65 @@ const VALUE_LABEL = i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadat
|
|||
defaultMessage: 'Value',
|
||||
});
|
||||
|
||||
/**
|
||||
* Component translations
|
||||
*/
|
||||
const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.infra.hostsViewPage.hostDetail.metadata.searchForMetadata',
|
||||
{
|
||||
defaultMessage: 'Search for metadata…',
|
||||
}
|
||||
);
|
||||
|
||||
const NO_METADATA_FOUND = i18n.translate(
|
||||
'xpack.infra.hostsViewPage.hostDetail.metadata.noMetadataFound',
|
||||
{
|
||||
defaultMessage: 'No metadata found.',
|
||||
}
|
||||
);
|
||||
|
||||
const LOADING = i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadata.loading', {
|
||||
defaultMessage: 'Loading...',
|
||||
});
|
||||
|
||||
export const Table = (props: Props) => {
|
||||
const { rows } = props;
|
||||
const { rows, loading } = props;
|
||||
const [searchError, setSearchError] = useState<SearchErrorType | null>(null);
|
||||
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen();
|
||||
|
||||
const debouncedSearchOnChange = useMemo(
|
||||
() =>
|
||||
debounce<(queryText: string) => void>((queryText) => {
|
||||
setHostFlyoutOpen({ metadataSearch: String(queryText) ?? '' });
|
||||
}, 500),
|
||||
[setHostFlyoutOpen]
|
||||
);
|
||||
|
||||
const searchBarOnChange = useCallback(
|
||||
({ queryText, error }) => {
|
||||
if (error) {
|
||||
setSearchError(error);
|
||||
} else {
|
||||
setSearchError(null);
|
||||
debouncedSearchOnChange(queryText);
|
||||
}
|
||||
},
|
||||
[debouncedSearchOnChange]
|
||||
);
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
onChange: searchBarOnChange,
|
||||
box: {
|
||||
'data-test-subj': 'infraHostMetadataSearchBarInput',
|
||||
incremental: true,
|
||||
schema: true,
|
||||
placeholder: SEARCH_PLACEHOLDER,
|
||||
},
|
||||
query: hostFlyoutOpen.metadataSearch
|
||||
? Query.parse(hostFlyoutOpen.metadataSearch)
|
||||
: Query.MATCH_ALL,
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -54,12 +126,22 @@ export const Table = (props: Props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="infraMetadataTable"
|
||||
tableLayout={'fixed'}
|
||||
responsive={false}
|
||||
columns={columns}
|
||||
items={rows}
|
||||
search={search}
|
||||
loading={loading}
|
||||
error={searchError ? `${searchError.message}` : ''}
|
||||
message={
|
||||
loading ? (
|
||||
<div data-test-subj="infraHostMetadataLoading">{LOADING}</div>
|
||||
) : (
|
||||
<div data-test-subj="infraHostMetadataNoData">{NO_METADATA_FOUND}</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export const GET_DEFAULT_TABLE_PROPERTIES = {
|
|||
clickedItemId: '',
|
||||
selectedTabId: FlyoutTabIds.METADATA,
|
||||
searchFilter: '',
|
||||
metadataSearch: '',
|
||||
};
|
||||
const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'hostFlyoutOpen';
|
||||
|
||||
|
@ -55,12 +56,22 @@ const SetSearchFilterRT = rt.partial({
|
|||
searchFilter: SearchFilterRT,
|
||||
});
|
||||
|
||||
const ActionRT = rt.intersection([SetClickedItemIdRT, SetFlyoutTabId, SetSearchFilterRT]);
|
||||
const SetMetadataSearchRT = rt.partial({
|
||||
metadataSearch: SearchFilterRT,
|
||||
});
|
||||
|
||||
const ActionRT = rt.intersection([
|
||||
SetClickedItemIdRT,
|
||||
SetFlyoutTabId,
|
||||
SetSearchFilterRT,
|
||||
SetMetadataSearchRT,
|
||||
]);
|
||||
|
||||
const HostFlyoutOpenRT = rt.type({
|
||||
clickedItemId: ClickedItemIdRT,
|
||||
selectedTabId: FlyoutTabIdRT,
|
||||
searchFilter: SearchFilterRT,
|
||||
metadataSearch: SearchFilterRT,
|
||||
});
|
||||
|
||||
type HostFlyoutOpen = rt.TypeOf<typeof HostFlyoutOpenRT>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue