diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 51d8b43dac0ea..b58a450d26644 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -5,6 +5,7 @@ */ import { EuiLink } from '@elastic/eui'; +import { Location } from 'history'; import React from 'react'; import url from 'url'; import rison, { RisonValue } from 'rison-node'; @@ -12,6 +13,7 @@ import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; interface Props { query: { @@ -30,10 +32,15 @@ interface Props { children: React.ReactNode; } -export function DiscoverLink({ query = {}, ...rest }: Props) { - const { core } = useApmPluginContext(); - const location = useLocation(); - +export const getDiscoverHref = ({ + basePath, + location, + query +}: { + basePath: AppMountContextBasePath; + location: Location; + query: Props['query']; +}) => { const risonQuery = { _g: getTimepickerRisonData(location.search), _a: { @@ -43,11 +50,23 @@ export function DiscoverLink({ query = {}, ...rest }: Props) { }; const href = url.format({ - pathname: core.http.basePath.prepend('/app/kibana'), + pathname: basePath.prepend('/app/kibana'), hash: `/discover?_g=${rison.encode(risonQuery._g)}&_a=${rison.encode( risonQuery._a as RisonValue )}` }); + return href; +}; + +export function DiscoverLink({ query = {}, ...rest }: Props) { + const { core } = useApmPluginContext(); + const location = useLocation(); + + const href = getDiscoverHref({ + basePath: core.http.basePath, + query, + location + }); return ; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx index 8ff5e3010d6cc..e4f3557a2ce51 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -10,11 +10,13 @@ import React from 'react'; import url from 'url'; import { fromQuery } from './url_helpers'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { AppMountContextBasePath } from '../../../context/ApmPluginContext'; interface InfraQueryParams { time?: number; from?: number; to?: number; + filter?: string; } interface Props extends EuiLinkAnchorProps { @@ -23,12 +25,24 @@ interface Props extends EuiLinkAnchorProps { children?: React.ReactNode; } -export function InfraLink({ path, query = {}, ...rest }: Props) { - const { core } = useApmPluginContext(); +export const getInfraHref = ({ + basePath, + query, + path +}: { + basePath: AppMountContextBasePath; + query: InfraQueryParams; + path?: string; +}) => { const nextSearch = fromQuery(query); - const href = url.format({ - pathname: core.http.basePath.prepend('/app/infra'), + return url.format({ + pathname: basePath.prepend('/app/infra'), hash: compact([path, nextSearch]).join('?') }); +}; + +export function InfraLink({ path, query = {}, ...rest }: Props) { + const { core } = useApmPluginContext(); + const href = getInfraHref({ basePath: core.http.basePath, query, path }); return ; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 040d29aaa56dd..99f0b0d4fc223 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -4,240 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPopover, - EuiLink -} from '@elastic/eui'; -import url from 'url'; +import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useState, FunctionComponent } from 'react'; -import { pick } from 'lodash'; +import React, { FunctionComponent, useState } from 'react'; +import { + ActionMenu, + ActionMenuDivider, + Section, + SectionLink, + SectionLinks, + SectionSubtitle, + SectionTitle +} from '../../../../../../../plugins/observability/public'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; -import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransactionLink'; -import { InfraLink } from '../Links/InfraLink'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { fromQuery } from '../Links/url_helpers'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; - -function getInfraMetricsQuery(transaction: Transaction) { - const plus5 = new Date(transaction['@timestamp']); - const minus5 = new Date(plus5.getTime()); - - plus5.setMinutes(plus5.getMinutes() + 5); - minus5.setMinutes(minus5.getMinutes() - 5); - - return { - from: minus5.getTime(), - to: plus5.getTime() - }; -} - -function ActionMenuButton({ onClick }: { onClick: () => void }) { - return ( - - {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { - defaultMessage: 'Actions' - })} - - ); -} +import { useLocation } from '../../../hooks/useLocation'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { getSections } from './sections'; interface Props { readonly transaction: Transaction; } -interface InfraConfigItem { - icon: string; - label: string; - condition?: boolean; - path: string; - query: Record; -} - -export const TransactionActionMenu: FunctionComponent = ( - props: Props -) => { - const { transaction } = props; - +const ActionMenuButton = ({ onClick }: { onClick: () => void }) => ( + + {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { + defaultMessage: 'Actions' + })} + +); + +export const TransactionActionMenu: FunctionComponent = ({ + transaction +}: Props) => { const { core } = useApmPluginContext(); - - const [isOpen, setIsOpen] = useState(false); - + const location = useLocation(); const { urlParams } = useUrlParams(); - const hostName = transaction.host?.hostname; - const podId = transaction.kubernetes?.pod.uid; - const containerId = transaction.container?.id; - - const time = Math.round(transaction.timestamp.us / 1000); - const infraMetricsQuery = getInfraMetricsQuery(transaction); - - const infraConfigItems: InfraConfigItem[] = [ - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showPodLogsLinkLabel', - { defaultMessage: 'Show pod logs' } - ), - condition: !!podId, - path: `/link-to/pod-logs/${podId}`, - query: { time } - }, - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel', - { defaultMessage: 'Show container logs' } - ), - condition: !!containerId, - path: `/link-to/container-logs/${containerId}`, - query: { time } - }, - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showHostLogsLinkLabel', - { defaultMessage: 'Show host logs' } - ), - condition: !!hostName, - path: `/link-to/host-logs/${hostName}`, - query: { time } - }, - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showTraceLogsLinkLabel', - { defaultMessage: 'Show trace logs' } - ), - condition: true, - path: `/link-to/logs`, - query: { - time, - filter: `trace.id:"${transaction.trace.id}" OR ${transaction.trace.id}` - } - }, - { - icon: 'metricsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel', - { defaultMessage: 'Show pod metrics' } - ), - condition: !!podId, - path: `/link-to/pod-detail/${podId}`, - query: infraMetricsQuery - }, - { - icon: 'metricsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel', - { defaultMessage: 'Show container metrics' } - ), - condition: !!containerId, - path: `/link-to/container-detail/${containerId}`, - query: infraMetricsQuery - }, - { - icon: 'metricsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel', - { defaultMessage: 'Show host metrics' } - ), - condition: !!hostName, - path: `/link-to/host-detail/${hostName}`, - query: infraMetricsQuery - } - ]; - - const infraItems = infraConfigItems.map( - ({ icon, label, condition, path, query }, index) => ({ - icon, - key: `infra-link-${index}`, - child: ( - - {label} - - ), - condition - }) - ); + const [isOpen, setIsOpen] = useState(false); - const uptimeLink = url.format({ - pathname: core.http.basePath.prepend('/app/uptime'), - hash: `/?${fromQuery( - pick( - { - dateRangeStart: urlParams.rangeFrom, - dateRangeEnd: urlParams.rangeTo, - search: `url.domain:"${transaction.url?.domain}"` - }, - (val: string) => !!val - ) - )}` + const sections = getSections({ + transaction, + basePath: core.http.basePath, + location, + urlParams }); - const menuItems = [ - ...infraItems, - { - icon: 'discoverApp', - key: 'discover-transaction', - condition: true, - child: ( - - {i18n.translate( - 'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel', - { - defaultMessage: 'View sample document' - } - )} - - ) - }, - { - icon: 'uptimeApp', - key: 'uptime', - child: ( - - {i18n.translate('xpack.apm.transactionActionMenu.viewInUptime', { - defaultMessage: 'View monitor status' - })} - - ), - condition: transaction.url?.domain - } - ] - .filter(({ condition }) => condition) - .map(({ icon, key, child }) => ( - - - {child} - - - - - - )); - return ( - setIsOpen(!isOpen)} />} - isOpen={isOpen} closePopover={() => setIsOpen(false)} + isOpen={isOpen} anchorPosition="downRight" - panelPaddingSize="none" + button={ setIsOpen(!isOpen)} />} > - - + {sections.map((section, idx) => { + const isLastSection = idx !== sections.length - 1; + return ( +
+ {section.map(item => ( +
+ {item.title && {item.title}} + {item.subtitle && ( + {item.subtitle} + )} + + {item.actions.map(action => ( + + ))} + +
+ ))} + {isLastSection && } +
+ ); + })} + ); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 2bfa5cf1274fa..e9f89034f58ee 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -36,7 +36,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show trace logs')).not.toBeNull(); + expect(queryByText('Trace logs')).not.toBeNull(); }); it('should not render the pod links when there is no pod id', async () => { @@ -44,8 +44,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show pod logs')).toBeNull(); - expect(queryByText('Show pod metrics')).toBeNull(); + expect(queryByText('Pod logs')).toBeNull(); + expect(queryByText('Pod metrics')).toBeNull(); }); it('should render the pod links when there is a pod id', async () => { @@ -53,8 +53,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithKubernetesData ); - expect(queryByText('Show pod logs')).not.toBeNull(); - expect(queryByText('Show pod metrics')).not.toBeNull(); + expect(queryByText('Pod logs')).not.toBeNull(); + expect(queryByText('Pod metrics')).not.toBeNull(); }); it('should not render the container links when there is no container id', async () => { @@ -62,8 +62,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show container logs')).toBeNull(); - expect(queryByText('Show container metrics')).toBeNull(); + expect(queryByText('Container logs')).toBeNull(); + expect(queryByText('Container metrics')).toBeNull(); }); it('should render the container links when there is a container id', async () => { @@ -71,8 +71,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithContainerData ); - expect(queryByText('Show container logs')).not.toBeNull(); - expect(queryByText('Show container metrics')).not.toBeNull(); + expect(queryByText('Container logs')).not.toBeNull(); + expect(queryByText('Container metrics')).not.toBeNull(); }); it('should not render the host links when there is no hostname', async () => { @@ -80,8 +80,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show host logs')).toBeNull(); - expect(queryByText('Show host metrics')).toBeNull(); + expect(queryByText('Host logs')).toBeNull(); + expect(queryByText('Host metrics')).toBeNull(); }); it('should render the host links when there is a hostname', async () => { @@ -89,8 +89,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithHostData ); - expect(queryByText('Show host logs')).not.toBeNull(); - expect(queryByText('Show host metrics')).not.toBeNull(); + expect(queryByText('Host logs')).not.toBeNull(); + expect(queryByText('Host metrics')).not.toBeNull(); }); it('should not render the uptime link if there is no url available', async () => { @@ -98,7 +98,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('View monitor status')).toBeNull(); + expect(queryByText('Status')).toBeNull(); }); it('should not render the uptime link if there is no domain available', async () => { @@ -106,7 +106,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithUrlWithoutDomain ); - expect(queryByText('View monitor status')).toBeNull(); + expect(queryByText('Status')).toBeNull(); }); it('should render the uptime link if there is a url with a domain', async () => { @@ -114,7 +114,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithUrlAndDomain ); - expect(queryByText('View monitor status')).not.toBeNull(); + expect(queryByText('Status')).not.toBeNull(); }); it('should match the snapshot', async () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts new file mode 100644 index 0000000000000..52c2d27eabb82 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Location } from 'history'; +import { getSections } from '../sections'; +import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; +import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; + +describe('Transaction action menu', () => { + const basePath = ({ + prepend: jest.fn() + } as unknown) as AppMountContextBasePath; + const date = '2020-02-06T11:00:00.000Z'; + const timestamp = { us: new Date(date).getTime() }; + + it('shows required sections only', () => { + const transaction = ({ + timestamp, + trace: { id: '123' }, + transaction: { id: '123' }, + '@timestamp': date + } as unknown) as Transaction; + expect( + getSections({ + transaction, + basePath, + location: ({} as unknown) as Location, + urlParams: {} + }) + ).toEqual([ + [ + { + key: 'traceDetails', + title: 'Trace details', + subtitle: 'View trace logs to get further details.', + actions: [ + { + key: 'traceLogs', + label: 'Trace logs', + href: + '#/link-to/logs?time=1580986800&filter=trace.id:%22123%22%20OR%20123', + condition: true + } + ] + } + ], + [ + { + key: 'kibana', + actions: [ + { + key: 'sampleDocument', + label: 'View sample document', + href: + '#/discover?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + condition: true + } + ] + } + ] + ]); + }); + + it('shows pod and required sections only', () => { + const transaction = ({ + kubernetes: { pod: { uid: '123' } }, + timestamp, + trace: { id: '123' }, + transaction: { id: '123' }, + '@timestamp': date + } as unknown) as Transaction; + expect( + getSections({ + transaction, + basePath, + location: ({} as unknown) as Location, + urlParams: {} + }) + ).toEqual([ + [ + { + key: 'podDetails', + title: 'Pod details', + subtitle: + 'View logs and metrics for this pod to get further details.', + actions: [ + { + key: 'podLogs', + label: 'Pod logs', + href: '#/link-to/pod-logs/123?time=1580986800', + condition: true + }, + { + key: 'podMetrics', + label: 'Pod metrics', + href: + '#/link-to/pod-detail/123?from=1580986500000&to=1580987100000', + condition: true + } + ] + }, + { + key: 'traceDetails', + title: 'Trace details', + subtitle: 'View trace logs to get further details.', + actions: [ + { + key: 'traceLogs', + label: 'Trace logs', + href: + '#/link-to/logs?time=1580986800&filter=trace.id:%22123%22%20OR%20123', + condition: true + } + ] + } + ], + [ + { + key: 'kibana', + actions: [ + { + key: 'sampleDocument', + label: 'View sample document', + href: + '#/discover?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + condition: true + } + ] + } + ] + ]); + }); + + it('shows host and required sections only', () => { + const transaction = ({ + host: { hostname: 'foo' }, + timestamp, + trace: { id: '123' }, + transaction: { id: '123' }, + '@timestamp': date + } as unknown) as Transaction; + expect( + getSections({ + transaction, + basePath, + location: ({} as unknown) as Location, + urlParams: {} + }) + ).toEqual([ + [ + { + key: 'hostDetails', + title: 'Host details', + subtitle: 'View host logs and metrics to get further details.', + actions: [ + { + key: 'hostLogs', + label: 'Host logs', + href: '#/link-to/host-logs/foo?time=1580986800', + condition: true + }, + { + key: 'hostMetrics', + label: 'Host metrics', + href: + '#/link-to/host-detail/foo?from=1580986500000&to=1580987100000', + condition: true + } + ] + }, + { + key: 'traceDetails', + title: 'Trace details', + subtitle: 'View trace logs to get further details.', + actions: [ + { + key: 'traceLogs', + label: 'Trace logs', + href: + '#/link-to/logs?time=1580986800&filter=trace.id:%22123%22%20OR%20123', + condition: true + } + ] + } + ], + [ + { + key: 'kibana', + actions: [ + { + key: 'sampleDocument', + label: 'View sample document', + href: + '#/discover?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + condition: true + } + ] + } + ] + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts new file mode 100644 index 0000000000000..77445a2600960 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -0,0 +1,299 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { Location } from 'history'; +import { pick, isEmpty } from 'lodash'; +import moment from 'moment'; +import url from 'url'; +import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; +import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; +import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; +import { getInfraHref } from '../Links/InfraLink'; +import { fromQuery } from '../Links/url_helpers'; +import { AppMountContextBasePath } from '../../../context/ApmPluginContext'; + +function getInfraMetricsQuery(transaction: Transaction) { + const timestamp = new Date(transaction['@timestamp']).getTime(); + const fiveMinutes = moment.duration(5, 'minutes').asMilliseconds(); + + return { + from: timestamp - fiveMinutes, + to: timestamp + fiveMinutes + }; +} + +interface Action { + key: string; + label: string; + href: string; + condition: boolean; +} + +interface Section { + key: string; + title?: string; + subtitle?: string; + actions: Action[]; +} + +type SectionRecord = Record; + +export const getSections = ({ + transaction, + basePath, + location, + urlParams +}: { + transaction: Transaction; + basePath: AppMountContextBasePath; + location: Location; + urlParams: IUrlParams; +}) => { + const hostName = transaction.host?.hostname; + const podId = transaction.kubernetes?.pod.uid; + const containerId = transaction.container?.id; + + const time = Math.round(transaction.timestamp.us / 1000); + const infraMetricsQuery = getInfraMetricsQuery(transaction); + + const uptimeLink = url.format({ + pathname: basePath.prepend('/app/uptime'), + hash: `/?${fromQuery( + pick( + { + dateRangeStart: urlParams.rangeFrom, + dateRangeEnd: urlParams.rangeTo, + search: `url.domain:"${transaction.url?.domain}"` + }, + (val: string) => !isEmpty(val) + ) + )}` + }); + + const podActions: Action[] = [ + { + key: 'podLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showPodLogsLinkLabel', + { defaultMessage: 'Pod logs' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/pod-logs/${podId}`, + query: { time } + }), + condition: !!podId + }, + { + key: 'podMetrics', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel', + { defaultMessage: 'Pod metrics' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/pod-detail/${podId}`, + query: infraMetricsQuery + }), + condition: !!podId + } + ]; + + const containerActions: Action[] = [ + { + key: 'containerLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel', + { defaultMessage: 'Container logs' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/container-logs/${containerId}`, + query: { time } + }), + condition: !!containerId + }, + { + key: 'containerMetrics', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel', + { defaultMessage: 'Container metrics' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/container-detail/${containerId}`, + query: infraMetricsQuery + }), + condition: !!containerId + } + ]; + + const hostActions: Action[] = [ + { + key: 'hostLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showHostLogsLinkLabel', + { defaultMessage: 'Host logs' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/host-logs/${hostName}`, + query: { time } + }), + condition: !!hostName + }, + { + key: 'hostMetrics', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel', + { defaultMessage: 'Host metrics' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/host-detail/${hostName}`, + query: infraMetricsQuery + }), + condition: !!hostName + } + ]; + + const logActions: Action[] = [ + { + key: 'traceLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showTraceLogsLinkLabel', + { defaultMessage: 'Trace logs' } + ), + href: getInfraHref({ + basePath, + path: `/link-to/logs`, + query: { + time, + filter: `trace.id:"${transaction.trace.id}" OR ${transaction.trace.id}` + } + }), + condition: true + } + ]; + + const uptimeActions: Action[] = [ + { + key: 'monitorStatus', + label: i18n.translate('xpack.apm.transactionActionMenu.viewInUptime', { + defaultMessage: 'Status' + }), + href: uptimeLink, + condition: !!transaction.url?.domain + } + ]; + + const kibanaActions: Action[] = [ + { + key: 'sampleDocument', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel', + { + defaultMessage: 'View sample document' + } + ), + href: getDiscoverHref({ + basePath, + query: getDiscoverQuery(transaction), + location + }), + condition: true + } + ]; + + const sectionRecord: SectionRecord = { + observability: [ + { + key: 'podDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.pod.title', { + defaultMessage: 'Pod details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.pod.subtitle', + { + defaultMessage: + 'View logs and metrics for this pod to get further details.' + } + ), + actions: podActions + }, + { + key: 'containerDetails', + title: i18n.translate( + 'xpack.apm.transactionActionMenu.container.title', + { + defaultMessage: 'Container details' + } + ), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.container.subtitle', + { + defaultMessage: + 'View logs and metrics for this container to get further details.' + } + ), + actions: containerActions + }, + { + key: 'hostDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.host.title', { + defaultMessage: 'Host details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.host.subtitle', + { + defaultMessage: 'View host logs and metrics to get further details.' + } + ), + actions: hostActions + }, + { + key: 'traceDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.trace.title', { + defaultMessage: 'Trace details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.trace.subtitle', + { + defaultMessage: 'View trace logs to get further details.' + } + ), + actions: logActions + }, + { + key: 'statusDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.status.title', { + defaultMessage: 'Status details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.status.subtitle', + { + defaultMessage: 'View status to get further details.' + } + ), + actions: uptimeActions + } + ], + kibana: [{ key: 'kibana', actions: kibanaActions }] + }; + + // Filter out actions that shouldnt be shown and sections without any actions. + return Object.values(sectionRecord) + .map(sections => + sections + .map(section => ({ + ...section, + actions: section.actions.filter(action => action.condition) + })) + .filter(section => !isEmpty(section.actions)) + ) + .filter(sections => !isEmpty(sections)); +}; diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx index 86efd9b31974e..7a9aaa6dfb920 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx +++ b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx @@ -8,6 +8,8 @@ import { createContext } from 'react'; import { AppMountContext, PackageInfo } from 'kibana/public'; import { ApmPluginSetupDeps, ConfigSchema } from '../new-platform/plugin'; +export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; + export interface ApmPluginContextValue { config: ConfigSchema; core: AppMountContext['core']; diff --git a/x-pack/plugins/observability/public/components/action_menu.tsx b/x-pack/plugins/observability/public/components/action_menu.tsx index 6e964dde3aecf..a5f59ecd5506f 100644 --- a/x-pack/plugins/observability/public/components/action_menu.tsx +++ b/x-pack/plugins/observability/public/components/action_menu.tsx @@ -16,6 +16,7 @@ import { import React, { HTMLAttributes } from 'react'; import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; +import styled from 'styled-components'; type Props = EuiPopoverProps & HTMLAttributes; @@ -45,7 +46,12 @@ export const SectionLinks: React.FC<{}> = props => ( export const SectionSpacer: React.FC<{}> = () => ; -export const Section: React.FC<{}> = props => <>{props.children}; +export const Section = styled.div` + margin-bottom: 24px; + &:last-of-type { + margin-bottom: 0; + } +`; export type SectionLinkProps = EuiListGroupItemProps; export const SectionLink: React.FC = props => ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6b2dc984eefda..505e55f8dc30c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4613,7 +4613,6 @@ "xpack.apm.tracesTable.tracesPerMinuteColumnLabel": "1 分あたりのトレース", "xpack.apm.tracesTable.tracesPerMinuteUnitLabel": "1分あたりトランザクション数", "xpack.apm.transactionActionMenu.actionsButtonLabel": "アクション", - "xpack.apm.transactionActionMenu.actionsLabel": "アクション", "xpack.apm.transactionActionMenu.showContainerLogsLinkLabel": "コンテナーログを表示", "xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel": "コンテナーメトリックを表示", "xpack.apm.transactionActionMenu.showHostLogsLinkLabel": "ホストログを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c4d94c31ecfdc..fe733f0abd24b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4613,7 +4613,6 @@ "xpack.apm.tracesTable.tracesPerMinuteColumnLabel": "每分钟追溯次数", "xpack.apm.tracesTable.tracesPerMinuteUnitLabel": "tpm", "xpack.apm.transactionActionMenu.actionsButtonLabel": "操作", - "xpack.apm.transactionActionMenu.actionsLabel": "操作", "xpack.apm.transactionActionMenu.showContainerLogsLinkLabel": "显示容器日志", "xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel": "显示容器指标", "xpack.apm.transactionActionMenu.showHostLogsLinkLabel": "显示主机日志",