diff --git a/packages/deeplinks/observability/locators/index.ts b/packages/deeplinks/observability/locators/index.ts index 73fe4b64bce9f..67e79ecb577ea 100644 --- a/packages/deeplinks/observability/locators/index.ts +++ b/packages/deeplinks/observability/locators/index.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ +export * from './dataset_quality'; export * from './logs_explorer'; export * from './observability_logs_explorer'; export * from './observability_onboarding'; -export * from './dataset_quality'; +export * from './uptime'; diff --git a/packages/deeplinks/observability/locators/uptime.ts b/packages/deeplinks/observability/locators/uptime.ts new file mode 100644 index 0000000000000..a0f42801b6610 --- /dev/null +++ b/packages/deeplinks/observability/locators/uptime.ts @@ -0,0 +1,23 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { SerializableRecord } from '@kbn/utility-types'; + +export const uptimeOverviewLocatorID = 'UPTIME_OVERVIEW_LOCATOR'; + +export interface UptimeOverviewLocatorInfraParams extends SerializableRecord { + ip?: string; + host?: string; + container?: string; + pod?: string; +} + +export interface UptimeOverviewLocatorParams extends SerializableRecord { + dateRangeStart?: string; + dateRangeEnd?: string; + search?: string; +} diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx index 94b46ffbaf49c..a38626fd10eae 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx @@ -5,29 +5,55 @@ * 2.0. */ +import { coreMock } from '@kbn/core/public/mocks'; +import { + uptimeOverviewLocatorID, + UptimeOverviewLocatorInfraParams, + UptimeOverviewLocatorParams, +} from '@kbn/deeplinks-observability'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { MockUrlService } from '@kbn/share-plugin/common/mocks'; +import { type UrlService } from '@kbn/share-plugin/common/url_service'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; import { subj as testSubject } from '@kbn/test-subj-selector'; -import React, { FC, PropsWithChildren } from 'react'; +import React, { FC } from 'react'; import { act } from 'react-dom/test-utils'; - -// import { mount } from 'enzyme'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { coreMock } from '@kbn/core/public/mocks'; -import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; const coreStartMock = coreMock.createStart(); coreStartMock.application.getUrlForApp.mockImplementation((app, options) => { return `/test-basepath/s/test-space/app/${app}${options?.path}`; }); -const ProviderWrapper: FC> = ({ children }) => { - return {children}; +const emptyUrlService = new MockUrlService(); +const urlServiceWithUptimeLocator = new MockUrlService(); +// we can't use the actual locator here because its import would create a +// forbidden ts project reference cycle +urlServiceWithUptimeLocator.locators.create< + UptimeOverviewLocatorInfraParams | UptimeOverviewLocatorParams +>({ + id: uptimeOverviewLocatorID, + getLocation: async (params) => { + return { app: 'uptime', path: '/overview', state: {} }; + }, +}); + +const ProviderWrapper: FC<{ urlService?: UrlService }> = ({ + children, + urlService = emptyUrlService, +}) => { + return ( + + {children} + + ); }; describe('LogEntryActionsMenu component', () => { const time = new Date().toISOString(); - describe('uptime link', () => { - it('renders with a host ip filter when present in log entry', () => { + + describe('uptime link with legacy uptime disabled', () => { + it('renders as disabled even when a supported field is present', () => { const elementWrapper = mount( { elementWrapper.update(); expect( - elementWrapper.find(`a${testSubject('~uptimeLogEntryActionsMenuItem')}`).prop('href') - ).toBe('/test-basepath/s/test-space/app/uptime#/?search=host.ip:HOST_IP'); + elementWrapper + .find(`${testSubject('~uptimeLogEntryActionsMenuItem')}`) + .first() + .prop('disabled') + ).toEqual(true); }); + }); - it('renders with a container id filter when present in log entry', () => { + describe('uptime link with legacy uptime enabled', () => { + it('renders as enabled when a host ip is present in the log entry', () => { const elementWrapper = mount( - + { expect( elementWrapper.find(`a${testSubject('~uptimeLogEntryActionsMenuItem')}`).prop('href') - ).toBe('/test-basepath/s/test-space/app/uptime#/?search=container.id:CONTAINER_ID'); + ).toEqual(expect.any(String)); }); - it('renders with a pod uid filter when present in log entry', () => { + it('renders as enabled when a container id is present in the log entry', () => { const elementWrapper = mount( - + { expect( elementWrapper.find(`a${testSubject('~uptimeLogEntryActionsMenuItem')}`).prop('href') - ).toBe('/test-basepath/s/test-space/app/uptime#/?search=kubernetes.pod.uid:POD_UID'); + ).toEqual(expect.any(String)); }); - it('renders with a disjunction of filters when multiple present in log entry', () => { + it('renders as enabled when a pod uid is present in the log entry', () => { const elementWrapper = mount( - + { expect( elementWrapper.find(`a${testSubject('~uptimeLogEntryActionsMenuItem')}`).prop('href') - ).toBe( - '/test-basepath/s/test-space/app/uptime#/?search=container.id:CONTAINER_ID%20or%20host.ip:HOST_IP%20or%20kubernetes.pod.uid:POD_UID' - ); + ).toEqual(expect.any(String)); }); - it('renders as disabled when no supported field is present in log entry', () => { + it('renders as disabled when no supported field is present in the log entry', () => { const elementWrapper = mount( - + { expect( elementWrapper - .find(`button${testSubject('~uptimeLogEntryActionsMenuItem')}`) + .find(`${testSubject('~uptimeLogEntryActionsMenuItem')}`) + .first() .prop('disabled') ).toEqual(true); }); diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index e99304a61b453..4f16d34a489ac 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -6,28 +6,36 @@ */ import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { + uptimeOverviewLocatorID, + type UptimeOverviewLocatorInfraParams, +} from '@kbn/deeplinks-observability'; import { FormattedMessage } from '@kbn/i18n-react'; +import { LinkDescriptor, useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { getRouterLinkProps } from '@kbn/router-utils'; +import { ILocatorClient } from '@kbn/share-plugin/common/url_service'; import React, { useMemo } from 'react'; -import { useLinkProps, LinkDescriptor } from '@kbn/observability-shared-plugin/public'; -import { useVisibilityState } from '../../../utils/use_visibility_state'; import { LogEntry } from '../../../../common/search_strategies/log_entries/log_entry'; - -const UPTIME_FIELDS = ['container.id', 'host.ip', 'kubernetes.pod.uid']; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { useVisibilityState } from '../../../utils/use_visibility_state'; export interface LogEntryActionsMenuProps { logEntry: LogEntry; } export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => { + const { + services: { + share: { + url: { locators }, + }, + }, + } = useKibanaContextForPlugin(); const { hide, isVisible, toggle } = useVisibilityState(false); const apmLinkDescriptor = useMemo(() => getAPMLink(logEntry), [logEntry]); - const uptimeLinkDescriptor = useMemo(() => getUptimeLink(logEntry), [logEntry]); - const uptimeLinkProps = useLinkProps({ - app: 'uptime', - ...(uptimeLinkDescriptor ? uptimeLinkDescriptor : {}), - }); + const uptimeLinkProps = getUptimeLink({ locators })(logEntry); const apmLinkProps = useLinkProps({ app: 'apm', @@ -38,7 +46,7 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => { () => [ { /> , ], - [uptimeLinkDescriptor, apmLinkDescriptor, apmLinkProps, uptimeLinkProps] + [apmLinkDescriptor, apmLinkProps, uptimeLinkProps] ); const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]); @@ -92,25 +100,40 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => { ); }; -const getUptimeLink = (logEntry: LogEntry): LinkDescriptor | undefined => { - const searchExpressions = logEntry.fields - .filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field)) - .reduce((acc, fieldItem) => { - const { field, value } = fieldItem; - return acc.concat(value.map((val) => `${field}:${val}`)); - }, []); - - if (searchExpressions.length === 0) { - return undefined; - } - return { - app: 'uptime', - hash: '/', - search: { - search: `${searchExpressions.join(' or ')}`, - }, +const getUptimeLink = + ({ locators }: { locators: ILocatorClient }) => + (logEntry: LogEntry): ContextRouterLinkProps | undefined => { + const uptimeLocator = locators.get(uptimeOverviewLocatorID); + + if (!uptimeLocator) { + return undefined; + } + + const ipValue = logEntry.fields.find(({ field }) => field === 'host.ip')?.value?.[0]; + const containerValue = logEntry.fields.find(({ field }) => field === 'container.id') + ?.value?.[0]; + const podValue = logEntry.fields.find(({ field }) => field === 'kubernetes.pod.uid') + ?.value?.[0]; + const hostValue = logEntry.fields.find(({ field }) => field === 'host.name')?.value?.[0]; + + const uptimeLocatorParams: UptimeOverviewLocatorInfraParams = { + ...(typeof ipValue === 'string' && { ip: ipValue }), + ...(typeof containerValue === 'string' && { container: containerValue }), + ...(typeof podValue === 'string' && { pod: podValue }), + ...(typeof hostValue === 'string' && { host: hostValue }), + }; + + if (Object.keys(uptimeLocatorParams).length === 0) { + return undefined; + } + + // Coercing the return value to ContextRouterLinkProps because + // EuiContextMenuItem defines a too broad type for onClick + return getRouterLinkProps({ + href: uptimeLocator.getRedirectUrl(uptimeLocatorParams), + onClick: () => uptimeLocator.navigate(uptimeLocatorParams), + }) as ContextRouterLinkProps; }; -}; const getAPMLink = (logEntry: LogEntry): LinkDescriptor | undefined => { const traceId = logEntry.fields.find( @@ -153,3 +176,8 @@ function getApmTraceUrl({ }) { return `/link-to/trace/${traceId}?` + new URLSearchParams({ rangeFrom, rangeTo }).toString(); } + +export interface ContextRouterLinkProps { + href: string | undefined; + onClick: (event: React.MouseEvent) => void; +} diff --git a/x-pack/plugins/observability_solution/logs_shared/public/types.ts b/x-pack/plugins/observability_solution/logs_shared/public/types.ts index 0e89358d2e0fa..9f0d344294880 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/types.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/types.ts @@ -10,8 +10,8 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; -import { SharePluginSetup } from '@kbn/share-plugin/public'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { LogsSharedLocators } from '../common/locators'; import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant'; @@ -38,6 +38,7 @@ export interface LogsSharedClientStartDeps { dataViews: DataViewsPublicPluginStart; discoverShared: DiscoverSharedPublicStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; + share: SharePluginStart; uiActions: UiActionsStart; } diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json index df4b5e8bcc6ff..d826cd78618c5 100644 --- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json +++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json @@ -39,6 +39,7 @@ "@kbn/search-types", "@kbn/discover-shared-plugin", "@kbn/react-kibana-context-theme", - "@kbn/test-jest-helpers" + "@kbn/test-jest-helpers", + "@kbn/router-utils", ] } diff --git a/x-pack/plugins/observability_solution/observability/common/index.ts b/x-pack/plugins/observability_solution/observability/common/index.ts index c8247e0d55dc0..30765fed43e3d 100644 --- a/x-pack/plugins/observability_solution/observability/common/index.ts +++ b/x-pack/plugins/observability_solution/observability/common/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export type { AsDuration, AsPercent, TimeUnitChar, TimeFormatter } from './utils/formatters'; export { @@ -76,7 +75,7 @@ export const observabilityFeatureId = 'observability'; // Name of a locator created by the uptime plugin. Intended for use // by other plugins as well, so defined here to prevent cross-references. -export const uptimeOverviewLocatorID = 'UPTIME_OVERVIEW_LOCATOR'; +export { uptimeOverviewLocatorID } from '@kbn/deeplinks-observability'; export const syntheticsMonitorDetailLocatorID = 'SYNTHETICS_MONITOR_DETAIL_LOCATOR'; export const syntheticsEditMonitorLocatorID = 'SYNTHETICS_EDIT_MONITOR_LOCATOR'; export const syntheticsSettingsLocatorID = 'SYNTHETICS_SETTINGS'; diff --git a/x-pack/plugins/observability_solution/uptime/public/locators/overview.ts b/x-pack/plugins/observability_solution/uptime/public/locators/overview.ts index 15a1c52406eb1..5e276cbc55d73 100644 --- a/x-pack/plugins/observability_solution/uptime/public/locators/overview.ts +++ b/x-pack/plugins/observability_solution/uptime/public/locators/overview.ts @@ -5,25 +5,17 @@ * 2.0. */ -import { uptimeOverviewLocatorID } from '@kbn/observability-plugin/public'; +import { + uptimeOverviewLocatorID, + type UptimeOverviewLocatorInfraParams, + type UptimeOverviewLocatorParams, +} from '@kbn/deeplinks-observability'; import type { LocatorDefinition } from '@kbn/share-plugin/common'; -import type { SerializableRecord } from '@kbn/utility-types'; import { OVERVIEW_ROUTE } from '../../common/constants'; -const formatSearchKey = (key: string, value: string) => `${key}: "${value}"`; - -export interface UptimeOverviewLocatorInfraParams extends SerializableRecord { - ip?: string; - host?: string; - container?: string; - pod?: string; -} +export type { UptimeOverviewLocatorInfraParams, UptimeOverviewLocatorParams }; -export interface UptimeOverviewLocatorParams extends SerializableRecord { - dateRangeStart?: string; - dateRangeEnd?: string; - search?: string; -} +const formatSearchKey = (key: string, value: string) => `${key}: "${value}"`; function isUptimeOverviewLocatorParams( args: UptimeOverviewLocatorInfraParams | UptimeOverviewLocatorParams diff --git a/x-pack/plugins/observability_solution/uptime/tsconfig.json b/x-pack/plugins/observability_solution/uptime/tsconfig.json index 8fc38a304c26b..041ff7a84a4fb 100644 --- a/x-pack/plugins/observability_solution/uptime/tsconfig.json +++ b/x-pack/plugins/observability_solution/uptime/tsconfig.json @@ -76,7 +76,8 @@ "@kbn/repo-info", "@kbn/react-kibana-context-render", "@kbn/react-kibana-context-theme", - "@kbn/react-kibana-mount" + "@kbn/react-kibana-mount", + "@kbn/deeplinks-observability" ], "exclude": ["target/**/*"] }