Skip to content

Commit

Permalink
[Infra UI] Propagate flyout state to node details page and some fixes (
Browse files Browse the repository at this point in the history
…elastic#165956)

closes [elastic#164300](elastic#164300)

## Summary

This PR enables state propagation between asset details flyout and full
page view.


https://github.com/elastic/kibana/assets/2767137/7e05a3c9-afa1-447c-98fd-91c40ee6cefb

There are other places in Kibana that redirect to node details outside
the infra plugin such as APM and Observability/Overview. They use the
`link-to/${assetType}-detail` path, so It's best, for now, to keep
retro-compatibility with this route and propagate the state via query
string.

I've also refactored how we were persisting state via route navigation,
to use native the `state` attribute found in the `location` object from
`react-router`

### How to tests
- Start a Kibana instance
- Navigate to `Infrastructure` > `Host`
- Open the flyout, click in one of the tabs, and click on "Open as
Page". The page should open in the same state as the flyout.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
crespocarlos and kibanamachine authored Sep 8, 2023
1 parent 7ba3554 commit c6f72c7
Show file tree
Hide file tree
Showing 31 changed files with 284 additions and 262 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,42 @@
*/

import { i18n } from '@kbn/i18n';
import { type AssetDetailsProps, FlyoutTabIds, type Tab } from '../../../types';
import { type AssetDetailsProps, ContentTabIds, type Tab } from '../../../types';

const links: AssetDetailsProps['links'] = ['alertRule', 'nodeDetails', 'apmServices'];
const tabs: Tab[] = [
{
id: FlyoutTabIds.OVERVIEW,
id: ContentTabIds.OVERVIEW,
name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', {
defaultMessage: 'Overview',
}),
},
{
id: FlyoutTabIds.LOGS,
id: ContentTabIds.LOGS,
name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', {
defaultMessage: 'Logs',
}),
},
{
id: FlyoutTabIds.METADATA,
id: ContentTabIds.METADATA,
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.metadata', {
defaultMessage: 'Metadata',
}),
},
{
id: FlyoutTabIds.PROCESSES,
id: ContentTabIds.PROCESSES,
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', {
defaultMessage: 'Processes',
}),
},
{
id: FlyoutTabIds.ANOMALIES,
id: ContentTabIds.ANOMALIES,
name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', {
defaultMessage: 'Anomalies',
}),
},
{
id: FlyoutTabIds.LINK_TO_APM,
id: ContentTabIds.LINK_TO_APM,
name: i18n.translate('xpack.infra.infra.nodeDetails.apmTabLabel', {
defaultMessage: 'APM',
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
export const ASSET_DETAILS_FLYOUT_COMPONENT_NAME = 'infraAssetDetailsFlyout';
export const METRIC_CHART_HEIGHT = 300;
export const APM_HOST_FILTER_FIELD = 'host.hostname';

export const ASSET_DETAILS_URL_STATE_KEY = 'assetDetails';
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,51 @@ import React from 'react';
import { DatePicker } from '../date_picker/date_picker';
import { useTabSwitcherContext } from '../hooks/use_tab_switcher';
import { Anomalies, Metadata, Processes, Osquery, Logs, Overview } from '../tabs';
import { FlyoutTabIds } from '../types';
import { ContentTabIds } from '../types';

export const Content = () => {
return (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<DatePickerWrapper
visibleFor={[
FlyoutTabIds.OVERVIEW,
FlyoutTabIds.LOGS,
FlyoutTabIds.METADATA,
FlyoutTabIds.PROCESSES,
FlyoutTabIds.ANOMALIES,
ContentTabIds.OVERVIEW,
ContentTabIds.LOGS,
ContentTabIds.METADATA,
ContentTabIds.PROCESSES,
ContentTabIds.ANOMALIES,
]}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TabPanel activeWhen={FlyoutTabIds.ANOMALIES}>
<TabPanel activeWhen={ContentTabIds.ANOMALIES}>
<Anomalies />
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.OVERVIEW}>
<TabPanel activeWhen={ContentTabIds.OVERVIEW}>
<Overview />
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.LOGS}>
<TabPanel activeWhen={ContentTabIds.LOGS}>
<Logs />
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.METADATA}>
<TabPanel activeWhen={ContentTabIds.METADATA}>
<Metadata />
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.OSQUERY}>
<TabPanel activeWhen={ContentTabIds.OSQUERY}>
<Osquery />
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.PROCESSES}>
<TabPanel activeWhen={ContentTabIds.PROCESSES}>
<Processes />
</TabPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

