* [APM] Consistent flyout headers (#46312) * [APM] Consistent flyout headers Closes #46078 and #46074. * Tooltips for span flyout badges * Review feedback * Fix import for SERVICE_NAME in StickySpanProperties
This commit is contained in:
parent
cc37c24944
commit
f582da0f0a
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* 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 { shallow, ShallowWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IStickyProperty } from '../../../shared/StickyProperties';
|
||||
import { StickyErrorProperties } from './StickyErrorProperties';
|
||||
import {
|
||||
ERROR_PAGE_URL,
|
||||
URL_FULL
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
describe('StickyErrorProperties', () => {
|
||||
it('should render StickyProperties', () => {
|
||||
const transaction = {
|
||||
http: { request: { method: 'GET' } },
|
||||
url: { full: 'myUrl' },
|
||||
trace: { id: 'traceId' },
|
||||
transaction: {
|
||||
type: 'myTransactionType',
|
||||
name: 'myTransactionName',
|
||||
id: 'myTransactionName'
|
||||
},
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' }
|
||||
} as Transaction;
|
||||
|
||||
const error = {
|
||||
'@timestamp': 'myTimestamp',
|
||||
agent: { name: 'nodejs' },
|
||||
http: { request: { method: 'GET' } },
|
||||
url: { full: 'myUrl' },
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' },
|
||||
error: { exception: [{ handled: true }] },
|
||||
transaction: { id: 'myTransactionId', sampled: true }
|
||||
} as APMError;
|
||||
|
||||
const wrapper = shallow(
|
||||
<StickyErrorProperties error={error} transaction={transaction} />
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('url.full', () => {
|
||||
const error = {
|
||||
agent: { name: 'nodejs' },
|
||||
url: { full: 'myFullUrl' }
|
||||
} as APMError;
|
||||
|
||||
const wrapper = shallow(
|
||||
<StickyErrorProperties error={error} transaction={undefined} />
|
||||
);
|
||||
const urlValue = getValueByFieldName(wrapper, URL_FULL);
|
||||
expect(urlValue).toBe('myFullUrl');
|
||||
});
|
||||
|
||||
it('error.page.url', () => {
|
||||
const error = {
|
||||
agent: { name: 'rum-js' },
|
||||
error: { page: { url: 'myPageUrl' } }
|
||||
} as APMError;
|
||||
|
||||
const wrapper = shallow(
|
||||
<StickyErrorProperties error={error} transaction={undefined} />
|
||||
);
|
||||
const urlValue = getValueByFieldName(wrapper, ERROR_PAGE_URL);
|
||||
expect(urlValue).toBe('myPageUrl');
|
||||
});
|
||||
|
||||
describe('error.exception.handled', () => {
|
||||
it('should should render "true"', () => {
|
||||
const error = {
|
||||
agent: { name: 'nodejs' },
|
||||
error: { exception: [{ handled: true }] }
|
||||
} as APMError;
|
||||
const wrapper = shallow(
|
||||
<StickyErrorProperties error={error} transaction={undefined} />
|
||||
);
|
||||
const value = getValueByFieldName(wrapper, 'error.exception.handled');
|
||||
expect(value).toBe('true');
|
||||
});
|
||||
|
||||
it('should should render "false"', () => {
|
||||
const error = {
|
||||
agent: { name: 'nodejs' },
|
||||
error: { exception: [{ handled: false }] }
|
||||
} as APMError;
|
||||
const wrapper = shallow(
|
||||
<StickyErrorProperties error={error} transaction={undefined} />
|
||||
);
|
||||
const value = getValueByFieldName(wrapper, 'error.exception.handled');
|
||||
expect(value).toBe('false');
|
||||
});
|
||||
|
||||
it('should should render "N/A"', () => {
|
||||
const error = { agent: { name: 'nodejs' } } as APMError;
|
||||
const wrapper = shallow(
|
||||
<StickyErrorProperties error={error} transaction={undefined} />
|
||||
);
|
||||
const value = getValueByFieldName(wrapper, 'error.exception.handled');
|
||||
expect(value).toBe('N/A');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getValueByFieldName(wrapper: ShallowWrapper, fieldName: string) {
|
||||
const stickyProps = wrapper.prop('stickyProperties') as IStickyProperty[];
|
||||
const prop = stickyProps.find(p => p.fieldName === fieldName);
|
||||
return prop && prop.val;
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* 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 { isBoolean } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import {
|
||||
ERROR_EXC_HANDLED,
|
||||
HTTP_REQUEST_METHOD,
|
||||
TRANSACTION_ID,
|
||||
URL_FULL,
|
||||
USER_ID,
|
||||
ERROR_PAGE_URL
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
|
||||
import { isRumAgentName } from '../../../../../common/agent_name';
|
||||
|
||||
interface Props {
|
||||
error: APMError;
|
||||
transaction: Transaction | undefined;
|
||||
}
|
||||
|
||||
function TransactionLinkWrapper({
|
||||
transaction
|
||||
}: {
|
||||
transaction: Transaction | undefined;
|
||||
}) {
|
||||
if (!transaction) {
|
||||
return <Fragment>{NOT_AVAILABLE_LABEL}</Fragment>;
|
||||
}
|
||||
|
||||
const isSampled = transaction.transaction.sampled;
|
||||
if (!isSampled) {
|
||||
return <Fragment>{transaction.transaction.id}</Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransactionDetailLink
|
||||
serviceName={transaction.service.name}
|
||||
transactionId={transaction.transaction.id}
|
||||
traceId={transaction.trace.id}
|
||||
transactionName={transaction.transaction.name}
|
||||
transactionType={transaction.transaction.type}
|
||||
>
|
||||
{transaction.transaction.id}
|
||||
</TransactionDetailLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function StickyErrorProperties({ error, transaction }: Props) {
|
||||
const isHandled = idx(error, _ => _.error.exception[0].handled);
|
||||
const isRumAgent = isRumAgentName(error.agent.name);
|
||||
|
||||
const { urlFieldName, urlValue } = isRumAgent
|
||||
? {
|
||||
urlFieldName: ERROR_PAGE_URL,
|
||||
urlValue: idx(error, _ => _.error.page.url)
|
||||
}
|
||||
: {
|
||||
urlFieldName: URL_FULL,
|
||||
urlValue: idx(error, _ => _.url.full)
|
||||
};
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.timestampLabel', {
|
||||
defaultMessage: 'Timestamp'
|
||||
}),
|
||||
val: error['@timestamp'],
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: urlFieldName,
|
||||
label: 'URL',
|
||||
val: urlValue || NOT_AVAILABLE_LABEL,
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: HTTP_REQUEST_METHOD,
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.requestMethodLabel', {
|
||||
defaultMessage: 'Request method'
|
||||
}),
|
||||
val: idx(error, _ => _.http.request.method) || NOT_AVAILABLE_LABEL,
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: ERROR_EXC_HANDLED,
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.handledLabel', {
|
||||
defaultMessage: 'Handled'
|
||||
}),
|
||||
val: isBoolean(isHandled) ? String(isHandled) : NOT_AVAILABLE_LABEL,
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: TRANSACTION_ID,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.transactionSampleIdLabel',
|
||||
{
|
||||
defaultMessage: 'Transaction sample ID'
|
||||
}
|
||||
),
|
||||
val: <TransactionLinkWrapper transaction={transaction} />,
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: USER_ID,
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.userIdLabel', {
|
||||
defaultMessage: 'User ID'
|
||||
}),
|
||||
val: idx(error, _ => _.user.id) || NOT_AVAILABLE_LABEL,
|
||||
width: '25%'
|
||||
}
|
||||
];
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StickyErrorProperties should render StickyProperties 1`] = `
|
||||
<StickyProperties
|
||||
stickyProperties={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "@timestamp",
|
||||
"label": "Timestamp",
|
||||
"val": "myTimestamp",
|
||||
"width": "50%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "url.full",
|
||||
"label": "URL",
|
||||
"truncated": true,
|
||||
"val": "myUrl",
|
||||
"width": "50%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "http.request.method",
|
||||
"label": "Request method",
|
||||
"val": "GET",
|
||||
"width": "25%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "error.exception.handled",
|
||||
"label": "Handled",
|
||||
"val": "true",
|
||||
"width": "25%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "transaction.id",
|
||||
"label": "Transaction sample ID",
|
||||
"val": <TransactionLinkWrapper
|
||||
transaction={
|
||||
Object {
|
||||
"http": Object {
|
||||
"request": Object {
|
||||
"method": "GET",
|
||||
},
|
||||
},
|
||||
"service": Object {
|
||||
"name": "myService",
|
||||
},
|
||||
"trace": Object {
|
||||
"id": "traceId",
|
||||
},
|
||||
"transaction": Object {
|
||||
"id": "myTransactionName",
|
||||
"name": "myTransactionName",
|
||||
"type": "myTransactionType",
|
||||
},
|
||||
"url": Object {
|
||||
"full": "myUrl",
|
||||
},
|
||||
"user": Object {
|
||||
"id": "myUserId",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"width": "25%",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "user.id",
|
||||
"label": "User ID",
|
||||
"val": "myUserId",
|
||||
"width": "25%",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -4,7 +4,6 @@ exports[`DetailView should render Discover button 1`] = `
|
|||
<DiscoverErrorLink
|
||||
error={
|
||||
Object {
|
||||
"@timestamp": "myTimestamp",
|
||||
"error": Object {
|
||||
"exception": Object {
|
||||
"handled": true,
|
||||
|
@ -18,6 +17,9 @@ exports[`DetailView should render Discover button 1`] = `
|
|||
"service": Object {
|
||||
"name": "myService",
|
||||
},
|
||||
"timestamp": Object {
|
||||
"us": 0,
|
||||
},
|
||||
"transaction": Object {
|
||||
"id": "myTransactionId",
|
||||
"sampled": true,
|
||||
|
@ -49,8 +51,10 @@ exports[`DetailView should render TabContent 1`] = `
|
|||
}
|
||||
error={
|
||||
Object {
|
||||
"@timestamp": "myTimestamp",
|
||||
"context": Object {},
|
||||
"timestamp": Object {
|
||||
"us": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -34,7 +34,9 @@ describe('DetailView', () => {
|
|||
occurrencesCount: 10,
|
||||
transaction: undefined,
|
||||
error: {
|
||||
'@timestamp': 'myTimestamp',
|
||||
timestamp: {
|
||||
us: 0
|
||||
},
|
||||
http: { request: { method: 'GET' } },
|
||||
url: { full: 'myUrl' },
|
||||
service: { name: 'myService' },
|
||||
|
@ -56,10 +58,14 @@ describe('DetailView', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render StickyProperties', () => {
|
||||
it('should render a Summary', () => {
|
||||
const errorGroup = {
|
||||
occurrencesCount: 10,
|
||||
error: {} as any,
|
||||
error: {
|
||||
timestamp: {
|
||||
us: 0
|
||||
}
|
||||
} as any,
|
||||
transaction: undefined
|
||||
};
|
||||
const wrapper = shallow(
|
||||
|
@ -68,7 +74,7 @@ describe('DetailView', () => {
|
|||
urlParams={{}}
|
||||
location={{} as Location}
|
||||
/>
|
||||
).find('StickyErrorProperties');
|
||||
).find('Summary');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
@ -78,7 +84,9 @@ describe('DetailView', () => {
|
|||
occurrencesCount: 10,
|
||||
transaction: undefined,
|
||||
error: {
|
||||
'@timestamp': 'myTimestamp',
|
||||
timestamp: {
|
||||
us: 0
|
||||
},
|
||||
service: {},
|
||||
user: {}
|
||||
} as any
|
||||
|
@ -100,7 +108,9 @@ describe('DetailView', () => {
|
|||
occurrencesCount: 10,
|
||||
transaction: undefined,
|
||||
error: {
|
||||
'@timestamp': 'myTimestamp',
|
||||
timestamp: {
|
||||
us: 0
|
||||
},
|
||||
context: {}
|
||||
} as any
|
||||
};
|
||||
|
|
|
@ -10,7 +10,9 @@ import {
|
|||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiTitle
|
||||
EuiTitle,
|
||||
EuiIcon,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
|
@ -21,7 +23,7 @@ import { idx } from '@kbn/elastic-idx';
|
|||
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { px, unit } from '../../../../style/variables';
|
||||
import { px, unit, units } from '../../../../style/variables';
|
||||
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
|
||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { history } from '../../../../utils/history';
|
||||
|
@ -33,7 +35,10 @@ import {
|
|||
getTabs,
|
||||
logStacktraceTab
|
||||
} from './ErrorTabs';
|
||||
import { StickyErrorProperties } from './StickyErrorProperties';
|
||||
import { Summary } from '../../../shared/Summary';
|
||||
import { TimestampSummaryItem } from '../../../shared/Summary/TimestampSummaryItem';
|
||||
import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem';
|
||||
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
|
@ -42,6 +47,12 @@ const HeaderContainer = styled.div`
|
|||
margin-bottom: ${px(unit)};
|
||||
`;
|
||||
|
||||
const TransactionLinkName = styled.div`
|
||||
margin-left: ${px(units.half)};
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
errorGroup: ErrorGroupAPIResponse;
|
||||
urlParams: IUrlParams;
|
||||
|
@ -67,6 +78,12 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
|
|||
const tabs = getTabs(error);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
|
||||
const errorUrl =
|
||||
idx(error, _ => _.error.page.url) || idx(error, _ => _.url.full);
|
||||
|
||||
const method = idx(error, _ => _.http.request.method);
|
||||
const status = idx(error, _ => _.http.response.status_code);
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<HeaderContainer>
|
||||
|
@ -94,7 +111,41 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
|
|||
</DiscoverErrorLink>
|
||||
</HeaderContainer>
|
||||
|
||||
<StickyErrorProperties error={error} transaction={transaction} />
|
||||
<Summary
|
||||
items={[
|
||||
<TimestampSummaryItem time={error.timestamp.us / 1000} />,
|
||||
errorUrl && method ? (
|
||||
<HttpInfoSummaryItem
|
||||
url={errorUrl}
|
||||
method={method}
|
||||
status={status}
|
||||
/>
|
||||
) : null,
|
||||
transaction && (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.relatedTransactionSample',
|
||||
{
|
||||
defaultMessage: 'Related transaction sample'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<TransactionDetailLink
|
||||
traceId={transaction.trace.id}
|
||||
transactionId={transaction.transaction.id}
|
||||
transactionName={transaction.transaction.name}
|
||||
transactionType={transaction.transaction.type}
|
||||
serviceName={transaction.service.name}
|
||||
>
|
||||
<EuiIcon type="merge" />
|
||||
<TransactionLinkName>
|
||||
{transaction.transaction.name}
|
||||
</TransactionLinkName>
|
||||
</TransactionDetailLink>
|
||||
</EuiToolTip>
|
||||
)
|
||||
]}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
|
|
|
@ -9,28 +9,30 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { asPercent } from '../../../../utils/formatters';
|
||||
|
||||
interface PercentOfTraceProps {
|
||||
interface PercentOfParentProps {
|
||||
duration: number;
|
||||
totalDuration?: number;
|
||||
parentType: 'trace' | 'transaction';
|
||||
}
|
||||
|
||||
export function PercentOfTrace({
|
||||
export function PercentOfParent({
|
||||
duration,
|
||||
totalDuration
|
||||
}: PercentOfTraceProps) {
|
||||
totalDuration,
|
||||
parentType
|
||||
}: PercentOfParentProps) {
|
||||
totalDuration = totalDuration || duration;
|
||||
const isOver100 = duration > totalDuration;
|
||||
const percentOfTrace = isOver100
|
||||
const percentOfParent = isOver100
|
||||
? '>100%'
|
||||
: asPercent(duration, totalDuration, '');
|
||||
|
||||
const percentOfTraceText = i18n.translate(
|
||||
'xpack.apm.transactionDetails.percentOfTrace',
|
||||
{
|
||||
defaultMessage: '{value} of trace',
|
||||
values: { value: percentOfTrace }
|
||||
}
|
||||
);
|
||||
const percentOfParentText = i18n.translate('xpack.apm.percentOfParent', {
|
||||
defaultMessage:
|
||||
'({value} of {parentType, select, transaction { transaction } trace {trace} })',
|
||||
values: { value: percentOfParent, parentType }
|
||||
});
|
||||
|
||||
const childType = parentType === 'trace' ? 'transaction' : 'span';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -40,14 +42,18 @@ export function PercentOfTrace({
|
|||
'xpack.apm.transactionDetails.percentOfTraceLabelExplanation',
|
||||
{
|
||||
defaultMessage:
|
||||
'The % of trace exceeds 100% because this transaction takes longer than the root transaction.'
|
||||
'The % of {parentType, select, transaction {transaction} trace {trace} } exceeds 100% because this {childType, select, span {span} transaction {transaction} } takes longer than the root transaction.',
|
||||
values: {
|
||||
parentType,
|
||||
childType
|
||||
}
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>{percentOfTraceText}</>
|
||||
<>{percentOfParentText}</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
`(${percentOfTraceText})`
|
||||
percentOfParentText
|
||||
)}
|
||||
</>
|
||||
);
|
|
@ -25,7 +25,7 @@ import {
|
|||
import { ErrorCountBadge } from './ErrorCountBadge';
|
||||
import { isRumAgentName } from '../../../../../common/agent_name';
|
||||
import { fontSize } from '../../../../style/variables';
|
||||
import { PercentOfTrace } from './PercentOfTrace';
|
||||
import { PercentOfParent } from './PercentOfParent';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
|
@ -96,7 +96,13 @@ export function StickyTransactionProperties({
|
|||
defaultMessage: '% of trace'
|
||||
}
|
||||
),
|
||||
val: <PercentOfTrace duration={duration} totalDuration={totalDuration} />,
|
||||
val: (
|
||||
<PercentOfParent
|
||||
duration={duration}
|
||||
totalDuration={totalDuration}
|
||||
parentType="trace"
|
||||
/>
|
||||
),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiToolTip, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { asTime } from '../../../../../utils/formatters';
|
||||
import { PercentOfTrace } from '../PercentOfTrace';
|
||||
|
||||
interface DurationProps {
|
||||
duration: number;
|
||||
totalDuration?: number;
|
||||
}
|
||||
|
||||
const Span = styled('span')`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export function Duration({ duration, totalDuration }: DurationProps) {
|
||||
totalDuration = totalDuration || duration;
|
||||
const label = i18n.translate('xpack.apm.transactionDetails.durationLabel', {
|
||||
defaultMessage: 'Duration'
|
||||
});
|
||||
|
||||
return (
|
||||
<Span>
|
||||
<EuiToolTip content={label}>
|
||||
<EuiText>{asTime(duration)}</EuiText>
|
||||
</EuiToolTip>{' '}
|
||||
<PercentOfTrace duration={duration} totalDuration={totalDuration} />
|
||||
</Span>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import moment from 'moment';
|
||||
import { EuiToolTip, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface TimestampProps {
|
||||
transactionTimestamp: string;
|
||||
}
|
||||
|
||||
export function Timestamp({ transactionTimestamp }: TimestampProps) {
|
||||
const timestamp = moment(transactionTimestamp).format(
|
||||
'MMM Do YYYY HH:mm:ss.SSS'
|
||||
);
|
||||
|
||||
const label = i18n.translate('xpack.apm.transactionDetails.timestampLabel', {
|
||||
defaultMessage: 'Timestamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiToolTip content={label}>
|
||||
<EuiText>{timestamp}</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { isRumAgentName } from '../../../../../../common/agent_name';
|
||||
import { ErrorCountBadge } from '../ErrorCountBadge';
|
||||
import { Duration } from './Duration';
|
||||
import { Timestamp } from './Timestamp';
|
||||
import { HttpInfo } from './HttpInfo';
|
||||
import { Result } from './Result';
|
||||
import { px, units } from '../../../../../../public/style/variables';
|
||||
|
||||
// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840)
|
||||
const theme = euiLightVars;
|
||||
|
||||
const Item = styled(EuiFlexItem)`
|
||||
flex-wrap: nowrap;
|
||||
border-right: 1px solid ${theme.euiColorMediumShade};
|
||||
padding-right: ${px(units.half)};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
errorCount: number;
|
||||
totalDuration?: number;
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
export function TraceSummary({
|
||||
errorCount,
|
||||
totalDuration,
|
||||
transaction
|
||||
}: Props) {
|
||||
const result = idx(transaction, _ => _.transaction.result);
|
||||
const isRumAgent = isRumAgentName(transaction.agent.name);
|
||||
const url = isRumAgent
|
||||
? idx(transaction, _ => _.transaction.page.url)
|
||||
: idx(transaction, _ => _.url.full);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" wrap={true}>
|
||||
<Item grow={false}>
|
||||
<Timestamp transactionTimestamp={transaction['@timestamp']} />
|
||||
</Item>
|
||||
<Item grow={false}>
|
||||
<Duration
|
||||
duration={transaction.transaction.duration.us}
|
||||
totalDuration={totalDuration}
|
||||
/>
|
||||
</Item>
|
||||
{url && (
|
||||
<Item grow={false}>
|
||||
<HttpInfo transaction={transaction} url={url} />
|
||||
</Item>
|
||||
)}
|
||||
{result && !url && (
|
||||
<Item grow={false}>
|
||||
<Result result={result} />
|
||||
</Item>
|
||||
)}
|
||||
{errorCount > 0 && (
|
||||
<Item grow={false}>
|
||||
<span>
|
||||
<ErrorCountBadge title={undefined}>
|
||||
{i18n.translate('xpack.apm.transactionDetails.errorCount', {
|
||||
defaultMessage:
|
||||
'{errorCount, number} {errorCount, plural, one {Error} other {Errors}}',
|
||||
values: { errorCount }
|
||||
})}
|
||||
</ErrorCountBadge>
|
||||
</span>
|
||||
</Item>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { TraceSummary } from './TraceSummary';
|
|
@ -35,7 +35,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
|
|||
{transaction.service.name}
|
||||
</TransactionOverviewLink>
|
||||
),
|
||||
width: '50%'
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.apm.transactionDetails.transactionLabel', {
|
||||
|
@ -53,7 +53,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
|
|||
{transaction.transaction.name}
|
||||
</TransactionDetailLink>
|
||||
),
|
||||
width: '50%'
|
||||
width: '25%'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -6,68 +6,63 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import {
|
||||
SPAN_ACTION,
|
||||
SPAN_DURATION,
|
||||
SPAN_NAME,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE
|
||||
TRANSACTION_NAME,
|
||||
SERVICE_NAME
|
||||
} from '../../../../../../../../common/elasticsearch_fieldnames';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
|
||||
import { asMillis, asPercent } from '../../../../../../../utils/formatters';
|
||||
import { StickyProperties } from '../../../../../../shared/StickyProperties';
|
||||
|
||||
function formatType(type: string) {
|
||||
switch (type) {
|
||||
case 'db':
|
||||
return 'DB';
|
||||
case 'hard-navigation':
|
||||
return i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel',
|
||||
{
|
||||
defaultMessage: 'Navigation timing'
|
||||
}
|
||||
);
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSubtype(subtype: string) {
|
||||
switch (subtype) {
|
||||
case 'mysql':
|
||||
return 'MySQL';
|
||||
default:
|
||||
return subtype;
|
||||
}
|
||||
}
|
||||
|
||||
function getSpanTypes(span: Span) {
|
||||
const { type, subtype, action } = span.span;
|
||||
|
||||
const [primaryType, subtypeFromType, actionFromType] = type.split('.'); // This is to support 6.x data
|
||||
|
||||
return {
|
||||
spanType: formatType(primaryType),
|
||||
spanSubtype: formatSubtype(subtype || subtypeFromType),
|
||||
spanAction: action || actionFromType
|
||||
};
|
||||
}
|
||||
import { TransactionOverviewLink } from '../../../../../../shared/Links/apm/TransactionOverviewLink';
|
||||
import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink';
|
||||
|
||||
interface Props {
|
||||
span: Span;
|
||||
totalDuration?: number;
|
||||
transaction?: Transaction;
|
||||
}
|
||||
|
||||
export function StickySpanProperties({ span, totalDuration }: Props) {
|
||||
if (!totalDuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function StickySpanProperties({ span, transaction }: Props) {
|
||||
const spanName = span.span.name;
|
||||
const spanDuration = span.span.duration.us;
|
||||
const { spanType, spanSubtype, spanAction } = getSpanTypes(span);
|
||||
const transactionStickyProperties = transaction
|
||||
? [
|
||||
{
|
||||
label: i18n.translate('xpack.apm.transactionDetails.serviceLabel', {
|
||||
defaultMessage: 'Service'
|
||||
}),
|
||||
fieldName: SERVICE_NAME,
|
||||
val: (
|
||||
<TransactionOverviewLink serviceName={transaction.service.name}>
|
||||
{transaction.service.name}
|
||||
</TransactionOverviewLink>
|
||||
),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.transactionLabel',
|
||||
{
|
||||
defaultMessage: 'Transaction'
|
||||
}
|
||||
),
|
||||
fieldName: TRANSACTION_NAME,
|
||||
val: (
|
||||
<TransactionDetailLink
|
||||
serviceName={transaction.service.name}
|
||||
transactionId={transaction.transaction.id}
|
||||
traceId={transaction.trace.id}
|
||||
transactionName={transaction.transaction.name}
|
||||
transactionType={transaction.transaction.type}
|
||||
>
|
||||
{transaction.transaction.name}
|
||||
</TransactionDetailLink>
|
||||
),
|
||||
width: '25%'
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: i18n.translate(
|
||||
|
@ -79,72 +74,10 @@ export function StickySpanProperties({ span, totalDuration }: Props) {
|
|||
fieldName: SPAN_NAME,
|
||||
val: spanName || NOT_AVAILABLE_LABEL,
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: SPAN_DURATION,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.durationLabel',
|
||||
{
|
||||
defaultMessage: 'Duration'
|
||||
}
|
||||
),
|
||||
val: asMillis(spanDuration),
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.percentOfTransactionLabel',
|
||||
{
|
||||
defaultMessage: '% of transaction'
|
||||
}
|
||||
),
|
||||
val: asPercent(spanDuration, totalDuration),
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: SPAN_TYPE,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.typeLabel',
|
||||
{
|
||||
defaultMessage: 'Type'
|
||||
}
|
||||
),
|
||||
val: spanType,
|
||||
truncated: true,
|
||||
width: '15%'
|
||||
}
|
||||
...transactionStickyProperties
|
||||
];
|
||||
|
||||
if (spanSubtype) {
|
||||
stickyProperties.push({
|
||||
fieldName: SPAN_SUBTYPE,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.subtypeLabel',
|
||||
{
|
||||
defaultMessage: 'Subtype'
|
||||
}
|
||||
),
|
||||
val: spanSubtype,
|
||||
truncated: true,
|
||||
width: '15%'
|
||||
});
|
||||
}
|
||||
|
||||
if (spanAction) {
|
||||
stickyProperties.push({
|
||||
fieldName: SPAN_ACTION,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.actionLabel',
|
||||
{
|
||||
defaultMessage: 'Action'
|
||||
}
|
||||
),
|
||||
val: spanAction,
|
||||
truncated: true,
|
||||
width: '15%'
|
||||
});
|
||||
}
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
||||
|
|
|
@ -15,27 +15,72 @@ import {
|
|||
EuiPortal,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiTitle
|
||||
EuiTitle,
|
||||
EuiBadge,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { get, keys } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { px, units } from '../../../../../../../style/variables';
|
||||
import { Summary } from '../../../../../../shared/Summary';
|
||||
import { TimestampSummaryItem } from '../../../../../../shared/Summary/TimestampSummaryItem';
|
||||
import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink';
|
||||
import { Stacktrace } from '../../../../../../shared/Stacktrace';
|
||||
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||
import { ResponsiveFlyout } from '../ResponsiveFlyout';
|
||||
import { DatabaseContext } from './DatabaseContext';
|
||||
import { HttpContext } from './HttpContext';
|
||||
import { StickySpanProperties } from './StickySpanProperties';
|
||||
|
||||
function formatType(type: string) {
|
||||
switch (type) {
|
||||
case 'db':
|
||||
return 'DB';
|
||||
case 'hard-navigation':
|
||||
return i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel',
|
||||
{
|
||||
defaultMessage: 'Navigation timing'
|
||||
}
|
||||
);
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSubtype(subtype: string | undefined) {
|
||||
switch (subtype) {
|
||||
case 'mysql':
|
||||
return 'MySQL';
|
||||
default:
|
||||
return subtype;
|
||||
}
|
||||
}
|
||||
|
||||
function getSpanTypes(span: Span) {
|
||||
const { type, subtype, action } = span.span;
|
||||
|
||||
return {
|
||||
spanType: formatType(type),
|
||||
spanSubtype: formatSubtype(subtype),
|
||||
spanAction: action
|
||||
};
|
||||
}
|
||||
|
||||
const TagName = styled.div`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const SpanBadge = styled(EuiBadge)`
|
||||
display: inline-block;
|
||||
margin-right: ${px(units.quarter)};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
span?: Span;
|
||||
parentTransaction?: Transaction;
|
||||
|
@ -62,6 +107,7 @@ export function SpanFlyout({
|
|||
key,
|
||||
value: get(spanLabels, key)
|
||||
}));
|
||||
const spanTypes = getSpanTypes(span);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
|
@ -96,9 +142,50 @@ export function SpanFlyout({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<FlyoutTopLevelProperties transaction={parentTransaction} />
|
||||
<EuiHorizontalRule />
|
||||
<StickySpanProperties span={span} totalDuration={totalDuration} />
|
||||
<StickySpanProperties span={span} transaction={parentTransaction} />
|
||||
<EuiSpacer size="m" />
|
||||
<Summary
|
||||
items={[
|
||||
<TimestampSummaryItem time={span.timestamp.us / 1000} />,
|
||||
<DurationSummaryItem
|
||||
duration={span.span.duration.us}
|
||||
totalDuration={totalDuration}
|
||||
parentType="transaction"
|
||||
/>,
|
||||
<>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.spanType',
|
||||
{ defaultMessage: 'Type' }
|
||||
)}
|
||||
>
|
||||
<SpanBadge color="hollow">{spanTypes.spanType}</SpanBadge>
|
||||
</EuiToolTip>
|
||||
{spanTypes.spanSubtype && (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.spanSubtype',
|
||||
{ defaultMessage: 'Subtype' }
|
||||
)}
|
||||
>
|
||||
<SpanBadge color="hollow">
|
||||
{spanTypes.spanSubtype}
|
||||
</SpanBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
{spanTypes.spanAction && (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanFlyout.spanAction',
|
||||
{ defaultMessage: 'Action' }
|
||||
)}
|
||||
>
|
||||
<SpanBadge color="hollow">{spanTypes.spanAction}</SpanBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</>
|
||||
]}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
<HttpContext httpContext={httpContext} />
|
||||
<DatabaseContext dbContext={dbContext} />
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiPortal,
|
||||
EuiSpacer,
|
||||
EuiTitle
|
||||
|
@ -18,7 +17,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu';
|
||||
import { StickyTransactionProperties } from '../../../StickyTransactionProperties';
|
||||
import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary';
|
||||
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||
import { ResponsiveFlyout } from '../ResponsiveFlyout';
|
||||
import { TransactionMetadata } from '../../../../../../shared/MetadataTable/TransactionMetadata';
|
||||
|
@ -82,13 +81,13 @@ export function TransactionFlyout({
|
|||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<FlyoutTopLevelProperties transaction={transactionDoc} />
|
||||
<EuiHorizontalRule />
|
||||
<StickyTransactionProperties
|
||||
errorCount={errorCount}
|
||||
<EuiSpacer size="m" />
|
||||
<TransactionSummary
|
||||
transaction={transactionDoc}
|
||||
totalDuration={traceRootDuration}
|
||||
errorCount={errorCount}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer size="m" />
|
||||
<DroppedSpansWarning transactionDoc={transactionDoc} />
|
||||
<TransactionPropertiesTable transaction={transactionDoc} />
|
||||
</EuiFlyoutBody>
|
||||
|
|
|
@ -25,7 +25,7 @@ import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/Tra
|
|||
import { TransactionTabs } from './TransactionTabs';
|
||||
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
||||
import { TraceSummary } from './TraceSummary/TraceSummary';
|
||||
import { TransactionSummary } from '../../../shared/Summary/TransactionSummary';
|
||||
|
||||
function MaybeViewTraceLink({
|
||||
transaction,
|
||||
|
@ -165,7 +165,7 @@ export const WaterfallWithSummmary: React.SFC<Props> = ({
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TraceSummary
|
||||
<TransactionSummary
|
||||
errorCount={sum(Object.values(waterfall.errorCountByTransactionId))}
|
||||
totalDuration={waterfall.traceRootDuration}
|
||||
transaction={entryTransaction}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiToolTip, EuiText } from '@elastic/eui';
|
||||
import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent';
|
||||
import { asTime } from '../../../utils/formatters';
|
||||
|
||||
interface Props {
|
||||
duration: number;
|
||||
totalDuration: number | undefined;
|
||||
parentType: 'trace' | 'transaction';
|
||||
}
|
||||
|
||||
const DurationSummaryItem = ({
|
||||
duration,
|
||||
totalDuration,
|
||||
parentType
|
||||
}: Props) => {
|
||||
const calculatedTotalDuration =
|
||||
totalDuration === undefined ? duration : totalDuration;
|
||||
|
||||
const label = i18n.translate('xpack.apm.transactionDurationLabel', {
|
||||
defaultMessage: 'Duration'
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip content={label}>
|
||||
<EuiText>{asTime(duration)}</EuiText>
|
||||
</EuiToolTip>
|
||||
|
||||
<EuiText size="s">
|
||||
<PercentOfParent
|
||||
duration={duration}
|
||||
totalDuration={calculatedTotalDuration}
|
||||
parentType={parentType}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { DurationSummaryItem };
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { px } from '../../../../../code/public/style/variables';
|
||||
import { ErrorCountBadge } from '../../app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge';
|
||||
import { units } from '../../../style/variables';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const Badge = styled(ErrorCountBadge)`
|
||||
margin-top: ${px(units.eighth)};
|
||||
`;
|
||||
|
||||
const ErrorCountSummaryItem = ({ count }: Props) => {
|
||||
return (
|
||||
<Badge>
|
||||
{i18n.translate('xpack.apm.transactionDetails.errorCount', {
|
||||
defaultMessage:
|
||||
'{errorCount, number} {errorCount, plural, one {Error} other {Errors}}',
|
||||
values: { errorCount: count }
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export { ErrorCountSummaryItem };
|
|
@ -6,24 +6,26 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { HttpInfo } from './HttpInfo';
|
||||
import * as exampleTransactions from './__fixtures__/transactions';
|
||||
import { palettes } from '@elastic/eui';
|
||||
import { cloneDeep, set } from 'lodash';
|
||||
import { HttpInfoSummaryItem } from './';
|
||||
import * as exampleTransactions from '../__fixtures__/transactions';
|
||||
|
||||
describe('HttpInfo', () => {
|
||||
describe('HttpInfoSummaryItem', () => {
|
||||
describe('render', () => {
|
||||
const transaction = exampleTransactions.httpOk;
|
||||
const url = 'https://example.com';
|
||||
const props = { transaction, url };
|
||||
const method = 'get';
|
||||
const props = { transaction, url, method, status: 200 };
|
||||
|
||||
it('renders', () => {
|
||||
expect(() => shallow(<HttpInfo {...props} />)).not.toThrowError();
|
||||
expect(() =>
|
||||
shallow(<HttpInfoSummaryItem {...props} />)
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
describe('with status code 200', () => {
|
||||
it('shows a success color', () => {
|
||||
const wrapper = mount(<HttpInfo {...props} />);
|
||||
const wrapper = mount(<HttpInfoSummaryItem {...props} />);
|
||||
|
||||
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
|
||||
palettes.euiPaletteForStatus.colors[0]
|
||||
|
@ -33,10 +35,9 @@ describe('HttpInfo', () => {
|
|||
|
||||
describe('with status code 301', () => {
|
||||
it('shows a warning color', () => {
|
||||
const p = cloneDeep(props);
|
||||
set(p, 'transaction.http.response.status_code', 301);
|
||||
const p = { ...props, status: 301 };
|
||||
|
||||
const wrapper = mount(<HttpInfo {...p} />);
|
||||
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
|
||||
|
||||
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
|
||||
palettes.euiPaletteForStatus.colors[4]
|
||||
|
@ -46,10 +47,9 @@ describe('HttpInfo', () => {
|
|||
|
||||
describe('with status code 404', () => {
|
||||
it('shows a error color', () => {
|
||||
const p = cloneDeep(props);
|
||||
set(p, 'transaction.http.response.status_code', 404);
|
||||
const p = { ...props, status: 404 };
|
||||
|
||||
const wrapper = mount(<HttpInfo {...p} />);
|
||||
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
|
||||
|
||||
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
|
||||
palettes.euiPaletteForStatus.colors[9]
|
||||
|
@ -59,10 +59,9 @@ describe('HttpInfo', () => {
|
|||
|
||||
describe('with status code 502', () => {
|
||||
it('shows a error color', () => {
|
||||
const p = cloneDeep(props);
|
||||
set(p, 'transaction.http.response.status_code', 502);
|
||||
const p = { ...props, status: 502 };
|
||||
|
||||
const wrapper = mount(<HttpInfo {...p} />);
|
||||
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
|
||||
|
||||
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
|
||||
palettes.euiPaletteForStatus.colors[9]
|
||||
|
@ -72,10 +71,9 @@ describe('HttpInfo', () => {
|
|||
|
||||
describe('with some other status code', () => {
|
||||
it('shows the default color', () => {
|
||||
const p = cloneDeep(props);
|
||||
set(p, 'transaction.http.response.status_code', 700);
|
||||
const p = { ...props, status: 700 };
|
||||
|
||||
const wrapper = mount(<HttpInfo {...p} />);
|
||||
const wrapper = mount(<HttpInfoSummaryItem {...p} />);
|
||||
|
||||
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
|
||||
'default'
|
|
@ -8,10 +8,8 @@ import React from 'react';
|
|||
import { EuiToolTip, EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { idx } from '@kbn/elastic-idx/target';
|
||||
import { palettes } from '@elastic/eui';
|
||||
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { units, px, truncate, unit } from '../../../../../style/variables';
|
||||
import { units, px, truncate, unit } from '../../../../style/variables';
|
||||
import { statusCodes } from './statusCodes';
|
||||
|
||||
const statusColors = {
|
||||
|
@ -58,7 +56,8 @@ const Url = styled('span')`
|
|||
${truncate(px(unit * 24))};
|
||||
`;
|
||||
interface HttpInfoProps {
|
||||
transaction: Transaction;
|
||||
method: string;
|
||||
status?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
|
@ -66,12 +65,7 @@ const Span = styled('span')`
|
|||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export function HttpInfo({ transaction, url }: HttpInfoProps) {
|
||||
const method = (
|
||||
idx(transaction, _ => _.http.request.method) || ''
|
||||
).toUpperCase();
|
||||
const status = idx(transaction, _ => _.http.response.status_code);
|
||||
|
||||
export function HttpInfoSummaryItem({ status, method, url }: HttpInfoProps) {
|
||||
const methodLabel = i18n.translate(
|
||||
'xpack.apm.transactionDetails.requestMethodLabel',
|
||||
{
|
||||
|
@ -83,7 +77,7 @@ export function HttpInfo({ transaction, url }: HttpInfoProps) {
|
|||
<Span>
|
||||
<HttpInfoBadge title={undefined}>
|
||||
<EuiToolTip content={methodLabel}>
|
||||
<strong>{method}</strong>
|
||||
<>{method.toUpperCase()}</>
|
||||
</EuiToolTip>{' '}
|
||||
<EuiToolTip content={url}>
|
||||
<Url>{url}</Url>
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
interface Props {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const TimestampSummaryItem = (props: Props) => {
|
||||
const time = moment.tz(props.time, moment.tz.guess());
|
||||
const relativeTimeLabel = time.fromNow();
|
||||
const absoluteTimeLabel = time.format('MMM Do YYYY HH:mm:ss.SSS zz');
|
||||
|
||||
return (
|
||||
<EuiToolTip content={absoluteTimeLabel}>
|
||||
<>{relativeTimeLabel}</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export { TimestampSummaryItem };
|
|
@ -8,11 +8,11 @@ import React from 'react';
|
|||
import { EuiToolTip, EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface ResultProps {
|
||||
result: string;
|
||||
interface Props {
|
||||
transactionResult: string;
|
||||
}
|
||||
|
||||
export function Result({ result }: ResultProps) {
|
||||
export function TransactionResultSummaryItem({ transactionResult }: Props) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.apm.transactionDetails.resultLabel', {
|
||||
|
@ -20,7 +20,7 @@ export function Result({ result }: ResultProps) {
|
|||
})}
|
||||
>
|
||||
<EuiBadge color="default" title={undefined}>
|
||||
{result}
|
||||
{transactionResult}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { TraceSummary } from '.';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { TransactionSummary } from './TransactionSummary';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import * as exampleTransactions from './__fixtures__/transactions';
|
||||
|
||||
describe('TraceSummary', () => {
|
||||
describe('TransactionSummary', () => {
|
||||
describe('render', () => {
|
||||
const transaction: Transaction = exampleTransactions.httpOk;
|
||||
|
||||
|
@ -21,7 +21,9 @@ describe('TraceSummary', () => {
|
|||
};
|
||||
|
||||
it('renders', () => {
|
||||
expect(() => shallow(<TraceSummary {...props} />)).not.toThrowError();
|
||||
expect(() =>
|
||||
shallow(<TransactionSummary {...props} />)
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import { Summary } from './';
|
||||
import { TimestampSummaryItem } from './TimestampSummaryItem';
|
||||
import { DurationSummaryItem } from './DurationSummaryItem';
|
||||
import { ErrorCountSummaryItem } from './ErrorCountSummaryItem';
|
||||
import { isRumAgentName } from '../../../../common/agent_name';
|
||||
import { HttpInfoSummaryItem } from './HttpInfoSummaryItem';
|
||||
import { TransactionResultSummaryItem } from './TransactionResultSummaryItem';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
totalDuration: number | undefined;
|
||||
errorCount: number;
|
||||
}
|
||||
|
||||
const getTransactionResultSummaryItem = (transaction: Transaction) => {
|
||||
const result = idx(transaction, _ => _.transaction.result);
|
||||
const isRumAgent = isRumAgentName(transaction.agent.name);
|
||||
const url = isRumAgent
|
||||
? idx(transaction, _ => _.transaction.page.url)
|
||||
: idx(transaction, _ => _.url.full);
|
||||
|
||||
if (url) {
|
||||
const method = idx(transaction, _ => _.http.request.method) || '';
|
||||
const status = idx(transaction, _ => _.http.response.status_code);
|
||||
|
||||
return <HttpInfoSummaryItem method={method} status={status} url={url} />;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return <TransactionResultSummaryItem transactionResult={result} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const TransactionSummary = ({
|
||||
transaction,
|
||||
totalDuration,
|
||||
errorCount
|
||||
}: Props) => {
|
||||
const items = [
|
||||
<TimestampSummaryItem time={transaction.timestamp.us / 1000} />,
|
||||
<DurationSummaryItem
|
||||
duration={transaction.transaction.duration.us}
|
||||
totalDuration={totalDuration}
|
||||
parentType="trace"
|
||||
/>,
|
||||
getTransactionResultSummaryItem(transaction),
|
||||
errorCount ? <ErrorCountSummaryItem count={errorCount} /> : null
|
||||
];
|
||||
|
||||
return <Summary items={items}></Summary>;
|
||||
};
|
||||
|
||||
export { TransactionSummary };
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
|
||||
export const httpOk: Transaction = {
|
||||
'@timestamp': '0',
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { px, units } from '../../../../public/style/variables';
|
||||
|
||||
interface Props {
|
||||
items: Array<React.ReactElement | null | undefined>;
|
||||
}
|
||||
|
||||
// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840)
|
||||
const theme = euiLightVars;
|
||||
|
||||
const Item = styled(EuiFlexItem)`
|
||||
flex-wrap: nowrap;
|
||||
border-right: 1px solid ${theme.euiColorLightShade};
|
||||
padding-right: ${px(units.half)};
|
||||
flex-flow: row nowrap;
|
||||
line-height: 1.5;
|
||||
align-items: center !important;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Summary = ({ items }: Props) => {
|
||||
const filteredItems = items.filter(Boolean) as React.ReactElement[];
|
||||
|
||||
return (
|
||||
<EuiFlexGrid gutterSize="s">
|
||||
{filteredItems.map((item, index) => (
|
||||
<Item key={index} grow={false}>
|
||||
{item}
|
||||
</Item>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export { Summary };
|
|
@ -3315,17 +3315,12 @@
|
|||
"xpack.apm.errorGroupDetails.errorGroupTitle": "エラーグループ {errorGroupId}",
|
||||
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "エラーのオカレンス",
|
||||
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "例外メッセージ",
|
||||
"xpack.apm.errorGroupDetails.handledLabel": "対応済み",
|
||||
"xpack.apm.errorGroupDetails.logMessageLabel": "ログメッセージ",
|
||||
"xpack.apm.errorGroupDetails.noErrorsLabel": "エラーが見つかりませんでした",
|
||||
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "オカレンス",
|
||||
"xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 件",
|
||||
"xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 件",
|
||||
"xpack.apm.errorGroupDetails.requestMethodLabel": "リクエストメソド",
|
||||
"xpack.apm.errorGroupDetails.timestampLabel": "タイムスタンプ",
|
||||
"xpack.apm.errorGroupDetails.transactionSampleIdLabel": "トランザクションサンプル ID",
|
||||
"xpack.apm.errorGroupDetails.unhandledLabel": "未対応",
|
||||
"xpack.apm.errorGroupDetails.userIdLabel": "ユーザー ID",
|
||||
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "ディスカバリで {occurrencesCount} 件のオカレンスを表示",
|
||||
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因",
|
||||
"xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID",
|
||||
|
@ -3494,16 +3489,11 @@
|
|||
"xpack.apm.transactionDetails.resultLabel": "結果",
|
||||
"xpack.apm.transactionDetails.serviceLabel": "サービス",
|
||||
"xpack.apm.transactionDetails.servicesTitle": "サービス",
|
||||
"xpack.apm.transactionDetails.spanFlyout.actionLabel": "アクション",
|
||||
"xpack.apm.transactionDetails.spanFlyout.databaseStatementTitle": "データベースステートメント",
|
||||
"xpack.apm.transactionDetails.spanFlyout.durationLabel": "期間",
|
||||
"xpack.apm.transactionDetails.spanFlyout.nameLabel": "名前",
|
||||
"xpack.apm.transactionDetails.spanFlyout.percentOfTransactionLabel": "トランザクションの %",
|
||||
"xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle": "スパン詳細",
|
||||
"xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "ナビゲーションタイミング",
|
||||
"xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "スタックトレース",
|
||||
"xpack.apm.transactionDetails.spanFlyout.subtypeLabel": "サブタイプ",
|
||||
"xpack.apm.transactionDetails.spanFlyout.typeLabel": "タイプ",
|
||||
"xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "ディスカバリでスパンを表示",
|
||||
"xpack.apm.transactionDetails.timestampLabel": "タイムスタンプ",
|
||||
"xpack.apm.transactionDetails.transactionLabel": "トランザクション",
|
||||
|
@ -11572,4 +11562,4 @@
|
|||
"xpack.fileUpload.fileParser.noFileProvided": "エラー、ファイルが提供されていません",
|
||||
"xpack.fileUpload.jsonIndexFilePicker.errorGettingIndexName": "インデックス名の取得中にエラーが発生: {errorMessage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3316,17 +3316,12 @@
|
|||
"xpack.apm.errorGroupDetails.errorGroupTitle": "错误组 {errorGroupId}",
|
||||
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "错误发生",
|
||||
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "异常消息",
|
||||
"xpack.apm.errorGroupDetails.handledLabel": "已处理",
|
||||
"xpack.apm.errorGroupDetails.logMessageLabel": "日志消息",
|
||||
"xpack.apm.errorGroupDetails.noErrorsLabel": "未找到任何错误",
|
||||
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "发生次数",
|
||||
"xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 次发生",
|
||||
"xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 次发生",
|
||||
"xpack.apm.errorGroupDetails.requestMethodLabel": "请求方法",
|
||||
"xpack.apm.errorGroupDetails.timestampLabel": "时间戳",
|
||||
"xpack.apm.errorGroupDetails.transactionSampleIdLabel": "事务样例 ID",
|
||||
"xpack.apm.errorGroupDetails.unhandledLabel": "未处理",
|
||||
"xpack.apm.errorGroupDetails.userIdLabel": "用户 ID",
|
||||
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "在 Discover 中查看 {occurrencesCount} 次发生",
|
||||
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因",
|
||||
"xpack.apm.errorsTable.groupIdColumnLabel": "组 ID",
|
||||
|
@ -3495,16 +3490,11 @@
|
|||
"xpack.apm.transactionDetails.resultLabel": "结果",
|
||||
"xpack.apm.transactionDetails.serviceLabel": "服务",
|
||||
"xpack.apm.transactionDetails.servicesTitle": "服务",
|
||||
"xpack.apm.transactionDetails.spanFlyout.actionLabel": "操作",
|
||||
"xpack.apm.transactionDetails.spanFlyout.databaseStatementTitle": "数据库语句",
|
||||
"xpack.apm.transactionDetails.spanFlyout.durationLabel": "持续时间",
|
||||
"xpack.apm.transactionDetails.spanFlyout.nameLabel": "名称",
|
||||
"xpack.apm.transactionDetails.spanFlyout.percentOfTransactionLabel": "事务的 %",
|
||||
"xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle": "跨度详情",
|
||||
"xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "导航定时",
|
||||
"xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "堆栈追溯",
|
||||
"xpack.apm.transactionDetails.spanFlyout.subtypeLabel": "子类型",
|
||||
"xpack.apm.transactionDetails.spanFlyout.typeLabel": "类型",
|
||||
"xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "在 Discover 中查看跨度",
|
||||
"xpack.apm.transactionDetails.timestampLabel": "时间戳",
|
||||
"xpack.apm.transactionDetails.transactionLabel": "事务",
|
||||
|
@ -11574,4 +11564,4 @@
|
|||
"xpack.fileUpload.fileParser.noFileProvided": "错误,未提供任何文件",
|
||||
"xpack.fileUpload.jsonIndexFilePicker.errorGettingIndexName": "检索索引名称时出错:{errorMessage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue