Skip to content

Commit

Permalink
[Logs UI] Fix fly-out link to the legacy Uptime app (#186328)
Browse files Browse the repository at this point in the history
(cherry picked from commit c3c4dca)
  • Loading branch information
weltenwort committed Jul 8, 2024
1 parent b10c5bc commit f59bf8a
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 84 deletions.
3 changes: 2 additions & 1 deletion packages/deeplinks/observability/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
23 changes: 23 additions & 0 deletions packages/deeplinks/observability/locators/uptime.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropsWithChildren<unknown>> = ({ children }) => {
return <KibanaContextProvider services={{ ...coreStartMock }}>{children}</KibanaContextProvider>;
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 (
<KibanaContextProvider services={{ ...coreStartMock, share: { url: urlService } }}>
{children}
</KibanaContextProvider>
);
};

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(
<ProviderWrapper>
<LogEntryActionsMenu
Expand All @@ -53,16 +79,21 @@ describe('LogEntryActionsMenu component', () => {
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(
<ProviderWrapper>
<ProviderWrapper urlService={urlServiceWithUptimeLocator}>
<LogEntryActionsMenu
logEntry={{
fields: [{ field: 'container.id', value: ['CONTAINER_ID'] }],
fields: [{ field: 'host.ip', value: ['HOST_IP'] }],
id: 'ITEM_ID',
index: 'INDEX',
cursor: {
Expand All @@ -84,15 +115,15 @@ describe('LogEntryActionsMenu component', () => {

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(
<ProviderWrapper>
<ProviderWrapper urlService={urlServiceWithUptimeLocator}>
<LogEntryActionsMenu
logEntry={{
fields: [{ field: 'kubernetes.pod.uid', value: ['POD_UID'] }],
fields: [{ field: 'container.id', value: ['CONTAINER_ID'] }],
id: 'ITEM_ID',
index: 'INDEX',
cursor: {
Expand All @@ -114,19 +145,15 @@ describe('LogEntryActionsMenu component', () => {

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(
<ProviderWrapper>
<ProviderWrapper urlService={urlServiceWithUptimeLocator}>
<LogEntryActionsMenu
logEntry={{
fields: [
{ field: 'container.id', value: ['CONTAINER_ID'] },
{ field: 'host.ip', value: ['HOST_IP'] },
{ field: 'kubernetes.pod.uid', value: ['POD_UID'] },
],
fields: [{ field: 'kubernetes.pod.uid', value: ['POD_UID'] }],
id: 'ITEM_ID',
index: 'INDEX',
cursor: {
Expand All @@ -148,14 +175,12 @@ describe('LogEntryActionsMenu component', () => {

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(
<ProviderWrapper>
<ProviderWrapper urlService={urlServiceWithUptimeLocator}>
<LogEntryActionsMenu
logEntry={{
fields: [],
Expand All @@ -180,7 +205,8 @@ describe('LogEntryActionsMenu component', () => {

expect(
elementWrapper
.find(`button${testSubject('~uptimeLogEntryActionsMenuItem')}`)
.find(`${testSubject('~uptimeLogEntryActionsMenuItem')}`)
.first()
.prop('disabled')
).toEqual(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -38,7 +46,7 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => {
() => [
<EuiContextMenuItem
data-test-subj="logEntryActionsMenuItem uptimeLogEntryActionsMenuItem"
disabled={!uptimeLinkDescriptor}
disabled={!uptimeLinkProps}
icon="uptimeApp"
key="uptimeLink"
{...uptimeLinkProps}
Expand All @@ -61,7 +69,7 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => {
/>
</EuiContextMenuItem>,
],
[uptimeLinkDescriptor, apmLinkDescriptor, apmLinkProps, uptimeLinkProps]
[apmLinkDescriptor, apmLinkProps, uptimeLinkProps]
);

const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]);
Expand Down Expand Up @@ -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<string[]>((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<UptimeOverviewLocatorInfraParams>(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(
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,6 +38,7 @@ export interface LogsSharedClientStartDeps {
dataViews: DataViewsPublicPluginStart;
discoverShared: DiscoverSharedPublicStart;
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
share: SharePluginStart;
uiActions: UiActionsStart;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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';
Expand Down
Loading

0 comments on commit f59bf8a

Please sign in to comment.