const DatePickerWrapper = ({ visibleFor }: { visibleFor: FlyoutTabIds[] }) => {
const DatePickerWrapper = ({ visibleFor }: { visibleFor: ContentTabIds[] }) => {
const { activeTabId } = useTabSwitcherContext();

return (
<div hidden={!visibleFor.includes(activeTabId as FlyoutTabIds)}>
<div hidden={!visibleFor.includes(activeTabId as ContentTabIds)}>
<DatePicker />
</div>
);
Expand All @@ -64,7 +64,7 @@ const TabPanel = ({
activeWhen,
children,
}: {
activeWhen: FlyoutTabIds;
activeWhen: ContentTabIds;
children: React.ReactNode;
}) => {
const { renderedTabsSet, activeTabId } = useTabSwitcherContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@

import createContainer from 'constate';
import type { AssetDetailsProps } from '../types';
import { useAssetDetailsUrlState } from './use_asset_details_url_state';
import { useMetadataStateProviderContext } from './use_metadata_state';

export interface UseAssetDetailsRenderProps {
props: Pick<AssetDetailsProps, 'asset' | 'assetType' | 'overrides' | 'renderMode'>;
}

export function useAssetDetailsRenderProps({ props }: UseAssetDetailsRenderProps) {
const [urlState] = useAssetDetailsUrlState();
const { metadata } = useMetadataStateProviderContext();
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.
const loading = !asset.name && !metadata?.name;
const loading = !urlState?.name && !asset.name && !metadata?.name;

return {
asset: {
...asset,
name: asset.name || metadata?.name || 'asset-name',
name: urlState?.name || asset.name || metadata?.name || '',
},
assetType,
overrides,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ 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 '../types';
import { ContentTabIds } from '../types';
import { useUrlState } from '../../../utils/use_url_state';
import { ASSET_DETAILS_URL_STATE_KEY } from '../constants';
import { getDefaultDateRange } from '../utils';

export const DEFAULT_STATE: AssetDetailsState = {
tabId: FlyoutTabIds.OVERVIEW,
processSearch: undefined,
metadataSearch: undefined,
export const DEFAULT_STATE: AssetDetailsUrlState = {
tabId: ContentTabIds.OVERVIEW,
dateRange: getDefaultDateRange(),
};
const ASSET_DETAILS_URL_STATE_KEY = 'asset_details';

type SetAssetDetailsState = (newProp: Payload | null) => void;

Expand All @@ -44,34 +44,35 @@ export const useAssetDetailsUrlState = (): [AssetDetailsUrl, SetAssetDetailsStat
};

const TabIdRT = rt.union([
rt.literal(FlyoutTabIds.OVERVIEW),
rt.literal(FlyoutTabIds.METADATA),
rt.literal(FlyoutTabIds.PROCESSES),
rt.literal(FlyoutTabIds.LOGS),
rt.literal(FlyoutTabIds.ANOMALIES),
rt.literal(FlyoutTabIds.OSQUERY),
rt.literal(ContentTabIds.OVERVIEW),
rt.literal(ContentTabIds.METADATA),
rt.literal(ContentTabIds.PROCESSES),
rt.literal(ContentTabIds.LOGS),
rt.literal(ContentTabIds.ANOMALIES),
rt.literal(ContentTabIds.OSQUERY),
]);

const AssetDetailsStateRT = rt.intersection([
const AssetDetailsUrlStateRT = rt.intersection([
rt.type({
tabId: TabIdRT,
}),
rt.partial({
dateRange: rt.type({
from: rt.string,
to: rt.string,
}),
}),
rt.partial({
tabId: TabIdRT,
name: rt.string,
processSearch: rt.string,
metadataSearch: rt.string,
logsSearch: rt.string,
}),
]);

const AssetDetailsUrlRT = rt.union([AssetDetailsStateRT, rt.null]);
const AssetDetailsUrlRT = rt.union([AssetDetailsUrlStateRT, rt.null]);

export type AssetDetailsState = rt.TypeOf<typeof AssetDetailsStateRT>;
export type AssetDetailsUrlState = rt.TypeOf<typeof AssetDetailsUrlStateRT>;
type AssetDetailsUrl = rt.TypeOf<typeof AssetDetailsUrlRT>;
type Payload = Partial<AssetDetailsState>;
type Payload = Partial<AssetDetailsUrlState>;

const encodeUrlState = AssetDetailsUrlRT.encode;
const decodeUrlState = (value: unknown) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,16 @@ import createContainer from 'constate';
import { useCallback, useState } from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import { parseDateRange } from '../../../utils/datemath';
import { toTimestampRange } from '../utils';
import { getDefaultDateRange, toTimestampRange } from '../utils';
import { useAssetDetailsUrlState } from './use_asset_details_url_state';

export interface UseDateRangeProviderProps {
initialDateRange: TimeRange;
}

const DEFAULT_FROM_IN_MILLISECONDS = 15 * 60000;
const getDefaultDateRange = () => {
const now = Date.now();

return {
from: new Date(now - DEFAULT_FROM_IN_MILLISECONDS).toISOString(),
to: new Date(now).toISOString(),
};
};

export function useDateRangeProvider({ initialDateRange }: UseDateRangeProviderProps) {
export function useDateRangeProvider({
initialDateRange = getDefaultDateRange(),
}: UseDateRangeProviderProps) {
const [urlState, setUrlState] = useAssetDetailsUrlState();
const dateRange: TimeRange = urlState?.dateRange ?? initialDateRange;
const [parsedDateRange, setParsedDateRange] = useState(parseDateRange(dateRange));
Expand All @@ -36,7 +28,7 @@ export function useDateRangeProvider({ initialDateRange }: UseDateRangeProviderP
useEffectOnce(() => {
const { from, to } = getParsedDateRange();

// forces the date picker to initiallize with absolute dates.
// forces the date picker to initialize with absolute dates.
setUrlState({ dateRange: { from, to } });
});

Expand All @@ -56,9 +48,10 @@ export function useDateRangeProvider({ initialDateRange }: UseDateRangeProviderP
return { from, to };
}, [parsedDateRange]);

const getDateRangeInTimestamp = useCallback(() => {
return toTimestampRange(getParsedDateRange());
}, [getParsedDateRange]);
const getDateRangeInTimestamp = useCallback(
() => toTimestampRange(getParsedDateRange()),
[getParsedDateRange]
);

return {
dateRange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
* 2.0.
*/

import { useEffect } from 'react';
import createContainer from 'constate';
import { findInventoryModel } from '../../../../common/inventory_models';
import { useSourceContext } from '../../../containers/metrics_source';
import { useMetadata } from './use_metadata';
import { AssetDetailsProps } from '../types';
import { useDateRangeProviderContext } from './use_date_range';
import { useAssetDetailsUrlState } from './use_asset_details_url_state';

export type UseMetadataProviderProps = Pick<AssetDetailsProps, 'asset' | 'assetType'>;

export function useMetadataProvider({ asset, assetType }: UseMetadataProviderProps) {
const [, setUrlState] = useAssetDetailsUrlState();
const { getDateRangeInTimestamp } = useDateRangeProviderContext();
const inventoryModel = findInventoryModel(assetType);
const { sourceId } = useSourceContext();
Expand All @@ -27,6 +30,12 @@ export function useMetadataProvider({ asset, assetType }: UseMetadataProviderPro
getDateRangeInTimestamp()
);

useEffect(() => {
if (metadata?.name) {
setUrlState({ name: metadata.name });
}
}, [metadata?.name, setUrlState]);

return {
loading,
error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { APM_HOST_FILTER_FIELD } from '../constants';
import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails } from '../links';
import { FlyoutTabIds, type RouteState, type LinkOptions, type Tab, type TabIds } from '../types';
import { ContentTabIds, type RouteState, type LinkOptions, type Tab, type TabIds } from '../types';
import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props';
import { useDateRangeProviderContext } from './use_date_range';
import { useTabSwitcherContext } from './use_tab_switcher';

type TabItem = NonNullable<Pick<EuiPageHeaderProps, 'tabs'>['tabs']>[number];
Expand Down Expand Up @@ -89,22 +88,15 @@ export const useTemplateHeaderBreadcrumbs = () => {
};

const useRightSideItems = (links?: LinkOptions[]) => {
const { getDateRangeInTimestamp } = useDateRangeProviderContext();
const { asset, assetType, overrides } = useAssetDetailsRenderPropsContext();

const topCornerLinkComponents: Record<LinkOptions, JSX.Element> = useMemo(
() => ({
nodeDetails: (
<LinkToNodeDetails
asset={asset}
assetType={assetType}
dateRangeTimestamp={getDateRangeInTimestamp()}
/>
),
nodeDetails: <LinkToNodeDetails asset={asset} assetType={assetType} />,
alertRule: <LinkToAlertsRule onClick={overrides?.alertRule?.onCreateRuleClick} />,
apmServices: <LinkToApmServices assetName={asset.name} apmField={APM_HOST_FILTER_FIELD} />,
}),
[asset, assetType, getDateRangeInTimestamp, overrides?.alertRule?.onCreateRuleClick]
[asset, assetType, overrides?.alertRule?.onCreateRuleClick]
);

const rightSideItems = useMemo(
Expand Down Expand Up @@ -157,7 +149,7 @@ const useTabs = (tabs: Tab[]) => {
const tabEntries: TabItem[] = useMemo(
() =>
tabs.map(({ name, ...tab }) => {
if (tab.id === FlyoutTabIds.LINK_TO_APM) {
if (tab.id === ContentTabIds.LINK_TO_APM) {
return getTabToApmTraces(name);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import createContainer from 'constate';
import { useLazyRef } from '../../../hooks/use_lazy_ref';
import type { TabIds } from '../types';
import { AssetDetailsState, useAssetDetailsUrlState } from './use_asset_details_url_state';
import { AssetDetailsUrlState, useAssetDetailsUrlState } from './use_asset_details_url_state';

interface TabSwitcherParams {
defaultActiveTabId?: TabIds;
Expand All @@ -26,7 +26,7 @@ export function useTabSwitcher({ defaultActiveTabId }: TabSwitcherParams) {
// On a tab click, mark the tab content as allowed to be rendered
renderedTabsSet.current.add(tabId);

setUrlState({ tabId: tabId as AssetDetailsState['tabId'] });
setUrlState({ tabId: tabId as AssetDetailsUrlState['tabId'] });
};

return {
Expand Down
Loading

0 comments on commit c6f72c7

Please sign in to comment.