diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx index 95c5b4836a196..8b3a4b2c706d8 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx @@ -28,15 +28,12 @@ export const AssetDetails = ({ tabs, links, renderMode, - activeTabId, metricAlias, ...props }: AssetDetailsProps) => { return ( - 0 ? activeTabId ?? tabs[0].id : undefined} - > + diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx index 3aff0ea4b2548..45bfb7d46a59f 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx @@ -71,14 +71,12 @@ export class AssetDetailsEmbeddable extends Embeddable
diff --git a/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx b/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx index 6af455dff2b02..a275849e18edf 100644 --- a/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { AssetDetailsStateProvider } from './hooks/use_asset_details_state'; +import { AssetDetailsRenderPropsProvider } from './hooks/use_asset_details_render_props'; import { DateRangeProvider } from './hooks/use_date_range'; import { MetadataStateProvider } from './hooks/use_metadata_state'; import { AssetDetailsProps } from './types'; @@ -17,21 +17,20 @@ export const ContextProviders = ({ }: { props: Omit } & { children: React.ReactNode; }) => { - const { asset, dateRange, overrides, onTabsStateChange, assetType = 'host', renderMode } = props; + const { asset, dateRange, overrides, assetType = 'host', renderMode } = props; return ( - {children} - + ); diff --git a/x-pack/plugins/infra/public/components/asset_details/date_picker/date_picker.tsx b/x-pack/plugins/infra/public/components/asset_details/date_picker/date_picker.tsx index b4ec1128494d8..c7a90a3601c6c 100644 --- a/x-pack/plugins/infra/public/components/asset_details/date_picker/date_picker.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/date_picker/date_picker.tsx @@ -7,22 +7,17 @@ import { EuiSuperDatePicker, type OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state'; import { useDateRangeProviderContext } from '../hooks/use_date_range'; export const DatePicker = () => { - const { onTabsStateChange } = useAssetDetailsStateContext(); const { dateRange, setDateRange } = useDateRangeProviderContext(); const onTimeChange = useCallback( ({ start, end, isInvalid }: OnTimeChangeProps) => { if (!isInvalid) { setDateRange({ from: start, to: end }); - if (onTabsStateChange) { - onTabsStateChange({ dateRange: { from: start, to: end } }); - } } }, - [onTabsStateChange, setDateRange] + [setDateRange] ); return ( diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_render_props.ts similarity index 61% rename from x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts rename to x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_render_props.ts index 53bb74beef943..6ddba44e6da7e 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_render_props.ts @@ -9,16 +9,13 @@ import createContainer from 'constate'; import type { AssetDetailsProps } from '../types'; import { useMetadataStateProviderContext } from './use_metadata_state'; -export interface UseAssetDetailsStateProps { - state: Pick< - AssetDetailsProps, - 'asset' | 'assetType' | 'overrides' | 'onTabsStateChange' | 'renderMode' - >; +export interface UseAssetDetailsRenderProps { + props: Pick; } -export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { +export function useAssetDetailsRenderProps({ props }: UseAssetDetailsRenderProps) { const { metadata } = useMetadataStateProviderContext(); - const { asset, assetType, onTabsStateChange, overrides, renderMode } = state; + const { asset, assetType, overrides, renderMode } = props; // When the asset asset.name is known we can load the page faster // Otherwise we need to use metadata response. @@ -30,12 +27,12 @@ export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { name: asset.name || metadata?.name || 'asset-name', }, assetType, - onTabsStateChange, overrides, renderMode, loading, }; } -export const AssetDetailsState = createContainer(useAssetDetailsState); -export const [AssetDetailsStateProvider, useAssetDetailsStateContext] = AssetDetailsState; +export const AssetDetailsRenderProps = createContainer(useAssetDetailsRenderProps); +export const [AssetDetailsRenderPropsProvider, useAssetDetailsRenderPropsContext] = + AssetDetailsRenderProps; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_url_state.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts similarity index 53% rename from x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_url_state.ts rename to x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts index 62d8fdb302d3e..370778555f7d0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_url_state.ts +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts @@ -9,28 +9,27 @@ import * as rt from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; -import { FlyoutTabIds } from '../../../../components/asset_details/types'; -import { useUrlState } from '../../../../utils/use_url_state'; +import { FlyoutTabIds } from '../types'; +import { useUrlState } from '../../../utils/use_url_state'; -export const DEFAULT_STATE: HostFlyout = { - itemId: '', +export const DEFAULT_STATE: AssetDetailsState = { tabId: FlyoutTabIds.OVERVIEW, processSearch: undefined, metadataSearch: undefined, }; -const HOST_FLYOUT_URL_STATE_KEY = 'flyout'; +const ASSET_DETAILS_URL_STATE_KEY = 'asset_details'; -type SetHostFlyoutState = (newProp: Payload | null) => void; +type SetAssetDetailsState = (newProp: Payload | null) => void; -export const useHostFlyoutUrlState = (): [HostFlyoutUrl, SetHostFlyoutState] => { - const [urlState, setUrlState] = useUrlState({ +export const useAssetDetailsUrlState = (): [AssetDetailsUrl, SetAssetDetailsState] => { + const [urlState, setUrlState] = useUrlState({ defaultState: null, decodeUrlState, encodeUrlState, - urlStateKey: HOST_FLYOUT_URL_STATE_KEY, + urlStateKey: ASSET_DETAILS_URL_STATE_KEY, }); - const setHostFlyoutState = (newProps: Payload | null) => { + const setAssetDetailsState = (newProps: Payload | null) => { if (!newProps) { setUrlState(DEFAULT_STATE); } else { @@ -41,10 +40,10 @@ export const useHostFlyoutUrlState = (): [HostFlyoutUrl, SetHostFlyoutState] => } }; - return [urlState as HostFlyoutUrl, setHostFlyoutState]; + return [urlState as AssetDetailsUrl, setAssetDetailsState]; }; -const FlyoutTabIdRT = rt.union([ +const TabIdRT = rt.union([ rt.literal(FlyoutTabIds.OVERVIEW), rt.literal(FlyoutTabIds.METADATA), rt.literal(FlyoutTabIds.PROCESSES), @@ -53,10 +52,9 @@ const FlyoutTabIdRT = rt.union([ rt.literal(FlyoutTabIds.OSQUERY), ]); -const HostFlyoutStateRT = rt.intersection([ +const AssetDetailsStateRT = rt.intersection([ rt.type({ - itemId: rt.string, - tabId: FlyoutTabIdRT, + tabId: TabIdRT, }), rt.partial({ dateRange: rt.type({ @@ -69,14 +67,13 @@ const HostFlyoutStateRT = rt.intersection([ }), ]); -const HostFlyoutUrlRT = rt.union([HostFlyoutStateRT, rt.null]); +const AssetDetailsUrlRT = rt.union([AssetDetailsStateRT, rt.null]); -type HostFlyoutState = rt.TypeOf; -type HostFlyoutUrl = rt.TypeOf; -type Payload = Partial; -export type HostFlyout = rt.TypeOf; +export type AssetDetailsState = rt.TypeOf; +type AssetDetailsUrl = rt.TypeOf; +type Payload = Partial; -const encodeUrlState = HostFlyoutUrlRT.encode; +const encodeUrlState = AssetDetailsUrlRT.encode; const decodeUrlState = (value: unknown) => { - return pipe(HostFlyoutUrlRT.decode(value), fold(constant(undefined), identity)); + return pipe(AssetDetailsUrlRT.decode(value), fold(constant(undefined), identity)); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts index 8253b7e8b5685..7e98c56834529 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts @@ -7,22 +7,31 @@ import type { TimeRange } from '@kbn/es-query'; import createContainer from 'constate'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { parseDateRange } from '../../../utils/datemath'; import { toTimestampRange } from '../utils'; +import { useAssetDetailsUrlState } from './use_asset_details_url_state'; const DEFAULT_DATE_RANGE: TimeRange = { from: 'now-15m', to: 'now', }; -export interface UseAssetDetailsStateProps { +export interface UseDateRangeProviderProps { initialDateRange: TimeRange; } -export function useDateRangeProvider({ initialDateRange }: UseAssetDetailsStateProps) { - const [dateRange, setDateRange] = useState(initialDateRange); +export function useDateRangeProvider({ initialDateRange }: UseDateRangeProviderProps) { + const [urlState, setUrlState] = useAssetDetailsUrlState(); + const dateRange: TimeRange = urlState?.dateRange ?? initialDateRange; + + const setDateRange = useCallback( + (newDateRange: TimeRange) => { + setUrlState({ dateRange: newDateRange }); + }, + [setUrlState] + ); const parsedDateRange = useMemo(() => { const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } = diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx index da8d99b433821..07baa3c5f589b 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -13,7 +13,7 @@ import { capitalize } from 'lodash'; import { APM_HOST_FILTER_FIELD } from '../constants'; import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails } from '../links'; import { FlyoutTabIds, type LinkOptions, type Tab, type TabIds } from '../types'; -import { useAssetDetailsStateContext } from './use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props'; import { useDateRangeProviderContext } from './use_date_range'; import { useTabSwitcherContext } from './use_tab_switcher'; @@ -28,7 +28,7 @@ export const usePageHeader = (tabs: Tab[], links?: LinkOptions[]) => { const useRightSideItems = (links?: LinkOptions[]) => { const { getDateRangeInTimestamp } = useDateRangeProviderContext(); - const { asset, assetType, overrides } = useAssetDetailsStateContext(); + const { asset, assetType, overrides } = useAssetDetailsRenderPropsContext(); const topCornerLinkComponents: Record = useMemo( () => ({ @@ -55,7 +55,7 @@ const useRightSideItems = (links?: LinkOptions[]) => { const useTabs = (tabs: Tab[]) => { const { showTab, activeTabId } = useTabSwitcherContext(); - const { asset } = useAssetDetailsStateContext(); + const { asset } = useAssetDetailsRenderPropsContext(); const { euiTheme } = useEuiTheme(); const onTabClick = useCallback( diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx index 6bdcbca214d37..5de6383099e1d 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_tab_switcher.tsx @@ -5,35 +5,31 @@ * 2.0. */ -import { useState } from 'react'; import createContainer from 'constate'; import { useLazyRef } from '../../../hooks/use_lazy_ref'; import type { TabIds } from '../types'; -import { useAssetDetailsStateContext } from './use_asset_details_state'; +import { AssetDetailsState, useAssetDetailsUrlState } from './use_asset_details_url_state'; interface TabSwitcherParams { - initialActiveTabId?: TabIds; + defaultActiveTabId?: TabIds; } -export function useTabSwitcher({ initialActiveTabId }: TabSwitcherParams) { - const { onTabsStateChange } = useAssetDetailsStateContext(); - const [activeTabId, setActiveTabId] = useState(initialActiveTabId); +export function useTabSwitcher({ defaultActiveTabId }: TabSwitcherParams) { + const [urlState, setUrlState] = useAssetDetailsUrlState(); + const activeTabId: TabIds | undefined = urlState?.tabId || defaultActiveTabId; // This set keeps track of which tabs content have been rendered the first time. // We need it in order to load a tab content only if it gets clicked, and then keep it in the DOM for performance improvement. - const renderedTabsSet = useLazyRef(() => new Set([initialActiveTabId])); + const renderedTabsSet = useLazyRef(() => new Set([activeTabId])); const showTab = (tabId: TabIds) => { - renderedTabsSet.current.add(tabId); // On a tab click, mark the tab content as allowed to be rendered - setActiveTabId(tabId); + // On a tab click, mark the tab content as allowed to be rendered + renderedTabsSet.current.add(tabId); - if (onTabsStateChange) { - onTabsStateChange({ activeTabId: tabId }); - } + setUrlState({ tabId: tabId as AssetDetailsState['tabId'] }); }; return { - initialActiveTabId, activeTabId, renderedTabsSet, showTab, diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx index d834a6ca9e0d7..4b0e8a52d400c 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx @@ -10,7 +10,6 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import { getNodeDetailUrl } from '../../../pages/link_to'; import type { InventoryItemType } from '../../../../common/inventory_models/types'; -import type { Asset } from '../types'; export interface LinkToNodeDetailsProps { dateRangeTimestamp: { from: number; to: number }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/anomalies/anomalies.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/anomalies/anomalies.tsx index 001678d6e6c01..7d6cabc54c3ab 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/anomalies/anomalies.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/anomalies/anomalies.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { AnomaliesTable } from '../../../../pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table'; -import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDateRangeProviderContext } from '../../hooks/use_date_range'; export const Anomalies = () => { const { dateRange } = useDateRangeProviderContext(); - const { asset, overrides } = useAssetDetailsStateContext(); + const { asset, overrides } = useAssetDetailsRenderPropsContext(); const { onClose = () => {} } = overrides?.anomalies ?? {}; return ( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index 68f0ea074f292..a19861968335c 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -16,33 +16,32 @@ import { DEFAULT_LOG_VIEW, LogViewReference } from '@kbn/logs-shared-plugin/comm import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { findInventoryFields } from '../../../../../common/inventory_models'; import { InfraLoadingPanel } from '../../../loading'; -import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDataViewsProviderContext } from '../../hooks/use_data_views'; import { useDateRangeProviderContext } from '../../hooks/use_date_range'; +import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state'; const TEXT_QUERY_THROTTLE_INTERVAL_MS = 500; export const Logs = () => { const { getDateRangeInTimestamp } = useDateRangeProviderContext(); - const { asset, assetType, overrides, onTabsStateChange } = useAssetDetailsStateContext(); + const [urlState, setUrlState] = useAssetDetailsUrlState(); + const { asset, assetType } = useAssetDetailsRenderPropsContext(); const { logs } = useDataViewsProviderContext(); - const { query: overrideQuery } = overrides?.logs ?? {}; const { loading: logViewLoading, reference: logViewReference } = logs ?? {}; const { services } = useKibanaContextForPlugin(); const { locators } = services; - const [textQuery, setTextQuery] = useState(overrideQuery ?? ''); - const [textQueryDebounced, setTextQueryDebounced] = useState(overrideQuery ?? ''); + const [textQuery, setTextQuery] = useState(urlState?.logsSearch ?? ''); + const [textQueryDebounced, setTextQueryDebounced] = useState(urlState?.logsSearch ?? ''); const currentTimestamp = getDateRangeInTimestamp().to; const startTimestamp = currentTimestamp - 60 * 60 * 1000; // 60 minutes useDebounce( () => { - if (onTabsStateChange) { - onTabsStateChange({ logs: { query: textQuery } }); - } + setUrlState({ logsSearch: textQuery }); setTextQueryDebounced(textQuery); }, TEXT_QUERY_THROTTLE_INTERVAL_MS, diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx index a2d04b1c36184..49d912fab3f50 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_metadata_filter_button.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; import { useMetricsDataViewContext } from '../../../../pages/metrics/hosts/hooks/use_data_view'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { useUnifiedSearchContext } from '../../../../pages/metrics/hosts/hooks/use_unified_search'; import { buildMetadataFilter } from './build_metadata_filter'; +import { useUnifiedSearchContext } from '../../../../pages/metrics/hosts/hooks/use_unified_search'; interface AddMetadataFilterButtonProps { item: { diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx index f3b27b9a8caca..7c7d4b92f468f 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx @@ -12,7 +12,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { Table } from './table'; import { getAllFields } from './utils'; import { useMetadataStateProviderContext } from '../../hooks/use_metadata_state'; -import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; +import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state'; export interface MetadataSearchUrlState { metadataSearchUrlState: string; @@ -20,23 +21,22 @@ export interface MetadataSearchUrlState { } export const Metadata = () => { - const { overrides, onTabsStateChange } = useAssetDetailsStateContext(); + const [urlState, setUrlState] = useAssetDetailsUrlState(); + const { overrides } = useAssetDetailsRenderPropsContext(); const { metadata, loading: metadataLoading, error: fetchMetadataError, } = useMetadataStateProviderContext(); - const { query, showActionsColumn = false } = overrides?.metadata ?? {}; + const { showActionsColumn = false } = overrides?.metadata ?? {}; const fields = useMemo(() => getAllFields(metadata), [metadata]); const onSearchChange = useCallback( (newQuery: string) => { - if (onTabsStateChange) { - onTabsStateChange({ metadata: { query: newQuery } }); - } + setUrlState({ metadataSearch: newQuery }); }, - [onTabsStateChange] + [setUrlState] ); if (fetchMetadataError) { @@ -71,7 +71,7 @@ export const Metadata = () => { return ( { const { dateRange } = useDateRangeProviderContext(); - const { asset, assetType, renderMode } = useAssetDetailsStateContext(); + const { asset, assetType, renderMode } = useAssetDetailsRenderPropsContext(); const { metadata, loading: metadataLoading, diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx index 0d96ff1d2264e..f0568332328dc 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -29,8 +29,9 @@ import { ProcessListContextProvider, } from '../../../../pages/metrics/inventory_view/hooks/use_process_list'; import { getFieldByType } from '../../../../../common/inventory_models'; -import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDateRangeProviderContext } from '../../hooks/use_date_range'; +import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state'; const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({ value, @@ -39,11 +40,10 @@ const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string] export const Processes = () => { const { getDateRangeInTimestamp } = useDateRangeProviderContext(); - const { asset, assetType, overrides, onTabsStateChange } = useAssetDetailsStateContext(); + const [urlState, setUrlState] = useAssetDetailsUrlState(); + const { asset, assetType } = useAssetDetailsRenderPropsContext(); - const { query: overrideQuery } = overrides?.processes ?? {}; - - const [searchText, setSearchText] = useState(overrideQuery ?? ''); + const [searchText, setSearchText] = useState(urlState?.processSearch ?? ''); const [searchBarState, setSearchBarState] = useState(() => searchText ? Query.parse(searchText) : Query.MATCH_ALL ); @@ -69,12 +69,10 @@ export const Processes = () => { const debouncedSearchOnChange = useMemo(() => { return debounce<(queryText: string) => void>((queryText) => { - if (onTabsStateChange) { - onTabsStateChange({ processes: { query: queryText } }); - } + setUrlState({ processSearch: queryText }); setSearchText(queryText); }, 500); - }, [onTabsStateChange]); + }, [setUrlState]); const searchBarOnChange = useCallback( ({ query, queryText }) => { @@ -86,11 +84,9 @@ export const Processes = () => { const clearSearchBar = useCallback(() => { setSearchBarState(Query.MATCH_ALL); - if (onTabsStateChange) { - onTabsStateChange({ processes: { query: '' } }); - } + setUrlState({ processSearch: '' }); setSearchText(''); - }, [onTabsStateChange]); + }, [setUrlState]); return ( @@ -139,6 +135,7 @@ export const Processes = () => { query={searchBarState} onChange={searchBarOnChange} box={{ + 'data-test-subj': 'infraAssetDetailsProcessesSearchBarInput', incremental: true, placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', { defaultMessage: 'Search for processes…', diff --git a/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx b/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx index 9bc9a60777fad..f45aca765610d 100644 --- a/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx @@ -14,7 +14,7 @@ import { InfraLoadingPanel } from '../../loading'; import { ASSET_DETAILS_FLYOUT_COMPONENT_NAME } from '../constants'; import { Content } from '../content/content'; import { FlyoutHeader } from '../header/flyout_header'; -import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; import { usePageHeader } from '../hooks/use_page_header'; import { useTabSwitcherContext } from '../hooks/use_tab_switcher'; import type { ContentTemplateProps } from '../types'; @@ -23,9 +23,9 @@ export const Flyout = ({ header: { tabs = [], links = [] }, closeFlyout, }: ContentTemplateProps & { closeFlyout: () => void }) => { - const { asset, assetType, loading } = useAssetDetailsStateContext(); + const { asset, assetType, loading } = useAssetDetailsRenderPropsContext(); const { rightSideItems, tabEntries } = usePageHeader(tabs, links); - const { initialActiveTabId } = useTabSwitcherContext(); + const { activeTabId } = useTabSwitcherContext(); const { services: { telemetry }, } = useKibanaContextForPlugin(); @@ -34,7 +34,7 @@ export const Flyout = ({ telemetry.reportAssetDetailsFlyoutViewed({ componentName: ASSET_DETAILS_FLYOUT_COMPONENT_NAME, assetType, - tabId: initialActiveTabId, + tabId: activeTabId, }); }); diff --git a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx index a95638bfca55a..bbf23196bf7ee 100644 --- a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx @@ -12,12 +12,12 @@ import React from 'react'; import { useKibanaHeader } from '../../../hooks/use_kibana_header'; import { InfraLoadingPanel } from '../../loading'; import { Content } from '../content/content'; -import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state'; +import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; import { usePageHeader } from '../hooks/use_page_header'; import type { ContentTemplateProps } from '../types'; export const Page = ({ header: { tabs = [], links = [] } }: ContentTemplateProps) => { - const { asset, loading } = useAssetDetailsStateContext(); + const { asset, loading } = useAssetDetailsRenderPropsContext(); const { rightSideItems, tabEntries } = usePageHeader(tabs, links); const { headerHeight } = useKibanaHeader(); diff --git a/x-pack/plugins/infra/public/components/asset_details/types.ts b/x-pack/plugins/infra/public/components/asset_details/types.ts index 839154a4b9d33..dfa6155b0adae 100644 --- a/x-pack/plugins/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/infra/public/components/asset_details/types.ts @@ -28,21 +28,14 @@ export type TabIds = `${FlyoutTabIds}`; export interface OverridableTabState { metadata?: { - query?: string; showActionsColumn?: boolean; }; - processes?: { - query?: string; - }; anomalies?: { onClose?: () => void; }; alertRule?: { onCreateRuleClick?: () => void; }; - logs?: { - query?: string; - }; } export interface TabState extends OverridableTabState { @@ -72,10 +65,8 @@ export interface AssetDetailsProps { assetType: InventoryItemType; dateRange: TimeRange; tabs: Tab[]; - activeTabId?: TabIds; overrides?: OverridableTabState; renderMode: RenderMode; - onTabsStateChange?: TabsStateChangeFn; links?: LinkOptions[]; // This is temporary. Once we start using the asset details in other plugins, // It will have to retrieve the metricAlias internally rather than receive it via props diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx index ee4115a2ed981..065b6739c41fc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { useSourceContext } from '../../../../../containers/metrics_source'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import type { HostNodeRow } from '../../hooks/use_hosts_table'; -import { HostFlyout, useHostFlyoutUrlState } from '../../hooks/use_host_flyout_url_state'; import { AssetDetails } from '../../../../../components/asset_details/asset_details'; import { orderedFlyoutTabs } from './tabs'; +import { useAssetDetailsUrlState } from '../../../../../components/asset_details/hooks/use_asset_details_url_state'; export interface Props { node: HostNodeRow; @@ -22,35 +22,18 @@ export interface Props { export const FlyoutWrapper = ({ node: { name }, closeFlyout }: Props) => { const { source } = useSourceContext(); const { parsedDateRange } = useUnifiedSearchContext(); - const [hostFlyoutState, setHostFlyoutState] = useHostFlyoutUrlState(); + const [urlState] = useAssetDetailsUrlState(); return source ? ( - setHostFlyoutState({ - dateRange: state.dateRange, - metadataSearch: state.metadata?.query, - processSearch: state.processes?.query, - logsSearch: state.logs?.query, - tabId: state.activeTabId as HostFlyout['tabId'], - }) - } tabs={orderedFlyoutTabs} links={['apmServices', 'nodeDetails']} renderMode={{ diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 22677d692fdb9..19ba258e91b11 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -24,15 +24,14 @@ import type { InfraAssetMetricsItem, InfraAssetMetricType, } from '../../../../../common/http_api'; -import { useHostFlyoutUrlState } from './use_host_flyout_url_state'; import { Sorting, useHostsTableUrlState } from './use_hosts_table_url_state'; import { useHostsViewContext } from './use_hosts_view'; -import { useUnifiedSearchContext } from './use_unified_search'; import { useMetricsDataViewContext } from './use_data_view'; import { ColumnHeader } from '../components/table/column_header'; import { TABLE_COLUMN_LABEL } from '../translations'; import { METRICS_TOOLTIP } from '../../../../common/visualizations'; import { buildCombinedHostsFilter } from '../../../../utils/filters/build'; +import { useUnifiedSearchContext } from './use_unified_search'; /** * Columns and items types @@ -129,7 +128,7 @@ export const useHostsTable = () => { const [selectedItems, setSelectedItems] = useState([]); const { hostNodes } = useHostsViewContext(); const { parsedDateRange } = useUnifiedSearchContext(); - const [{ pagination, sorting }, setProperties] = useHostsTableUrlState(); + const [{ detailsItemId, pagination, sorting }, setProperties] = useHostsTableUrlState(); const { services: { telemetry, @@ -140,11 +139,10 @@ export const useHostsTable = () => { } = useKibanaContextForPlugin(); const { dataView } = useMetricsDataViewContext(); - const [hostFlyoutState, setHostFlyoutState] = useHostFlyoutUrlState(); const popoverContainerRef = useRef(null); const tableRef = useRef(null); - const closeFlyout = useCallback(() => setHostFlyoutState(null), [setHostFlyoutState]); + const closeFlyout = useCallback(() => setProperties({ detailsItemId: null }), [setProperties]); const onSelectionChange = (newSelectedItems: HostNodeRow[]) => { setSelectedItems(newSelectedItems); @@ -195,8 +193,8 @@ export const useHostsTable = () => { const items = useMemo(() => buildItemsList(hostNodes), [hostNodes]); const clickedItem = useMemo( - () => items.find(({ id }) => id === hostFlyoutState?.itemId), - [hostFlyoutState?.itemId, items] + () => items.find(({ id }) => id === detailsItemId), + [detailsItemId, items] ); const currentPage = useMemo(() => { @@ -218,19 +216,13 @@ export const useHostsTable = () => { { name: TABLE_COLUMN_LABEL.toggleDialogAction, description: TABLE_COLUMN_LABEL.toggleDialogAction, - icon: ({ id }) => - hostFlyoutState?.itemId && id === hostFlyoutState?.itemId ? 'minimize' : 'expand', + icon: ({ id }) => (id === detailsItemId ? 'minimize' : 'expand'), type: 'icon', 'data-test-subj': 'hostsView-flyout-button', onClick: ({ id }) => { - setHostFlyoutState({ - itemId: id, + setProperties({ + detailsItemId: id === detailsItemId ? null : id, }); - if (id === hostFlyoutState?.itemId) { - setHostFlyoutState(null); - } else { - setHostFlyoutState({ itemId: id }); - } }, }, ], @@ -351,7 +343,7 @@ export const useHostsTable = () => { width: '120px', }, ], - [hostFlyoutState?.itemId, parsedDateRange, reportHostEntryClick, setHostFlyoutState] + [detailsItemId, parsedDateRange, reportHostEntryClick, setProperties] ); const selection: EuiTableSelectionType = { @@ -365,7 +357,7 @@ export const useHostsTable = () => { currentPage, closeFlyout, items, - isFlyoutOpen: !!hostFlyoutState?.itemId, + isFlyoutOpen: detailsItemId !== null, onTableChange, pagination, sorting, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts index 760ada008c8c0..c1dcafefaccc3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts @@ -16,6 +16,7 @@ import { useUrlState } from '../../../../utils/use_url_state'; import { DEFAULT_PAGE_SIZE, LOCAL_STORAGE_PAGE_SIZE_KEY } from '../constants'; export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = { + detailsItemId: null, sorting: { direction: 'asc', field: 'name', @@ -29,7 +30,7 @@ export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = { const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; const reducer = (prevState: TableProperties, params: Payload) => { - const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => !!v)); + const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => v !== undefined)); return { ...prevState, @@ -77,6 +78,7 @@ const SortingRT = rt.intersection([ ]); const TableStateRT = rt.type({ + detailsItemId: rt.union([rt.string, rt.null]), pagination: PaginationRT, sorting: SortingRT, }); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx index 31559352a67de..10af65db3fdd9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx @@ -5,19 +5,19 @@ * 2.0. */ -import type { TimeRange } from '@kbn/es-query'; -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { useLocation, useRouteMatch } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import type { TimeRange } from '@kbn/es-query'; import { NoRemoteCluster } from '../../../components/empty_states'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; -import { FlyoutTabIds, type Tab, type TabState } from '../../../components/asset_details/types'; +import { FlyoutTabIds, type Tab } from '../../../components/asset_details/types'; import type { InventoryItemType } from '../../../../common/inventory_models/types'; import { AssetDetails } from '../../../components/asset_details/asset_details'; -import { useMetricsTimeContext } from './hooks/use_metrics_time'; import { MetricsPageTemplate } from '../page_template'; +import { useMetricsTimeContext } from './hooks/use_metrics_time'; const orderedFlyoutTabs: Tab[] = [ { @@ -70,7 +70,7 @@ export const AssetDetailPage = () => { return queryParams.get('assetName') ?? undefined; }, [search]); - const { timeRange, setTimeRange } = useMetricsTimeContext(); + const { timeRange } = useMetricsTimeContext(); const dateRange: TimeRange = useMemo( () => ({ @@ -83,23 +83,6 @@ export const AssetDetailPage = () => { [timeRange.from, timeRange.to] ); - // Retrocompatibility - const handleTabStateChange = useCallback( - ({ dateRange: newDateRange }: TabState) => { - if (newDateRange) { - setTimeRange( - { - from: newDateRange.from, - to: newDateRange.to, - interval: timeRange.interval, - }, - false - ); - } - }, - [setTimeRange, timeRange.interval] - ); - const { metricIndicesExist, remoteClustersExist } = source?.status ?? {}; if (isLoading || !source) return ; @@ -131,8 +114,6 @@ export const AssetDetailPage = () => { }} assetType={nodeType} dateRange={dateRange} - onTabsStateChange={handleTabStateChange} - activeTabId={FlyoutTabIds.OVERVIEW} tabs={orderedFlyoutTabs} links={['apmServices']} renderMode={{ diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 91270e7ebde58..a225ea4c476e4 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -459,6 +459,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + describe('Host details page navigation', () => { + after(async () => { + await pageObjects.common.navigateToApp(HOSTS_VIEW_PATH); + await pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.timePicker.setAbsoluteRange( + START_DATE.format(DATE_PICKER_FORMAT), + END_DATE.format(DATE_PICKER_FORMAT) + ); + + await waitForPageToLoad(); + }); + + it('maintains selected date range when navigating to the individual host details', async () => { + const start = START_HOST_PROCESSES_DATE.format(DATE_PICKER_FORMAT); + const end = END_HOST_PROCESSES_DATE.format(DATE_PICKER_FORMAT); + + await pageObjects.timePicker.setAbsoluteRange(start, end); + + const hostDetailLinks = await pageObjects.infraHostsView.getAllHostDetailLinks(); + expect(hostDetailLinks.length).not.to.equal(0); + + await hostDetailLinks[0].click(); + + expect(await pageObjects.timePicker.timePickerExists()).to.be(true); + + const datePickerValue = await pageObjects.timePicker.getTimeConfig(); + expect(datePickerValue.start).to.equal(start); + expect(datePickerValue.end).to.equal(end); + }); + }); + describe('KPIs', () => { [ { metric: 'hostsCount', value: '6' }, diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index 7e2bdba6ecf39..1175e733e2568 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -51,6 +51,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }; + const refreshPageWithDelay = async () => { + /** + * Delay gives React a chance to finish + * running effects (like updating the URL) before + * refreshing the page. + */ + await pageObjects.common.sleep(1000); + await browser.refresh(); + }; + describe('Node Details', () => { describe('#With Asset Details', () => { before(async () => { @@ -111,6 +121,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } ); + + it('preserves selected date range between page reloads', async () => { + const start = moment.utc(START_HOST_ALERTS_DATE).format(DATE_PICKER_FORMAT); + const end = moment.utc(END_HOST_ALERTS_DATE).format(DATE_PICKER_FORMAT); + + await pageObjects.timePicker.setAbsoluteRange(start, end); + await refreshPageWithDelay(); + + const datePickerValue = await pageObjects.timePicker.getTimeConfig(); + + expect(datePickerValue.start).to.equal(start); + expect(datePickerValue.end).to.equal(end); + }); }); describe('#Asset Type: host', () => { @@ -121,6 +144,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); + it('preserves selected tab between page reloads', async () => { + await testSubjects.missingOrFail('infraAssetDetailsMetadataTable'); + await pageObjects.assetDetails.clickMetadataTab(); + await pageObjects.assetDetails.metadataTableExists(); + + await refreshPageWithDelay(); + + await pageObjects.assetDetails.metadataTableExists(); + }); + describe('Overview Tab', () => { before(async () => { await pageObjects.assetDetails.clickOverviewTab(); @@ -181,6 +214,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.assetDetails.clickRemoveMetadataPin(); expect(await pageObjects.assetDetails.metadataRemovePinExists()).to.be(false); }); + + it('preserves search term between page reloads', async () => { + const searchInput = await pageObjects.assetDetails.getMetadataSearchField(); + + expect(await searchInput.getAttribute('value')).to.be(''); + + await searchInput.type('test'); + await refreshPageWithDelay(); + + await retry.try(async () => { + expect(await searchInput.getAttribute('value')).to.be('test'); + }); + await searchInput.clearValue(); + }); }); describe('Processes Tab', () => { @@ -200,6 +247,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.assetDetails.getProcessesTableBody(); await pageObjects.assetDetails.clickProcessesTableExpandButton(); }); + + it('preserves search term between page reloads', async () => { + const searchInput = await pageObjects.assetDetails.getProcessesSearchField(); + + expect(await searchInput.getAttribute('value')).to.be(''); + + await searchInput.type('test'); + await refreshPageWithDelay(); + + await retry.try(async () => { + expect(await searchInput.getAttribute('value')).to.be('test'); + }); + await searchInput.clearValue(); + }); }); describe('Logs Tab', () => { @@ -210,6 +271,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should render logs tab', async () => { await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); }); + + it('preserves search term between page reloads', async () => { + const searchInput = await pageObjects.assetDetails.getLogsSearchField(); + + expect(await searchInput.getAttribute('value')).to.be(''); + + await searchInput.type('test'); + await refreshPageWithDelay(); + + await retry.try(async () => { + expect(await searchInput.getAttribute('value')).to.be('test'); + }); + await searchInput.clearValue(); + }); }); describe('Osquery Tab', () => { diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index bcb8738b997bf..c7600d6f1b5f4 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -77,6 +77,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return testSubjects.existOrFail('infraAssetDetailsMetadataTable'); }, + async metadataTableMissing() { + return await testSubjects.missingOrFail('infraAssetDetailsMetadataTable'); + }, + async metadataRemovePinExists() { return testSubjects.exists('infraAssetDetailsMetadataRemovePin'); }, @@ -92,6 +96,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return testSubjects.exists('infraAssetDetailsMetadataRemoveFilterButton'); }, + async getMetadataSearchField() { + return await testSubjects.find('infraAssetDetailsMetadataSearchBarInput'); + }, + // Processes async clickProcessesTab() { return testSubjects.click('infraAssetDetailsProcessesTab'); @@ -124,6 +132,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return testSubjects.click('infraProcessRowButton'); }, + async getProcessesSearchField() { + return await testSubjects.find('infraAssetDetailsProcessesSearchBarInput'); + }, + // Logs async clickLogsTab() { return testSubjects.click('infraAssetDetailsLogsTab'); @@ -133,6 +145,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); }, + async getLogsSearchField() { + return await testSubjects.find('infraAssetDetailsLogsTabFieldSearch'); + }, + // Anomalies async clickAnomaliesTab() { return testSubjects.click('infraAssetDetailsAnomaliesTab'); diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index e449fb8f05b57..1cd0cf15996ec 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -110,6 +110,10 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.find('hostsView-metricChart'); }, + async getAllHostDetailLinks() { + return testSubjects.findAll('hostsViewTableEntryTitleLink'); + }, + // Metrics Tab async getMetricsTab() { return testSubjects.find('hostsView-tabs-metrics');