[APM] Color by span type when there's only one service (#90424)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2021-03-07 07:52:38 +01:00 committed by GitHub
parent acb7b7726e
commit e2abb03ad0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 666 additions and 160 deletions

View file

@ -11,7 +11,7 @@ import { getErrorMarks } from './get_error_marks';
describe('getErrorMarks', () => {
describe('returns empty array', () => {
it('when items are missing', () => {
expect(getErrorMarks([], {})).toEqual([]);
expect(getErrorMarks([])).toEqual([]);
});
});
@ -22,17 +22,17 @@ describe('getErrorMarks', () => {
offset: 10,
skew: 5,
doc: { error: { id: 1 }, service: { name: 'opbeans-java' } },
color: 'red',
} as unknown,
{
docType: 'error',
offset: 50,
skew: 0,
doc: { error: { id: 2 }, service: { name: 'opbeans-node' } },
color: 'blue',
} as unknown,
] as IWaterfallError[];
expect(
getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' })
).toEqual([
expect(getErrorMarks(items)).toEqual([
{
type: 'errorMark',
offset: 15,
@ -59,22 +59,24 @@ describe('getErrorMarks', () => {
offset: 10,
skew: 5,
doc: { error: { id: 1 }, service: { name: 'opbeans-java' } },
color: '',
} as unknown,
{
docType: 'error',
offset: 50,
skew: 0,
doc: { error: { id: 2 }, service: { name: 'opbeans-node' } },
color: '',
} as unknown,
] as IWaterfallError[];
expect(getErrorMarks(items, {})).toEqual([
expect(getErrorMarks(items)).toEqual([
{
type: 'errorMark',
offset: 15,
verticalLine: false,
id: 1,
error: { error: { id: 1 }, service: { name: 'opbeans-java' } },
serviceColor: undefined,
serviceColor: '',
},
{
type: 'errorMark',
@ -82,7 +84,7 @@ describe('getErrorMarks', () => {
verticalLine: false,
id: 2,
error: { error: { id: 2 }, service: { name: 'opbeans-node' } },
serviceColor: undefined,
serviceColor: '',
},
]);
});

View file

@ -7,22 +7,16 @@
import { isEmpty } from 'lodash';
import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/error_raw';
import {
IWaterfallError,
IServiceColors,
} from '../Waterfall/waterfall_helpers/waterfall_helpers';
import { IWaterfallError } from '../Waterfall/waterfall_helpers/waterfall_helpers';
import { Mark } from '.';
export interface ErrorMark extends Mark {
type: 'errorMark';
error: ErrorRaw;
serviceColor?: string;
serviceColor: string;
}
export const getErrorMarks = (
errorItems: IWaterfallError[],
serviceColors: IServiceColors
): ErrorMark[] => {
export const getErrorMarks = (errorItems: IWaterfallError[]): ErrorMark[] => {
if (isEmpty(errorItems)) {
return [];
}
@ -33,6 +27,6 @@ export const getErrorMarks = (
verticalLine: false,
id: error.doc.error.id,
error: error.doc,
serviceColor: serviceColors[error.doc.service.name],
serviceColor: error.color,
}));
};

View file

@ -1,46 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { px, unit } from '../../../../../style/variables';
import { Legend } from '../../../../shared/charts/Legend';
import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers';
const Legends = euiStyled.div`
display: flex;
> * {
margin-right: ${px(unit)};
&:last-child {
margin-right: 0;
}
}
`;
interface Props {
serviceColors: IServiceColors;
}
export function ServiceLegends({ serviceColors }: Props) {
return (
<Legends>
<EuiTitle size="xxxs">
<span>
{i18n.translate('xpack.apm.transactionDetails.servicesTitle', {
defaultMessage: 'Services',
})}
</span>
</EuiTitle>
{Object.entries(serviceColors).map(([label, color]) => (
<Legend key={color} color={color} text={label} />
))}
</Legends>
);
}

View file

@ -14,7 +14,7 @@ import { asDuration } from '../../../../../../../common/utils/formatters';
import { isRumAgentName } from '../../../../../../../common/agent_name';
import { px, unit, units } from '../../../../../../style/variables';
import { ErrorCount } from '../../ErrorCount';
import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers';
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink';
import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames';
import { SyncBadge } from './SyncBadge';
@ -75,14 +75,14 @@ const ItemText = euiStyled.span`
interface IWaterfallItemProps {
timelineMargins: Margins;
totalDuration?: number;
item: IWaterfallItem;
item: IWaterfallSpanOrTransaction;
color: string;
isSelected: boolean;
errorCount: number;
onClick: () => unknown;
}
function PrefixIcon({ item }: { item: IWaterfallItem }) {
function PrefixIcon({ item }: { item: IWaterfallSpanOrTransaction }) {
switch (item.docType) {
case 'span': {
// icon for database spans
@ -110,7 +110,7 @@ function PrefixIcon({ item }: { item: IWaterfallItem }) {
interface SpanActionToolTipProps {
children: ReactNode;
item?: IWaterfallItem;
item?: IWaterfallSpanOrTransaction;
}
function SpanActionToolTip({ item, children }: SpanActionToolTipProps) {
@ -124,7 +124,7 @@ function SpanActionToolTip({ item, children }: SpanActionToolTipProps) {
return <>{children}</>;
}
function Duration({ item }: { item: IWaterfallItem }) {
function Duration({ item }: { item: IWaterfallSpanOrTransaction }) {
return (
<EuiText color="subdued" size="xs">
{asDuration(item.duration)}
@ -132,7 +132,7 @@ function Duration({ item }: { item: IWaterfallItem }) {
);
}
function HttpStatusCode({ item }: { item: IWaterfallItem }) {
function HttpStatusCode({ item }: { item: IWaterfallSpanOrTransaction }) {
// http status code for transactions of type 'request'
const httpStatusCode =
item.docType === 'transaction' && item.doc.transaction.type === 'request'
@ -146,7 +146,7 @@ function HttpStatusCode({ item }: { item: IWaterfallItem }) {
return <EuiText size="xs">{httpStatusCode}</EuiText>;
}
function NameLabel({ item }: { item: IWaterfallItem }) {
function NameLabel({ item }: { item: IWaterfallSpanOrTransaction }) {
switch (item.docType) {
case 'span':
return <EuiText size="s">{item.doc.span.name}</EuiText>;
@ -156,8 +156,6 @@ function NameLabel({ item }: { item: IWaterfallItem }) {
<h5>{item.doc.transaction.name}</h5>
</EuiTitle>
);
default:
return null;
}
}

View file

@ -14,22 +14,21 @@ import { Margins } from '../../../../../shared/charts/Timeline';
import { WaterfallItem } from './WaterfallItem';
import {
IWaterfall,
IWaterfallItem,
IWaterfallSpanOrTransaction,
} from './waterfall_helpers/waterfall_helpers';
interface AccordionWaterfallProps {
isOpen: boolean;
item: IWaterfallItem;
item: IWaterfallSpanOrTransaction;
level: number;
serviceColors: IWaterfall['serviceColors'];
duration: IWaterfall['duration'];
waterfallItemId?: string;
location: Location;
errorsPerTransaction: IWaterfall['errorsPerTransaction'];
childrenByParentId: Record<string, IWaterfallItem[]>;
childrenByParentId: Record<string, IWaterfallSpanOrTransaction[]>;
onToggleEntryTransaction?: () => void;
timelineMargins: Margins;
onClickWaterfallItem: (item: IWaterfallItem) => void;
onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void;
}
const StyledAccordion = euiStyled(EuiAccordion).withConfig({
@ -98,7 +97,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
const {
item,
level,
serviceColors,
duration,
childrenByParentId,
waterfallItemId,
@ -134,7 +132,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
<WaterfallItem
key={item.id}
timelineMargins={timelineMargins}
color={serviceColors[item.doc.service.name]}
color={item.color}
item={item}
totalDuration={duration}
isSelected={item.id === waterfallItemId}
@ -161,7 +159,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
isOpen={isOpen}
item={child}
level={nextLevel}
serviceColors={serviceColors}
waterfallItemId={waterfallItemId}
location={location}
errorsPerTransaction={errorsPerTransaction}

View file

@ -21,6 +21,7 @@ import { WaterfallFlyout } from './WaterfallFlyout';
import {
IWaterfall,
IWaterfallItem,
IWaterfallSpanOrTransaction,
} from './waterfall_helpers/waterfall_helpers';
const Container = euiStyled.div`
@ -76,13 +77,13 @@ export function Waterfall({
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
const waterfallHeight = itemContainerHeight * waterfall.items.length;
const { serviceColors, duration } = waterfall;
const { duration } = waterfall;
const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc);
const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors);
const errorMarks = getErrorMarks(waterfall.errorItems);
function renderItems(
childrenByParentId: Record<string | number, IWaterfallItem[]>
childrenByParentId: Record<string | number, IWaterfallSpanOrTransaction[]>
) {
const { entryWaterfallTransaction } = waterfall;
if (!entryWaterfallTransaction) {
@ -95,7 +96,6 @@ export function Waterfall({
isOpen={isAccordionOpen}
item={entryWaterfallTransaction}
level={0}
serviceColors={serviceColors}
waterfallItemId={waterfallItemId}
location={location}
errorsPerTransaction={waterfall.errorsPerTransaction}

View file

@ -15,6 +15,7 @@ import {
IWaterfallItem,
IWaterfallTransaction,
IWaterfallError,
IWaterfallSpanOrTransaction,
} from './waterfall_helpers';
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
@ -377,7 +378,12 @@ describe('waterfall_helpers', () => {
describe('getWaterfallItems', () => {
it('should order items correctly', () => {
const items: IWaterfallItem[] = [
const legendValues = {
serviceName: 'opbeans-java',
spanType: '',
};
const items: IWaterfallSpanOrTransaction[] = [
{
docType: 'span',
doc: {
@ -397,6 +403,8 @@ describe('waterfall_helpers', () => {
duration: 210,
offset: 0,
skew: 0,
legendValues,
color: '',
},
{
docType: 'span',
@ -417,6 +425,8 @@ describe('waterfall_helpers', () => {
duration: 4694,
offset: 0,
skew: 0,
legendValues,
color: '',
},
{
docType: 'span',
@ -437,6 +447,8 @@ describe('waterfall_helpers', () => {
duration: 4694,
offset: 0,
skew: 0,
legendValues,
color: '',
},
{
docType: 'transaction',
@ -451,6 +463,8 @@ describe('waterfall_helpers', () => {
duration: 3581,
offset: 0,
skew: 0,
legendValues,
color: '',
},
{
docType: 'transaction',
@ -466,6 +480,8 @@ describe('waterfall_helpers', () => {
duration: 9480,
offset: 0,
skew: 0,
legendValues,
color: '',
},
];
@ -489,7 +505,7 @@ describe('waterfall_helpers', () => {
transaction: { id: 'a' },
timestamp: { us: 10 },
} as unknown) as Transaction,
} as IWaterfallItem,
} as IWaterfallSpanOrTransaction,
{
docType: 'span',
id: 'b',
@ -501,7 +517,7 @@ describe('waterfall_helpers', () => {
parent: { id: 'a' },
timestamp: { us: 20 },
} as unknown) as Span,
} as IWaterfallItem,
} as IWaterfallSpanOrTransaction,
];
const childrenByParentId = groupBy(items, (hit) =>
hit.parentId ? hit.parentId : 'root'
@ -522,7 +538,7 @@ describe('waterfall_helpers', () => {
timestamp: { us: 0 },
},
duration: 50,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
const parent = {
docType: 'transaction',
@ -531,7 +547,7 @@ describe('waterfall_helpers', () => {
},
duration: 100,
skew: 5,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
expect(getClockSkew(child, parent)).toBe(130);
});
@ -543,7 +559,7 @@ describe('waterfall_helpers', () => {
timestamp: { us: 250 },
},
duration: 50,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
const parent = {
docType: 'transaction',
@ -552,7 +568,7 @@ describe('waterfall_helpers', () => {
},
duration: 100,
skew: 5,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
expect(getClockSkew(child, parent)).toBe(0);
});
@ -564,7 +580,7 @@ describe('waterfall_helpers', () => {
timestamp: { us: 150 },
},
duration: 50,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
const parent = {
docType: 'transaction',
@ -573,7 +589,7 @@ describe('waterfall_helpers', () => {
},
duration: 100,
skew: 5,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
expect(getClockSkew(child, parent)).toBe(0);
});
@ -590,7 +606,7 @@ describe('waterfall_helpers', () => {
},
duration: 100,
skew: 5,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
expect(getClockSkew(child, parent)).toBe(5);
});
@ -607,7 +623,7 @@ describe('waterfall_helpers', () => {
},
duration: 100,
skew: 5,
} as IWaterfallItem;
} as IWaterfallSpanOrTransaction;
expect(getClockSkew(child, parent)).toBe(5);
});

View file

@ -5,17 +5,8 @@
* 2.0.
*/
import theme from '@elastic/eui/dist/eui_theme_light.json';
import {
first,
flatten,
groupBy,
isEmpty,
sortBy,
sum,
uniq,
zipObject,
} from 'lodash';
import { euiPaletteColorBlind } from '@elastic/eui';
import { first, flatten, groupBy, isEmpty, sortBy, sum, uniq } from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace';
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
@ -23,11 +14,16 @@ import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
interface IWaterfallGroup {
[key: string]: IWaterfallItem[];
[key: string]: IWaterfallSpanOrTransaction[];
}
const ROOT_ID = 'root';
export enum WaterfallLegendType {
ServiceName = 'serviceName',
SpanType = 'spanType',
}
export interface IWaterfall {
entryWaterfallTransaction?: IWaterfallTransaction;
rootTransaction?: Transaction;
@ -37,46 +33,69 @@ export interface IWaterfall {
*/
duration: number;
items: IWaterfallItem[];
childrenByParentId: Record<string | number, IWaterfallItem[]>;
childrenByParentId: Record<string | number, IWaterfallSpanOrTransaction[]>;
errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'];
errorsCount: number;
serviceColors: IServiceColors;
legends: IWaterfallLegend[];
errorItems: IWaterfallError[];
}
interface IWaterfallItemBase<T, U> {
docType: U;
doc: T;
id: string;
parent?: IWaterfallItem;
parentId?: string;
interface IWaterfallSpanItemBase<TDocument, TDoctype>
extends IWaterfallItemBase<TDocument, TDoctype> {
/**
* Latency in us
*/
duration: number;
legendValues: Record<WaterfallLegendType, string>;
}
interface IWaterfallItemBase<TDocument, TDoctype> {
doc: TDocument;
docType: TDoctype;
id: string;
parent?: IWaterfallSpanOrTransaction;
parentId?: string;
color: string;
/**
* offset from first item in us
*/
offset: number;
/**
* skew from timestamp in us
*/
skew: number;
}
export type IWaterfallTransaction = IWaterfallItemBase<
export type IWaterfallError = IWaterfallItemBase<APMError, 'error'>;
export type IWaterfallTransaction = IWaterfallSpanItemBase<
Transaction,
'transaction'
>;
export type IWaterfallSpan = IWaterfallItemBase<Span, 'span'>;
export type IWaterfallError = IWaterfallItemBase<APMError, 'error'>;
export type IWaterfallItem = IWaterfallTransaction | IWaterfallSpan;
export type IWaterfallSpan = IWaterfallSpanItemBase<Span, 'span'>;
export type IWaterfallSpanOrTransaction =
| IWaterfallTransaction
| IWaterfallSpan;
export type IWaterfallItem = IWaterfallSpanOrTransaction | IWaterfallError;
export interface IWaterfallLegend {
type: WaterfallLegendType;
value: string | undefined;
color: string;
}
function getLegendValues(transactionOrSpan: Transaction | Span) {
return {
[WaterfallLegendType.ServiceName]: transactionOrSpan.service.name,
[WaterfallLegendType.SpanType]:
'span' in transactionOrSpan
? transactionOrSpan.span.subtype || transactionOrSpan.span.type
: '',
};
}
function getTransactionItem(transaction: Transaction): IWaterfallTransaction {
return {
@ -87,6 +106,8 @@ function getTransactionItem(transaction: Transaction): IWaterfallTransaction {
duration: transaction.transaction.duration.us,
offset: 0,
skew: 0,
legendValues: getLegendValues(transaction),
color: '',
};
}
@ -99,6 +120,8 @@ function getSpanItem(span: Span): IWaterfallSpan {
duration: span.span.duration.us,
offset: 0,
skew: 0,
legendValues: getLegendValues(span),
color: '',
};
}
@ -110,7 +133,8 @@ function getErrorItem(
const entryTimestamp = entryWaterfallTransaction?.doc.timestamp.us ?? 0;
const parent = items.find(
(waterfallItem) => waterfallItem.id === error.parent?.id
);
) as IWaterfallSpanOrTransaction | undefined;
const errorItem: IWaterfallError = {
docType: 'error',
doc: error,
@ -119,7 +143,7 @@ function getErrorItem(
parentId: parent?.id,
offset: error.timestamp.us - entryTimestamp,
skew: 0,
duration: 0,
color: '',
};
return {
@ -130,7 +154,7 @@ function getErrorItem(
export function getClockSkew(
item: IWaterfallItem | IWaterfallError,
parentItem?: IWaterfallItem
parentItem?: IWaterfallSpanOrTransaction
) {
if (!parentItem) {
return 0;
@ -168,9 +192,9 @@ export function getOrderedWaterfallItems(
const visitedWaterfallItemSet = new Set();
function getSortedChildren(
item: IWaterfallItem,
parentItem?: IWaterfallItem
): IWaterfallItem[] {
item: IWaterfallSpanOrTransaction,
parentItem?: IWaterfallSpanOrTransaction
): IWaterfallSpanOrTransaction[] {
if (visitedWaterfallItemSet.has(item)) {
return [];
}
@ -203,27 +227,39 @@ function getRootTransaction(childrenByParentId: IWaterfallGroup) {
}
}
export type IServiceColors = Record<string, string>;
function getLegends(waterfallItems: IWaterfallItem[]) {
const onlyBaseSpanItems = waterfallItems.filter(
(item) => item.docType === 'span' || item.docType === 'transaction'
) as IWaterfallSpanOrTransaction[];
function getServiceColors(waterfallItems: IWaterfallItem[]) {
const services = uniq(waterfallItems.map((item) => item.doc.service.name));
const legends = [
WaterfallLegendType.ServiceName,
WaterfallLegendType.SpanType,
].flatMap((legendType) => {
const allLegendValues = uniq(
onlyBaseSpanItems.map((item) => item.legendValues[legendType])
);
const assignedColors = [
theme.euiColorVis1,
theme.euiColorVis0,
theme.euiColorVis3,
theme.euiColorVis2,
theme.euiColorVis6,
theme.euiColorVis7,
theme.euiColorVis5,
];
const palette = euiPaletteColorBlind({
rotations: Math.ceil(allLegendValues.length / 10),
});
return zipObject(services, assignedColors) as IServiceColors;
return allLegendValues.map((value, index) => ({
type: legendType,
value,
color: palette[index],
}));
});
return legends;
}
const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) =>
Math.max(
...waterfallItems.map((item) => item.offset + item.skew + item.duration),
...waterfallItems.map(
(item) =>
item.offset + item.skew + ('duration' in item ? item.duration : 0)
),
0
);
@ -238,7 +274,7 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) =>
}
});
function reparentSpans(waterfallItems: IWaterfallItem[]) {
function reparentSpans(waterfallItems: IWaterfallSpanOrTransaction[]) {
// find children that needs to be re-parented and map them to their correct parent id
const childIdToParentIdMapping = Object.fromEntries(
flatten(
@ -266,7 +302,9 @@ function reparentSpans(waterfallItems: IWaterfallItem[]) {
});
}
const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) =>
const getChildrenGroupedByParentId = (
waterfallItems: IWaterfallSpanOrTransaction[]
) =>
groupBy(waterfallItems, (item) => (item.parentId ? item.parentId : ROOT_ID));
const getEntryWaterfallTransaction = (
@ -329,13 +367,15 @@ export function getWaterfall(
items: [],
errorsPerTransaction,
errorsCount: sum(Object.values(errorsPerTransaction)),
serviceColors: {},
legends: [],
errorItems: [],
childrenByParentId: {},
};
}
const waterfallItems: IWaterfallItem[] = getWaterfallItems(trace.items);
const waterfallItems: IWaterfallSpanOrTransaction[] = getWaterfallItems(
trace.items
);
const childrenByParentId = getChildrenGroupedByParentId(
reparentSpans(waterfallItems)
@ -358,7 +398,7 @@ export function getWaterfall(
const rootTransaction = getRootTransaction(childrenByParentId);
const duration = getWaterfallDuration(items);
const serviceColors = getServiceColors(items);
const legends = getLegends(items);
return {
entryWaterfallTransaction,
@ -367,7 +407,7 @@ export function getWaterfall(
items,
errorsPerTransaction,
errorsCount: errorItems.length,
serviceColors,
legends,
errorItems,
childrenByParentId: getChildrenGroupedByParentId(items),
};

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Legend } from '../../../../shared/charts/Legend';
import {
IWaterfallLegend,
WaterfallLegendType,
} from './Waterfall/waterfall_helpers/waterfall_helpers';
interface Props {
legends: IWaterfallLegend[];
type: WaterfallLegendType;
}
const LEGEND_LABELS = {
[WaterfallLegendType.ServiceName]: i18n.translate(
'xpack.apm.transactionDetails.servicesTitle',
{
defaultMessage: 'Services',
}
),
[WaterfallLegendType.SpanType]: i18n.translate(
'xpack.apm.transactionDetails.spanTypeLegendTitle',
{
defaultMessage: 'Type',
}
),
};
export function WaterfallLegends({ legends, type }: Props) {
return (
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<span>{LEGEND_LABELS[type]}</span>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="s">
{legends.map((legend) => (
<EuiFlexItem grow={false} key={legend.value}>
<Legend color={legend.color} text={legend.value} />
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -7,10 +7,15 @@
import { Location } from 'history';
import React from 'react';
import { keyBy } from 'lodash';
import { useParams } from 'react-router-dom';
import { IUrlParams } from '../../../../../context/url_params_context/types';
import { ServiceLegends } from './ServiceLegends';
import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
import {
IWaterfall,
WaterfallLegendType,
} from './Waterfall/waterfall_helpers/waterfall_helpers';
import { Waterfall } from './Waterfall';
import { WaterfallLegends } from './WaterfallLegends';
interface Props {
urlParams: IUrlParams;
@ -25,13 +30,59 @@ export function WaterfallContainer({
waterfall,
exceedsMax,
}: Props) {
const { serviceName } = useParams<{ serviceName: string }>();
if (!waterfall) {
return null;
}
const { legends, items } = waterfall;
// Service colors are needed to color the dot in the error popover
const serviceLegends = legends.filter(
({ type }) => type === WaterfallLegendType.ServiceName
);
const serviceColors = serviceLegends.reduce((colorMap, legend) => {
return {
...colorMap,
[legend.value!]: legend.color,
};
}, {} as Record<string, string>);
// only color by span type if there are only events for one service
const colorBy =
serviceLegends.length > 1
? WaterfallLegendType.ServiceName
: WaterfallLegendType.SpanType;
const displayedLegends = legends.filter((legend) => legend.type === colorBy);
const legendsByValue = keyBy(displayedLegends, 'value');
// mutate items rather than rebuilding both items and childrenByParentId
items.forEach((item) => {
let color = '';
if ('legendValues' in item) {
color = legendsByValue[item.legendValues[colorBy]].color;
}
if (!color) {
// fall back to service color if there's no span.type, e.g. for transactions
color = serviceColors[item.doc.service.name];
}
item.color = color;
});
// default to serviceName if value is empty, e.g. for transactions (which don't
// have span.type or span.subtype)
const legendsWithFallbackLabel = displayedLegends.map((legend) => {
return { ...legend, value: !legend.value ? serviceName : legend.value };
});
return (
<div>
<ServiceLegends serviceColors={waterfall.serviceColors} />
<WaterfallLegends legends={legendsWithFallbackLabel} type={colorBy} />
<Waterfall
location={location}
waterfallItemId={urlParams.waterfallItemId}