Skip to content

Commit

Permalink
[Infra] Register kibana locator to link to infras asset details and i…
Browse files Browse the repository at this point in the history
…nventory uis (#181749)

Relates to #176667 

PR 1 of 3

## Summary

This PR replaces the asset details `link-to` component with the asset
details locator and the usage link-to inside infra with the asset
details locator

The other locators and other plugins changes will follow in separate PRs

## Testing: 
Check redirects to asset details (with different assets eg host, pod,
etc., and different links - open as page, hosts table, inventory flyout,
etc.)
On the button/link hover the link path should look similar to
`/app/r?l=ASSET_DETAILS_LOCATOR&v=8.15.0&lz={Long String}`:

- Metrics Explorer

<img width="1599" alt="image"
src="https://github.com/elastic/kibana/assets/14139027/ac54ad1c-9976-423c-8515-adb548b80edb">

- Hosts & Inventory



https://github.com/elastic/kibana/assets/14139027/65de04c0-d6da-457b-a24c-40e26c3d1b3d

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
jennypavlova and kibanamachine authored May 13, 2024
1 parent a4bb0ab commit b4084b6
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 161 deletions.
9 changes: 5 additions & 4 deletions packages/kbn-router-utils/src/get_router_link_props/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@

export interface RouterLinkProps {
href: string | undefined;
onClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
onClick: (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement, MouseEvent>) => void;
}

interface GetRouterLinkPropsDeps {
href?: string;
onClick(): void;
}

const isModifiedEvent = (event: React.MouseEvent<HTMLAnchorElement>) =>
const isModifiedEvent = (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

const isLeftClickEvent = (event: React.MouseEvent<HTMLAnchorElement>) => event.button === 0;
const isLeftClickEvent = (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) =>
event.button === 0;

/**
*
Expand All @@ -34,7 +35,7 @@ const isLeftClickEvent = (event: React.MouseEvent<HTMLAnchorElement>) => event.b
* manage behaviours such as leftClickEvent and event with modifiers (Ctrl, Shift, etc)
*/
export const getRouterLinkProps = ({ href, onClick }: GetRouterLinkPropsDeps): RouterLinkProps => {
const guardedClickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => {
const guardedClickHandler = (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
if (event.defaultPrevented) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButtonEmpty } from '@elastic/eui';
import { useLinkProps } from '@kbn/observability-shared-plugin/public';
import { parse } from '@kbn/datemath';
import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common';
import { useNodeDetailsRedirect } from '../../../pages/link_to';
Expand All @@ -27,17 +26,15 @@ export const LinkToNodeDetails = ({ assetId, assetName, assetType }: LinkToNodeD
// don't propagate the autoRefresh to the details page
const { dateRange, autoRefresh: _, ...assetDetails } = state ?? {};

const nodeDetailMenuItemLinkProps = useLinkProps({
...getNodeDetailUrl({
assetType,
assetId,
search: {
...assetDetails,
name: assetName,
from: parse(dateRange?.from ?? '')?.valueOf(),
to: parse(dateRange?.to ?? '')?.valueOf(),
},
}),
const nodeDetailMenuItemLinkProps = getNodeDetailUrl({
assetType,
assetId,
search: {
...assetDetails,
name: assetName,
from: parse(dateRange?.from ?? '')?.valueOf(),
to: parse(dateRange?.to ?? '')?.valueOf(),
},
});

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

import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import React, { useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';

import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time';
import type { SerializableRecord } from '@kbn/utility-types';
import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/public';
import { useHostIpToName } from './use_host_ip_to_name';
import { getFromFromLocation, getToFromLocation } from './query_params';
import { LoadingPage } from '../../components/loading_page';
import { Error } from '../error';
import { useSourceContext } from '../../containers/metrics_source';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import { getSearchParams } from './redirect_to_node_detail';

type RedirectToHostDetailType = RouteComponentProps<{
hostIp: string;
Expand All @@ -27,12 +29,30 @@ export const RedirectToHostDetailViaIP = ({
location,
}: RedirectToHostDetailType) => {
const { source } = useSourceContext();
const {
services: { share },
} = useKibanaContextForPlugin();
const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID);

const { error, name } = useHostIpToName(
hostIp,
(source && source.configuration && source.configuration.metricAlias) || null
);

useEffect(() => {
if (name) {
const queryParams = new URLSearchParams(location.search);
const search = getSearchParams('host', queryParams);

baseLocator?.navigate({
...search,
assetType: 'host',
assetId: name,
state: location.state as SerializableRecord,
});
}
}, [baseLocator, location.search, location.state, name]);

if (error) {
return (
<Error
Expand All @@ -44,21 +64,16 @@ export const RedirectToHostDetailViaIP = ({
);
}

const searchString = replaceMetricTimeInQueryString(
getFromFromLocation(location),
getToFromLocation(location)
)('');

if (name) {
return <Redirect to={`/detail/host/${name}?${searchString}`} />;
if (!name) {
return (
<LoadingPage
message={i18n.translate('xpack.infra.linkTo.hostWithIp.loading', {
defaultMessage: 'Loading host with IP address "{hostIp}".',
values: { hostIp },
})}
/>
);
}

return (
<LoadingPage
message={i18n.translate('xpack.infra.linkTo.hostWithIp.loading', {
defaultMessage: 'Loading host with IP address "{hostIp}".',
values: { hostIp },
})}
/>
);
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
* 2.0.
*/

import React from 'react';
import { Redirect, useLocation, useRouteMatch } from 'react-router-dom';
import { useEffect } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom';
import rison from '@kbn/rison';
import { InventoryItemType } from '@kbn/metrics-data-access-plugin/common';
import { replaceStateKeyInQueryString } from '../../../common/url_state_storage_service';
import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time';
import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common';
import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/public';
import type { SerializableRecord } from '@kbn/utility-types';
import { AssetDetailsUrlState } from '../../components/asset_details/types';
import { ASSET_DETAILS_URL_STATE_KEY } from '../../components/asset_details/constants';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';

export const REDIRECT_NODE_DETAILS_FROM_KEY = 'from';
export const REDIRECT_NODE_DETAILS_TO_KEY = 'to';
Expand All @@ -23,43 +24,61 @@ const getHostDetailSearch = (queryParams: URLSearchParams) => {
const to = queryParams.get(REDIRECT_NODE_DETAILS_TO_KEY);
const assetDetailsParam = queryParams.get(REDIRECT_ASSET_DETAILS_KEY);

return replaceStateKeyInQueryString(ASSET_DETAILS_URL_STATE_KEY, {
...(assetDetailsParam ? (rison.decode(assetDetailsParam) as AssetDetailsUrlState) : undefined),
dateRange: {
from: from ? new Date(parseFloat(from)).toISOString() : undefined,
to: to ? new Date(parseFloat(to)).toISOString() : undefined,
return {
[ASSET_DETAILS_URL_STATE_KEY]: {
...(assetDetailsParam
? (rison.decode(assetDetailsParam) as AssetDetailsUrlState)
: undefined),
dateRange: {
from: from ? new Date(parseFloat(from)).toISOString() : undefined,
to: to ? new Date(parseFloat(to)).toISOString() : undefined,
},
},
} as AssetDetailsUrlState)('');
} as AssetDetailsUrlState;
};

const getNodeDetailSearch = (queryParams: URLSearchParams) => {
const from = queryParams.get(REDIRECT_NODE_DETAILS_FROM_KEY);
const to = queryParams.get(REDIRECT_NODE_DETAILS_TO_KEY);

return replaceMetricTimeInQueryString(
from ? parseFloat(from) : NaN,
to ? parseFloat(to) : NaN
)('');
return {
_a: {
time:
from && to
? {
from: new Date(parseFloat(from)).toISOString(),
interval: '>=1m',
to: new Date(parseFloat(to)).toISOString(),
}
: undefined,
},
};
};

export const getSearchParams = (nodeType: InventoryItemType, queryParams: URLSearchParams) =>
nodeType === 'host' ? getHostDetailSearch(queryParams) : getNodeDetailSearch(queryParams);

export const RedirectToNodeDetail = () => {
const {
params: { nodeType, nodeId },
} = useRouteMatch<{ nodeType: InventoryItemType; nodeId: string }>();

const {
services: { share },
} = useKibanaContextForPlugin();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID);

useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const search = getSearchParams(nodeType, queryParams);

const search =
nodeType === 'host' ? getHostDetailSearch(queryParams) : getNodeDetailSearch(queryParams);
baseLocator?.navigate({
...search,
assetType: nodeType,
assetId: nodeId,
state: location.state as SerializableRecord,
});
}, [baseLocator, location.search, location.state, nodeId, nodeType]);

return (
<Redirect
to={{
pathname: `/detail/${nodeType}/${nodeId}`,
search,
state: location.state,
}}
/>
);
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { renderHook } from '@testing-library/react-hooks';
import { useNodeDetailsRedirect } from './use_node_details_redirect';
import { coreMock } from '@kbn/core/public/mocks';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';

const coreStartMock = coreMock.createStart();
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Expand All @@ -21,8 +20,20 @@ jest.mock('react-router-dom', () => ({
})),
}));

const MOCK_HREF = '/app/r?l=ASSET_DETAILS_LOCATOR&v=8.15.0&lz=MoCkLoCaToRvAlUe';
const coreStartMock = coreMock.createStart();
const shareMock = sharePluginMock.createSetupContract();

// @ts-expect-error This object is missing some properties that we're not using in the UI
shareMock.url.locators.get = (id: string) => ({
getRedirectUrl: (params: Record<string, any>): string | undefined => MOCK_HREF,
navigate: () => {},
});

const wrapper = ({ children }: { children: React.ReactNode }): JSX.Element => (
<KibanaContextProvider services={{ ...coreStartMock }}>{children}</KibanaContextProvider>
<KibanaContextProvider services={{ ...coreStartMock, share: shareMock }}>
{children}
</KibanaContextProvider>
);

describe('useNodeDetailsRedirect', () => {
Expand All @@ -32,21 +43,18 @@ describe('useNodeDetailsRedirect', () => {
const fromDateStrig = '2019-01-01T11:00:00Z';
const toDateStrig = '2019-01-01T12:00:00Z';

expect(
result.current.getNodeDetailUrl({
assetType: 'pod',
assetId: 'example-01',
search: {
from: new Date(fromDateStrig).getTime(),
to: new Date(toDateStrig).getTime(),
},
})
).toStrictEqual({
app: 'metrics',
pathname: 'link-to/pod-detail/example-01',
search: { from: '1546340400000', to: '1546344000000' },
state: {},
const getLinkProps = result.current.getNodeDetailUrl({
assetType: 'pod',
assetId: 'example-01',
search: {
from: new Date(fromDateStrig).getTime(),
to: new Date(toDateStrig).getTime(),
},
});

expect(getLinkProps).toHaveProperty('href');
expect(getLinkProps.href).toEqual(MOCK_HREF);
expect(getLinkProps).toHaveProperty('onClick');
});

it('should return the LinkProperties for assetType host', () => {
Expand All @@ -55,25 +63,18 @@ describe('useNodeDetailsRedirect', () => {
const fromDateStrig = '2019-01-01T11:00:00Z';
const toDateStrig = '2019-01-01T12:00:00Z';

expect(
result.current.getNodeDetailUrl({
assetType: 'host',
assetId: 'example-01',
search: {
from: new Date(fromDateStrig).getTime(),
to: new Date(toDateStrig).getTime(),
name: 'example-01',
},
})
).toStrictEqual({
app: 'metrics',
pathname: 'link-to/host-detail/example-01',
const getLinkProps = result.current.getNodeDetailUrl({
assetType: 'host',
assetId: 'example-01',
search: {
from: '1546340400000',
to: '1546344000000',
assetDetails: '(name:example-01)',
from: new Date(fromDateStrig).getTime(),
to: new Date(toDateStrig).getTime(),
name: 'example-01',
},
state: {},
});

expect(getLinkProps).toHaveProperty('href');
expect(getLinkProps.href).toEqual(MOCK_HREF);
expect(getLinkProps).toHaveProperty('onClick');
});
});
Loading

0 comments on commit b4084b6

Please sign in to comment.