From 2f6be1b00513d43769f0221aa5bf81f4ef8133e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Mon, 9 Sep 2024 18:34:25 +0200 Subject: [PATCH] [Infra] Replacement of link-to links with infra locators (#191578) ## Summary Replacing `link-to` urls with infra locators, so that we can deprecate `link-to` for infra. ### How to test - Go to Infrastructure -> Inventory page. - Choose either Hosts/Pods in the Show dropdown. - Select List view instead of Default - Click on an instance, select Metrics option on the popup #### Expected Result Should go to the assets detail page, for both Hosts (supported by new assets details params) and Pods (not supported). Closes #176667 --------- Co-authored-by: Elastic Machine --- .../instance_actions_menu/index.tsx | 7 +++ .../instance_actions_menu/menu_sections.ts | 43 +++++++------ .../transaction_action_menu/sections.test.ts | 21 ++++++- .../transaction_action_menu/sections.ts | 61 +++++++++++-------- .../transaction_action_menu.test.tsx | 24 +++++++- .../transaction_action_menu.tsx | 10 ++- .../common/alerting/metrics/alert_link.ts | 2 +- .../infra/common/asset_details/types.ts | 10 --- .../public/pages/link_to/link_to_metrics.tsx | 5 ++ .../pages/link_to/redirect_to_node_detail.tsx | 2 +- .../components/sections/metrics/host_link.tsx | 27 +++++++- .../observability_shared/common/index.ts | 1 + .../locators/infra/asset_details_locator.ts | 27 +++++++- .../common/locators/infra/locators.test.ts | 18 ++++++ .../actions_popover/integration_group.tsx | 15 ++++- .../get_infra_href.test.ts | 52 +++++++++------- .../get_infra_href.ts | 61 +++++++++++-------- 17 files changed, 269 insertions(+), 117 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/infra/common/asset_details/types.ts diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx index 6c70f6e5091fb0..4e609b35935f1f 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx @@ -20,6 +20,10 @@ import { ALL_DATASETS_LOCATOR_ID, } from '@kbn/deeplinks-observability/locators'; import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; +import { + ASSET_DETAILS_LOCATOR_ID, + type AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { isJavaAgentName } from '../../../../../../common/agent_name'; import { SERVICE_NODE_NAME } from '../../../../../../common/es_fields/apm'; import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; @@ -55,6 +59,8 @@ export function InstanceActionsMenu({ serviceName, serviceNodeName, kuery, onClo const allDatasetsLocator = share.url.locators.get(ALL_DATASETS_LOCATOR_ID)!; const { nodeLogsLocator } = getLogsLocatorsFromUrlService(share.url); + const assetDetailsLocator = + share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); if (isPending(status)) { return ( @@ -95,6 +101,7 @@ export function InstanceActionsMenu({ serviceName, serviceNodeName, kuery, onClo metricsHref, allDatasetsLocator, nodeLogsLocator, + assetDetailsLocator, }); return ( diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts b/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts index a907a9c5c16706..7d7ced6c5a9905 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts @@ -12,8 +12,8 @@ import { AllDatasetsLocatorParams } from '@kbn/deeplinks-observability/locators' import type { LocatorPublic } from '@kbn/share-plugin/public'; import { NodeLogsLocatorParams } from '@kbn/logs-shared-plugin/common'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; +import { type AssetDetailsLocator } from '@kbn/observability-shared-plugin/common'; import { APIReturnType } from '../../../../../services/rest/create_call_apm_api'; -import { getInfraHref } from '../../../../shared/links/infra_link'; import { Action, getNonEmptySections, @@ -25,14 +25,14 @@ type InstaceDetails = function getInfraMetricsQuery(timestamp?: string) { if (!timestamp) { - return { from: 0, to: 0 }; + return undefined; } const timeInMilliseconds = new Date(timestamp).getTime(); const fiveMinutes = moment.duration(5, 'minutes').asMilliseconds(); return { - from: timeInMilliseconds - fiveMinutes, - to: timeInMilliseconds + fiveMinutes, + from: new Date(timeInMilliseconds - fiveMinutes).toISOString(), + to: new Date(timeInMilliseconds + fiveMinutes).toISOString(), }; } @@ -43,6 +43,7 @@ export function getMenuSections({ metricsHref, allDatasetsLocator, nodeLogsLocator, + assetDetailsLocator, }: { instanceDetails: InstaceDetails; basePath: IBasePath; @@ -50,6 +51,7 @@ export function getMenuSections({ metricsHref: string; allDatasetsLocator: LocatorPublic; nodeLogsLocator: LocatorPublic; + assetDetailsLocator?: AssetDetailsLocator; }) { const podId = instanceDetails.kubernetes?.pod?.uid; const containerId = instanceDetails.container?.id; @@ -70,6 +72,9 @@ export function getMenuSections({ time, }); + const hasPodLink = !!podId && !!assetDetailsLocator; + const hasContainerLink = !!containerId && !!assetDetailsLocator; + const podActions: Action[] = [ { key: 'podLogs', @@ -84,13 +89,14 @@ export function getMenuSections({ label: i18n.translate('xpack.apm.serviceOverview.instancesTable.actionMenus.podMetrics', { defaultMessage: 'Pod metrics', }), - href: getInfraHref({ - app: 'metrics', - basePath, - path: `/link-to/pod-detail/${podId}`, - query: infraMetricsQuery, - }), - condition: !!podId, + href: hasPodLink + ? assetDetailsLocator.getRedirectUrl({ + assetId: podId, + assetType: 'pod', + assetDetails: { dateRange: infraMetricsQuery }, + }) + : undefined, + condition: hasPodLink, }, ]; @@ -109,13 +115,14 @@ export function getMenuSections({ 'xpack.apm.serviceOverview.instancesTable.actionMenus.containerMetrics', { defaultMessage: 'Container metrics' } ), - href: getInfraHref({ - app: 'metrics', - basePath, - path: `/link-to/container-detail/${containerId}`, - query: infraMetricsQuery, - }), - condition: !!containerId, + href: hasContainerLink + ? assetDetailsLocator.getRedirectUrl({ + assetId: containerId, + assetType: 'container', + assetDetails: { dateRange: infraMetricsQuery }, + }) + : undefined, + condition: hasContainerLink, }, ]; diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.test.ts b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.test.ts index 4660ac85f092cd..0ec439f177222c 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.test.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.test.ts @@ -6,12 +6,17 @@ */ import { createMemoryHistory } from 'history'; +import rison from '@kbn/rison'; import { IBasePath } from '@kbn/core/public'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { getSections } from './sections'; import { apmRouter as apmRouterBase, ApmRouter } from '../../routing/apm_route_config'; import { logsLocatorsMock } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { + AssetDetailsLocatorParams, + AssetDetailsLocator, +} from '@kbn/observability-shared-plugin/common'; const apmRouter = { ...apmRouterBase, @@ -21,6 +26,15 @@ const apmRouter = { const { nodeLogsLocator, traceLogsLocator } = logsLocatorsMock; const uptimeLocator = sharePluginMock.createLocator(); +const mockAssetDetailsLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + ({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) => + `/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}` + ), +} as unknown as jest.Mocked; + const expectLogsLocatorsToBeCalled = () => { expect(nodeLogsLocator.getRedirectUrl).toBeCalledTimes(3); expect(traceLogsLocator.getRedirectUrl).toBeCalledTimes(1); @@ -69,6 +83,7 @@ describe('Transaction action menu', () => { rangeTo: 'now', environment: 'ENVIRONMENT_ALL', dataViewId: 'apm_static_data_view_id_default', + assetDetailsLocator: mockAssetDetailsLocator, }) ).toEqual([ [ @@ -137,6 +152,7 @@ describe('Transaction action menu', () => { rangeTo: 'now', environment: 'ENVIRONMENT_ALL', dataViewId: 'apm_static_data_view_id_default', + assetDetailsLocator: mockAssetDetailsLocator, }) ).toEqual([ [ @@ -153,7 +169,7 @@ describe('Transaction action menu', () => { { key: 'podMetrics', label: 'Pod metrics', - href: 'some-basepath/app/metrics/link-to/pod-detail/123?from=1580986500000&to=1580987100000', + href: "/node-mock/pod/123?receivedParams=(dateRange:(from:'2020-02-06T10:55:00.000Z',to:'2020-02-06T11:05:00.000Z'))", condition: true, }, ], @@ -223,6 +239,7 @@ describe('Transaction action menu', () => { rangeTo: 'now', environment: 'ENVIRONMENT_ALL', dataViewId: 'apm_static_data_view_id_default', + assetDetailsLocator: mockAssetDetailsLocator, }) ).toEqual([ [ @@ -239,7 +256,7 @@ describe('Transaction action menu', () => { { key: 'hostMetrics', label: 'Host metrics', - href: 'some-basepath/app/metrics/link-to/host-detail/foo?from=1580986500000&to=1580987100000', + href: "/node-mock/host/foo?receivedParams=(dateRange:(from:'2020-02-06T10:55:00.000Z',to:'2020-02-06T11:05:00.000Z'))", condition: true, }, ], diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.ts b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.ts index 781a992e1f6124..f30deed376e866 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/sections.ts @@ -13,13 +13,13 @@ import moment from 'moment'; import type { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import type { ProfilingLocators } from '@kbn/observability-shared-plugin/public'; +import type { AssetDetailsLocator } from '@kbn/observability-shared-plugin/common'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import { Environment } from '../../../../common/environment_rt'; import type { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { getDiscoverHref } from '../links/discover_links/discover_link'; import { getDiscoverQuery } from '../links/discover_links/discover_transaction_link'; -import { getInfraHref } from '../links/infra_link'; import { SectionRecord, getNonEmptySections, Action } from './sections_helper'; import { HOST_NAME, TRACE_ID } from '../../../../common/es_fields/apm'; import { ApmRouter } from '../../routing/apm_route_config'; @@ -29,8 +29,8 @@ function getInfraMetricsQuery(transaction: Transaction) { const fiveMinutes = moment.duration(5, 'minutes').asMilliseconds(); return { - from: timestamp - fiveMinutes, - to: timestamp + fiveMinutes, + from: new Date(timestamp - fiveMinutes).toISOString(), + to: new Date(timestamp + fiveMinutes).toISOString(), }; } @@ -47,6 +47,7 @@ export const getSections = ({ environment, logsLocators, dataViewId, + assetDetailsLocator, }: { transaction?: Transaction; basePath: IBasePath; @@ -60,6 +61,7 @@ export const getSections = ({ environment: Environment; logsLocators: ReturnType; dataViewId?: string; + assetDetailsLocator?: AssetDetailsLocator; }) => { if (!transaction) return []; @@ -103,6 +105,10 @@ export const getSections = ({ time, }); + const hasPodLink = !!podId && infraLinksAvailable && !!assetDetailsLocator; + const hasContainerLink = !!containerId && infraLinksAvailable && !!assetDetailsLocator; + const hasHostLink = !!hostName && infraLinksAvailable && !!assetDetailsLocator; + const podActions: Action[] = [ { key: 'podLogs', @@ -117,13 +123,16 @@ export const getSections = ({ label: i18n.translate('xpack.apm.transactionActionMenu.showPodMetricsLinkLabel', { defaultMessage: 'Pod metrics', }), - href: getInfraHref({ - app: 'metrics', - basePath, - path: `/link-to/pod-detail/${podId}`, - query: infraMetricsQuery, - }), - condition: !!podId && infraLinksAvailable, + href: hasPodLink + ? assetDetailsLocator.getRedirectUrl({ + assetId: podId, + assetType: 'pod', + assetDetails: { + dateRange: infraMetricsQuery, + }, + }) + : undefined, + condition: hasPodLink, }, ]; @@ -141,13 +150,14 @@ export const getSections = ({ label: i18n.translate('xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel', { defaultMessage: 'Container metrics', }), - href: getInfraHref({ - app: 'metrics', - basePath, - path: `/link-to/container-detail/${containerId}`, - query: infraMetricsQuery, - }), - condition: !!containerId && infraLinksAvailable, + href: hasContainerLink + ? assetDetailsLocator.getRedirectUrl({ + assetId: containerId, + assetType: 'container', + assetDetails: { dateRange: infraMetricsQuery }, + }) + : undefined, + condition: hasContainerLink, }, ]; @@ -165,13 +175,16 @@ export const getSections = ({ label: i18n.translate('xpack.apm.transactionActionMenu.showHostMetricsLinkLabel', { defaultMessage: 'Host metrics', }), - href: getInfraHref({ - app: 'metrics', - basePath, - path: `/link-to/host-detail/${hostName}`, - query: infraMetricsQuery, - }), - condition: !!hostName && infraLinksAvailable, + href: hasHostLink + ? assetDetailsLocator.getRedirectUrl({ + assetId: hostName, + assetType: 'host', + assetDetails: { + dateRange: infraMetricsQuery, + }, + }) + : undefined, + condition: hasHostLink, }, { key: 'hostProfilingFlamegraph', diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx index 29e5da6842165f..90cbbec97ac16a 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { License } from '@kbn/licensing-plugin/common/license'; +import rison from '@kbn/rison'; import { LOGS_LOCATOR_ID, NODE_LOGS_LOCATOR_ID, @@ -32,6 +33,20 @@ import * as useAdHocApmDataView from '../../../hooks/use_adhoc_apm_data_view'; import { useProfilingIntegrationSetting } from '../../../hooks/use_profiling_integration_setting'; import { uptimeOverviewLocatorID } from '@kbn/observability-plugin/common'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { + AssetDetailsLocator, + AssetDetailsLocatorParams, + ASSET_DETAILS_LOCATOR_ID, +} from '@kbn/observability-shared-plugin/common'; + +const mockAssetDetailsLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + ({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) => + `/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}` + ), +} as unknown as jest.Mocked; const apmContextMock = { ...mockApmPluginContextValue, @@ -63,6 +78,9 @@ const apmContextMock = { ), }; } + if (id === ASSET_DETAILS_LOCATOR_ID) { + return mockAssetDetailsLocator; + } }, }, }, @@ -185,7 +203,7 @@ describe('TransactionActionMenu ', () => { const { getByText } = await renderTransaction(Transactions.transactionWithKubernetesData); expect((getByText('Pod metrics').parentElement as HTMLAnchorElement).href).toEqual( - 'http://localhost/basepath/app/metrics/link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952' + 'http://localhost/node-mock/pod/pod123456abcdef?receivedParams=(dateRange:(from:%272018-12-18T00:09:30.952Z%27,to:%272018-12-18T00:19:30.952Z%27))' ); }); }); @@ -215,7 +233,7 @@ describe('TransactionActionMenu ', () => { const { getByText } = await renderTransaction(Transactions.transactionWithContainerData); expect((getByText('Container metrics').parentElement as HTMLAnchorElement).href).toEqual( - 'http://localhost/basepath/app/metrics/link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952' + 'http://localhost/node-mock/container/container123456abcdef?receivedParams=(dateRange:(from:%272018-12-18T00:09:30.952Z%27,to:%272018-12-18T00:19:30.952Z%27))' ); }); }); @@ -245,7 +263,7 @@ describe('TransactionActionMenu ', () => { const { getByText } = await renderTransaction(Transactions.transactionWithHostData); expect((getByText('Host metrics').parentElement as HTMLAnchorElement).href).toEqual( - 'http://localhost/basepath/app/metrics/link-to/host-detail/227453131a17?from=1545091770952&to=1545092370952' + 'http://localhost/node-mock/host/227453131a17?receivedParams=(dateRange:(from:%272018-12-18T00:09:30.952Z%27,to:%272018-12-18T00:19:30.952Z%27))' ); }); }); diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx index 05aaa7ccdad73e..4f45441ea19372 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx @@ -7,7 +7,11 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common'; +import { + ASSET_DETAILS_LOCATOR_ID, + type AssetDetailsLocatorParams, + ObservabilityTriggerId, +} from '@kbn/observability-shared-plugin/common'; import { ActionMenu, ActionMenuDivider, @@ -127,6 +131,9 @@ function ActionMenuSections({ const infraLinksAvailable = useApmFeatureFlag(ApmFeatureFlagName.InfraUiAvailable); + const assetDetailsLocator = + share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const { query: { rangeFrom, rangeTo, environment }, } = useAnyOfApmParams( @@ -149,6 +156,7 @@ function ActionMenuSections({ environment, logsLocators, dataViewId: dataView?.id, + assetDetailsLocator, }); const externalMenuItems = useAsync(() => { diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts index 95a2fd85eb9fa0..a0df751014e78a 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts @@ -11,6 +11,7 @@ import { encode } from '@kbn/rison'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; import { type InventoryItemType, findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; import type { LocatorPublic } from '@kbn/share-plugin/common'; +import { SupportedAssetTypes } from '@kbn/observability-shared-plugin/common'; import { MetricsExplorerLocatorParams, type AssetDetailsLocatorParams, @@ -18,7 +19,6 @@ import { } from '@kbn/observability-shared-plugin/common'; import { castArray } from 'lodash'; import { fifteenMinutesInMilliseconds } from '../../constants'; -import { SupportedAssetTypes } from '../../asset_details/types'; const ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID = `${ALERT_RULE_PARAMETERS}.criteria.metric`; export const ALERT_RULE_PARAMETERS_NODE_TYPE = `${ALERT_RULE_PARAMETERS}.nodeType`; diff --git a/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts deleted file mode 100644 index 685b2bcacb2e4c..00000000000000 --- a/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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. - */ -export enum SupportedAssetTypes { - container = 'container', - host = 'host', -} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/link_to_metrics.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/link_to_metrics.tsx index a1280cb394079d..64724c210a8f4c 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/link_to_metrics.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/link_to_metrics.tsx @@ -20,6 +20,11 @@ interface LinkToPageProps { const ITEM_TYPES = inventoryModels.map((m) => m.id).join('|'); +/** + * @deprecated Link-to routes shouldn't be used anymore + * Instead please use locators registered for the infra plugin + * InventoryLocator & AssetDetailsLocator + */ export const LinkToMetricsPage: React.FC = (props) => { return ( diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx index d0bac8d8c9bf25..045bdccc913bfe 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -12,9 +12,9 @@ import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; import { ASSET_DETAILS_LOCATOR_ID, type AssetDetailsLocatorParams, + SupportedAssetTypes, } from '@kbn/observability-shared-plugin/common'; import type { SerializableRecord } from '@kbn/utility-types'; -import { SupportedAssetTypes } from '../../../common/asset_details/types'; import { type AssetDetailsUrlState } from '../../components/asset_details/types'; import { ASSET_DETAILS_URL_STATE_KEY } from '../../components/asset_details/constants'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/host_link.tsx b/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/host_link.tsx index 91db16d08144aa..405c3c0381438e 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/host_link.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/overview/components/sections/metrics/host_link.tsx @@ -5,19 +5,40 @@ * 2.0. */ import React from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { + ASSET_DETAILS_LOCATOR_ID, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; +import { SharePluginStart } from '@kbn/share-plugin/public'; import { StringOrNull } from '../../../../..'; interface Props { name: StringOrNull; - id: StringOrNull; + id: string; timerange: { from: number; to: number }; } export function HostLink({ name, id, timerange }: Props) { - const link = `../../app/metrics/link-to/host-detail/${id}?from=${timerange.from}&to=${timerange.to}`; + const { services } = useKibana<{ share?: SharePluginStart }>(); + + const assetDetailsLocator = + services.share?.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + + const href = assetDetailsLocator?.getRedirectUrl({ + assetType: 'host', + assetId: id, + assetDetails: { + dateRange: { + from: new Date(timerange.from).toISOString(), + to: new Date(timerange.to).toISOString(), + }, + }, + }); + return ( <> - {name} + {name} ); } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index 083bcdd2debde5..ab49080f313ba8 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -178,6 +178,7 @@ export { AssetDetailsFlyoutLocatorDefinition, ASSET_DETAILS_LOCATOR_ID, AssetDetailsLocatorDefinition, + SupportedAssetTypes, HostsLocatorDefinition, INVENTORY_LOCATOR_ID, InventoryLocatorDefinition, diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts index 244660fc05f71d..9a5ec5c64796da 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts @@ -9,6 +9,11 @@ import rison from '@kbn/rison'; import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common'; import { type AlertStatus } from '@kbn/rule-data-utils'; +export enum SupportedAssetTypes { + container = 'container', + host = 'host', +} + export type AssetDetailsLocator = LocatorPublic; export interface AssetDetailsLocatorParams extends SerializableRecord { @@ -47,8 +52,26 @@ export class AssetDetailsLocatorDefinition implements LocatorDefinition { - const legacyNodeDetailsQueryParams = rison.encodeUnknown(params._a); - const assetDetailsQueryParams = rison.encodeUnknown(params.assetDetails); + // Check which asset types are currently supported + const isSupportedByAssetDetails = Object.values(SupportedAssetTypes).includes( + params.assetType as SupportedAssetTypes + ); + + // Map the compatible parameters to _a compatible shape + const mappedAssetParams = + params.assetDetails && !isSupportedByAssetDetails + ? { + time: params.assetDetails.dateRange, + } + : undefined; + + const legacyNodeDetailsQueryParams = !isSupportedByAssetDetails + ? rison.encodeUnknown({ ...mappedAssetParams, ...params._a }) + : undefined; + + const assetDetailsQueryParams = isSupportedByAssetDetails + ? rison.encodeUnknown(params.assetDetails) + : undefined; const queryParams = []; if (assetDetailsQueryParams !== undefined) { diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts index 8c7dc0d4b61136..fb9cbb8456dd30 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts @@ -77,6 +77,24 @@ describe('Infra Locators', () => { expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); }); + + it('should return correct fallback params for non-supported assetType using assetDetails', async () => { + const { assetDetailsLocator } = await setupAssetDetailsLocator(); + + const { app, path, state } = await assetDetailsLocator.getLocation({ + ...params, + assetType: 'pod', + }); + + const expectedDetails = rison.encodeUnknown({ + time: params.assetDetails.dateRange, + }); + + expect(app).toBe('metrics'); + expect(path).toBe(`/detail/pod/${params.assetId}?_a=${expectedDetails}`); + expect(state).toBeDefined(); + expect(Object.keys(state)).toHaveLength(0); + }); }); describe('Asset Details Flyout Locator', () => { diff --git a/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx index 40374e6d49d714..f662d08e194750 100644 --- a/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx +++ b/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx @@ -9,6 +9,12 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { + ASSET_DETAILS_LOCATOR_ID, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; +import { SharePluginStart } from '@kbn/share-plugin/public'; import { IntegrationLink } from './integration_link'; import { getLegacyApmHref, @@ -55,6 +61,11 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { const { domain, podUid, containerId, ip } = extractSummaryValues(summary); + const { services } = useKibana<{ share?: SharePluginStart }>(); + + const assetDetailsLocator = + services.share?.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + return isApmAvailable || isInfraAvailable || isLogsAvailable ? ( {isApmAvailable ? ( @@ -127,7 +138,7 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { description: 'This value is shown as the aria label value for screen readers.', } )} - href={getInfraKubernetesHref(summary, basePath)} + href={getInfraKubernetesHref(summary, assetDetailsLocator)} iconType="metricsApp" message={i18n.translate( 'xpack.uptime.monitorList.infraIntegrationAction.kubernetes.message', @@ -156,7 +167,7 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { defaultMessage: `Check Infrastructure UI for this monitor's container ID`, } )} - href={getInfraContainerHref(summary, basePath)} + href={getInfraContainerHref(summary, assetDetailsLocator)} iconType="metricsApp" message={i18n.translate( 'xpack.uptime.monitorList.infraIntegrationAction.container.message', diff --git a/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.test.ts b/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.test.ts index 970ac86f3777d1..b41e244e7ec517 100644 --- a/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.test.ts +++ b/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.test.ts @@ -5,9 +5,21 @@ * 2.0. */ +import { + AssetDetailsLocator, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getInfraContainerHref, getInfraKubernetesHref, getInfraIpHref } from './get_infra_href'; import { MonitorSummary, makePing, Ping } from '../../../../../common/runtime_types'; +const mockAssetDetailsLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + ({ assetId, assetType }: AssetDetailsLocatorParams) => `/node-mock/${assetType}/${assetId}` + ), +} as unknown as jest.Mocked; + describe('getInfraHref', () => { let summary: MonitorSummary; beforeEach(() => { @@ -38,21 +50,17 @@ describe('getInfraHref', () => { }); it('getInfraContainerHref creates a link for valid parameters', () => { - const result = getInfraContainerHref(summary, 'foo'); - expect(result).toMatchInlineSnapshot( - `"foo/app/metrics/link-to/container-detail/test-container-id"` - ); + const result = getInfraContainerHref(summary, mockAssetDetailsLocator); + expect(result).toMatchInlineSnapshot(`"/node-mock/container/test-container-id"`); }); - it('getInfraContainerHref does not specify a base path when none is available', () => { - expect(getInfraContainerHref(summary, '')).toMatchInlineSnapshot( - `"/app/metrics/link-to/container-detail/test-container-id"` - ); + it('getInfraContainerHref returns undefined when no locator is available', () => { + expect(getInfraContainerHref(summary, undefined)).toBeUndefined(); }); it('getInfraContainerHref returns undefined when no container id is present', () => { summary.state.summaryPings = []; - expect(getInfraContainerHref(summary, 'foo')).toBeUndefined(); + expect(getInfraContainerHref(summary, mockAssetDetailsLocator)).toBeUndefined(); }); it('getInfraContainerHref returns the first item when multiple container ids are supplied', () => { @@ -74,32 +82,30 @@ describe('getInfraHref', () => { container: { id: 'test-container-id-foo' }, }; summary.state.summaryPings = [pingTestContainerId, pingTestFooContainerId]; - expect(getInfraContainerHref(summary, 'bar')).toMatchInlineSnapshot( - `"bar/app/metrics/link-to/container-detail/test-container-id"` + expect(getInfraContainerHref(summary, mockAssetDetailsLocator)).toMatchInlineSnapshot( + `"/node-mock/container/test-container-id"` ); }); it('getInfraContainerHref returns undefined when summaryPings are undefined', () => { // @ts-expect-error delete summary.state.summaryPings; - expect(getInfraContainerHref(summary, '')).toBeUndefined(); + expect(getInfraContainerHref(summary, mockAssetDetailsLocator)).toBeUndefined(); }); it('getInfraKubernetesHref creates a link for valid parameters', () => { - const result = getInfraKubernetesHref(summary, 'foo'); + const result = getInfraKubernetesHref(summary, mockAssetDetailsLocator); expect(result).not.toBeUndefined(); - expect(result).toMatchInlineSnapshot(`"foo/app/metrics/link-to/pod-detail/test-pod-uid"`); + expect(result).toMatchInlineSnapshot(`"/node-mock/pod/test-pod-uid"`); }); - it('getInfraKubernetesHref does not specify a base path when none is available', () => { - expect(getInfraKubernetesHref(summary, '')).toMatchInlineSnapshot( - `"/app/metrics/link-to/pod-detail/test-pod-uid"` - ); + it('getInfraKubernetesHref return undefined when no locator is available', () => { + expect(getInfraKubernetesHref(summary, undefined)).toBeUndefined(); }); it('getInfraKubernetesHref returns undefined when no pod data is present', () => { summary.state.summaryPings = []; - expect(getInfraKubernetesHref(summary, 'foo')).toBeUndefined(); + expect(getInfraKubernetesHref(summary, mockAssetDetailsLocator)).toBeUndefined(); }); it('getInfraKubernetesHref selects the first pod uid when there are multiple', () => { @@ -121,20 +127,20 @@ describe('getInfraHref', () => { kubernetes: { pod: { uid: 'test-pod-uid-bar' } }, }; summary.state.summaryPings = [pingTestPodId, pingTestBarPodId]; - expect(getInfraKubernetesHref(summary, '')).toMatchInlineSnapshot( - `"/app/metrics/link-to/pod-detail/test-pod-uid"` + expect(getInfraKubernetesHref(summary, mockAssetDetailsLocator)).toMatchInlineSnapshot( + `"/node-mock/pod/test-pod-uid"` ); }); it('getInfraKubernetesHref returns undefined when summaryPings are undefined', () => { // @ts-expect-error delete summary.state.summaryPings; - expect(getInfraKubernetesHref(summary, '')).toBeUndefined(); + expect(getInfraKubernetesHref(summary, mockAssetDetailsLocator)).toBeUndefined(); }); it('getInfraKubernetesHref returns undefined when summaryPings are null', () => { delete summary.state.summaryPings![0]!.kubernetes!.pod!.uid; - expect(getInfraKubernetesHref(summary, '')).toBeUndefined(); + expect(getInfraKubernetesHref(summary, mockAssetDetailsLocator)).toBeUndefined(); }); it('getInfraIpHref creates a link for valid parameters', () => { diff --git a/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.ts b/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.ts index e1cf922983d215..91869dbe7465be 100644 --- a/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.ts +++ b/x-pack/plugins/observability_solution/uptime/public/legacy_uptime/lib/helper/observability_integration/get_infra_href.ts @@ -5,44 +5,51 @@ * 2.0. */ +import { AssetDetailsLocator } from '@kbn/observability-shared-plugin/common'; import { MonitorSummary, Ping } from '../../../../../common/runtime_types'; import { addBasePath } from './add_base_path'; import { buildHref } from './build_href'; export const getInfraContainerHref = ( - summary: MonitorSummary, - basePath: string + { state }: MonitorSummary, + locator?: AssetDetailsLocator ): string | undefined => { - const getHref = (value: string | string[] | undefined) => { - if (!value) { - return undefined; - } - const ret = !Array.isArray(value) ? value : value[0]; - return addBasePath( - basePath, - `/app/metrics/link-to/container-detail/${encodeURIComponent(ret)}` - ); - }; - return buildHref(summary.state.summaryPings || [], (ping: Ping) => ping?.container?.id, getHref); + if (!locator) { + return undefined; + } + + const pings = Array.isArray(state.summaryPings) ? state.summaryPings : [state.summaryPings]; + + // Pick the first container id if one is available + const containerId = pings[0]?.container?.id; + + return containerId + ? locator.getRedirectUrl({ + assetType: 'container', + assetId: containerId, + }) + : undefined; }; export const getInfraKubernetesHref = ( - summary: MonitorSummary, - basePath: string + { state }: MonitorSummary, + locator?: AssetDetailsLocator ): string | undefined => { - const getHref = (value: string | string[] | undefined) => { - if (!value) { - return undefined; - } - const ret = !Array.isArray(value) ? value : value[0]; - return addBasePath(basePath, `/app/metrics/link-to/pod-detail/${encodeURIComponent(ret)}`); - }; + if (!locator) { + return undefined; + } + + const pings = Array.isArray(state.summaryPings) ? state.summaryPings : [state.summaryPings]; + + // Pick the first pod id if one is available + const podId = pings[0]?.kubernetes?.pod?.uid; - return buildHref( - summary.state.summaryPings || [], - (ping: Ping) => ping?.kubernetes?.pod?.uid, - getHref - ); + return podId + ? locator.getRedirectUrl({ + assetType: 'pod', + assetId: podId, + }) + : undefined; }; export const getInfraIpHref = (summary: MonitorSummary, basePath: string) => {