Skip to content

Commit

Permalink
[Infra UI] Add date picker to asset details (#164450)
Browse files Browse the repository at this point in the history
closes: #160378

## Summary

This PR adds a date-picker to the Asset Details (full page and flyout)

**Flyout**
<img width="1435" alt="image"
src="https://github.com/elastic/kibana/assets/2767137/a1adb3b0-b3d5-4ca3-9076-6dafd44eebbc">


**Full page view**
<img width="1422" alt="image"
src="https://github.com/elastic/kibana/assets/2767137/ad14764b-fdc2-4773-b8d0-7f2e271d89a5">

**Setting a date range**


https://github.com/elastic/kibana/assets/2767137/4c41d9ee-6c18-45af-9bda-3cae6f372b7b

**Brush event filter**


https://github.com/elastic/kibana/assets/2767137/5830dcd7-7d66-45ed-a463-15bb79607f2a

### Bonus

The uptime link was removed since the UI won't work.

### How to test

- Start a local Kibana instance
- Start metricbeat with system module enabled
- Navigate to `Infrastructure` > `Hosts`
  - Click on a host name and check if the new Node Details page opened
- Check if the date range is set with the same date range from the Host
Detail view
- Navigate to `Infrastructure` > `Hosts`
   -  Open the flyout
- Click on Open as page and check if the new Node Details page opened
with the same date range from the Host Detail view
- Navigate to Infrastructure > Inventory
- With Show: Host selected, click on a waffle item to open the flyout,
click on Open as page and check if the new Node Details page opened with
the same date range from the Inventory UI

Validate if the date picker value is applied to all tabs that have the
component (Overview, Metadata, Processes, Logs, Anomalies)
Validate if the date range changes when selecting a time range in the
charts (in the flyout, the date range from the Hosts View must not
change)

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
crespocarlos and kibanamachine authored Aug 25, 2023
1 parent 4b88b10 commit c7dd76d
Show file tree
Hide file tree
Showing 32 changed files with 667 additions and 500 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const DecorateWithAssetDetailsStateContext: DecoratorFn = (story) => {
dateRange: {
from: '2023-04-09T11:07:49Z',
to: '2023-04-09T11:23:49Z',
mode: 'absolute',
},
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,58 @@
* 2.0.
*/

import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
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';

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

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

return (
<div hidden={!visibleFor.includes(activeTabId as FlyoutTabIds)}>
<DatePicker />
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const ContextProviders = ({
}) => {
const { asset, dateRange, overrides, onTabsStateChange, assetType = 'host', renderMode } = props;
return (
<DateRangeProvider dateRange={dateRange}>
<DateRangeProvider initialDateRange={dateRange}>
<MetadataStateProvider asset={asset} assetType={assetType}>
<AssetDetailsStateProvider
state={{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

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]
);

return (
<EuiSuperDatePicker
start={dateRange.from}
end={dateRange.to}
updateButtonProps={{ iconOnly: true }}
onTimeChange={onTimeChange}
width="full"
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import createContainer from 'constate';
import type { AssetDetailsProps } from '../types';
import { useDateRangeProviderContext } from './use_date_range';
import { useMetadataStateProviderContext } from './use_metadata_state';

export interface UseAssetDetailsStateProps {
Expand All @@ -19,7 +18,6 @@ export interface UseAssetDetailsStateProps {

export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) {
const { metadata } = useMetadataStateProviderContext();
const { dateRange, dateRangeTs } = useDateRangeProviderContext();
const { asset, assetType, onTabsStateChange, overrides, renderMode } = state;

// When the asset asset.name is known we can load the page faster
Expand All @@ -32,8 +30,6 @@ export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) {
name: asset.name || metadata?.name || 'asset-name',
},
assetType,
dateRange,
dateRangeTs,
onTabsStateChange,
overrides,
renderMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,38 @@
* 2.0.
*/

import type { TimeRange } from '@kbn/es-query';
import createContainer from 'constate';
import { useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { parseDateRange } from '../../../utils/datemath';
import type { AssetDetailsProps } from '../types';

import { toTimestampRange } from '../utils';

const DEFAULT_DATE_RANGE = {
const DEFAULT_DATE_RANGE: TimeRange = {
from: 'now-15m',
to: 'now',
};

export type UseAssetDetailsStateProps = Pick<AssetDetailsProps, 'dateRange'>;
export interface UseAssetDetailsStateProps {
initialDateRange: TimeRange;
}

export function useDateRangeProvider({ initialDateRange }: UseAssetDetailsStateProps) {
const [dateRange, setDateRange] = useState(initialDateRange);

export function useDateRangeProvider({ dateRange: rawDateRange }: UseAssetDetailsStateProps) {
const dateRange = useMemo(() => {
const parsedDateRange = useMemo(() => {
const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } =
parseDateRange(rawDateRange);
parseDateRange(dateRange);

return { from, to };
}, [rawDateRange]);
}, [dateRange]);

const dateRangeTs = toTimestampRange(dateRange);
const getDateRangeInTimestamp = useCallback(
() => toTimestampRange(parsedDateRange),
[parsedDateRange]
);

return {
dateRange,
dateRangeTs,
};
return { dateRange, setDateRange, getDateRangeInTimestamp };
}

export const [DateRangeProvider, useDateRangeProviderContext] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useDateRangeProviderContext } from './use_date_range';
export type UseMetadataProviderProps = Pick<AssetDetailsProps, 'asset' | 'assetType'>;

export function useMetadataProvider({ asset, assetType }: UseMetadataProviderProps) {
const { dateRangeTs } = useDateRangeProviderContext();
const { getDateRangeInTimestamp } = useDateRangeProviderContext();
const inventoryModel = findInventoryModel(assetType);
const { sourceId } = useSourceContext();

Expand All @@ -24,7 +24,7 @@ export function useMetadataProvider({ asset, assetType }: UseMetadataProviderPro
assetType,
inventoryModel.requiredMetrics,
sourceId,
dateRangeTs
getDateRangeInTimestamp()
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,13 @@ import { useEuiTheme, EuiIcon, type EuiPageHeaderProps } from '@elastic/eui';
import { css } from '@emotion/react';
import { useLinkProps } from '@kbn/observability-shared-plugin/public';
import React, { useCallback, useMemo } from 'react';
import { uptimeOverviewLocatorID } from '@kbn/observability-plugin/public';
import { capitalize } from 'lodash';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { APM_HOST_FILTER_FIELD } from '../constants';
import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails, LinkToUptime } from '../links';
import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails } from '../links';
import { FlyoutTabIds, type LinkOptions, type Tab, type TabIds } from '../types';
import { toTimestampRange } from '../utils';
import { useAssetDetailsStateContext } from './use_asset_details_state';
import { useDateRangeProviderContext } from './use_date_range';
import { useTabSwitcherContext } from './use_tab_switcher';
import { useMetadataStateProviderContext } from './use_metadata_state';

type TabItem = NonNullable<Pick<EuiPageHeaderProps, 'tabs'>['tabs']>[number];

Expand All @@ -31,34 +27,22 @@ export const usePageHeader = (tabs: Tab[], links?: LinkOptions[]) => {
};

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

const topCornerLinkComponents: Record<LinkOptions, JSX.Element> = useMemo(
() => ({
nodeDetails: (
<LinkToNodeDetails
asset={asset}
assetType={assetType}
currentTimestamp={toTimestampRange(dateRange).to}
dateRangeTimestamp={getDateRangeInTimestamp()}
/>
),
alertRule: <LinkToAlertsRule onClick={overrides?.alertRule?.onCreateRuleClick} />,
apmServices: <LinkToApmServices assetName={asset.name} apmField={APM_HOST_FILTER_FIELD} />,
uptime: (
<LinkToUptime
assetName={asset.name}
assetType={assetType}
ip={
(Array.isArray(metadata?.info?.host?.ip)
? metadata?.info?.host?.ip ?? []
: [metadata?.info?.host?.ip])[0]
}
/>
),
}),
[asset, assetType, dateRange, metadata?.info?.host?.ip, overrides?.alertRule?.onCreateRuleClick]
[asset, assetType, getDateRangeInTimestamp, overrides?.alertRule?.onCreateRuleClick]
);

const rightSideItems = useMemo(
Expand All @@ -71,9 +55,7 @@ const useRightSideItems = (links?: LinkOptions[]) => {

const useTabs = (tabs: Tab[]) => {
const { showTab, activeTabId } = useTabSwitcherContext();
const { asset, assetType } = useAssetDetailsStateContext();
const { metadata } = useMetadataStateProviderContext();
const { share } = useKibanaContextForPlugin().services;
const { asset } = useAssetDetailsStateContext();
const { euiTheme } = useEuiTheme();

const onTabClick = useCallback(
Expand Down Expand Up @@ -110,48 +92,11 @@ const useTabs = (tabs: Tab[]) => {
[apmTracesMenuItemLinkProps, euiTheme.size.xs]
);

const getTabToUptime = useCallback(
(name: string) => ({
'data-test-subj': 'infraAssetDetailsUptimeLinkTab',
onClick: () =>
share.url.locators.get(uptimeOverviewLocatorID)!.navigate({
[assetType]: asset.id,
ip: (Array.isArray(metadata?.info?.host?.ip)
? metadata?.info?.host?.ip ?? []
: [metadata?.info?.host?.ip])[0],
}),
label: (
<>
<EuiIcon
type="popout"
css={css`
margin-right: ${euiTheme.size.xs};
`}
/>
{name}
</>
),
}),
[asset.id, assetType, euiTheme.size.xs, metadata?.info?.host?.ip, share.url.locators]
);

const tabbedLinks = useMemo(
() => ({
[FlyoutTabIds.LINK_TO_APM]: getTabToApmTraces,
[FlyoutTabIds.LINK_TO_UPTIME]: getTabToUptime,
}),
[getTabToApmTraces, getTabToUptime]
);

const tabEntries: TabItem[] = useMemo(
() =>
tabs.map(({ name, ...tab }) => {
if (Object.keys(tabbedLinks).includes(tab.id)) {
return tabbedLinks[tab.id as keyof typeof tabbedLinks](name);
}

if (tab.id === FlyoutTabIds.LINK_TO_APM) {
return getTabToUptime(name);
return getTabToApmTraces(name);
}

return {
Expand All @@ -162,7 +107,7 @@ const useTabs = (tabs: Tab[]) => {
label: name,
};
}),
[activeTabId, getTabToUptime, onTabClick, tabbedLinks, tabs]
[activeTabId, getTabToApmTraces, onTabClick, tabs]
);

return { tabEntries };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
*/

export { LinkToApmServices } from './link_to_apm_services';
export { LinkToUptime } from './link_to_uptime';
export { LinkToAlertsRule } from './link_to_alerts';
export { LinkToNodeDetails } from './link_to_node_details';
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,26 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButtonEmpty } from '@elastic/eui';
import { useLinkProps } from '@kbn/observability-shared-plugin/public';
import { getNodeDetailUrl } from '../../../pages/link_to';
import { findInventoryModel } from '../../../../common/inventory_models';
import type { InventoryItemType } from '../../../../common/inventory_models/types';
import type { Asset } from '../types';

export interface LinkToNodeDetailsProps {
currentTimestamp: number;
dateRangeTimestamp: { from: number; to: number };
asset: Asset;
assetType: InventoryItemType;
}

export const LinkToNodeDetails = ({
asset,
assetType,
currentTimestamp,
dateRangeTimestamp,
}: LinkToNodeDetailsProps) => {
const inventoryModel = findInventoryModel(assetType);
const nodeDetailFrom = currentTimestamp - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;

const nodeDetailMenuItemLinkProps = useLinkProps({
...getNodeDetailUrl({
nodeType: assetType,
nodeId: asset.id,
from: nodeDetailFrom,
to: currentTimestamp,
from: dateRangeTimestamp.from,
to: dateRangeTimestamp.to,
assetName: asset.name,
}),
});
Expand Down
Loading

0 comments on commit c7dd76d

Please sign in to comment.