Skip to content

Commit

Permalink
[Infra UI] Store asset details page state in the URL state (#164601)
Browse files Browse the repository at this point in the history
Closes #164147 

## Summary

This PR moves the responsibility of updating the URL state into the
`<AssetDetails>` instead of providing callbacks for parent components to
manage the state themselves. This way logic for updating the URL state
is centralized and shared between the Host details flyout and the Host
details page.

## How to test

- Run from the branch locally
- Navigate to "Hosts" screen
- Open the host details flyout
- Switch between tabs and refresh the page
- Make sure your last active tab stays active after the refresh
- Type into search fields on the "Metadata", "Processes" or "Logs" tab
and refresh the page
- Make sure the search term persists
- Change the date range using the date picker and refresh the page
- Make sure that the date range persists
- All of the above should work the same for the full-page host details
view

---------

Co-authored-by: Carlos Crespo <[email protected]>
Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
3 people authored Aug 29, 2023
1 parent 1543750 commit fc6034a
Show file tree
Hide file tree
Showing 27 changed files with 241 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,12 @@ export const AssetDetails = ({
tabs,
links,
renderMode,
activeTabId,
metricAlias,
...props
}: AssetDetailsProps) => {
return (
<ContextProviders props={{ ...props, renderMode }}>
<TabSwitcherProvider
initialActiveTabId={tabs.length > 0 ? activeTabId ?? tabs[0].id : undefined}
>
<TabSwitcherProvider defaultActiveTabId={tabs[0]?.id}>
<DataViewsProvider metricAlias={metricAlias}>
<ContentTemplate header={{ tabs, links }} renderMode={renderMode} />
</DataViewsProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,12 @@ export class AssetDetailsEmbeddable extends Embeddable<AssetDetailsEmbeddableInp
<EuiThemeProvider>
<div style={{ width: '100%' }}>
<LazyAssetDetailsWrapper
activeTabId={this.input.activeTabId}
dateRange={this.input.dateRange}
asset={this.input.asset}
assetType={this.input.assetType}
overrides={this.input.overrides}
renderMode={this.input.renderMode}
tabs={this.input.tabs}
onTabsStateChange={this.input.onTabsStateChange}
links={this.input.links}
metricAlias={this.input.metricAlias}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,21 +17,20 @@ export const ContextProviders = ({
}: { props: Omit<AssetDetailsProps, 'links' | 'tabs' | 'activeTabId' | 'metricAlias'> } & {
children: React.ReactNode;
}) => {
const { asset, dateRange, overrides, onTabsStateChange, assetType = 'host', renderMode } = props;
const { asset, dateRange, overrides, assetType = 'host', renderMode } = props;
return (
<DateRangeProvider initialDateRange={dateRange}>
<MetadataStateProvider asset={asset} assetType={assetType}>
<AssetDetailsStateProvider
state={{
<AssetDetailsRenderPropsProvider
props={{
asset,
assetType,
overrides,
onTabsStateChange,
renderMode,
}}
>
{children}
</AssetDetailsStateProvider>
</AssetDetailsRenderPropsProvider>
</MetadataStateProvider>
</DateRangeProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssetDetailsProps, 'asset' | 'assetType' | 'overrides' | 'renderMode'>;
}

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.
Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostFlyoutUrl>({
export const useAssetDetailsUrlState = (): [AssetDetailsUrl, SetAssetDetailsState] => {
const [urlState, setUrlState] = useUrlState<AssetDetailsUrl>({
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 {
Expand All @@ -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),
Expand All @@ -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({
Expand All @@ -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<typeof HostFlyoutStateRT>;
type HostFlyoutUrl = rt.TypeOf<typeof HostFlyoutUrlRT>;
type Payload = Partial<HostFlyoutState>;
export type HostFlyout = rt.TypeOf<typeof HostFlyoutStateRT>;
export type AssetDetailsState = rt.TypeOf<typeof AssetDetailsStateRT>;
type AssetDetailsUrl = rt.TypeOf<typeof AssetDetailsUrlRT>;
type Payload = Partial<AssetDetailsState>;

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));
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<LinkOptions, JSX.Element> = useMemo(
() => ({
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TabIds | undefined>(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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading

0 comments on commit fc6034a

Please sign in to comment.