mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* Refactors property table usage with new components and tests * Changes default tab for transaction detail view * Small refactors to property table * Review feedback * Updates translations * Refactors metadata and adds tests * Rearranges documentation links to prefer declarative component * Removes unused component * Improves metadata component tests and removes giant snapshots
This commit is contained in:
parent
239f604f94
commit
b86e285dab
40 changed files with 987 additions and 1464 deletions
|
@ -378,6 +378,7 @@
|
|||
"istanbul-instrumenter-loader": "3.0.1",
|
||||
"jest": "^24.1.0",
|
||||
"jest-cli": "^24.1.0",
|
||||
"jest-dom": "^3.1.3",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"jimp": "0.2.28",
|
||||
"json5": "^1.0.1",
|
||||
|
|
|
@ -8,31 +8,20 @@ import { i18n } from '@kbn/i18n';
|
|||
import { isEmpty } from 'lodash';
|
||||
import { idx } from '../../../../../common/idx';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
import {
|
||||
getTabsFromObject,
|
||||
PropertyTab
|
||||
} from '../../../shared/PropertiesTable/tabConfig';
|
||||
|
||||
export type ErrorTab = PropertyTab | ExceptionTab | LogTab;
|
||||
|
||||
interface LogTab {
|
||||
key: 'log_stacktrace';
|
||||
export interface ErrorTab {
|
||||
key: 'log_stacktrace' | 'exception_stacktrace' | 'metadata';
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const logStacktraceTab: LogTab = {
|
||||
export const logStacktraceTab: ErrorTab = {
|
||||
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 = {
|
||||
export const exceptionStacktraceTab: ErrorTab = {
|
||||
key: 'exception_stacktrace',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel',
|
||||
|
@ -42,11 +31,18 @@ export const exceptionStacktraceTab: ExceptionTab = {
|
|||
)
|
||||
};
|
||||
|
||||
export const metadataTab: ErrorTab = {
|
||||
key: 'metadata',
|
||||
label: i18n.translate('xpack.apm.propertiesTable.tabs.metadataLabel', {
|
||||
defaultMessage: 'Metadata'
|
||||
})
|
||||
};
|
||||
|
||||
export function getTabs(error: APMError) {
|
||||
const hasLogStacktrace = !isEmpty(idx(error, _ => _.error.log.stacktrace));
|
||||
return [
|
||||
...(hasLogStacktrace ? [logStacktraceTab] : []),
|
||||
exceptionStacktraceTab,
|
||||
...getTabsFromObject(error)
|
||||
metadataTab
|
||||
];
|
||||
}
|
||||
|
|
|
@ -75,26 +75,10 @@ exports[`DetailView should render tabs 1`] = `
|
|||
<EuiTab
|
||||
disabled={false}
|
||||
isSelected={false}
|
||||
key="service"
|
||||
key="metadata"
|
||||
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
|
||||
Metadata
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
`;
|
||||
|
|
|
@ -14,9 +14,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { first } from 'lodash';
|
||||
import { idx } from '../../../../../common/idx';
|
||||
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
|
@ -24,8 +24,7 @@ import { IUrlParams } from '../../../../store/urlParams';
|
|||
import { px, unit } from '../../../../style/variables';
|
||||
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { PropertiesTable } from '../../../shared/PropertiesTable';
|
||||
import { getCurrentTab } from '../../../shared/PropertiesTable/tabConfig';
|
||||
import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata';
|
||||
import { Stacktrace } from '../../../shared/Stacktrace';
|
||||
import {
|
||||
ErrorTab,
|
||||
|
@ -48,6 +47,15 @@ interface Props {
|
|||
location: Location;
|
||||
}
|
||||
|
||||
// TODO: Move query-string-based tabs into a re-usable component?
|
||||
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 function DetailView({ errorGroup, urlParams, location }: Props) {
|
||||
const { transaction, error, occurrencesCount } = errorGroup;
|
||||
|
||||
|
@ -124,7 +132,6 @@ export function TabContent({
|
|||
currentTab: ErrorTab;
|
||||
}) {
|
||||
const codeLanguage = error.service.name;
|
||||
const agentName = error.agent.name;
|
||||
const excStackframes = idx(error, _ => _.error.exception[0].stacktrace);
|
||||
const logStackframes = idx(error, _ => _.error.exception[0].stacktrace);
|
||||
|
||||
|
@ -138,13 +145,6 @@ export function TabContent({
|
|||
<Stacktrace stackframes={excStackframes} codeLanguage={codeLanguage} />
|
||||
);
|
||||
default:
|
||||
const propData = get(error, currentTab.key);
|
||||
return (
|
||||
<PropertiesTable
|
||||
propData={propData}
|
||||
propKey={currentTab.key}
|
||||
agentName={agentName}
|
||||
/>
|
||||
);
|
||||
return <ErrorMetadata error={error} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,9 +33,9 @@ import styled from 'styled-components';
|
|||
import chrome from 'ui/chrome';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { XPACK_DOCS } from '../../../../utils/documentation/xpack';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch';
|
||||
import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
|
||||
|
||||
type ScheduleKey = keyof Schedule;
|
||||
|
||||
|
@ -297,14 +297,18 @@ export class WatcherFlyout extends Component<
|
|||
To learn more about Watcher, please read our {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackWatcher}>
|
||||
<ElasticDocsLink
|
||||
target="_blank"
|
||||
section="/x-pack"
|
||||
path="/watcher-getting-started.html"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</ElasticDocsLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
@ -499,14 +503,18 @@ export class WatcherFlyout extends Component<
|
|||
defaultMessage="If you have not configured email, please see the {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackEmails}>
|
||||
<ElasticDocsLink
|
||||
target="_blank"
|
||||
section="/x-pack"
|
||||
path="/actions-email.html#configuring-email"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</ElasticDocsLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,66 +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 { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { PropertiesTable } from '../../../shared/PropertiesTable';
|
||||
import {
|
||||
getCurrentTab,
|
||||
getTabsFromObject
|
||||
} from '../../../shared/PropertiesTable/tabConfig';
|
||||
|
||||
interface Props {
|
||||
location: Location;
|
||||
transaction: Transaction;
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
||||
export const TransactionPropertiesTableForFlyout: React.SFC<Props> = ({
|
||||
location,
|
||||
transaction,
|
||||
urlParams
|
||||
}) => {
|
||||
const tabs = getTabsFromObject(transaction);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.flyoutDetailTab);
|
||||
const agentName = transaction.agent.name;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiTabs>
|
||||
{tabs.map(({ key, label }) => {
|
||||
return (
|
||||
<EuiTab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
flyoutDetailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
isSelected={currentTab.key === key}
|
||||
key={key}
|
||||
>
|
||||
{label}
|
||||
</EuiTab>
|
||||
);
|
||||
})}
|
||||
</EuiTabs>
|
||||
<EuiSpacer />
|
||||
<PropertiesTable
|
||||
propData={get(transaction, currentTab.key)}
|
||||
propKey={currentTab.key}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -7,38 +7,28 @@
|
|||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { px, units } from '../../../../style/variables';
|
||||
import { HeightRetainer } from '../../../shared/HeightRetainer';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { PropertiesTable } from '../../../shared/PropertiesTable';
|
||||
import {
|
||||
getCurrentTab,
|
||||
getTabsFromObject
|
||||
} from '../../../shared/PropertiesTable/tabConfig';
|
||||
import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata';
|
||||
import { WaterfallContainer } from './WaterfallContainer';
|
||||
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const TableContainer = styled.div`
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
`;
|
||||
|
||||
interface TimelineTab {
|
||||
key: 'timeline';
|
||||
label: string;
|
||||
}
|
||||
|
||||
const timelineTab: TimelineTab = {
|
||||
const timelineTab = {
|
||||
key: 'timeline',
|
||||
label: i18n.translate('xpack.apm.propertiesTable.tabs.timelineLabel', {
|
||||
defaultMessage: 'Timeline'
|
||||
})
|
||||
};
|
||||
|
||||
const metadataTab = {
|
||||
key: 'metadata',
|
||||
label: i18n.translate('xpack.apm.propertiesTable.tabs.metadataLabel', {
|
||||
defaultMessage: 'Metadata'
|
||||
})
|
||||
};
|
||||
|
||||
interface Props {
|
||||
location: Location;
|
||||
transaction: Transaction;
|
||||
|
@ -52,14 +42,12 @@ export function TransactionTabs({
|
|||
urlParams,
|
||||
waterfall
|
||||
}: Props) {
|
||||
const tabs = [timelineTab, ...getTabsFromObject(transaction)];
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
const agentName = transaction.agent.name;
|
||||
const tabs = [timelineTab, metadataTab];
|
||||
const currentTab =
|
||||
urlParams.detailTab === metadataTab.key ? metadataTab : timelineTab;
|
||||
|
||||
return (
|
||||
<HeightRetainer
|
||||
key={`${transaction.trace.id}:${transaction.transaction.id}`}
|
||||
>
|
||||
<React.Fragment>
|
||||
<EuiTabs>
|
||||
{tabs.map(({ key, label }) => {
|
||||
return (
|
||||
|
@ -92,14 +80,8 @@ export function TransactionTabs({
|
|||
waterfall={waterfall}
|
||||
/>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<PropertiesTable
|
||||
propData={get(transaction, currentTab.key)}
|
||||
propKey={currentTab.key}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</TableContainer>
|
||||
<TransactionMetadata transaction={transaction} />
|
||||
)}
|
||||
</HeightRetainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { idx } from '../../../../../../../../common/idx';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { ElasticDocsLink } from '../../../../../../shared/Links/ElasticDocsLink';
|
||||
|
||||
export function DroppedSpansWarning({
|
||||
transactionDoc
|
||||
}: {
|
||||
transactionDoc: Transaction;
|
||||
}) {
|
||||
const dropped = idx(transactionDoc, _ => _.transaction.span_count.dropped);
|
||||
if (!dropped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiCallOut size="s">
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The APM agent that reported this transaction dropped {dropped} spans or more based on its configuration.',
|
||||
values: { dropped }
|
||||
}
|
||||
)}{' '}
|
||||
<ElasticDocsLink
|
||||
section="/apm/get-started"
|
||||
path="/transaction-spans.html#dropped-spans"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText',
|
||||
{
|
||||
defaultMessage: 'Learn more about dropped spans.'
|
||||
}
|
||||
)}
|
||||
</ElasticDocsLink>
|
||||
</EuiCallOut>
|
||||
<EuiHorizontalRule />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -5,78 +5,51 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiLink,
|
||||
EuiPortal,
|
||||
EuiSpacer,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { idx } from '../../../../../../../../common/idx';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IUrlParams } from '../../../../../../../store/urlParams';
|
||||
import { DROPPED_SPANS_DOCS } from '../../../../../../../utils/documentation/apm-get-started';
|
||||
import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu';
|
||||
import { StickyTransactionProperties } from '../../../StickyTransactionProperties';
|
||||
import { TransactionPropertiesTableForFlyout } from '../../../TransactionPropertiesTableForFlyout';
|
||||
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||
import { ResponsiveFlyout } from '../ResponsiveFlyout';
|
||||
import { TransactionMetadata } from '../../../../../../shared/MetadataTable/TransactionMetadata';
|
||||
import { DroppedSpansWarning } from './DroppedSpansWarning';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
transaction?: Transaction;
|
||||
location: Location;
|
||||
urlParams: IUrlParams;
|
||||
errorCount: number;
|
||||
traceRootDuration?: number;
|
||||
}
|
||||
|
||||
function DroppedSpansWarning({
|
||||
transactionDoc
|
||||
function TransactionPropertiesTable({
|
||||
transaction
|
||||
}: {
|
||||
transactionDoc: Transaction;
|
||||
transaction: Transaction;
|
||||
}) {
|
||||
const dropped = idx(transactionDoc, _ => _.transaction.span_count.dropped);
|
||||
if (!dropped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiCallOut size="s">
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The APM agent that reported this transaction dropped {dropped} spans or more based on its configuration.',
|
||||
values: { dropped }
|
||||
}
|
||||
)}{' '}
|
||||
<EuiLink href={DROPPED_SPANS_DOCS} target="_blank">
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText',
|
||||
{
|
||||
defaultMessage: 'Learn more about dropped spans.'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiCallOut>
|
||||
<EuiHorizontalRule />
|
||||
</React.Fragment>
|
||||
<div>
|
||||
<EuiTitle size="s">
|
||||
<h4>Metadata</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<TransactionMetadata transaction={transaction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TransactionFlyout({
|
||||
transaction: transactionDoc,
|
||||
onClose,
|
||||
location,
|
||||
urlParams,
|
||||
errorCount,
|
||||
traceRootDuration
|
||||
}: Props) {
|
||||
|
@ -117,11 +90,7 @@ export function TransactionFlyout({
|
|||
/>
|
||||
<EuiHorizontalRule />
|
||||
<DroppedSpansWarning transactionDoc={transactionDoc} />
|
||||
<TransactionPropertiesTableForFlyout
|
||||
transaction={transactionDoc}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
<TransactionPropertiesTable transaction={transactionDoc} />
|
||||
</EuiFlyoutBody>
|
||||
</ResponsiveFlyout>
|
||||
</EuiPortal>
|
||||
|
|
|
@ -87,7 +87,7 @@ export class Waterfall extends Component<Props> {
|
|||
};
|
||||
|
||||
public getFlyOut = () => {
|
||||
const { waterfall, location, urlParams } = this.props;
|
||||
const { waterfall, urlParams } = this.props;
|
||||
|
||||
const currentItem =
|
||||
urlParams.waterfallItemId &&
|
||||
|
@ -116,8 +116,6 @@ export class Waterfall extends Component<Props> {
|
|||
<TransactionFlyout
|
||||
transaction={currentItem.transaction}
|
||||
onClose={this.onCloseFlyout}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
traceRootDuration={waterfall.traceRootDuration}
|
||||
errorCount={currentItem.errorCount}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { isBoolean, isNumber, isObject } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
|
||||
const EmptyValue = styled.span`
|
||||
color: ${theme.euiColorMediumShade};
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
export function FormattedKey({
|
||||
k,
|
||||
value
|
||||
}: {
|
||||
k: string;
|
||||
value: unknown;
|
||||
}): JSX.Element {
|
||||
if (value == null) {
|
||||
return <EmptyValue>{k}</EmptyValue>;
|
||||
}
|
||||
|
||||
return <React.Fragment>{k}</React.Fragment>;
|
||||
}
|
||||
|
||||
export function FormattedValue({ value }: { value: any }): JSX.Element {
|
||||
if (isObject(value)) {
|
||||
return <pre>{JSON.stringify(value, null, 4)}</pre>;
|
||||
} else if (isBoolean(value) || isNumber(value)) {
|
||||
return <React.Fragment>{String(value)}</React.Fragment>;
|
||||
} else if (!value) {
|
||||
return <EmptyValue>{NOT_AVAILABLE_LABEL}</EmptyValue>;
|
||||
}
|
||||
|
||||
return <React.Fragment>{value}</React.Fragment>;
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { DottedKeyValueTable } from '..';
|
||||
import { cleanup, render } from 'react-testing-library';
|
||||
|
||||
function getKeys(output: ReturnType<typeof render>) {
|
||||
const keys = output.getAllByTestId('dot-key');
|
||||
return Array.isArray(keys) ? keys.map(node => node.textContent) : [];
|
||||
}
|
||||
|
||||
function getValues(output: ReturnType<typeof render>) {
|
||||
const values = output.getAllByTestId('value');
|
||||
return Array.isArray(values) ? values.map(node => node.textContent) : [];
|
||||
}
|
||||
|
||||
describe('DottedKeyValueTable', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('should display a nested object with alpha-ordered, dot notation keys and values', () => {
|
||||
const data = {
|
||||
name: {
|
||||
first: 'Jo',
|
||||
last: 'Smith'
|
||||
},
|
||||
age: 29,
|
||||
active: true,
|
||||
useless: false,
|
||||
start: null,
|
||||
end: undefined,
|
||||
nested: {
|
||||
b: {
|
||||
c: 'ccc'
|
||||
},
|
||||
a: 'aaa'
|
||||
}
|
||||
};
|
||||
const output = render(<DottedKeyValueTable data={data} />);
|
||||
const rows = output.container.querySelectorAll('tr');
|
||||
expect(rows.length).toEqual(9);
|
||||
|
||||
expect(getKeys(output)).toEqual([
|
||||
'active',
|
||||
'age',
|
||||
'end',
|
||||
'name.first',
|
||||
'name.last',
|
||||
'nested.a',
|
||||
'nested.b.c',
|
||||
'start',
|
||||
'useless'
|
||||
]);
|
||||
|
||||
expect(getValues(output)).toEqual([
|
||||
'true',
|
||||
'29',
|
||||
'N/A',
|
||||
'Jo',
|
||||
'Smith',
|
||||
'aaa',
|
||||
'ccc',
|
||||
'N/A',
|
||||
'false'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should respect max depth', () => {
|
||||
const data = {
|
||||
nested: { b: { c: 'ccc' }, a: 'aaa' }
|
||||
};
|
||||
const output = render(<DottedKeyValueTable data={data} maxDepth={1} />);
|
||||
const rows = output.container.querySelectorAll('tr');
|
||||
expect(rows.length).toEqual(2);
|
||||
|
||||
expect(getKeys(output)).toEqual(['nested.a', 'nested.b']);
|
||||
|
||||
expect(getValues(output)).toEqual([
|
||||
'aaa',
|
||||
JSON.stringify({ c: 'ccc' }, null, 4)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should prepend a provided parent key to all of the dot-notation keys', () => {
|
||||
const data = {
|
||||
name: {
|
||||
first: 'Jo',
|
||||
last: 'Smith'
|
||||
},
|
||||
age: 29,
|
||||
active: true
|
||||
};
|
||||
const output = render(<DottedKeyValueTable data={data} parentKey="top" />);
|
||||
const rows = output.container.querySelectorAll('tr');
|
||||
expect(rows.length).toEqual(4);
|
||||
|
||||
expect(getKeys(output)).toEqual([
|
||||
'top.active',
|
||||
'top.age',
|
||||
'top.name.first',
|
||||
'top.name.last'
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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, { TableHTMLAttributes } from 'react';
|
||||
import { compact, isObject } from 'lodash';
|
||||
import {
|
||||
EuiTable,
|
||||
EuiTableProps,
|
||||
EuiTableBody,
|
||||
EuiTableRow,
|
||||
EuiTableRowCell
|
||||
} from '@elastic/eui';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { FormattedValue } from './FormattedValue';
|
||||
|
||||
interface PathifyOptions {
|
||||
maxDepth?: number;
|
||||
parentKey?: string;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
interface PathifyResult {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a deeply-nested object into a one-level object
|
||||
* with dot-notation paths as keys.
|
||||
*/
|
||||
export function pathify(
|
||||
item: StringMap<any>,
|
||||
{ maxDepth, parentKey = '', depth = 0 }: PathifyOptions
|
||||
): PathifyResult {
|
||||
return Object.keys(item)
|
||||
.sort()
|
||||
.reduce((pathified, key) => {
|
||||
const currentKey = compact([parentKey, key]).join('.');
|
||||
if ((!maxDepth || depth + 1 <= maxDepth) && isObject(item[key])) {
|
||||
return {
|
||||
...pathified,
|
||||
...pathify(item[key], {
|
||||
maxDepth,
|
||||
parentKey: currentKey,
|
||||
depth: depth + 1
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return { ...pathified, [currentKey]: item[key] };
|
||||
}
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function DottedKeyValueTable({
|
||||
data,
|
||||
parentKey,
|
||||
maxDepth,
|
||||
tableProps = {}
|
||||
}: {
|
||||
data: StringMap;
|
||||
parentKey?: string;
|
||||
maxDepth?: number;
|
||||
tableProps?: EuiTableProps & TableHTMLAttributes<HTMLTableElement>;
|
||||
}) {
|
||||
const pathified = pathify(data, { maxDepth, parentKey });
|
||||
const rows = Object.keys(pathified)
|
||||
.sort()
|
||||
.map(k => [k, pathified[k]]);
|
||||
return (
|
||||
<EuiTable compressed {...tableProps}>
|
||||
<EuiTableBody>
|
||||
{rows.map(([key, value]) => (
|
||||
<EuiTableRow key={key}>
|
||||
<EuiTableRowCell>
|
||||
<strong data-testid="dot-key">{key}</strong>
|
||||
</EuiTableRowCell>
|
||||
<EuiTableRowCell data-testid="value">
|
||||
<FormattedValue value={value} />
|
||||
</EuiTableRowCell>
|
||||
</EuiTableRow>
|
||||
))}
|
||||
</EuiTableBody>
|
||||
</EuiTable>
|
||||
);
|
||||
}
|
|
@ -1,29 +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, { useEffect, useRef } from 'react';
|
||||
|
||||
export const HeightRetainer: React.SFC = props => {
|
||||
const containerElement = useRef<HTMLDivElement>(null);
|
||||
const minHeight = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerElement.current) {
|
||||
const currentHeight = containerElement.current.clientHeight;
|
||||
if (minHeight.current < currentHeight) {
|
||||
minHeight.current = currentHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={containerElement}
|
||||
style={{ minHeight: minHeight.current }}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
|
||||
import { metadata } from 'ui/metadata';
|
||||
|
||||
// TODO: metadata should be read from a useContext hook in new platform
|
||||
const STACK_VERSION = metadata.branch;
|
||||
|
||||
// union type constisting of valid guide sections that we link to
|
||||
type DocsSection = '/apm/get-started' | '/x-pack';
|
||||
|
||||
interface Props extends EuiLinkAnchorProps {
|
||||
section: DocsSection;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export function ElasticDocsLink({ section, path, ...rest }: Props) {
|
||||
const href = `https://www.elastic.co/guide/en${section}/${STACK_VERSION}${path}`;
|
||||
return <EuiLink href={href} {...rest} />;
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { ErrorMetadata } from '..';
|
||||
import { render, cleanup } from 'react-testing-library';
|
||||
import { APMError } from '../../../../../../typings/es_schemas/ui/APMError';
|
||||
import 'jest-dom/extend-expect';
|
||||
import {
|
||||
expectTextsInDocument,
|
||||
expectTextsNotInDocument
|
||||
} from '../../../../../utils/testHelpers';
|
||||
|
||||
function getError() {
|
||||
return ({
|
||||
labels: { someKey: 'labels value' },
|
||||
http: { someKey: 'http value' },
|
||||
host: { someKey: 'host value' },
|
||||
container: { someKey: 'container value' },
|
||||
service: { someKey: 'service value' },
|
||||
process: { someKey: 'process value' },
|
||||
agent: { someKey: 'agent value' },
|
||||
url: { someKey: 'url value' },
|
||||
user: { someKey: 'user value' },
|
||||
notIncluded: 'not included value',
|
||||
error: {
|
||||
notIncluded: 'error not included value',
|
||||
custom: {
|
||||
someKey: 'custom value'
|
||||
}
|
||||
}
|
||||
} as unknown) as APMError;
|
||||
}
|
||||
|
||||
describe('ErrorMetadata', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('should render a error with all sections', () => {
|
||||
const error = getError();
|
||||
const output = render(<ErrorMetadata error={error} />);
|
||||
|
||||
// sections
|
||||
expectTextsInDocument(output, [
|
||||
'Labels',
|
||||
'HTTP',
|
||||
'Host',
|
||||
'Container',
|
||||
'Service',
|
||||
'Process',
|
||||
'Agent',
|
||||
'URL',
|
||||
'User',
|
||||
'Custom'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a error with all included dot notation keys', () => {
|
||||
const error = getError();
|
||||
const output = render(<ErrorMetadata error={error} />);
|
||||
|
||||
// included keys
|
||||
expectTextsInDocument(output, [
|
||||
'labels.someKey',
|
||||
'http.someKey',
|
||||
'host.someKey',
|
||||
'container.someKey',
|
||||
'service.someKey',
|
||||
'process.someKey',
|
||||
'agent.someKey',
|
||||
'url.someKey',
|
||||
'user.someKey',
|
||||
'error.custom.someKey'
|
||||
]);
|
||||
|
||||
// excluded keys
|
||||
expectTextsNotInDocument(output, ['notIncluded', 'error.notIncluded']);
|
||||
});
|
||||
|
||||
it('should render a error with all included values', () => {
|
||||
const error = getError();
|
||||
const output = render(<ErrorMetadata error={error} />);
|
||||
|
||||
// included values
|
||||
expectTextsInDocument(output, [
|
||||
'labels value',
|
||||
'http value',
|
||||
'host value',
|
||||
'container value',
|
||||
'service value',
|
||||
'process value',
|
||||
'agent value',
|
||||
'url value',
|
||||
'user value',
|
||||
'custom value'
|
||||
]);
|
||||
|
||||
// excluded values
|
||||
expectTextsNotInDocument(output, [
|
||||
'not included value',
|
||||
'error not included value'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a error with only the required sections', () => {
|
||||
const error = {} as APMError;
|
||||
const output = render(<ErrorMetadata error={error} />);
|
||||
|
||||
// required sections should be found
|
||||
expectTextsInDocument(output, ['Labels', 'User']);
|
||||
|
||||
// optional sections should NOT be found
|
||||
expectTextsNotInDocument(output, [
|
||||
'HTTP',
|
||||
'Host',
|
||||
'Container',
|
||||
'Service',
|
||||
'Process',
|
||||
'Agent',
|
||||
'URL',
|
||||
'Custom'
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { MetadataTable } from '..';
|
||||
import { ERROR_METADATA_SECTIONS } from './sections';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
|
||||
interface Props {
|
||||
error: APMError;
|
||||
}
|
||||
|
||||
export function ErrorMetadata({ error }: Props) {
|
||||
return <MetadataTable item={error} sections={ERROR_METADATA_SECTIONS} />;
|
||||
}
|
|
@ -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 * as SECTION_LABELS from '../sectionLabels';
|
||||
|
||||
export const ERROR_METADATA_SECTIONS = [
|
||||
{
|
||||
key: 'labels',
|
||||
label: SECTION_LABELS.LABELS,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'http',
|
||||
label: SECTION_LABELS.HTTP
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
label: SECTION_LABELS.HOST
|
||||
},
|
||||
{
|
||||
key: 'container',
|
||||
label: SECTION_LABELS.CONTAINER
|
||||
},
|
||||
{
|
||||
key: 'service',
|
||||
label: SECTION_LABELS.SERVICE
|
||||
},
|
||||
{
|
||||
key: 'process',
|
||||
label: SECTION_LABELS.PROCESS
|
||||
},
|
||||
{
|
||||
key: 'agent',
|
||||
label: SECTION_LABELS.AGENT
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: SECTION_LABELS.URL
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: SECTION_LABELS.USER,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'error.custom',
|
||||
label: SECTION_LABELS.CUSTOM
|
||||
}
|
||||
];
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 { TransactionMetadata } from '..';
|
||||
import { render, cleanup } from 'react-testing-library';
|
||||
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import 'jest-dom/extend-expect';
|
||||
import {
|
||||
expectTextsInDocument,
|
||||
expectTextsNotInDocument
|
||||
} from '../../../../../utils/testHelpers';
|
||||
|
||||
function getTransaction() {
|
||||
return ({
|
||||
labels: { someKey: 'labels value' },
|
||||
http: { someKey: 'http value' },
|
||||
host: { someKey: 'host value' },
|
||||
container: { someKey: 'container value' },
|
||||
service: { someKey: 'service value' },
|
||||
process: { someKey: 'process value' },
|
||||
agent: { someKey: 'agent value' },
|
||||
url: { someKey: 'url value' },
|
||||
user: { someKey: 'user value' },
|
||||
notIncluded: 'not included value',
|
||||
transaction: {
|
||||
notIncluded: 'transaction not included value',
|
||||
custom: {
|
||||
someKey: 'custom value'
|
||||
}
|
||||
}
|
||||
} as unknown) as Transaction;
|
||||
}
|
||||
|
||||
describe('TransactionMetadata', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('should render a transaction with all sections', () => {
|
||||
const transaction = getTransaction();
|
||||
const output = render(<TransactionMetadata transaction={transaction} />);
|
||||
|
||||
// sections
|
||||
expectTextsInDocument(output, [
|
||||
'Labels',
|
||||
'HTTP',
|
||||
'Host',
|
||||
'Container',
|
||||
'Service',
|
||||
'Process',
|
||||
'Agent',
|
||||
'URL',
|
||||
'User',
|
||||
'Custom'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a transaction with all included dot notation keys', () => {
|
||||
const transaction = getTransaction();
|
||||
const output = render(<TransactionMetadata transaction={transaction} />);
|
||||
|
||||
// included keys
|
||||
expectTextsInDocument(output, [
|
||||
'labels.someKey',
|
||||
'http.someKey',
|
||||
'host.someKey',
|
||||
'container.someKey',
|
||||
'service.someKey',
|
||||
'process.someKey',
|
||||
'agent.someKey',
|
||||
'url.someKey',
|
||||
'user.someKey',
|
||||
'transaction.custom.someKey'
|
||||
]);
|
||||
|
||||
// excluded keys
|
||||
expectTextsNotInDocument(output, [
|
||||
'notIncluded',
|
||||
'transaction.notIncluded'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a transaction with all included values', () => {
|
||||
const transaction = getTransaction();
|
||||
const output = render(<TransactionMetadata transaction={transaction} />);
|
||||
|
||||
// included values
|
||||
expectTextsInDocument(output, [
|
||||
'labels value',
|
||||
'http value',
|
||||
'host value',
|
||||
'container value',
|
||||
'service value',
|
||||
'process value',
|
||||
'agent value',
|
||||
'url value',
|
||||
'user value',
|
||||
'custom value'
|
||||
]);
|
||||
|
||||
// excluded values
|
||||
expectTextsNotInDocument(output, [
|
||||
'not included value',
|
||||
'transaction not included value'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a transaction with only the required sections', () => {
|
||||
const transaction = {} as Transaction;
|
||||
const output = render(<TransactionMetadata transaction={transaction} />);
|
||||
|
||||
// required sections should be found
|
||||
expectTextsInDocument(output, ['Labels', 'User']);
|
||||
|
||||
// optional sections should NOT be found
|
||||
expectTextsNotInDocument(output, [
|
||||
'HTTP',
|
||||
'Host',
|
||||
'Container',
|
||||
'Service',
|
||||
'Process',
|
||||
'Agent',
|
||||
'URL',
|
||||
'Custom'
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { MetadataTable } from '..';
|
||||
import { TRANSACTION_METADATA_SECTIONS } from './sections';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
export function TransactionMetadata({ transaction }: Props) {
|
||||
return (
|
||||
<MetadataTable
|
||||
item={transaction}
|
||||
sections={TRANSACTION_METADATA_SECTIONS}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 * as SECTION_LABELS from '../sectionLabels';
|
||||
|
||||
export const TRANSACTION_METADATA_SECTIONS = [
|
||||
{
|
||||
key: 'labels',
|
||||
label: SECTION_LABELS.LABELS,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'http',
|
||||
label: SECTION_LABELS.HTTP
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
label: SECTION_LABELS.HOST
|
||||
},
|
||||
{
|
||||
key: 'container',
|
||||
label: SECTION_LABELS.CONTAINER
|
||||
},
|
||||
{
|
||||
key: 'service',
|
||||
label: SECTION_LABELS.SERVICE
|
||||
},
|
||||
{
|
||||
key: 'process',
|
||||
label: SECTION_LABELS.PROCESS
|
||||
},
|
||||
{
|
||||
key: 'agent',
|
||||
label: SECTION_LABELS.AGENT
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: SECTION_LABELS.URL
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: SECTION_LABELS.USER,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'transaction.custom',
|
||||
label: SECTION_LABELS.CUSTOM
|
||||
}
|
||||
];
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { get, has } from 'lodash';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import { APMError } from '../../../../typings/es_schemas/ui/APMError';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { DottedKeyValueTable } from '../DottedKeyValueTable';
|
||||
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
|
||||
|
||||
type MetadataItem = Transaction | APMError;
|
||||
|
||||
interface Props {
|
||||
item: MetadataItem;
|
||||
sections: MetadataSection[];
|
||||
}
|
||||
|
||||
export interface MetadataSection {
|
||||
key: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export function MetadataTable({ item, sections }: Props) {
|
||||
const filteredSections = sections.filter(
|
||||
({ key, required }) => required || has(item, key)
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ElasticDocsLink section="/apm/get-started" path="/metadata.html">
|
||||
<EuiText size="s">
|
||||
<EuiIcon type="help" /> How to add labels and other data
|
||||
</EuiText>
|
||||
</ElasticDocsLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{filteredSections.map(section => (
|
||||
<div key={section.key}>
|
||||
<EuiTitle size="xs">
|
||||
<h6>{section.label}</h6>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<Section propData={get(item, section.key)} propKey={section.key} />
|
||||
<EuiSpacer size="xl" />
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
propData,
|
||||
propKey
|
||||
}: {
|
||||
propData?: StringMap;
|
||||
propKey?: string;
|
||||
}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{propData ? (
|
||||
<DottedKeyValueTable data={propData} parentKey={propKey} maxDepth={5} />
|
||||
) : (
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel',
|
||||
{ defaultMessage: 'No data available' }
|
||||
)}
|
||||
</EuiText>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const LABELS = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.labelsLabel',
|
||||
{
|
||||
defaultMessage: 'Labels'
|
||||
}
|
||||
);
|
||||
|
||||
export const HTTP = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.httpLabel',
|
||||
{
|
||||
defaultMessage: 'HTTP'
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.hostLabel',
|
||||
{
|
||||
defaultMessage: 'Host'
|
||||
}
|
||||
);
|
||||
|
||||
export const CONTAINER = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.containerLabel',
|
||||
{
|
||||
defaultMessage: 'Container'
|
||||
}
|
||||
);
|
||||
|
||||
export const SERVICE = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.serviceLabel',
|
||||
{
|
||||
defaultMessage: 'Service'
|
||||
}
|
||||
);
|
||||
|
||||
export const PROCESS = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.processLabel',
|
||||
{
|
||||
defaultMessage: 'Process'
|
||||
}
|
||||
);
|
||||
|
||||
export const AGENT = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.agentLabel',
|
||||
{
|
||||
defaultMessage: 'Agent'
|
||||
}
|
||||
);
|
||||
|
||||
export const URL = i18n.translate('xpack.apm.metadataTable.section.urlLabel', {
|
||||
defaultMessage: 'URL'
|
||||
});
|
||||
|
||||
export const USER = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.userLabel',
|
||||
{
|
||||
defaultMessage: 'User'
|
||||
}
|
||||
);
|
||||
|
||||
export const CUSTOM = i18n.translate(
|
||||
'xpack.apm.metadataTable.section.customLabel',
|
||||
{
|
||||
defaultMessage: 'Custom'
|
||||
}
|
||||
);
|
|
@ -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 theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { isBoolean, isNumber, isObject } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { fontFamilyCode, fontSize, px, units } from '../../../style/variables';
|
||||
import { sortKeysByConfig } from './tabConfig';
|
||||
|
||||
const Table = styled.table`
|
||||
font-family: ${fontFamilyCode};
|
||||
font-size: ${fontSize};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Row = styled.tr`
|
||||
border-bottom: ${px(1)} solid ${theme.euiColorLightShade};
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Cell = styled.td`
|
||||
vertical-align: top;
|
||||
padding: ${px(units.half)} 0;
|
||||
line-height: 1.5;
|
||||
|
||||
${Row}:first-child> & {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
${Row}:last-child> & {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
width: ${px(units.unit * 12)};
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
const EmptyValue = styled.span`
|
||||
color: ${theme.euiColorMediumShade};
|
||||
`;
|
||||
|
||||
export function FormattedKey({
|
||||
k,
|
||||
value
|
||||
}: {
|
||||
k: string;
|
||||
value: unknown;
|
||||
}): JSX.Element {
|
||||
if (value == null) {
|
||||
return <EmptyValue>{k}</EmptyValue>;
|
||||
}
|
||||
|
||||
return <React.Fragment>{k}</React.Fragment>;
|
||||
}
|
||||
|
||||
export function FormattedValue({ value }: { value: any }): JSX.Element {
|
||||
if (isObject(value)) {
|
||||
return <pre>{JSON.stringify(value, null, 4)}</pre>;
|
||||
} else if (isBoolean(value) || isNumber(value)) {
|
||||
return <React.Fragment>{String(value)}</React.Fragment>;
|
||||
} else if (!value) {
|
||||
return <EmptyValue>{NOT_AVAILABLE_LABEL}</EmptyValue>;
|
||||
}
|
||||
|
||||
return <React.Fragment>{value}</React.Fragment>;
|
||||
}
|
||||
|
||||
export function NestedValue({
|
||||
parentKey,
|
||||
value,
|
||||
depth
|
||||
}: {
|
||||
value: unknown;
|
||||
depth: number;
|
||||
parentKey?: string;
|
||||
}): JSX.Element {
|
||||
const MAX_LEVEL = 3;
|
||||
if (depth < MAX_LEVEL && isObject(value)) {
|
||||
return (
|
||||
<NestedKeyValueTable
|
||||
data={value as StringMap}
|
||||
parentKey={parentKey}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <FormattedValue value={value} />;
|
||||
}
|
||||
|
||||
export function NestedKeyValueTable({
|
||||
data,
|
||||
parentKey,
|
||||
depth
|
||||
}: {
|
||||
data: StringMap;
|
||||
parentKey?: string;
|
||||
depth: number;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Table>
|
||||
<tbody>
|
||||
{sortKeysByConfig(data, parentKey).map(key => (
|
||||
<Row key={key}>
|
||||
<Cell>
|
||||
<FormattedKey k={key} value={data[key]} />
|
||||
</Cell>
|
||||
<Cell>
|
||||
<NestedValue parentKey={key} value={data[key]} depth={depth} />
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
|
@ -1,113 +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 { mount, shallow } from 'enzyme';
|
||||
import 'jest-styled-components';
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedKey,
|
||||
FormattedValue,
|
||||
NestedKeyValueTable,
|
||||
NestedValue
|
||||
} from '../NestedKeyValueTable';
|
||||
|
||||
describe('NestedKeyValueTable component', () => {
|
||||
it('should render with data', () => {
|
||||
const testData = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: [3, 4, 5],
|
||||
d: { aa: 1, bb: 2 }
|
||||
};
|
||||
expect(
|
||||
shallow(<NestedKeyValueTable data={testData} depth={0} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an empty table if there is no data', () => {
|
||||
expect(
|
||||
shallow(<NestedKeyValueTable data={{}} depth={0} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NestedValue component', () => {
|
||||
it('should render a formatted value when depth is 0', () => {
|
||||
const wrapper = shallow(
|
||||
<NestedValue value={{ a: 'hello' }} depth={0} parentKey="who_cares" />
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.equals(
|
||||
<NestedKeyValueTable
|
||||
data={{ a: 'hello' }}
|
||||
depth={1}
|
||||
parentKey="who_cares"
|
||||
/>
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should render a formatted value when depth > 0 but value is not an object', () => {
|
||||
expect(
|
||||
shallow(<NestedValue value={2} depth={3} parentKey="who_cares" />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a nested KV Table when depth > 0 and value is an object', () => {
|
||||
expect(
|
||||
shallow(
|
||||
<NestedValue value={{ a: 'hello' }} depth={1} parentKey="who_cares" />
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormattedValue component', () => {
|
||||
it('should render an object', () => {
|
||||
expect(mount(<FormattedValue value={{ a: 'ok' }} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an array', () => {
|
||||
expect(mount(<FormattedValue value={[1, 2, 3]} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a boolean', () => {
|
||||
expect(mount(<FormattedValue value={true} />)).toMatchSnapshot();
|
||||
expect(mount(<FormattedValue value={false} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a number', () => {
|
||||
expect(mount(<FormattedValue value={243} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a string', () => {
|
||||
expect(mount(<FormattedValue value="hey ok cool" />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render null', () => {
|
||||
expect(mount(<FormattedValue value={null} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render undefined', () => {
|
||||
expect(mount(<FormattedValue value={undefined} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormattedKey component', () => {
|
||||
it('should render when the value is null or undefined', () => {
|
||||
expect(mount(<FormattedKey k="testKey" value={null} />)).toMatchSnapshot();
|
||||
expect(
|
||||
mount(<FormattedKey k="testKey" value={undefined} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render when the value is defined', () => {
|
||||
expect(mount(<FormattedKey k="testKey" value="hi" />)).toMatchSnapshot();
|
||||
expect(mount(<FormattedKey k="testKey" value={123} />)).toMatchSnapshot();
|
||||
expect(mount(<FormattedKey k="testKey" value={{}} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,73 +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 } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { PropertiesTable, TabHelpMessage } from '..';
|
||||
import * as agentDocs from '../../../../utils/documentation/agents';
|
||||
|
||||
describe('PropertiesTable', () => {
|
||||
describe('PropertiesTable component', () => {
|
||||
it('should render with data', () => {
|
||||
expect(
|
||||
shallow(
|
||||
<PropertiesTable
|
||||
propData={{ a: 'hello', b: 'bananas' }}
|
||||
propKey="kubernetes"
|
||||
agentName="java"
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render empty when data isn't present", () => {
|
||||
expect(
|
||||
shallow(<PropertiesTable propKey="kubernetes" agentName="java" />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should still render NestedKeyValueTable even when data has no keys', () => {
|
||||
expect(
|
||||
shallow(
|
||||
<PropertiesTable
|
||||
propData={{}}
|
||||
propKey="kubernetes"
|
||||
agentName="java"
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TabHelpMessage component', () => {
|
||||
const tabKey = 'user';
|
||||
const agentName = 'nodejs';
|
||||
|
||||
it('should render when docs are returned', () => {
|
||||
jest
|
||||
.spyOn(agentDocs, 'getAgentDocUrlForTab')
|
||||
.mockImplementation(() => 'mock-url');
|
||||
|
||||
expect(
|
||||
shallow(<TabHelpMessage tabKey={tabKey} agentName={agentName} />)
|
||||
).toMatchSnapshot();
|
||||
expect(agentDocs.getAgentDocUrlForTab).toHaveBeenCalledWith(
|
||||
tabKey,
|
||||
agentName
|
||||
);
|
||||
});
|
||||
|
||||
it('should render null empty string when no docs are returned', () => {
|
||||
jest
|
||||
.spyOn(agentDocs, 'getAgentDocUrlForTab')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
expect(
|
||||
shallow(<TabHelpMessage tabKey={tabKey} agentName={agentName} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,288 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormattedKey component should render when the value is defined 1`] = `
|
||||
<FormattedKey
|
||||
k="testKey"
|
||||
value="hi"
|
||||
>
|
||||
testKey
|
||||
</FormattedKey>
|
||||
`;
|
||||
|
||||
exports[`FormattedKey component should render when the value is defined 2`] = `
|
||||
<FormattedKey
|
||||
k="testKey"
|
||||
value={123}
|
||||
>
|
||||
testKey
|
||||
</FormattedKey>
|
||||
`;
|
||||
|
||||
exports[`FormattedKey component should render when the value is defined 3`] = `
|
||||
<FormattedKey
|
||||
k="testKey"
|
||||
value={Object {}}
|
||||
>
|
||||
testKey
|
||||
</FormattedKey>
|
||||
`;
|
||||
|
||||
exports[`FormattedKey component should render when the value is null or undefined 1`] = `
|
||||
.c0 {
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
<FormattedKey
|
||||
k="testKey"
|
||||
value={null}
|
||||
>
|
||||
<styled.span>
|
||||
<span
|
||||
className="c0"
|
||||
>
|
||||
testKey
|
||||
</span>
|
||||
</styled.span>
|
||||
</FormattedKey>
|
||||
`;
|
||||
|
||||
exports[`FormattedKey component should render when the value is null or undefined 2`] = `
|
||||
.c0 {
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
<FormattedKey
|
||||
k="testKey"
|
||||
>
|
||||
<styled.span>
|
||||
<span
|
||||
className="c0"
|
||||
>
|
||||
testKey
|
||||
</span>
|
||||
</styled.span>
|
||||
</FormattedKey>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render a boolean 1`] = `
|
||||
<FormattedValue
|
||||
value={true}
|
||||
>
|
||||
true
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render a boolean 2`] = `
|
||||
<FormattedValue
|
||||
value={false}
|
||||
>
|
||||
false
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render a number 1`] = `
|
||||
<FormattedValue
|
||||
value={243}
|
||||
>
|
||||
243
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render a string 1`] = `
|
||||
<FormattedValue
|
||||
value="hey ok cool"
|
||||
>
|
||||
hey ok cool
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render an array 1`] = `
|
||||
<FormattedValue
|
||||
value={
|
||||
Array [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
}
|
||||
>
|
||||
<pre>
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
</pre>
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render an object 1`] = `
|
||||
<FormattedValue
|
||||
value={
|
||||
Object {
|
||||
"a": "ok",
|
||||
}
|
||||
}
|
||||
>
|
||||
<pre>
|
||||
{
|
||||
"a": "ok"
|
||||
}
|
||||
</pre>
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render null 1`] = `
|
||||
.c0 {
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
<FormattedValue
|
||||
value={null}
|
||||
>
|
||||
<styled.span>
|
||||
<span
|
||||
className="c0"
|
||||
>
|
||||
N/A
|
||||
</span>
|
||||
</styled.span>
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`FormattedValue component should render undefined 1`] = `
|
||||
.c0 {
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
<FormattedValue>
|
||||
<styled.span>
|
||||
<span
|
||||
className="c0"
|
||||
>
|
||||
N/A
|
||||
</span>
|
||||
</styled.span>
|
||||
</FormattedValue>
|
||||
`;
|
||||
|
||||
exports[`NestedKeyValueTable component should render an empty table if there is no data 1`] = `
|
||||
<styled.table>
|
||||
<tbody />
|
||||
</styled.table>
|
||||
`;
|
||||
|
||||
exports[`NestedKeyValueTable component should render with data 1`] = `
|
||||
<styled.table>
|
||||
<tbody>
|
||||
<styled.tr
|
||||
key="a"
|
||||
>
|
||||
<styled.td>
|
||||
<FormattedKey
|
||||
k="a"
|
||||
value={1}
|
||||
/>
|
||||
</styled.td>
|
||||
<styled.td>
|
||||
<NestedValue
|
||||
depth={0}
|
||||
parentKey="a"
|
||||
value={1}
|
||||
/>
|
||||
</styled.td>
|
||||
</styled.tr>
|
||||
<styled.tr
|
||||
key="b"
|
||||
>
|
||||
<styled.td>
|
||||
<FormattedKey
|
||||
k="b"
|
||||
value={2}
|
||||
/>
|
||||
</styled.td>
|
||||
<styled.td>
|
||||
<NestedValue
|
||||
depth={0}
|
||||
parentKey="b"
|
||||
value={2}
|
||||
/>
|
||||
</styled.td>
|
||||
</styled.tr>
|
||||
<styled.tr
|
||||
key="c"
|
||||
>
|
||||
<styled.td>
|
||||
<FormattedKey
|
||||
k="c"
|
||||
value={
|
||||
Array [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</styled.td>
|
||||
<styled.td>
|
||||
<NestedValue
|
||||
depth={0}
|
||||
parentKey="c"
|
||||
value={
|
||||
Array [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</styled.td>
|
||||
</styled.tr>
|
||||
<styled.tr
|
||||
key="d"
|
||||
>
|
||||
<styled.td>
|
||||
<FormattedKey
|
||||
k="d"
|
||||
value={
|
||||
Object {
|
||||
"aa": 1,
|
||||
"bb": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</styled.td>
|
||||
<styled.td>
|
||||
<NestedValue
|
||||
depth={0}
|
||||
parentKey="d"
|
||||
value={
|
||||
Object {
|
||||
"aa": 1,
|
||||
"bb": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</styled.td>
|
||||
</styled.tr>
|
||||
</tbody>
|
||||
</styled.table>
|
||||
`;
|
||||
|
||||
exports[`NestedValue component should render a formatted value when depth > 0 but value is not an object 1`] = `
|
||||
<FormattedValue
|
||||
value={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`NestedValue component should render a nested KV Table when depth > 0 and value is an object 1`] = `
|
||||
<NestedKeyValueTable
|
||||
data={
|
||||
Object {
|
||||
"a": "hello",
|
||||
}
|
||||
}
|
||||
depth={2}
|
||||
parentKey="who_cares"
|
||||
/>
|
||||
`;
|
|
@ -1,67 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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 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"
|
||||
/>
|
||||
You can configure your agent to add contextual information about your users.
|
||||
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="mock-url"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
Learn more in the documentation.
|
||||
</EuiLink>
|
||||
</styled.div>
|
||||
`;
|
|
@ -1,81 +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.
|
||||
*/
|
||||
|
||||
jest.mock('../tabConfigConst', () => {
|
||||
return {
|
||||
TAB_CONFIG: [
|
||||
{
|
||||
key: 'testProperty',
|
||||
label: 'testPropertyLabel',
|
||||
required: false,
|
||||
presortedKeys: ['name', 'age']
|
||||
},
|
||||
{
|
||||
key: 'optionalProperty',
|
||||
label: 'optionalPropertyLabel',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
key: 'requiredProperty',
|
||||
label: 'requiredPropertyLabel',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
import * as propertyConfig from '../tabConfig';
|
||||
const { getTabsFromObject, sortKeysByConfig } = propertyConfig;
|
||||
|
||||
describe('tabConfig', () => {
|
||||
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'
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,125 +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 { 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 React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { AgentName } from '../../../../typings/es_schemas/ui/fields/Agent';
|
||||
import { fontSize, fontSizes, px, unit, units } from '../../../style/variables';
|
||||
import { getAgentDocUrlForTab } from '../../../utils/documentation/agents';
|
||||
import { NestedKeyValueTable } from './NestedKeyValueTable';
|
||||
import { PropertyTabKey } from './tabConfig';
|
||||
|
||||
const TableContainer = styled.div`
|
||||
padding-bottom: ${px(units.double)};
|
||||
`;
|
||||
|
||||
const TableInfo = styled.div`
|
||||
padding: ${px(unit)} 0 0;
|
||||
text-align: center;
|
||||
font-size: ${fontSize};
|
||||
color: ${theme.euiColorDarkShade};
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const TableInfoHeader = styled(TableInfo)`
|
||||
font-size: ${fontSizes.large};
|
||||
color: ${theme.euiColorDarkestShade};
|
||||
`;
|
||||
|
||||
const EuiIconWithSpace = styled(EuiIcon)`
|
||||
margin-right: ${px(units.half)};
|
||||
`;
|
||||
|
||||
function getTabHelpText(tabKey: PropertyTabKey) {
|
||||
switch (tabKey) {
|
||||
case 'user':
|
||||
return i18n.translate(
|
||||
'xpack.apm.propertiesTable.userTab.agentFeatureText',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can configure your agent to add contextual information about your users.'
|
||||
}
|
||||
);
|
||||
case 'labels':
|
||||
return i18n.translate(
|
||||
'xpack.apm.propertiesTable.labelsTab.agentFeatureText',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can configure your agent to add filterable tags on transactions.'
|
||||
}
|
||||
);
|
||||
case 'transaction.custom':
|
||||
case 'error.custom':
|
||||
return i18n.translate(
|
||||
'xpack.apm.propertiesTable.customTab.agentFeatureText',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can configure your agent to add custom contextual information on transactions.'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function TabHelpMessage({
|
||||
tabKey,
|
||||
agentName
|
||||
}: {
|
||||
tabKey?: PropertyTabKey;
|
||||
agentName?: AgentName;
|
||||
}) {
|
||||
if (!tabKey) {
|
||||
return null;
|
||||
}
|
||||
const docsUrl = getAgentDocUrlForTab(tabKey, agentName);
|
||||
if (!docsUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableInfo>
|
||||
<EuiIconWithSpace type="iInCircle" />
|
||||
{getTabHelpText(tabKey)}{' '}
|
||||
<EuiLink target="_blank" rel="noopener" href={docsUrl}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.propertiesTable.agentFeature.learnMoreLinkLabel',
|
||||
{ defaultMessage: 'Learn more in the documentation.' }
|
||||
)}
|
||||
</EuiLink>
|
||||
</TableInfo>
|
||||
);
|
||||
}
|
||||
|
||||
export function PropertiesTable({
|
||||
propData,
|
||||
propKey,
|
||||
agentName
|
||||
}: {
|
||||
propData?: StringMap;
|
||||
propKey?: PropertyTabKey;
|
||||
agentName?: AgentName;
|
||||
}) {
|
||||
return (
|
||||
<TableContainer>
|
||||
{propData ? (
|
||||
<NestedKeyValueTable data={propData} parentKey={propKey} depth={1} />
|
||||
) : (
|
||||
<TableInfoHeader>
|
||||
{i18n.translate(
|
||||
'xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel',
|
||||
{ defaultMessage: 'No data available' }
|
||||
)}
|
||||
</TableInfoHeader>
|
||||
)}
|
||||
|
||||
<TabHelpMessage tabKey={propKey} agentName={agentName} />
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
|
@ -1,106 +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';
|
||||
|
||||
export interface Tab {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const PROPERTY_CONFIG = [
|
||||
{
|
||||
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: []
|
||||
}
|
||||
];
|
|
@ -1,45 +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 { get, indexBy, uniq } from 'lodash';
|
||||
import { first, has } from 'lodash';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { APMError } from '../../../../typings/es_schemas/ui/APMError';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import {
|
||||
PropertyTab,
|
||||
PropertyTabKey,
|
||||
TAB_CONFIG,
|
||||
TabConfig
|
||||
} from './tabConfigConst';
|
||||
|
||||
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 { TAB_CONFIG, TabConfig, PropertyTab, PropertyTabKey };
|
|
@ -1,119 +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 { APMError } from '../../../../typings/es_schemas/ui/APMError';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
|
||||
export type PropertyTabKey =
|
||||
| keyof Transaction
|
||||
| keyof APMError
|
||||
| 'transaction.custom'
|
||||
| 'error.custom';
|
||||
|
||||
export interface PropertyTab {
|
||||
key: PropertyTabKey;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TabConfig extends PropertyTab {
|
||||
required: boolean;
|
||||
presortedKeys: string[];
|
||||
}
|
||||
|
||||
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: []
|
||||
}
|
||||
];
|
|
@ -17,7 +17,7 @@ import {
|
|||
units
|
||||
} from '../../../style/variables';
|
||||
import { Ellipsis } from '../Icons';
|
||||
import { PropertiesTable } from '../PropertiesTable';
|
||||
import { DottedKeyValueTable } from '../DottedKeyValueTable';
|
||||
|
||||
const VariablesContainer = styled.div`
|
||||
background: ${theme.euiColorEmptyShade};
|
||||
|
@ -66,7 +66,7 @@ export class Variables extends React.Component<Props> {
|
|||
</VariablesToggle>
|
||||
{this.state.isVisible && (
|
||||
<VariablesTableContainer>
|
||||
<PropertiesTable propData={this.props.vars} />
|
||||
<DottedKeyValueTable data={this.props.vars} maxDepth={5} />
|
||||
</VariablesTableContainer>
|
||||
)}
|
||||
</VariablesContainer>
|
||||
|
|
|
@ -1,60 +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 { AgentName } from '../../../typings/es_schemas/ui/fields/Agent';
|
||||
import { PropertyTabKey } from '../../components/shared/PropertiesTable/tabConfig';
|
||||
|
||||
const AGENT_URL_ROOT = 'https://www.elastic.co/guide/en/apm/agent';
|
||||
|
||||
type DocUrls = {
|
||||
[tabKey in PropertyTabKey]?: { [agentName in AgentName]: string | undefined }
|
||||
};
|
||||
|
||||
const customUrls = {
|
||||
'js-base': `${AGENT_URL_ROOT}/js-base/4.x/api.html#apm-set-custom-context`,
|
||||
'rum-js': `${AGENT_URL_ROOT}/js-base/4.x/api.html#apm-set-custom-context`,
|
||||
java: undefined,
|
||||
nodejs: `${AGENT_URL_ROOT}/nodejs/2.x/agent-api.html#apm-set-custom-context`,
|
||||
python: `${AGENT_URL_ROOT}/python/4.x/api.html#api-set-custom-context`,
|
||||
dotnet: undefined,
|
||||
ruby: `${AGENT_URL_ROOT}/ruby/2.x/context.html#_adding_custom_context`,
|
||||
go: undefined
|
||||
};
|
||||
|
||||
const AGENT_DOC_URLS: DocUrls = {
|
||||
user: {
|
||||
'js-base': `${AGENT_URL_ROOT}/js-base/4.x/api.html#apm-set-user-context`,
|
||||
'rum-js': `${AGENT_URL_ROOT}/js-base/4.x/api.html#apm-set-user-context`,
|
||||
java: `${AGENT_URL_ROOT}/java/1.x/public-api.html#api-transaction-set-user`,
|
||||
nodejs: `${AGENT_URL_ROOT}/nodejs/2.x/agent-api.html#apm-set-user-context`,
|
||||
python: `${AGENT_URL_ROOT}/python/4.x/api.html#api-set-user-context`,
|
||||
dotnet: undefined,
|
||||
ruby: `${AGENT_URL_ROOT}/ruby/2.x/context.html#_providing_info_about_the_user`,
|
||||
go: undefined
|
||||
},
|
||||
labels: {
|
||||
'js-base': `${AGENT_URL_ROOT}/js-base/4.x/api.html#apm-add-tags`,
|
||||
'rum-js': `${AGENT_URL_ROOT}/js-base/4.x/api.html#apm-add-tags`,
|
||||
java: `${AGENT_URL_ROOT}/java/1.x/public-api.html#api-transaction-add-tag`,
|
||||
nodejs: `${AGENT_URL_ROOT}/nodejs/2.x/agent-api.html#apm-set-tag`,
|
||||
python: `${AGENT_URL_ROOT}/python/4.x/api.html#api-tag`,
|
||||
dotnet: `${AGENT_URL_ROOT}/dotnet/current/public-api.html#api-transaction-tags`,
|
||||
ruby: `${AGENT_URL_ROOT}/ruby/2.x/context.html#_adding_tags`,
|
||||
go: undefined
|
||||
},
|
||||
'transaction.custom': customUrls,
|
||||
'error.custom': customUrls
|
||||
};
|
||||
|
||||
export function getAgentDocUrlForTab(
|
||||
tabKey: PropertyTabKey,
|
||||
agentName?: AgentName
|
||||
) {
|
||||
const agentUrls = AGENT_DOC_URLS[tabKey];
|
||||
if (agentUrls && agentName) {
|
||||
return agentUrls[agentName];
|
||||
}
|
||||
}
|
|
@ -1,10 +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 { metadata } from 'ui/metadata';
|
||||
const STACK_VERSION = metadata.branch;
|
||||
|
||||
export const DROPPED_SPANS_DOCS = `https://www.elastic.co/guide/en/apm/get-started/${STACK_VERSION}/transaction-spans.html#dropped-spans`;
|
|
@ -1,15 +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 { metadata } from 'ui/metadata';
|
||||
const STACK_VERSION = metadata.branch;
|
||||
|
||||
const XPACK_URL_ROOT = `https://www.elastic.co/guide/en/x-pack/${STACK_VERSION}`;
|
||||
|
||||
export const XPACK_DOCS = {
|
||||
xpackEmails: `${XPACK_URL_ROOT}/actions-email.html#configuring-email`,
|
||||
xpackWatcher: `${XPACK_URL_ROOT}/watcher-getting-started.html`
|
||||
};
|
|
@ -72,3 +72,25 @@ export function delay(ms: number) {
|
|||
|
||||
// Await this when you need to "flush" promises to immediately resolve or throw in tests
|
||||
export const tick = () => new Promise(resolve => setImmediate(resolve, 0));
|
||||
|
||||
export function expectTextsNotInDocument(output: any, texts: string[]) {
|
||||
texts.forEach(text => {
|
||||
try {
|
||||
output.getByText(text);
|
||||
} catch (err) {
|
||||
if (err.message.startsWith('Unable to find an element with the text:')) {
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected text found: ${text}`);
|
||||
});
|
||||
}
|
||||
|
||||
export function expectTextsInDocument(output: any, texts: string[]) {
|
||||
texts.forEach(text => {
|
||||
expect(output.getByText(text)).toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3287,16 +3287,10 @@
|
|||
"xpack.apm.metrics.transactionChart.transactionDurationLabel": "事务持续时间",
|
||||
"xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "每分钟事务数",
|
||||
"xpack.apm.notAvailableLabel": "不适用",
|
||||
"xpack.apm.propertiesTable.agentFeature.learnMoreLinkLabel": "在文档中详细了解。",
|
||||
"xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "没有可用数据",
|
||||
"xpack.apm.propertiesTable.customTab.agentFeatureText": "您可以配置代理以添加有关事务的定制上下文信息。",
|
||||
"xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel": "异常堆栈追溯",
|
||||
"xpack.apm.propertiesTable.tabs.logStacktraceLabel": "日志堆栈追溯",
|
||||
"xpack.apm.propertiesTable.tabs.processLabel": "进程",
|
||||
"xpack.apm.propertiesTable.tabs.serviceLabel": "服务",
|
||||
"xpack.apm.propertiesTable.tabs.timelineLabel": "时间线",
|
||||
"xpack.apm.propertiesTable.tabs.userLabel": "用户",
|
||||
"xpack.apm.propertiesTable.userTab.agentFeatureText": "您可以配置代理以添加有关用户的上下文信息。",
|
||||
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "当前有 {serviceName}({transactionType})的作业正在运行。",
|
||||
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业",
|
||||
"xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "作业已存在",
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -8151,7 +8151,12 @@ css-what@2.1, css-what@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
||||
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
||||
|
||||
css@2.X, css@^2.2.1, css@^2.2.4:
|
||||
css.escape@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
||||
integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=
|
||||
|
||||
css@2.X, css@^2.2.1, css@^2.2.3, css@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
|
||||
integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
|
||||
|
@ -14770,6 +14775,20 @@ jest-docblock@^24.0.0:
|
|||
dependencies:
|
||||
detect-newline "^2.1.0"
|
||||
|
||||
jest-dom@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-3.1.3.tgz#9490de549c02366fe586f23bdafffd8374bd1d65"
|
||||
integrity sha512-V9LdySiA74/spcAKEG3FRMRKnisKlcYr3EeCNYI4n7CWNE7uYg5WoBUHeGXirjWjRYLLZ5vx8rUaR/6x6o75oQ==
|
||||
dependencies:
|
||||
chalk "^2.4.1"
|
||||
css "^2.2.3"
|
||||
css.escape "^1.5.1"
|
||||
jest-diff "^24.0.0"
|
||||
jest-matcher-utils "^24.0.0"
|
||||
lodash "^4.17.11"
|
||||
pretty-format "^24.0.0"
|
||||
redent "^2.0.0"
|
||||
|
||||
jest-each@^24.0.0:
|
||||
version "24.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.0.0.tgz#10987a06b21c7ffbfb7706c89d24c52ed864be55"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue