[APM] Divide "Actions menu" into sections to improve readability (#56623)

* transaction actions menu

* transaction actions menu

* fixing pr comments

* fixing pr comments

* fixing pr comments

* fixing pr comments

* fixing unit test

* fixing unit test

* using moment to calculate the timestamp

* renaming labels

* Changing section subtitle

* fixing unit tests

* replacing div for react fragment

* refactoring

* removing marginbottom property

* div is needed to remove the margin from the correct element
This commit is contained in:
Cauê Marcondes 2020-02-17 12:05:46 +00:00 committed by GitHub
parent 5c7af8656f
commit f49581ce34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 630 additions and 243 deletions

View file

@ -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 <EuiLink {...rest} href={href} />;
}

View file

@ -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 <EuiLink {...rest} href={href} />;
}

View file

@ -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 (
<EuiButtonEmpty iconType="arrowDown" iconSide="right" onClick={onClick}>
{i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', {
defaultMessage: 'Actions'
})}
</EuiButtonEmpty>
);
}
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<string, any>;
}
export const TransactionActionMenu: FunctionComponent<Props> = (
props: Props
) => {
const { transaction } = props;
const ActionMenuButton = ({ onClick }: { onClick: () => void }) => (
<EuiButtonEmpty iconType="arrowDown" iconSide="right" onClick={onClick}>
{i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', {
defaultMessage: 'Actions'
})}
</EuiButtonEmpty>
);
export const TransactionActionMenu: FunctionComponent<Props> = ({
transaction
}: Props) => {
const { core } = useApmPluginContext();
const location = useLocation();
const { urlParams } = useUrlParams();
const [isOpen, setIsOpen] = useState(false);
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: (
<InfraLink path={path} query={query}>
{label}
</InfraLink>
),
condition
})
);
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: (
<DiscoverTransactionLink transaction={transaction}>
{i18n.translate(
'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel',
{
defaultMessage: 'View sample document'
}
)}
</DiscoverTransactionLink>
)
},
{
icon: 'uptimeApp',
key: 'uptime',
child: (
<EuiLink href={uptimeLink}>
{i18n.translate('xpack.apm.transactionActionMenu.viewInUptime', {
defaultMessage: 'View monitor status'
})}
</EuiLink>
),
condition: transaction.url?.domain
}
]
.filter(({ condition }) => condition)
.map(({ icon, key, child }) => (
<EuiContextMenuItem icon={icon} key={key}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>{child}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="popout" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiContextMenuItem>
));
return (
<EuiPopover
<ActionMenu
id="transactionActionMenu"
button={<ActionMenuButton onClick={() => setIsOpen(!isOpen)} />}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
isOpen={isOpen}
anchorPosition="downRight"
panelPaddingSize="none"
button={<ActionMenuButton onClick={() => setIsOpen(!isOpen)} />}
>
<EuiContextMenuPanel
items={menuItems}
title={i18n.translate('xpack.apm.transactionActionMenu.actionsLabel', {
defaultMessage: 'Actions'
})}
/>
</EuiPopover>
{sections.map((section, idx) => {
const isLastSection = idx !== sections.length - 1;
return (
<div key={idx}>
{section.map(item => (
<Section key={item.key}>
{item.title && <SectionTitle>{item.title}</SectionTitle>}
{item.subtitle && (
<SectionSubtitle>{item.subtitle}</SectionSubtitle>
)}
<SectionLinks>
{item.actions.map(action => (
<SectionLink
key={action.key}
label={action.label}
href={action.href}
/>
))}
</SectionLinks>
</Section>
))}
{isLastSection && <ActionMenuDivider />}
</div>
);
})}
</ActionMenu>
);
};

View file

@ -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 () => {

View file

@ -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
}
]
}
]
]);
});
});

View file

@ -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<string, Section[]>;
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));
};

View file

@ -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'];

View file

@ -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<HTMLDivElement>;
@ -45,7 +46,12 @@ export const SectionLinks: React.FC<{}> = props => (
export const SectionSpacer: React.FC<{}> = () => <EuiSpacer size={'l'} />;
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<EuiListGroupItemProps> = props => (

View file

@ -3945,7 +3945,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": "ホストログを表示",

View file

@ -3945,7 +3945,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": "显示主机日志",