[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:
parent
5c7af8656f
commit
f49581ce34
10 changed files with 630 additions and 243 deletions
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
};
|
|
@ -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'];
|
||||
|
|
|
@ -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 => (
|
||||
|
|
|
@ -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": "ホストログを表示",
|
||||
|
|
|
@ -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": "显示主机日志",
|
||||
|
|
Loading…
Reference in a new issue