mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Show color of row as a border under the row. Hide service name for sequential spans * Increase default span name column width. Smaller font for service and span names in span list * New background color on spans. Fixed hover of indent markers * Service name and span name style tweaks * Collapse hidden levels * Fixed test * Small tweak to Buffer size to make sure tests pass * Trigger runs * Update betterer results * Address comment * Style tweaks * Remove duplicated code * Rollback change to join <span> since they are needed for the tests
581 lines
18 KiB
TypeScript
581 lines
18 KiB
TypeScript
// Copyright (c) 2017 Uber Technologies, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { css, keyframes } from '@emotion/css';
|
|
import cx from 'classnames';
|
|
import * as React from 'react';
|
|
|
|
import { GrafanaTheme2, TraceKeyValuePair } from '@grafana/data';
|
|
import { Icon, stylesFactory, withTheme2 } from '@grafana/ui';
|
|
|
|
import { autoColor } from '../Theme';
|
|
import { DURATION, NONE, TAG } from '../settings/SpanBarSettings';
|
|
import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil } from '../types';
|
|
|
|
import SpanBar from './SpanBar';
|
|
import { SpanLinksMenu } from './SpanLinks';
|
|
import SpanTreeOffset from './SpanTreeOffset';
|
|
import Ticks from './Ticks';
|
|
import TimelineRow from './TimelineRow';
|
|
import { formatDuration, ViewedBoundsFunctionType } from './utils';
|
|
|
|
const spanBarClassName = 'spanBar';
|
|
const spanBarLabelClassName = 'spanBarLabel';
|
|
const nameWrapperClassName = 'nameWrapper';
|
|
const nameWrapperMatchingFilterClassName = 'nameWrapperMatchingFilter';
|
|
const viewClassName = 'jaegerView';
|
|
const nameColumnClassName = 'nameColumn';
|
|
|
|
const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly: boolean) => {
|
|
const animations = {
|
|
label: 'flash',
|
|
flash: keyframes`
|
|
from {
|
|
background-color: ${autoColor(theme, '#68b9ff')};
|
|
}
|
|
to {
|
|
background-color: 'default';
|
|
}
|
|
`,
|
|
};
|
|
const backgroundColor = showSpanFilterMatchesOnly ? '' : autoColor(theme, '#fffce4');
|
|
|
|
return {
|
|
nameWrapper: css`
|
|
label: nameWrapper;
|
|
line-height: 27px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
`,
|
|
nameWrapperMatchingFilter: css`
|
|
label: nameWrapperMatchingFilter;
|
|
background-color: ${backgroundColor};
|
|
`,
|
|
nameColumn: css`
|
|
label: nameColumn;
|
|
position: relative;
|
|
white-space: nowrap;
|
|
z-index: 1;
|
|
&:hover {
|
|
z-index: 1;
|
|
}
|
|
`,
|
|
endpointName: css`
|
|
label: endpointName;
|
|
color: ${autoColor(theme, '#484848')};
|
|
font-size: 0.9em;
|
|
`,
|
|
view: css`
|
|
label: view;
|
|
position: relative;
|
|
`,
|
|
viewExpanded: css`
|
|
label: viewExpanded;
|
|
background: ${autoColor(theme, '#f8f8f8')};
|
|
outline: 1px solid ${autoColor(theme, '#ddd')};
|
|
`,
|
|
viewExpandedAndMatchingFilter: css`
|
|
label: viewExpandedAndMatchingFilter;
|
|
background: ${autoColor(theme, '#fff3d7')};
|
|
outline: 1px solid ${autoColor(theme, '#ddd')};
|
|
`,
|
|
row: css`
|
|
label: row;
|
|
font-size: 0.9em;
|
|
&:hover .${spanBarClassName} {
|
|
opacity: 1;
|
|
}
|
|
&:hover .${spanBarLabelClassName} {
|
|
color: ${autoColor(theme, '#000')};
|
|
}
|
|
&:hover .${nameWrapperClassName} {
|
|
background: #f8f8f8;
|
|
background: linear-gradient(
|
|
90deg,
|
|
${autoColor(theme, '#fafafa')},
|
|
${autoColor(theme, '#f8f8f8')} 75%,
|
|
${autoColor(theme, '#eee')}
|
|
);
|
|
}
|
|
&:hover .${viewClassName} {
|
|
background-color: ${autoColor(theme, '#f5f5f5')};
|
|
outline: 1px solid ${autoColor(theme, '#ddd')};
|
|
}
|
|
`,
|
|
rowClippingLeft: css`
|
|
label: rowClippingLeft;
|
|
& .${nameColumnClassName}::before {
|
|
content: ' ';
|
|
height: 100%;
|
|
position: absolute;
|
|
width: 6px;
|
|
background-image: linear-gradient(
|
|
to right,
|
|
${autoColor(theme, 'rgba(25, 25, 25, 0.25)')},
|
|
${autoColor(theme, 'rgba(32, 32, 32, 0)')}
|
|
);
|
|
left: 100%;
|
|
z-index: -1;
|
|
}
|
|
`,
|
|
rowClippingRight: css`
|
|
label: rowClippingRight;
|
|
& .${viewClassName}::before {
|
|
content: ' ';
|
|
height: 100%;
|
|
position: absolute;
|
|
width: 6px;
|
|
background-image: linear-gradient(
|
|
to left,
|
|
${autoColor(theme, 'rgba(25, 25, 25, 0.25)')},
|
|
${autoColor(theme, 'rgba(25, 25, 25, 0.25)')}
|
|
);
|
|
right: 0%;
|
|
z-index: 1;
|
|
}
|
|
`,
|
|
rowExpanded: css`
|
|
label: rowExpanded;
|
|
& .${spanBarClassName} {
|
|
opacity: 1;
|
|
}
|
|
& .${spanBarLabelClassName} {
|
|
color: ${autoColor(theme, '#000')};
|
|
}
|
|
& .${nameWrapperClassName}, &:hover .${nameWrapperClassName} {
|
|
background: ${autoColor(theme, '#f0f0f0')};
|
|
box-shadow: 0 1px 0 ${autoColor(theme, '#ddd')};
|
|
}
|
|
& .${nameWrapperMatchingFilterClassName} {
|
|
background: ${autoColor(theme, '#fff3d7')};
|
|
}
|
|
&:hover .${viewClassName} {
|
|
background: ${autoColor(theme, '#eee')};
|
|
}
|
|
`,
|
|
rowMatchingFilter: css`
|
|
label: rowMatchingFilter;
|
|
// background-color: ${autoColor(theme, '#fffbde')};
|
|
&:hover .${nameWrapperClassName} {
|
|
background: linear-gradient(
|
|
90deg,
|
|
${autoColor(theme, '#fffbde')},
|
|
${autoColor(theme, '#fffbde')} 75%,
|
|
${autoColor(theme, '#f7f1c6')}
|
|
);
|
|
}
|
|
&:hover .${viewClassName} {
|
|
background-color: ${autoColor(theme, '#f7f1c6')};
|
|
outline: 1px solid ${autoColor(theme, '#ddd')};
|
|
}
|
|
`,
|
|
rowFocused: css`
|
|
label: rowFocused;
|
|
background-color: ${autoColor(theme, '#cbe7ff')};
|
|
animation: ${animations.flash} 1s cubic-bezier(0.12, 0, 0.39, 0);
|
|
& .${nameWrapperClassName}, .${viewClassName}, .${nameWrapperMatchingFilterClassName} {
|
|
background-color: ${autoColor(theme, '#cbe7ff')};
|
|
animation: ${animations.flash} 1s cubic-bezier(0.12, 0, 0.39, 0);
|
|
}
|
|
& .${spanBarClassName} {
|
|
opacity: 1;
|
|
}
|
|
& .${spanBarLabelClassName} {
|
|
color: ${autoColor(theme, '#000')};
|
|
}
|
|
&:hover .${nameWrapperClassName}, :hover .${viewClassName} {
|
|
background: ${autoColor(theme, '#d5ebff')};
|
|
box-shadow: 0 1px 0 ${autoColor(theme, '#ddd')};
|
|
}
|
|
`,
|
|
|
|
rowExpandedAndMatchingFilter: css`
|
|
label: rowExpandedAndMatchingFilter;
|
|
&:hover .${viewClassName} {
|
|
background: ${autoColor(theme, '#ffeccf')};
|
|
}
|
|
`,
|
|
|
|
name: css`
|
|
label: name;
|
|
color: ${autoColor(theme, '#000')};
|
|
cursor: pointer;
|
|
flex: 1 1 auto;
|
|
outline: none;
|
|
overflow-y: hidden;
|
|
overflow-x: auto;
|
|
padding-left: 4px;
|
|
padding-right: 0.25em;
|
|
position: relative;
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
&::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
&:focus {
|
|
text-decoration: none;
|
|
}
|
|
&:hover > span {
|
|
color: ${autoColor(theme, '#000')};
|
|
}
|
|
text-align: left;
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom-width: 1px;
|
|
border-bottom-style: solid;
|
|
`,
|
|
nameDetailExpanded: css`
|
|
label: nameDetailExpanded;
|
|
&::before {
|
|
bottom: 0;
|
|
}
|
|
`,
|
|
svcName: css`
|
|
label: svcName;
|
|
font-size: 0.9em;
|
|
font-weight: bold;
|
|
margin-right: 0.25rem;
|
|
`,
|
|
svcNameChildrenCollapsed: css`
|
|
label: svcNameChildrenCollapsed;
|
|
font-weight: bold;
|
|
font-style: italic;
|
|
`,
|
|
errorIcon: css`
|
|
label: errorIcon;
|
|
border-radius: 6.5px;
|
|
color: ${autoColor(theme, '#fff')};
|
|
font-size: 0.85em;
|
|
margin-right: 0.25rem;
|
|
padding: 1px;
|
|
`,
|
|
rpcColorMarker: css`
|
|
label: rpcColorMarker;
|
|
border-radius: 6.5px;
|
|
display: inline-block;
|
|
font-size: 0.85em;
|
|
height: 1em;
|
|
margin-right: 0.25rem;
|
|
padding: 1px;
|
|
width: 1em;
|
|
vertical-align: middle;
|
|
`,
|
|
labelRight: css`
|
|
label: labelRight;
|
|
left: 100%;
|
|
`,
|
|
labelLeft: css`
|
|
label: labelLeft;
|
|
right: 100%;
|
|
`,
|
|
};
|
|
});
|
|
|
|
export type SpanBarRowProps = {
|
|
className?: string;
|
|
theme: GrafanaTheme2;
|
|
color: string;
|
|
spanBarOptions: SpanBarOptions | undefined;
|
|
columnDivision: number;
|
|
isChildrenExpanded: boolean;
|
|
isDetailExpanded: boolean;
|
|
isMatchingFilter: boolean;
|
|
isFocused: boolean;
|
|
showSpanFilterMatchesOnly: boolean;
|
|
onDetailToggled: (spanID: string) => void;
|
|
onChildrenToggled: (spanID: string) => void;
|
|
numTicks: number;
|
|
showServiceName: boolean;
|
|
rpc?:
|
|
| {
|
|
viewStart: number;
|
|
viewEnd: number;
|
|
color: string;
|
|
operationName: string;
|
|
serviceName: string;
|
|
}
|
|
| TNil;
|
|
noInstrumentedServer?:
|
|
| {
|
|
color: string;
|
|
serviceName: string;
|
|
}
|
|
| TNil;
|
|
showErrorIcon: boolean;
|
|
getViewedBounds: ViewedBoundsFunctionType;
|
|
traceStartTime: number;
|
|
span: TraceSpan;
|
|
hoverIndentGuideIds: Set<string>;
|
|
addHoverIndentGuideId: (spanID: string) => void;
|
|
removeHoverIndentGuideId: (spanID: string) => void;
|
|
clippingLeft?: boolean;
|
|
clippingRight?: boolean;
|
|
createSpanLink?: SpanLinkFunc;
|
|
datasourceType: string;
|
|
visibleSpanIds: string[];
|
|
};
|
|
|
|
/**
|
|
* This was originally a stateless function, but changing to a PureComponent
|
|
* reduced the render time of expanding a span row detail by ~50%. This is
|
|
* even true in the case where the stateless function has the same prop types as
|
|
* this class and arrow functions are created in the stateless function as
|
|
* handlers to the onClick props. E.g. for now, the PureComponent is more
|
|
* performance than the stateless function.
|
|
*/
|
|
export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
|
static displayName = 'UnthemedSpanBarRow';
|
|
static defaultProps: Partial<SpanBarRowProps> = {
|
|
className: '',
|
|
rpc: null,
|
|
};
|
|
|
|
_detailToggle = () => {
|
|
this.props.onDetailToggled(this.props.span.spanID);
|
|
};
|
|
|
|
_childrenToggle = () => {
|
|
this.props.onChildrenToggled(this.props.span.spanID);
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
className,
|
|
color,
|
|
spanBarOptions,
|
|
columnDivision,
|
|
isChildrenExpanded,
|
|
isDetailExpanded,
|
|
isMatchingFilter,
|
|
showSpanFilterMatchesOnly,
|
|
isFocused,
|
|
numTicks,
|
|
rpc,
|
|
noInstrumentedServer,
|
|
showErrorIcon,
|
|
getViewedBounds,
|
|
traceStartTime,
|
|
span,
|
|
hoverIndentGuideIds,
|
|
addHoverIndentGuideId,
|
|
removeHoverIndentGuideId,
|
|
clippingLeft,
|
|
clippingRight,
|
|
theme,
|
|
createSpanLink,
|
|
datasourceType,
|
|
showServiceName,
|
|
visibleSpanIds,
|
|
} = this.props;
|
|
const {
|
|
duration,
|
|
hasChildren: isParent,
|
|
operationName,
|
|
process: { serviceName },
|
|
} = span;
|
|
const label = formatDuration(duration);
|
|
|
|
const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration);
|
|
const viewStart = viewBounds.start;
|
|
const viewEnd = viewBounds.end;
|
|
const styles = getStyles(theme, showSpanFilterMatchesOnly);
|
|
|
|
const labelDetail = `${serviceName}::${operationName}`;
|
|
let longLabel;
|
|
let hintClassName;
|
|
if (viewStart > 1 - viewEnd) {
|
|
longLabel = `${labelDetail} | ${label}`;
|
|
hintClassName = styles.labelLeft;
|
|
} else {
|
|
longLabel = `${label} | ${labelDetail}`;
|
|
hintClassName = styles.labelRight;
|
|
}
|
|
|
|
return (
|
|
<TimelineRow
|
|
className={cx(
|
|
styles.row,
|
|
{
|
|
[styles.rowExpanded]: isDetailExpanded,
|
|
[styles.rowMatchingFilter]: isMatchingFilter,
|
|
[styles.rowExpandedAndMatchingFilter]: isMatchingFilter && isDetailExpanded,
|
|
[styles.rowFocused]: isFocused,
|
|
[styles.rowClippingLeft]: clippingLeft,
|
|
[styles.rowClippingRight]: clippingRight,
|
|
},
|
|
className
|
|
)}
|
|
>
|
|
<TimelineRow.Cell className={cx(styles.nameColumn, nameColumnClassName)} width={columnDivision}>
|
|
<div
|
|
className={cx(styles.nameWrapper, nameWrapperClassName, {
|
|
[styles.nameWrapperMatchingFilter]: isMatchingFilter,
|
|
nameWrapperMatchingFilter: isMatchingFilter,
|
|
})}
|
|
>
|
|
<SpanTreeOffset
|
|
onClick={isParent ? this._childrenToggle : undefined}
|
|
childrenVisible={isChildrenExpanded}
|
|
span={span}
|
|
hoverIndentGuideIds={hoverIndentGuideIds}
|
|
addHoverIndentGuideId={addHoverIndentGuideId}
|
|
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
|
visibleSpanIds={visibleSpanIds}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className={cx(styles.name, { [styles.nameDetailExpanded]: isDetailExpanded })}
|
|
aria-checked={isDetailExpanded}
|
|
title={labelDetail}
|
|
onClick={this._detailToggle}
|
|
role="switch"
|
|
style={{ background: `${color}10`, borderBottomColor: `${color}CF` }}
|
|
tabIndex={0}
|
|
>
|
|
{showErrorIcon && (
|
|
<Icon
|
|
name={'exclamation-circle'}
|
|
style={{
|
|
backgroundColor: span.errorIconColor
|
|
? autoColor(theme, span.errorIconColor)
|
|
: autoColor(theme, '#db2828'),
|
|
}}
|
|
className={styles.errorIcon}
|
|
/>
|
|
)}
|
|
{showServiceName && (
|
|
<span
|
|
className={cx(styles.svcName, {
|
|
[styles.svcNameChildrenCollapsed]: isParent && !isChildrenExpanded,
|
|
})}
|
|
>
|
|
{`${serviceName} `}
|
|
</span>
|
|
)}
|
|
{rpc && (
|
|
<span>
|
|
<Icon name={'arrow-right'} />{' '}
|
|
<i className={styles.rpcColorMarker} style={{ background: rpc.color }} />
|
|
{rpc.serviceName}
|
|
</span>
|
|
)}
|
|
{noInstrumentedServer && (
|
|
<span>
|
|
<Icon name={'arrow-right'} />{' '}
|
|
<i className={styles.rpcColorMarker} style={{ background: noInstrumentedServer.color }} />
|
|
{noInstrumentedServer.serviceName}
|
|
</span>
|
|
)}
|
|
<span className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</span>
|
|
<span className={styles.endpointName}> {this.getSpanBarLabel(span, spanBarOptions, label)}</span>
|
|
</button>
|
|
{createSpanLink &&
|
|
(() => {
|
|
const links = createSpanLink(span);
|
|
const count = links?.length || 0;
|
|
if (links && count === 1) {
|
|
if (!links[0]) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<a
|
|
href={links[0].href}
|
|
// Needs to have target otherwise preventDefault would not work due to angularRouter.
|
|
target={'_blank'}
|
|
style={{ background: `${color}10`, borderBottom: `1px solid ${color}CF`, paddingRight: '4px' }}
|
|
rel="noopener noreferrer"
|
|
onClick={
|
|
links[0].onClick
|
|
? (event) => {
|
|
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && links[0].onClick) {
|
|
event.preventDefault();
|
|
links[0].onClick(event);
|
|
}
|
|
}
|
|
: undefined
|
|
}
|
|
>
|
|
{links[0].content}
|
|
</a>
|
|
);
|
|
} else if (links && count > 1) {
|
|
return <SpanLinksMenu links={links} datasourceType={datasourceType} color={color} />;
|
|
} else {
|
|
return null;
|
|
}
|
|
})()}
|
|
</div>
|
|
</TimelineRow.Cell>
|
|
<TimelineRow.Cell
|
|
className={cx(styles.view, viewClassName, {
|
|
[styles.viewExpanded]: isDetailExpanded,
|
|
[styles.viewExpandedAndMatchingFilter]: isMatchingFilter && isDetailExpanded,
|
|
})}
|
|
data-testid="span-view"
|
|
style={{ cursor: 'pointer' }}
|
|
width={1 - columnDivision}
|
|
onClick={this._detailToggle}
|
|
>
|
|
<Ticks numTicks={numTicks} />
|
|
<SpanBar
|
|
rpc={rpc}
|
|
viewStart={viewStart}
|
|
viewEnd={viewEnd}
|
|
getViewedBounds={getViewedBounds}
|
|
color={color}
|
|
shortLabel={label}
|
|
longLabel={longLabel}
|
|
traceStartTime={traceStartTime}
|
|
span={span}
|
|
labelClassName={`${spanBarLabelClassName} ${hintClassName}`}
|
|
className={spanBarClassName}
|
|
/>
|
|
</TimelineRow.Cell>
|
|
</TimelineRow>
|
|
);
|
|
}
|
|
|
|
getSpanBarLabel = (span: TraceSpan, spanBarOptions: SpanBarOptions | undefined, duration: string) => {
|
|
const type = spanBarOptions?.type ?? '';
|
|
|
|
if (type === NONE) {
|
|
return '';
|
|
} else if (type === '' || type === DURATION) {
|
|
return `(${duration})`;
|
|
} else if (type === TAG) {
|
|
const tagKey = spanBarOptions?.tag?.trim() ?? '';
|
|
if (tagKey !== '' && span.tags) {
|
|
const tag = span.tags?.find((tag: TraceKeyValuePair) => {
|
|
return tag.key === tagKey;
|
|
});
|
|
if (tag) {
|
|
return `(${tag.value})`;
|
|
}
|
|
|
|
const process = span.process?.tags?.find((process: TraceKeyValuePair) => {
|
|
return process.key === tagKey;
|
|
});
|
|
if (process) {
|
|
return `(${process.value})`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
};
|
|
}
|
|
|
|
export default withTheme2(UnthemedSpanBarRow);
|