[7.0] [APM] Improve types for tabs and properties table (#32462) (#32748)

* [APM] Improve types for tabs and properties table (#32462)

* [APM] Improve types for tabs

* Update translations

* Rename Tab to PropertyTab

* Add comment about agent names

# Conflicts:
#	x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
#	x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx
#	x-pack/plugins/apm/public/components/shared/PropertiesTable/index.tsx

* Typescript fixes
This commit is contained in:
Søren Louv-Jansen 2019-03-08 12:51:00 +01:00 committed by GitHub
parent 69fc0d8186
commit e757cb3e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 745 additions and 587 deletions

View file

@ -38,7 +38,7 @@ exports[`Error PROCESSOR_EVENT 1`] = `"error"`;
exports[`Error PROCESSOR_NAME 1`] = `"error"`;
exports[`Error SERVICE_AGENT_NAME 1`] = `"agent name"`;
exports[`Error SERVICE_AGENT_NAME 1`] = `"java"`;
exports[`Error SERVICE_LANGUAGE_NAME 1`] = `"nodejs"`;
@ -116,7 +116,7 @@ exports[`Span PROCESSOR_EVENT 1`] = `"span"`;
exports[`Span PROCESSOR_NAME 1`] = `"transaction"`;
exports[`Span SERVICE_AGENT_NAME 1`] = `"agent name"`;
exports[`Span SERVICE_AGENT_NAME 1`] = `"java"`;
exports[`Span SERVICE_LANGUAGE_NAME 1`] = `undefined`;
@ -194,7 +194,7 @@ exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`;
exports[`Transaction PROCESSOR_NAME 1`] = `"transaction"`;
exports[`Transaction SERVICE_AGENT_NAME 1`] = `"agent name"`;
exports[`Transaction SERVICE_AGENT_NAME 1`] = `"java"`;
exports[`Transaction SERVICE_LANGUAGE_NAME 1`] = `"nodejs"`;

View file

@ -14,7 +14,7 @@ describe('Transaction', () => {
const transaction: Transaction = {
'@timestamp': new Date().toString(),
agent: {
name: 'agent name',
name: 'java',
version: 'agent version'
},
http: {
@ -59,7 +59,7 @@ describe('Span', () => {
const span: Span = {
'@timestamp': new Date().toString(),
agent: {
name: 'agent name',
name: 'java',
version: 'agent version'
},
processor: {
@ -101,7 +101,7 @@ describe('Span', () => {
describe('Error', () => {
const errorDoc: APMError = {
agent: {
name: 'agent name',
name: 'java',
version: 'agent version'
},
error: {

View file

@ -0,0 +1,52 @@
/*
* 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 { isEmpty } from 'lodash';
import { idx } from 'x-pack/plugins/apm/common/idx';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import {
getTabsFromObject,
PropertyTab
} from '../../../shared/PropertiesTable/tabConfig';
export type ErrorTab = PropertyTab | ExceptionTab | LogTab;
interface LogTab {
key: 'log_stacktrace';
label: string;
}
export const logStacktraceTab: LogTab = {
key: 'log_stacktrace',
label: i18n.translate('xpack.apm.propertiesTable.tabs.logStacktraceLabel', {
defaultMessage: 'Log stacktrace'
})
};
interface ExceptionTab {
key: 'exception_stacktrace';
label: string;
}
export const exceptionStacktraceTab: ExceptionTab = {
key: 'exception_stacktrace',
label: i18n.translate(
'xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel',
{
defaultMessage: 'Exception stacktrace'
}
)
};
export function getTabs(error: APMError) {
const hasLogStacktrace = !isEmpty(idx(error, _ => _.error.log.stacktrace));
return [
...(hasLogStacktrace ? [logStacktraceTab] : []),
exceptionStacktraceTab,
...getTabsFromObject(error)
];
}

View file

@ -0,0 +1,44 @@
/*
* 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 } from 'enzyme';
import React from 'react';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { StickyErrorProperties } from './StickyErrorProperties';
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',
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();
});
});

View file

@ -0,0 +1,118 @@
/*
* 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 React, { Fragment } from 'react';
import {
ERROR_EXC_HANDLED,
HTTP_REQUEST_METHOD,
TRANSACTION_ID,
URL_FULL,
USER_ID
} from 'x-pack/plugins/apm/common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
import { idx } from 'x-pack/plugins/apm/common/idx';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { KibanaLink } from '../../../shared/Links/KibanaLink';
import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers';
import { StickyProperties } from '../../../shared/StickyProperties';
interface Props {
error: APMError;
transaction: Transaction | undefined;
}
function TransactionLink({ error, transaction }: Props) {
if (!transaction) {
return <Fragment>{NOT_AVAILABLE_LABEL}</Fragment>;
}
const isSampled = error.transaction.sampled;
if (!isSampled) {
return <Fragment>{transaction.transaction.id}</Fragment>;
}
const path = `/${
transaction.service.name
}/transactions/${legacyEncodeURIComponent(
transaction.transaction.type
)}/${legacyEncodeURIComponent(transaction.transaction.name)}`;
return (
<KibanaLink
hash={path}
query={{
transactionId: transaction.transaction.id,
traceId: transaction.trace.id
}}
>
{transaction.transaction.id}
</KibanaLink>
);
}
export function StickyErrorProperties({ error, transaction }: Props) {
const stickyProperties = [
{
fieldName: '@timestamp',
label: i18n.translate('xpack.apm.errorGroupDetails.timestampLabel', {
defaultMessage: 'Timestamp'
}),
val: error['@timestamp'],
width: '50%'
},
{
fieldName: URL_FULL,
label: 'URL',
val:
idx(error, _ => _.context.page.url) ||
idx(error, _ => _.url.full) ||
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:
String(idx(error, _ => _.error.exception[0].handled)) ||
NOT_AVAILABLE_LABEL,
width: '25%'
},
{
fieldName: TRANSACTION_ID,
label: i18n.translate(
'xpack.apm.errorGroupDetails.transactionSampleIdLabel',
{
defaultMessage: 'Transaction sample ID'
}
),
val: <TransactionLink transaction={transaction} error={error} />,
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} />;
}

View file

@ -1,48 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DetailView should render Discover button 1`] = `
<DiscoverErrorLink
error={
Object {
"@timestamp": "myTimestamp",
"error": Object {
"exception": Object {
"handled": true,
},
},
"http": Object {
"request": Object {
"method": "GET",
},
},
"service": Object {
"name": "myService",
},
"transaction": Object {
"id": "myTransactionId",
"sampled": true,
},
"url": Object {
"full": "myUrl",
},
"user": Object {
"id": "myUserId",
},
}
}
>
<EuiButtonEmpty
color="primary"
iconSide="left"
iconType="discoverApp"
type="button"
>
View 10 occurrences in Discover
</EuiButtonEmpty>
</DiscoverErrorLink>
`;
exports[`DetailView should render StickyProperties 1`] = `
exports[`StickyErrorProperties should render StickyProperties 1`] = `
<StickyProperties
stickyProperties={
Array [
@ -144,60 +102,3 @@ exports[`DetailView should render StickyProperties 1`] = `
}
/>
`;
exports[`DetailView should render TabContent 1`] = `
<TabContent
currentTab={
Object {
"key": "exception_stacktrace",
"label": "Exception stacktrace",
}
}
error={
Object {
"@timestamp": "myTimestamp",
"context": Object {},
}
}
/>
`;
exports[`DetailView should render tabs 1`] = `
<EuiTabs
expand={false}
size="m"
>
<EuiTab
disabled={false}
isSelected={true}
key="exception_stacktrace"
onClick={[Function]}
>
Exception stacktrace
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="service"
onClick={[Function]}
>
Service
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="user"
onClick={[Function]}
>
User
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="labels"
onClick={[Function]}
>
Labels
</EuiTab>
</EuiTabs>
`;

View file

@ -0,0 +1,100 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DetailView should render Discover button 1`] = `
<DiscoverErrorLink
error={
Object {
"@timestamp": "myTimestamp",
"error": Object {
"exception": Object {
"handled": true,
},
},
"http": Object {
"request": Object {
"method": "GET",
},
},
"service": Object {
"name": "myService",
},
"transaction": Object {
"id": "myTransactionId",
"sampled": true,
},
"url": Object {
"full": "myUrl",
},
"user": Object {
"id": "myUserId",
},
}
}
>
<EuiButtonEmpty
color="primary"
iconSide="left"
iconType="discoverApp"
type="button"
>
View 10 occurrences in Discover
</EuiButtonEmpty>
</DiscoverErrorLink>
`;
exports[`DetailView should render TabContent 1`] = `
<TabContent
currentTab={
Object {
"key": "exception_stacktrace",
"label": "Exception stacktrace",
}
}
error={
Object {
"@timestamp": "myTimestamp",
"context": Object {},
}
}
/>
`;
exports[`DetailView should render tabs 1`] = `
<EuiTabs
expand={false}
size="m"
>
<EuiTab
disabled={false}
isSelected={true}
key="exception_stacktrace"
onClick={[Function]}
>
Exception stacktrace
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="service"
onClick={[Function]}
>
Service
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="user"
onClick={[Function]}
>
User
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="labels"
onClick={[Function]}
>
Labels
</EuiTab>
</EuiTabs>
`;

View file

@ -10,10 +10,9 @@ import React from 'react';
import { RRRRenderResponse } from 'react-redux-request';
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
// @ts-ignore
import { mockMoment } from '../../../../../utils/testHelpers';
import { DetailView } from '../index';
import { mockMoment } from '../../../../utils/testHelpers';
import { DetailView } from './index';
describe('DetailView', () => {
beforeEach(() => {
@ -67,27 +66,7 @@ describe('DetailView', () => {
status: 'SUCCESS',
data: {
occurrencesCount: 10,
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,
error: {
'@timestamp': 'myTimestamp',
http: { request: { method: 'GET' } },
url: { full: 'myUrl' },
service: { name: 'myService' },
user: { id: 'myUserId' },
error: { exception: [{ handled: true }] },
transaction: { id: 'myTransactionId', sampled: true }
} as APMError
error: {} as APMError
}
};
const wrapper = shallow(
@ -96,10 +75,9 @@ describe('DetailView', () => {
urlParams={{}}
location={{} as Location}
/>
).find('StickyProperties');
).find('StickyErrorProperties');
expect(wrapper.exists()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
it('should render tabs', () => {

View file

@ -14,40 +14,36 @@ import {
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import { first, get, isEmpty } from 'lodash';
import React, { Fragment } from 'react';
import { get } from 'lodash';
import React from 'react';
import { RRRRenderResponse } from 'react-redux-request';
import styled from 'styled-components';
import {
ERROR_EXC_HANDLED,
HTTP_REQUEST_METHOD,
TRANSACTION_ID,
URL_FULL,
USER_ID
} from 'x-pack/plugins/apm/common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
import { idx } from 'x-pack/plugins/apm/common/idx';
import { KibanaLink } from 'x-pack/plugins/apm/public/components/shared/Links/KibanaLink';
import {
fromQuery,
history,
toQuery
} from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
import { legacyEncodeURIComponent } from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
import { STATUS } from 'x-pack/plugins/apm/public/constants';
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
import { ErrorGroupAPIResponse } from 'x-pack/plugins/apm/server/lib/errors/get_error_group';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { borderRadius, px, unit, units } from '../../../../style/variables';
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
import {
getPropertyTabNames,
PropertiesTable
} from '../../../shared/PropertiesTable';
import { Tab } from '../../../shared/PropertiesTable/propertyConfig';
import { PropertiesTable } from '../../../shared/PropertiesTable';
import { getCurrentTab } from '../../../shared/PropertiesTable/tabConfig';
import { Stacktrace } from '../../../shared/Stacktrace';
import { StickyProperties } from '../../../shared/StickyProperties';
import {
ErrorTab,
exceptionStacktraceTab,
getTabs,
logStacktraceTab
} from './ErrorTabs';
import { StickyErrorProperties } from './StickyErrorProperties';
const PaddedContainer = styled.div`
padding: ${px(units.plus)} ${px(units.plus)} 0;
`;
const Container = styled.div`
position: relative;
@ -64,26 +60,6 @@ const HeaderContainer = styled.div`
margin-bottom: ${px(unit)};
`;
const PaddedContainer = styled.div`
padding: ${px(units.plus)} ${px(units.plus)} 0;
`;
const logStacktraceTab = {
key: 'log_stacktrace',
label: i18n.translate('xpack.apm.propertiesTable.tabs.logStacktraceLabel', {
defaultMessage: 'Log stacktrace'
})
};
const exceptionStacktraceTab = {
key: 'exception_stacktrace',
label: i18n.translate(
'xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel',
{
defaultMessage: 'Exception stacktrace'
}
)
};
interface Props {
errorGroup: RRRRenderResponse<ErrorGroupAPIResponse>;
urlParams: IUrlParams;
@ -100,64 +76,6 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
return null;
}
const stickyProperties = [
{
fieldName: '@timestamp',
label: i18n.translate('xpack.apm.errorGroupDetails.timestampLabel', {
defaultMessage: 'Timestamp'
}),
val: error['@timestamp'],
width: '50%'
},
{
fieldName: URL_FULL,
label: 'URL',
val:
idx(error, _ => _.context.page.url) ||
idx(error, _ => _.url.full) ||
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:
String(idx(error, _ => _.error.exception[0].handled)) ||
NOT_AVAILABLE_LABEL,
width: '25%'
},
{
fieldName: TRANSACTION_ID,
label: i18n.translate(
'xpack.apm.errorGroupDetails.transactionSampleIdLabel',
{
defaultMessage: 'Transaction sample ID'
}
),
val: <TransactionLink transaction={transaction} error={error} />,
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%'
}
];
const tabs = getTabs(error);
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
@ -189,7 +107,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
</HeaderContainer>
<PaddedContainer>
<StickyProperties stickyProperties={stickyProperties} />
<StickyErrorProperties error={error} transaction={transaction} />
</PaddedContainer>
<EuiSpacer />
@ -223,46 +141,12 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
);
}
interface TransactionLinkProps {
error: APMError;
transaction?: Transaction;
}
function TransactionLink({ error, transaction }: TransactionLinkProps) {
if (!transaction) {
return <Fragment>{NOT_AVAILABLE_LABEL}</Fragment>;
}
const isSampled = idx(error, _ => _.transaction.sampled);
if (!isSampled) {
return <Fragment>{transaction.transaction.id}</Fragment>;
}
const path = `/${
transaction.service.name
}/transactions/${legacyEncodeURIComponent(
transaction.transaction.type
)}/${legacyEncodeURIComponent(transaction.transaction.name)}`;
return (
<KibanaLink
hash={path}
query={{
transactionId: transaction.transaction.id,
traceId: idx(transaction, _ => _.trace.id)
}}
>
{transaction.transaction.id}
</KibanaLink>
);
}
export function TabContent({
error,
currentTab
}: {
error: APMError;
currentTab: Tab;
currentTab: ErrorTab;
}) {
const codeLanguage = error.service.name;
const agentName = error.agent.name;
@ -289,18 +173,3 @@ export function TabContent({
);
}
}
// Ensure the selected tab exists or use the first
export function getCurrentTab(tabs: Tab[] = [], selectedTabKey?: string) {
const selectedTab = tabs.find(({ key }) => key === selectedTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
}
export function getTabs(error: APMError) {
const hasLogStacktrace = !isEmpty(idx(error, _ => _.error.log.stacktrace));
return [
...(hasLogStacktrace ? [logStacktraceTab] : []),
exceptionStacktraceTab,
...getPropertyTabNames(error)
];
}

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs';
import chrome from 'ui/chrome';
import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs';
jest.mock(
'ui/chrome',

View file

@ -6,7 +6,7 @@
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { Location } from 'history';
import { first, get } from 'lodash';
import { get } from 'lodash';
import React from 'react';
import {
fromQuery,
@ -15,21 +15,11 @@ import {
} from 'x-pack/plugins/apm/public/components/shared/Links/url_helpers';
import { Transaction } from '../../../../../typings/es_schemas/Transaction';
import { IUrlParams } from '../../../../store/urlParams';
import { PropertiesTable } from '../../../shared/PropertiesTable';
import {
getPropertyTabNames,
PropertiesTable
} from '../../../shared/PropertiesTable';
import { Tab } from '../../../shared/PropertiesTable/propertyConfig';
// Ensure the selected tab exists or use the first
function getCurrentTab(tabs: Tab[] = [], selectedTabKey?: string) {
const selectedTab = tabs.find(({ key }) => key === selectedTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
}
function getTabs(transaction: Transaction) {
return getPropertyTabNames(transaction);
}
getCurrentTab,
getTabsFromObject
} from '../../../shared/PropertiesTable/tabConfig';
interface Props {
location: Location;
@ -42,7 +32,7 @@ export const TransactionPropertiesTableForFlyout: React.SFC<Props> = ({
transaction,
urlParams
}) => {
const tabs = getTabs(transaction);
const tabs = getTabsFromObject(transaction);
const currentTab = getCurrentTab(tabs, urlParams.flyoutDetailTab);
const agentName = transaction.agent.name;

View file

@ -7,7 +7,7 @@
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import { first, get } from 'lodash';
import { get } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import {
@ -18,11 +18,11 @@ import {
import { Transaction } from '../../../../../typings/es_schemas/Transaction';
import { IUrlParams } from '../../../../store/urlParams';
import { px, units } from '../../../../style/variables';
import { PropertiesTable } from '../../../shared/PropertiesTable';
import {
getPropertyTabNames,
PropertiesTable
} from '../../../shared/PropertiesTable';
import { Tab } from '../../../shared/PropertiesTable/propertyConfig';
getCurrentTab,
getTabsFromObject
} from '../../../shared/PropertiesTable/tabConfig';
import { WaterfallContainer } from './WaterfallContainer';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
@ -30,41 +30,34 @@ const TableContainer = styled.div`
padding: ${px(units.plus)} ${px(units.plus)} 0;
`;
// Ensure the selected tab exists or use the first
function getCurrentTab(tabs: Tab[] = [], selectedTabKey?: string) {
const selectedTab = tabs.find(({ key }) => key === selectedTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
interface TimelineTab {
key: 'timeline';
label: string;
}
const timelineTab = {
const timelineTab: TimelineTab = {
key: 'timeline',
label: i18n.translate('xpack.apm.propertiesTable.tabs.timelineLabel', {
defaultMessage: 'Timeline'
})
};
function getTabs(transaction: Transaction) {
return [timelineTab, ...getPropertyTabNames(transaction)];
}
interface TransactionPropertiesTableProps {
interface Props {
location: Location;
transaction: Transaction;
urlParams: IUrlParams;
waterfall: IWaterfall;
}
export function TransactionPropertiesTable({
export function TransactionTabs({
location,
transaction,
urlParams,
waterfall
}: TransactionPropertiesTableProps) {
const tabs = getTabs(transaction);
}: Props) {
const tabs = [timelineTab, ...getTabsFromObject(transaction)];
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
const agentName = transaction.agent.name;
const isTimelineTab = currentTab.key === timelineTab.key;
return (
<div>
@ -92,7 +85,7 @@ export function TransactionPropertiesTable({
<EuiSpacer />
{isTimelineTab ? (
{currentTab.key === timelineTab.key ? (
<WaterfallContainer
transaction={transaction}
location={location}

View file

@ -21,7 +21,7 @@ import { IUrlParams } from '../../../../store/urlParams';
import { TransactionLink } from '../../../shared/Links/TransactionLink';
import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu';
import { StickyTransactionProperties } from './StickyTransactionProperties';
import { TransactionPropertiesTable } from './TransactionPropertiesTable';
import { TransactionTabs } from './TransactionTabs';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
function MaybeViewTraceLink({
@ -149,7 +149,7 @@ export const Transaction: React.SFC<Props> = ({
<EuiSpacer />
<TransactionPropertiesTable
<TransactionTabs
transaction={transaction}
location={location}
urlParams={urlParams}

View file

@ -11,8 +11,7 @@ import styled from 'styled-components';
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
import { StringMap } from '../../../../typings/common';
import { fontFamilyCode, fontSize, px, units } from '../../../style/variables';
export type KeySorter = (data: StringMap, parentKey?: string) => string[];
import { sortKeysByConfig } from './tabConfig';
const Table = styled.table`
font-family: ${fontFamilyCode};
@ -79,13 +78,11 @@ export function FormattedValue({ value }: { value: any }): JSX.Element {
export function NestedValue({
parentKey,
value,
depth,
keySorter
depth
}: {
value: unknown;
depth: number;
parentKey?: string;
keySorter?: KeySorter;
}): JSX.Element {
const MAX_LEVEL = 3;
if (depth < MAX_LEVEL && isObject(value)) {
@ -93,7 +90,6 @@ export function NestedValue({
<NestedKeyValueTable
data={value as StringMap}
parentKey={parentKey}
keySorter={keySorter}
depth={depth + 1}
/>
);
@ -105,29 +101,22 @@ export function NestedValue({
export function NestedKeyValueTable({
data,
parentKey,
keySorter = Object.keys,
depth
}: {
data: StringMap<unknown>;
parentKey?: string;
keySorter?: KeySorter;
depth: number;
}): JSX.Element {
return (
<Table>
<tbody>
{keySorter(data, parentKey).map(key => (
{sortKeysByConfig(data, parentKey).map(key => (
<Row key={key}>
<Cell>
<FormattedKey k={key} value={data[key]} />
</Cell>
<Cell>
<NestedValue
parentKey={key}
value={data[key]}
keySorter={keySorter}
depth={depth}
/>
<NestedValue parentKey={key} value={data[key]} depth={depth} />
</Cell>
</Row>
))}

View file

@ -6,32 +6,18 @@
import { shallow } from 'enzyme';
import React from 'react';
import {
AgentFeatureTipMessage,
getPropertyTabNames,
PropertiesTable,
sortKeysByConfig
} from '..';
import { PropertiesTable, TabHelpMessage } from '..';
import * as agentDocs from '../../../../utils/documentation/agents';
import * as propertyConfig from '../propertyConfig';
describe('PropertiesTable', () => {
beforeEach(() => {
mockPropertyConfig();
});
afterEach(() => {
unMockPropertyConfig();
});
describe('PropertiesTable component', () => {
it('should render with data', () => {
expect(
shallow(
<PropertiesTable
propData={{ a: 'hello', b: 'bananas' }}
propKey="testPropKey"
agentName="testAgentName"
propKey="kubernetes"
agentName="java"
/>
)
).toMatchSnapshot();
@ -39,9 +25,7 @@ describe('PropertiesTable', () => {
it("should render empty when data isn't present", () => {
expect(
shallow(
<PropertiesTable propKey="testPropKey" agentName="testAgentName" />
)
shallow(<PropertiesTable propKey="kubernetes" agentName="java" />)
).toMatchSnapshot();
});
@ -50,126 +34,40 @@ describe('PropertiesTable', () => {
shallow(
<PropertiesTable
propData={{}}
propKey="testPropKey"
agentName="testAgentName"
propKey="kubernetes"
agentName="java"
/>
)
).toMatchSnapshot();
});
});
describe('sortKeysByConfig', () => {
const testData = {
color: 'blue',
name: 'Jess',
age: '39',
numbers: [1, 2, 3],
_id: '44x099z'
};
it('should sort with presorted keys first', () => {
expect(sortKeysByConfig(testData, 'testProperty')).toEqual([
'name',
'age',
'_id',
'color',
'numbers'
]);
});
it('should alpha-sort keys when there is no config value found', () => {
expect(sortKeysByConfig(testData, 'nonExistentKey')).toEqual([
'_id',
'age',
'color',
'name',
'numbers'
]);
});
});
describe('getPropertyTabNames', () => {
it('should return selected and required keys only', () => {
const expectedTabsConfig = [
{
key: 'testProperty',
label: 'testPropertyLabel'
},
{
key: 'requiredProperty',
label: 'requiredPropertyLabel'
}
];
expect(getPropertyTabNames({ testProperty: {} } as any)).toEqual(
expectedTabsConfig
);
});
});
describe('AgentFeatureTipMessage component', () => {
const featureName = 'user';
describe('TabHelpMessage component', () => {
const tabKey = 'user';
const agentName = 'nodejs';
it('should render when docs are returned', () => {
jest
.spyOn(agentDocs, 'getAgentFeatureDocsUrl')
.spyOn(agentDocs, 'getAgentDocUrlForTab')
.mockImplementation(() => 'mock-url');
expect(
shallow(
<AgentFeatureTipMessage
featureName={featureName}
agentName={agentName}
/>
)
shallow(<TabHelpMessage tabKey={tabKey} agentName={agentName} />)
).toMatchSnapshot();
expect(agentDocs.getAgentFeatureDocsUrl).toHaveBeenCalledWith(
featureName,
expect(agentDocs.getAgentDocUrlForTab).toHaveBeenCalledWith(
tabKey,
agentName
);
});
it('should render null empty string when no docs are returned', () => {
jest
.spyOn(agentDocs, 'getAgentFeatureDocsUrl')
.spyOn(agentDocs, 'getAgentDocUrlForTab')
.mockImplementation(() => undefined);
expect(
shallow(
<AgentFeatureTipMessage
featureName={featureName}
agentName={agentName}
/>
)
shallow(<TabHelpMessage tabKey={tabKey} agentName={agentName} />)
).toMatchSnapshot();
});
});
});
function mockPropertyConfig() {
// @ts-ignore
propertyConfig.PROPERTY_CONFIG = [
{
key: 'testProperty',
label: 'testPropertyLabel',
required: false,
presortedKeys: ['name', 'age']
},
{
key: 'optionalProperty',
label: 'optionalPropertyLabel',
required: false
},
{
key: 'requiredProperty',
label: 'requiredPropertyLabel',
required: true
}
];
}
const originalPropertyConfig = propertyConfig.PROPERTY_CONFIG;
function unMockPropertyConfig() {
// @ts-ignore
propertyConfig.PROPERTY_CONFIG = originalPropertyConfig;
}

View file

@ -187,7 +187,6 @@ exports[`NestedKeyValueTable component should render with data 1`] = `
<styled.td>
<NestedValue
depth={0}
keySorter={[Function]}
parentKey="a"
value={1}
/>
@ -205,7 +204,6 @@ exports[`NestedKeyValueTable component should render with data 1`] = `
<styled.td>
<NestedValue
depth={0}
keySorter={[Function]}
parentKey="b"
value={2}
/>
@ -229,7 +227,6 @@ exports[`NestedKeyValueTable component should render with data 1`] = `
<styled.td>
<NestedValue
depth={0}
keySorter={[Function]}
parentKey="c"
value={
Array [
@ -258,7 +255,6 @@ exports[`NestedKeyValueTable component should render with data 1`] = `
<styled.td>
<NestedValue
depth={0}
keySorter={[Function]}
parentKey="d"
value={
Object {

View file

@ -1,8 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PropertiesTable AgentFeatureTipMessage component should render null empty string when no docs are returned 1`] = `""`;
exports[`PropertiesTable PropertiesTable component should render empty when data isn't present 1`] = `
<styled.div>
<Styled(styled.div)>
No data available
</Styled(styled.div)>
<TabHelpMessage
agentName="java"
tabKey="kubernetes"
/>
</styled.div>
`;
exports[`PropertiesTable AgentFeatureTipMessage component should render when docs are returned 1`] = `
exports[`PropertiesTable PropertiesTable component should render with data 1`] = `
<styled.div>
<NestedKeyValueTable
data={
Object {
"a": "hello",
"b": "bananas",
}
}
depth={1}
parentKey="kubernetes"
/>
<TabHelpMessage
agentName="java"
tabKey="kubernetes"
/>
</styled.div>
`;
exports[`PropertiesTable PropertiesTable component should still render NestedKeyValueTable even when data has no keys 1`] = `
<styled.div>
<NestedKeyValueTable
data={Object {}}
depth={1}
parentKey="kubernetes"
/>
<TabHelpMessage
agentName="java"
tabKey="kubernetes"
/>
</styled.div>
`;
exports[`PropertiesTable TabHelpMessage component should render null empty string when no docs are returned 1`] = `""`;
exports[`PropertiesTable TabHelpMessage component should render when docs are returned 1`] = `
<styled.div>
<Styled(EuiIcon)
type="iInCircle"
@ -20,50 +65,3 @@ exports[`PropertiesTable AgentFeatureTipMessage component should render when doc
</EuiLink>
</styled.div>
`;
exports[`PropertiesTable PropertiesTable component should render empty when data isn't present 1`] = `
<styled.div>
<Styled(styled.div)>
No data available
</Styled(styled.div)>
<AgentFeatureTipMessage
agentName="testAgentName"
featureName="testPropKey"
/>
</styled.div>
`;
exports[`PropertiesTable PropertiesTable component should render with data 1`] = `
<styled.div>
<NestedKeyValueTable
data={
Object {
"a": "hello",
"b": "bananas",
}
}
depth={1}
keySorter={[Function]}
parentKey="testPropKey"
/>
<AgentFeatureTipMessage
agentName="testAgentName"
featureName="testPropKey"
/>
</styled.div>
`;
exports[`PropertiesTable PropertiesTable component should still render NestedKeyValueTable even when data has no keys 1`] = `
<styled.div>
<NestedKeyValueTable
data={Object {}}
depth={1}
keySorter={[Function]}
parentKey="testPropKey"
/>
<AgentFeatureTipMessage
agentName="testAgentName"
featureName="testPropKey"
/>
</styled.div>
`;

View file

@ -0,0 +1,94 @@
/*
* 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 * as propertyConfig from '../tabConfig';
const { getTabsFromObject, sortKeysByConfig } = propertyConfig;
describe('tabConfig', () => {
beforeEach(() => {
mockPropertyConfig();
});
afterEach(() => {
unMockPropertyConfig();
});
describe('getTabsFromObject', () => {
it('should return selected and required keys only', () => {
const expectedTabs = [
{
key: 'testProperty',
label: 'testPropertyLabel'
},
{
key: 'requiredProperty',
label: 'requiredPropertyLabel'
}
];
expect(getTabsFromObject({ testProperty: {} } as any)).toEqual(
expectedTabs
);
});
});
describe('sortKeysByConfig', () => {
const testData = {
color: 'blue',
name: 'Jess',
age: '39',
numbers: [1, 2, 3],
_id: '44x099z'
};
it('should sort with presorted keys first', () => {
expect(sortKeysByConfig(testData, 'testProperty')).toEqual([
'name',
'age',
'_id',
'color',
'numbers'
]);
});
it('should alpha-sort keys when there is no config value found', () => {
expect(sortKeysByConfig(testData, 'nonExistentKey')).toEqual([
'_id',
'age',
'color',
'name',
'numbers'
]);
});
});
});
function mockPropertyConfig() {
// @ts-ignore
propertyConfig.TAB_CONFIG = [
{
key: 'testProperty',
label: 'testPropertyLabel',
required: false,
presortedKeys: ['name', 'age']
},
{
key: 'optionalProperty',
label: 'optionalPropertyLabel',
required: false
},
{
key: 'requiredProperty',
label: 'requiredPropertyLabel',
required: true
}
];
}
const originalPropertyConfig = propertyConfig.TAB_CONFIG;
function unMockPropertyConfig() {
// @ts-ignore
propertyConfig.TAB_CONFIG = originalPropertyConfig;
}

View file

@ -8,16 +8,14 @@ import { EuiIcon } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { get, has, indexBy, uniq } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { AgentName } from 'x-pack/plugins/apm/typings/es_schemas/APMDoc';
import { StringMap } from '../../../../typings/common';
import { fontSize, fontSizes, px, unit, units } from '../../../style/variables';
import { getAgentFeatureDocsUrl } from '../../../utils/documentation/agents';
import { KeySorter, NestedKeyValueTable } from './NestedKeyValueTable';
import { PROPERTY_CONFIG } from './propertyConfig';
import { getAgentDocUrlForTab } from '../../../utils/documentation/agents';
import { NestedKeyValueTable } from './NestedKeyValueTable';
import { PropertyTabKey } from './tabConfig';
const TableContainer = styled.div`
padding-bottom: ${px(units.double)};
@ -40,14 +38,8 @@ const EuiIconWithSpace = styled(EuiIcon)`
margin-right: ${px(units.half)};
`;
export function getPropertyTabNames(obj: Transaction | APMError) {
return PROPERTY_CONFIG.filter(
({ key, required }) => required || has(obj, key)
).map(({ key, label }) => ({ key, label }));
}
function getAgentFeatureText(featureName: string) {
switch (featureName) {
function getTabHelpText(tabKey: PropertyTabKey) {
switch (tabKey) {
case 'user':
return i18n.translate(
'xpack.apm.propertiesTable.userTab.agentFeatureText',
@ -56,15 +48,16 @@ function getAgentFeatureText(featureName: string) {
'You can configure your agent to add contextual information about your users.'
}
);
case 'tags':
case 'labels':
return i18n.translate(
'xpack.apm.propertiesTable.tagsTab.agentFeatureText',
'xpack.apm.propertiesTable.labelsTab.agentFeatureText',
{
defaultMessage:
'You can configure your agent to add filterable tags on transactions.'
}
);
case 'custom':
case 'transaction.custom':
case 'error.custom':
return i18n.translate(
'xpack.apm.propertiesTable.customTab.agentFeatureText',
{
@ -75,14 +68,17 @@ function getAgentFeatureText(featureName: string) {
}
}
export function AgentFeatureTipMessage({
featureName,
export function TabHelpMessage({
tabKey,
agentName
}: {
featureName: string;
agentName?: string;
tabKey?: PropertyTabKey;
agentName?: AgentName;
}) {
const docsUrl = getAgentFeatureDocsUrl(featureName, agentName);
if (!tabKey) {
return null;
}
const docsUrl = getAgentDocUrlForTab(tabKey, agentName);
if (!docsUrl) {
return null;
}
@ -90,7 +86,7 @@ export function AgentFeatureTipMessage({
return (
<TableInfo>
<EuiIconWithSpace type="iInCircle" />
{getAgentFeatureText(featureName)}{' '}
{getTabHelpText(tabKey)}{' '}
<EuiLink target="_blank" rel="noopener" href={docsUrl}>
{i18n.translate(
'xpack.apm.propertiesTable.agentFeature.learnMoreLinkLabel',
@ -101,34 +97,19 @@ export function AgentFeatureTipMessage({
);
}
export const sortKeysByConfig: KeySorter = (object, currentKey) => {
const indexedPropertyConfig = indexBy(PROPERTY_CONFIG, 'key');
const presorted = get(
indexedPropertyConfig,
`${currentKey}.presortedKeys`,
[]
);
return uniq([...presorted, ...Object.keys(object).sort()]);
};
export function PropertiesTable({
propData,
propKey,
agentName
}: {
propData?: StringMap<any>;
propKey: string;
agentName?: string;
propKey?: PropertyTabKey;
agentName?: AgentName;
}) {
return (
<TableContainer>
{propData ? (
<NestedKeyValueTable
data={propData}
parentKey={propKey}
keySorter={sortKeysByConfig}
depth={1}
/>
<NestedKeyValueTable data={propData} parentKey={propKey} depth={1} />
) : (
<TableInfoHeader>
{i18n.translate(
@ -137,7 +118,8 @@ export function PropertiesTable({
)}
</TableInfoHeader>
)}
<AgentFeatureTipMessage featureName={propKey} agentName={agentName} />
<TabHelpMessage tabKey={propKey} agentName={agentName} />
</TableContainer>
);
}

View file

@ -0,0 +1,148 @@
/*
* 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 { get, indexBy, uniq } from 'lodash';
import { first, has } from 'lodash';
import { StringMap } from 'x-pack/plugins/apm/typings/common';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
export type PropertyTabKey =
| keyof Transaction
| keyof APMError
| 'transaction.custom'
| 'error.custom';
export interface PropertyTab {
key: PropertyTabKey;
label: string;
}
interface TabConfig extends PropertyTab {
required: boolean;
presortedKeys: string[];
}
export function getTabsFromObject(obj: Transaction | APMError): PropertyTab[] {
return TAB_CONFIG.filter(
({ key, required }) => required || has(obj, key)
).map(({ key, label }) => ({ key, label }));
}
export type KeySorter = (data: StringMap, parentKey?: string) => string[];
export const sortKeysByConfig: KeySorter = (object, currentKey) => {
const indexedPropertyConfig = indexBy(TAB_CONFIG, 'key');
const presorted = get(
indexedPropertyConfig,
`${currentKey}.presortedKeys`,
[]
);
return uniq([...presorted, ...Object.keys(object).sort()]);
};
export function getCurrentTab<T extends { key: string; label: string }>(
tabs: T[] = [],
currentTabKey: string | undefined
): T {
const selectedTab = tabs.find(({ key }) => key === currentTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
}
export const TAB_CONFIG: TabConfig[] = [
{
key: 'http',
label: i18n.translate('xpack.apm.propertiesTable.tabs.httpLabel', {
defaultMessage: 'HTTP'
}),
required: false,
presortedKeys: []
},
{
key: 'host',
label: i18n.translate('xpack.apm.propertiesTable.tabs.hostLabel', {
defaultMessage: 'Host'
}),
required: false,
presortedKeys: ['hostname', 'architecture', 'platform']
},
{
key: 'service',
label: i18n.translate('xpack.apm.propertiesTable.tabs.serviceLabel', {
defaultMessage: 'Service'
}),
required: false,
presortedKeys: ['runtime', 'framework', 'version']
},
{
key: 'process',
label: i18n.translate('xpack.apm.propertiesTable.tabs.processLabel', {
defaultMessage: 'Process'
}),
required: false,
presortedKeys: ['pid', 'title', 'args']
},
{
key: 'agent',
label: i18n.translate('xpack.apm.propertiesTable.tabs.agentLabel', {
defaultMessage: 'Agent'
}),
required: false,
presortedKeys: []
},
{
key: 'url',
label: i18n.translate('xpack.apm.propertiesTable.tabs.urlLabel', {
defaultMessage: 'URL'
}),
required: false,
presortedKeys: []
},
{
key: 'container',
label: i18n.translate('xpack.apm.propertiesTable.tabs.containerLabel', {
defaultMessage: 'Container'
}),
required: false,
presortedKeys: []
},
{
key: 'user',
label: i18n.translate('xpack.apm.propertiesTable.tabs.userLabel', {
defaultMessage: 'User'
}),
required: true,
presortedKeys: ['id', 'username', 'email']
},
{
key: 'labels',
label: i18n.translate('xpack.apm.propertiesTable.tabs.labelsLabel', {
defaultMessage: 'Labels'
}),
required: true,
presortedKeys: []
},
{
key: 'transaction.custom',
label: i18n.translate(
'xpack.apm.propertiesTable.tabs.transactionCustomLabel',
{
defaultMessage: 'Custom'
}
),
required: false,
presortedKeys: []
},
{
key: 'error.custom',
label: i18n.translate('xpack.apm.propertiesTable.tabs.errorCustomLabel', {
defaultMessage: 'Custom'
}),
required: false,
presortedKeys: []
}
];

View file

@ -70,7 +70,7 @@ export class Variables extends React.Component<Props> {
</VariablesToggle>
{this.state.isVisible && (
<VariablesTableContainer>
<PropertiesTable propData={this.props.vars} propKey={'custom'} />
<PropertiesTable propData={this.props.vars} />
</VariablesTableContainer>
)}
</VariablesContainer>

View file

@ -8,13 +8,10 @@ import { shallow } from 'enzyme';
import 'jest-styled-components';
import React from 'react';
import { TransactionActionMenu } from '../TransactionActionMenu';
import { props } from './transactionActionMenuProps';
import { location, transaction } from './mockData';
describe('TransactionActionMenu component', () => {
it('should render with data', () => {
const transaction = props.transaction;
const location = props.location;
expect(
shallow(
<TransactionActionMenu transaction={transaction} location={location} />

View file

@ -7,9 +7,9 @@
import { Location } from 'history';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
const transaction: Transaction = {
export const transaction: Transaction = {
agent: {
name: '227453131a17',
name: 'java',
version: '7.0.0'
},
processor: {
@ -77,15 +77,10 @@ const transaction: Transaction = {
}
};
const location: Location = {
export const location: Location = {
state: '',
pathname:
'/opbeans-go/transactions/request/GET~20~2Fapi~2Fproducts~2F~3Aid~2Fcustomers',
search: '?_g=()&flyoutDetailTab=undefined&waterfallItemId=8b60bd32ecc6e150',
hash: ''
};
export const props = {
transaction,
location
};

View file

@ -4,45 +4,52 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { APMDoc } from 'x-pack/plugins/apm/typings/es_schemas/APMDoc';
import { PropertyTabKey } from '../../components/shared/PropertiesTable/tabConfig';
const AGENT_URL_ROOT = 'https://www.elastic.co/guide/en/apm/agent';
interface AgentNamedValues {
[agentName: string]: string;
}
type AgentName = APMDoc['agent']['name'];
type DocUrls = {
[tabKey in PropertyTabKey]?: { [agentName in AgentName]: string | undefined }
};
const APM_AGENT_FEATURE_DOCS: {
[featureName: string]: AgentNamedValues;
} = {
const customUrls = {
'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-custom-context`,
'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-custom-context`,
java: undefined,
nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-custom-context`,
python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-set-custom-context`,
ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_adding_custom_context`
};
const AGENT_DOC_URLS: DocUrls = {
user: {
'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-user-context`,
'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-user-context`,
java: `${AGENT_URL_ROOT}/java/0.7/public-api.html#api-transaction-set-user`,
nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-user-context`,
python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-set-user-context`,
ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_providing_info_about_the_user`,
'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-user-context`,
'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-user-context`
ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_providing_info_about_the_user`
},
tags: {
labels: {
'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-tags`,
'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-tags`,
java: `${AGENT_URL_ROOT}/java/0.7/public-api.html#api-transaction-add-tag`,
nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-tag`,
python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-tag`,
ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_adding_tags`,
'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-tags`,
'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-tags`
ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_adding_tags`
},
custom: {
nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-custom-context`,
python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-set-custom-context`,
ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_adding_custom_context`,
'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-custom-context`,
'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-custom-context`
}
'transaction.custom': customUrls,
'error.custom': customUrls
};
export function getAgentFeatureDocsUrl(
featureName: string,
agentName?: string
export function getAgentDocUrlForTab(
tabKey: PropertyTabKey,
agentName?: AgentName
) {
if (APM_AGENT_FEATURE_DOCS[featureName] && agentName) {
return APM_AGENT_FEATURE_DOCS[featureName][agentName];
const agentUrls = AGENT_DOC_URLS[tabKey];
if (agentUrls && agentName) {
return agentUrls[agentName];
}
}

View file

@ -4,11 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
// agent names can be any string. This list only defines the official agents that we might want to
// target specifically eg. linking to their documentation
export type AgentName =
| 'java'
| 'nodejs'
| 'python'
| 'ruby'
| 'js-react'
| 'js-base';
// all documents types extend APMDoc and inherit all properties
export interface APMDoc {
'@timestamp': string;
agent: {
name: string;
name: AgentName;
version: string;
};
timestamp: { us: number };

View file

@ -3371,7 +3371,6 @@
"xpack.apm.propertiesTable.tabs.serviceLabel": "服务",
"xpack.apm.propertiesTable.tabs.timelineLabel": "时间线",
"xpack.apm.propertiesTable.tabs.userLabel": "用户",
"xpack.apm.propertiesTable.tagsTab.agentFeatureText": "您可以配置代理以添加有关事务上的可筛选标记。",
"xpack.apm.propertiesTable.userTab.agentFeatureText": "您可以配置代理以添加有关用户的上下文信息。",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "当前有 {serviceName}{transactionType})的作业正在运行。",
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业",