Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Infra UI] Store asset details page state in the URL state #164601

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -17,7 +17,7 @@ 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}>
Expand All @@ -26,7 +26,6 @@ export const ContextProviders = ({
asset,
assetType,
overrides,
onTabsStateChange,
renderMode,
}}
>
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 @@ -10,15 +10,12 @@ import type { AssetDetailsProps } from '../types';
import { useMetadataStateProviderContext } from './use_metadata_state';

export interface UseAssetDetailsStateProps {
state: Pick<
AssetDetailsProps,
'asset' | 'assetType' | 'overrides' | 'onTabsStateChange' | 'renderMode'
>;
state: Pick<AssetDetailsProps, 'asset' | 'assetType' | 'overrides' | 'renderMode'>;
}

export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm under the impression that the names here have become confusing with the introduction of the use_asset_details_url_state. I wonder if it should be called something along the lines of useAssetDetailsRenderInfo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, will rename it 👍

const { metadata } = useMetadataStateProviderContext();
const { asset, assetType, onTabsStateChange, overrides, renderMode } = state;
const { asset, assetType, overrides, renderMode } = state;

// When the asset asset.name is known we can load the page faster
// Otherwise we need to use metadata response.
Expand All @@ -30,7 +27,6 @@ export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) {
name: asset.name || metadata?.name || 'asset-name',
},
assetType,
onTabsStateChange,
overrides,
renderMode,
loading,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@ 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 = {
export const DEFAULT_STATE: AssetDetailsState = {
itemId: '',
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 +41,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 +53,10 @@ const FlyoutTabIdRT = rt.union([
rt.literal(FlyoutTabIds.OSQUERY),
]);

const HostFlyoutStateRT = rt.intersection([
const AssetDetailsStateRT = rt.intersection([
rt.type({
itemId: rt.string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The itemId is only used in the use_hosts_table. It can be moved over to the use_hosts_table state. With that, the asset_details_url_state will contain only attributes relevant to the Asset Details component. Wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will move it to the useHostsTableUrlState

tabId: FlyoutTabIdRT,
tabId: TabIdRT,
}),
rt.partial({
dateRange: rt.type({
Expand All @@ -69,14 +69,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;
mykolaharmash marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -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 @@ -19,30 +19,29 @@ import { InfraLoadingPanel } from '../../../loading';
import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state';
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 } = useAssetDetailsStateContext();
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,30 @@ import { Table } from './table';
import { getAllFields } from './utils';
import { useMetadataStateProviderContext } from '../../hooks/use_metadata_state';
import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state';
import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state';

export interface MetadataSearchUrlState {
metadataSearchUrlState: string;
setMetadataSearchUrlState: (metadataSearch: { metadataSearch?: string }) => void;
}

export const Metadata = () => {
const { overrides, onTabsStateChange } = useAssetDetailsStateContext();
const [urlState, setUrlState] = useAssetDetailsUrlState();
const { overrides } = useAssetDetailsStateContext();
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) {
Expand Down Expand Up @@ -71,7 +71,7 @@ export const Metadata = () => {

return (
<Table
search={query}
search={urlState?.metadataSearch}
onSearchChange={onSearchChange}
showActionsColumn={showActionsColumn}
rows={fields}
Expand Down
Loading