Trace View: Span tree style updates (#113095)
* Style tweaks of span bar row * More tweaks * More tweaks to span style * 2px width for the service color border * Refactor SpanBarRow to function component * Refactor SpanDetailRow to function component * Refactor SpanTreeOffset to function component * eslint ignore * Service name font weight to 500 * Don't render the last indentGuide if the span doens't have children * Fix tests
This commit is contained in:
@@ -167,6 +167,16 @@
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90",
|
||||
"3fa414edcef6ad90"
|
||||
],
|
||||
[
|
||||
@@ -289,7 +299,17 @@
|
||||
"0000000000000074",
|
||||
"0000000000000075",
|
||||
"0000000000000076",
|
||||
"0000000000000077"
|
||||
"0000000000000077",
|
||||
"0000000000000078",
|
||||
"0000000000000079",
|
||||
"000000000000007a",
|
||||
"000000000000007b",
|
||||
"000000000000007c",
|
||||
"000000000000007d",
|
||||
"000000000000007e",
|
||||
"000000000000007f",
|
||||
"0000000000000080",
|
||||
"0000000000000081"
|
||||
],
|
||||
[
|
||||
"",
|
||||
@@ -411,7 +431,17 @@
|
||||
"0000000000000039",
|
||||
"000000000000003a",
|
||||
"000000000000003a",
|
||||
"000000000000003b"
|
||||
"000000000000003b",
|
||||
"000000000000003b",
|
||||
"0000000000000078",
|
||||
"0000000000000078",
|
||||
"0000000000000079",
|
||||
"0000000000000079",
|
||||
"000000000000007a",
|
||||
"000000000000007a",
|
||||
"000000000000007b",
|
||||
"000000000000007b",
|
||||
"000000000000007c"
|
||||
],
|
||||
[
|
||||
"GET /api/health",
|
||||
@@ -533,7 +563,17 @@
|
||||
"GET /rollback/prepare",
|
||||
"POST /commit/save",
|
||||
"GET /branch/merge",
|
||||
"POST /tag/create"
|
||||
"POST /tag/create",
|
||||
"GET /release/notes",
|
||||
"POST /changelog/generate",
|
||||
"GET /documentation/build",
|
||||
"POST /test/run",
|
||||
"GET /coverage/report",
|
||||
"POST /artifact/publish",
|
||||
"GET /registry/pull",
|
||||
"POST /image/push",
|
||||
"GET /manifest/inspect",
|
||||
"POST /deployment/verify"
|
||||
],
|
||||
[
|
||||
"api-gateway",
|
||||
@@ -655,7 +695,17 @@
|
||||
"rollback-mgr",
|
||||
"git-api",
|
||||
"merge-api",
|
||||
"tag-api"
|
||||
"tag-api",
|
||||
"release-api",
|
||||
"changelog-gen",
|
||||
"doc-builder",
|
||||
"test-runner",
|
||||
"coverage-tool",
|
||||
"artifact-mgr",
|
||||
"registry-client",
|
||||
"image-pusher",
|
||||
"manifest-parser",
|
||||
"deploy-verifier"
|
||||
],
|
||||
[
|
||||
1579270400000, 1579270401000, 1579270402000, 1579270403000, 1579270404000, 1579270405000, 1579270406000,
|
||||
@@ -675,7 +725,8 @@
|
||||
1579270498000, 1579270499000, 1579270500000, 1579270501000, 1579270502000, 1579270503000, 1579270504000,
|
||||
1579270505000, 1579270506000, 1579270507000, 1579270508000, 1579270509000, 1579270510000, 1579270511000,
|
||||
1579270512000, 1579270513000, 1579270514000, 1579270515000, 1579270516000, 1579270517000, 1579270518000,
|
||||
1579270519000
|
||||
1579270519000, 1579270520000, 1579270521000, 1579270522000, 1579270523000, 1579270524000, 1579270525000,
|
||||
1579270526000, 1579270527000, 1579270528000, 1579270529000
|
||||
],
|
||||
[
|
||||
100000, 50000, 75000, 200000, 25000, 150000, 80000, 300000, 45000, 120000, 60000, 180000, 35000, 250000,
|
||||
@@ -686,7 +737,8 @@
|
||||
190000, 58000, 145000, 92000, 220000, 48000, 175000, 85000, 205000, 65000, 180000, 72000, 195000, 55000,
|
||||
160000, 98000, 225000, 62000, 140000, 78000, 200000, 48000, 165000, 85000, 215000, 58000, 150000, 88000,
|
||||
230000, 45000, 175000, 72000, 185000, 62000, 155000, 95000, 210000, 52000, 170000, 82000, 195000, 68000,
|
||||
145000, 92000, 235000, 58000, 160000, 75000, 180000, 65000
|
||||
145000, 92000, 235000, 58000, 160000, 75000, 180000, 65000, 125000, 78000, 190000, 52000, 165000, 88000,
|
||||
215000, 62000, 155000, 95000
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,15 +33,15 @@ describe('Trace view', () => {
|
||||
|
||||
e2e.components.TraceViewer.spanBar().should('be.visible');
|
||||
|
||||
e2e.components.TraceViewer.spanBar().its('length').should('be.equal', 100);
|
||||
|
||||
e2e.pages.Explore.General.scrollView().children().first().scrollTo('bottom');
|
||||
|
||||
// After scrolling we should see 50 spans
|
||||
e2e.components.TraceViewer.spanBar()
|
||||
.its('length')
|
||||
.then((oldLength) => {
|
||||
e2e.pages.Explore.General.scrollView().children().first().scrollTo('center');
|
||||
|
||||
// After scrolling we should load more spans
|
||||
e2e.components.TraceViewer.spanBar().should(($span) => {
|
||||
expect($span.length).to.be.gt(oldLength);
|
||||
});
|
||||
.should(($span) => {
|
||||
expect($span).to.be.equal(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2886,31 +2886,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx": {
|
||||
"react-hooks/rules-of-hooks": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
|
||||
@@ -45,11 +45,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
bar: css({
|
||||
label: 'bar',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
borderRadius: theme.shape.radius.sm,
|
||||
minWidth: '2px',
|
||||
position: 'absolute',
|
||||
height: '36%',
|
||||
top: '32%',
|
||||
height: '40%',
|
||||
top: '30%',
|
||||
}),
|
||||
rpc: css({
|
||||
label: 'rpc',
|
||||
@@ -93,7 +93,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
criticalPath: css({
|
||||
position: 'absolute',
|
||||
top: '45%',
|
||||
top: '44%',
|
||||
height: '11%',
|
||||
zIndex: 2,
|
||||
overflow: 'hidden',
|
||||
|
||||
+277
-258
@@ -41,7 +41,7 @@ const nameWrapperMatchingFilterClassName = 'nameWrapperMatchingFilter';
|
||||
const viewClassName = 'jaegerView';
|
||||
const nameColumnClassName = 'nameColumn';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly: boolean) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly: boolean, serviceColor: string) => {
|
||||
const animations = {
|
||||
flash: keyframes`
|
||||
from {
|
||||
@@ -60,10 +60,18 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
lineHeight: '27px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
|
||||
[`& > *`]: {
|
||||
background: theme.colors.background.secondary,
|
||||
},
|
||||
}),
|
||||
nameWrapperMatchingFilter: css({
|
||||
label: 'nameWrapperMatchingFilter',
|
||||
backgroundColor: backgroundColor,
|
||||
|
||||
[`& > *`]: {
|
||||
background: backgroundColor,
|
||||
},
|
||||
}),
|
||||
nameColumn: css({
|
||||
label: 'nameColumn',
|
||||
@@ -96,6 +104,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
row: css({
|
||||
label: 'row',
|
||||
fontSize: '0.9em',
|
||||
|
||||
[`&:hover .${spanBarClassName}`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
@@ -114,6 +123,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
backgroundColor: autoColor(theme, '#f5f5f5'),
|
||||
outline: `1px solid ${autoColor(theme, '#ddd')}`,
|
||||
},
|
||||
['& .icon-wrapper']: {
|
||||
borderBottomColor: `${serviceColor}CF`,
|
||||
borderBottomWidth: '2px',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
}),
|
||||
rowClippingLeft: css({
|
||||
label: 'rowClippingLeft',
|
||||
@@ -168,7 +182,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
}),
|
||||
rowMatchingFilter: css({
|
||||
label: 'rowMatchingFilter',
|
||||
// background-color: ${autoColor(theme, '#fffbde')};
|
||||
|
||||
[`&:hover .${nameWrapperClassName}`]: {
|
||||
background: `linear-gradient(
|
||||
90deg,
|
||||
@@ -200,9 +214,22 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
[`& .${spanBarLabelClassName}`]: {
|
||||
color: autoColor(theme, '#000'),
|
||||
},
|
||||
['&:hover .${nameWrapperClassName}, :hover .${viewClassName}']: {
|
||||
background: autoColor(theme, '#d5ebff'),
|
||||
boxShadow: `0 1px 0 ${autoColor(theme, '#ddd')}`,
|
||||
}),
|
||||
|
||||
rowError: css({
|
||||
label: 'rowError',
|
||||
backgroundColor: theme.colors.error.transparent,
|
||||
|
||||
[`&:hover .${nameWrapperClassName}`]: {
|
||||
background: theme.colors.error.borderTransparent,
|
||||
},
|
||||
[`&:hover .${viewClassName}`]: {
|
||||
backgroundColor: theme.colors.error.borderTransparent,
|
||||
outline: `1px solid ${theme.colors.error.borderTransparent}`,
|
||||
},
|
||||
|
||||
[`& .${nameWrapperClassName} > *`]: {
|
||||
background: theme.colors.error.transparent,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -221,8 +248,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
outline: 'none',
|
||||
overflowY: 'hidden',
|
||||
overflowX: 'auto',
|
||||
paddingLeft: '4px',
|
||||
paddingRight: '0.25em',
|
||||
padding: '4px',
|
||||
position: 'relative',
|
||||
'-ms-overflow-style': 'none',
|
||||
scrollbarWidth: 'none',
|
||||
@@ -236,9 +262,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
color: autoColor(theme, '#000'),
|
||||
},
|
||||
textAlign: 'left',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: `${serviceColor}CF`,
|
||||
borderBottomWidth: '2px',
|
||||
borderBottomStyle: 'solid',
|
||||
}),
|
||||
nameDetailExpanded: css({
|
||||
@@ -250,27 +276,25 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
|
||||
svcName: css({
|
||||
label: 'svcName',
|
||||
fontSize: '0.9em',
|
||||
fontWeight: 'bold',
|
||||
fontWeight: '500',
|
||||
marginRight: '0.25rem',
|
||||
}),
|
||||
svcNameChildrenCollapsed: css({
|
||||
label: 'svcNameChildrenCollapsed',
|
||||
fontWeight: 'bold',
|
||||
fontWeight: '500',
|
||||
fontStyle: 'italic',
|
||||
}),
|
||||
errorIcon: css({
|
||||
label: 'errorIcon',
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: '6.5px',
|
||||
borderRadius: theme.shape.radius.md,
|
||||
color: autoColor(theme, '#fff'),
|
||||
fontSize: '0.85em',
|
||||
fontSize: '0.6em',
|
||||
marginRight: '0.25rem',
|
||||
padding: '1px',
|
||||
}),
|
||||
rpcColorMarker: css({
|
||||
label: 'rpcColorMarker',
|
||||
// eslint-disable-next-line @grafana/no-border-radius-literal
|
||||
borderRadius: '6.5px',
|
||||
borderRadius: theme.shape.radius.md,
|
||||
display: 'inline-block',
|
||||
fontSize: '0.85em',
|
||||
height: '1em',
|
||||
@@ -335,257 +359,252 @@ export type SpanBarRowProps = {
|
||||
criticalPath: CriticalPathSection[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
const UnthemedSpanBarRow = React.memo<SpanBarRowProps>((props) => {
|
||||
const {
|
||||
className = '',
|
||||
color,
|
||||
spanBarOptions,
|
||||
columnDivision,
|
||||
isChildrenExpanded,
|
||||
isDetailExpanded,
|
||||
isMatchingFilter,
|
||||
showSpanFilterMatchesOnly,
|
||||
isFocused,
|
||||
numTicks,
|
||||
rpc = null,
|
||||
noInstrumentedServer,
|
||||
showErrorIcon,
|
||||
getViewedBounds,
|
||||
traceStartTime,
|
||||
span,
|
||||
hoverIndentGuideIds,
|
||||
addHoverIndentGuideId,
|
||||
removeHoverIndentGuideId,
|
||||
clippingLeft,
|
||||
clippingRight,
|
||||
theme,
|
||||
createSpanLink,
|
||||
datasourceType,
|
||||
showServiceName,
|
||||
visibleSpanIds,
|
||||
criticalPath,
|
||||
onDetailToggled,
|
||||
onChildrenToggled,
|
||||
} = props;
|
||||
|
||||
_detailToggle = () => {
|
||||
this.props.onDetailToggled(this.props.span.spanID);
|
||||
};
|
||||
const {
|
||||
duration,
|
||||
hasChildren: isParent,
|
||||
operationName,
|
||||
process: { serviceName },
|
||||
} = span;
|
||||
const label = formatDuration(duration);
|
||||
|
||||
_childrenToggle = () => {
|
||||
this.props.onChildrenToggled(this.props.span.spanID);
|
||||
};
|
||||
const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration);
|
||||
const viewStart = viewBounds.start;
|
||||
const viewEnd = viewBounds.end;
|
||||
const styles = getStyles(theme, showSpanFilterMatchesOnly, color);
|
||||
|
||||
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,
|
||||
criticalPath,
|
||||
} = 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
|
||||
criticalPath={criticalPath}
|
||||
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>
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
getSpanBarLabel = (span: TraceSpan, spanBarOptions: SpanBarOptions | undefined, duration: string) => {
|
||||
const type = spanBarOptions?.type ?? '';
|
||||
const handleDetailToggle = React.useCallback(() => {
|
||||
onDetailToggled(span.spanID);
|
||||
}, [onDetailToggled, span.spanID]);
|
||||
|
||||
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 handleChildrenToggle = React.useCallback(() => {
|
||||
onChildrenToggled(span.spanID);
|
||||
}, [onChildrenToggled, span.spanID]);
|
||||
|
||||
const process = span.process?.tags?.find((process: TraceKeyValuePair) => {
|
||||
return process.key === tagKey;
|
||||
});
|
||||
if (process) {
|
||||
return `(${process.value})`;
|
||||
const getSpanBarLabel = React.useCallback(
|
||||
(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 '';
|
||||
};
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<TimelineRow
|
||||
className={cx(
|
||||
styles.row,
|
||||
{
|
||||
[styles.rowError]: showErrorIcon,
|
||||
[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 ? handleChildrenToggle : 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={handleDetailToggle}
|
||||
role="switch"
|
||||
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}> {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={{
|
||||
borderBottom: `2px solid ${color}CF`,
|
||||
paddingInline: '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,
|
||||
[styles.rowError]: showErrorIcon,
|
||||
})}
|
||||
data-testid="span-view"
|
||||
style={{ cursor: 'pointer' }}
|
||||
width={1 - columnDivision}
|
||||
onClick={handleDetailToggle}
|
||||
>
|
||||
<Ticks numTicks={numTicks} />
|
||||
<SpanBar
|
||||
criticalPath={criticalPath}
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
UnthemedSpanBarRow.displayName = 'UnthemedSpanBarRow';
|
||||
|
||||
export default withTheme2(UnthemedSpanBarRow);
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ import { createTheme, dateTime } from '@grafana/data';
|
||||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
|
||||
import DetailState from './SpanDetail/DetailState';
|
||||
import { UnthemedSpanDetailRow, SpanDetailRowProps } from './SpanDetailRow';
|
||||
import SpanDetailRow, { type SpanDetailRowProps } from './SpanDetailRow';
|
||||
|
||||
const testSpan = {
|
||||
spanID: 'testSpanID',
|
||||
@@ -57,7 +57,7 @@ const setup = (propOverrides?: SpanDetailRowProps) => {
|
||||
},
|
||||
...propOverrides,
|
||||
};
|
||||
return render(<UnthemedSpanDetailRow {...(props as SpanDetailRowProps)} />);
|
||||
return render(<SpanDetailRow {...(props as SpanDetailRowProps)} />);
|
||||
};
|
||||
|
||||
describe('SpanDetailRow tests', () => {
|
||||
|
||||
+85
-87
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceLog } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
@@ -117,95 +117,93 @@ export type SpanDetailRowProps = {
|
||||
app: CoreApp;
|
||||
};
|
||||
|
||||
export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
||||
_detailToggle = () => {
|
||||
this.props.onDetailToggled(this.props.span.spanID);
|
||||
};
|
||||
const UnthemedSpanDetailRow = React.memo<SpanDetailRowProps>((props) => {
|
||||
const {
|
||||
color,
|
||||
detailState,
|
||||
logItemToggle,
|
||||
logsToggle,
|
||||
processToggle,
|
||||
referenceItemToggle,
|
||||
referencesToggle,
|
||||
warningsToggle,
|
||||
stackTracesToggle,
|
||||
span,
|
||||
traceToProfilesOptions,
|
||||
timeZone,
|
||||
tagsToggle,
|
||||
traceStartTime,
|
||||
traceDuration,
|
||||
traceName,
|
||||
theme,
|
||||
createSpanLink,
|
||||
focusedSpanId,
|
||||
createFocusSpanLink,
|
||||
datasourceType,
|
||||
datasourceUid,
|
||||
traceFlameGraphs,
|
||||
setTraceFlameGraphs,
|
||||
setRedrawListView,
|
||||
timeRange,
|
||||
app,
|
||||
hoverIndentGuideIds,
|
||||
addHoverIndentGuideId,
|
||||
removeHoverIndentGuideId,
|
||||
visibleSpanIds,
|
||||
} = props;
|
||||
|
||||
render() {
|
||||
const {
|
||||
color,
|
||||
detailState,
|
||||
logItemToggle,
|
||||
logsToggle,
|
||||
processToggle,
|
||||
referenceItemToggle,
|
||||
referencesToggle,
|
||||
warningsToggle,
|
||||
stackTracesToggle,
|
||||
span,
|
||||
traceToProfilesOptions,
|
||||
timeZone,
|
||||
tagsToggle,
|
||||
traceStartTime,
|
||||
traceDuration,
|
||||
traceName,
|
||||
theme,
|
||||
createSpanLink,
|
||||
focusedSpanId,
|
||||
createFocusSpanLink,
|
||||
datasourceType,
|
||||
datasourceUid,
|
||||
traceFlameGraphs,
|
||||
setTraceFlameGraphs,
|
||||
setRedrawListView,
|
||||
timeRange,
|
||||
app,
|
||||
hoverIndentGuideIds,
|
||||
addHoverIndentGuideId,
|
||||
removeHoverIndentGuideId,
|
||||
visibleSpanIds,
|
||||
} = this.props;
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<TimelineRow>
|
||||
<TimelineRow.Cell width={1} className={styles.cell}>
|
||||
<div className={styles.indentSpacer}>
|
||||
<SpanTreeOffset
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<TimelineRow>
|
||||
<TimelineRow.Cell width={1} className={styles.cell}>
|
||||
<div className={styles.indentSpacer}>
|
||||
<SpanTreeOffset
|
||||
span={span}
|
||||
showChildrenIcon={false}
|
||||
hoverIndentGuideIds={hoverIndentGuideIds}
|
||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||
visibleSpanIds={visibleSpanIds}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailWrapper}>
|
||||
<div className={styles.infoWrapper} style={{ borderTopColor: color }}>
|
||||
<SpanDetail
|
||||
color={color}
|
||||
detailState={detailState}
|
||||
logItemToggle={logItemToggle}
|
||||
logsToggle={logsToggle}
|
||||
processToggle={processToggle}
|
||||
referenceItemToggle={referenceItemToggle}
|
||||
referencesToggle={referencesToggle}
|
||||
warningsToggle={warningsToggle}
|
||||
stackTracesToggle={stackTracesToggle}
|
||||
span={span}
|
||||
showChildrenIcon={false}
|
||||
hoverIndentGuideIds={hoverIndentGuideIds}
|
||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||
visibleSpanIds={visibleSpanIds}
|
||||
traceToProfilesOptions={traceToProfilesOptions}
|
||||
timeZone={timeZone}
|
||||
tagsToggle={tagsToggle}
|
||||
traceStartTime={traceStartTime}
|
||||
traceDuration={traceDuration}
|
||||
traceName={traceName}
|
||||
createSpanLink={createSpanLink}
|
||||
focusedSpanId={focusedSpanId}
|
||||
createFocusSpanLink={createFocusSpanLink}
|
||||
datasourceType={datasourceType}
|
||||
datasourceUid={datasourceUid}
|
||||
traceFlameGraphs={traceFlameGraphs}
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
setRedrawListView={setRedrawListView}
|
||||
timeRange={timeRange}
|
||||
app={app}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailWrapper}>
|
||||
<div className={styles.infoWrapper} style={{ borderTopColor: color }}>
|
||||
<SpanDetail
|
||||
color={color}
|
||||
detailState={detailState}
|
||||
logItemToggle={logItemToggle}
|
||||
logsToggle={logsToggle}
|
||||
processToggle={processToggle}
|
||||
referenceItemToggle={referenceItemToggle}
|
||||
referencesToggle={referencesToggle}
|
||||
warningsToggle={warningsToggle}
|
||||
stackTracesToggle={stackTracesToggle}
|
||||
span={span}
|
||||
traceToProfilesOptions={traceToProfilesOptions}
|
||||
timeZone={timeZone}
|
||||
tagsToggle={tagsToggle}
|
||||
traceStartTime={traceStartTime}
|
||||
traceDuration={traceDuration}
|
||||
traceName={traceName}
|
||||
createSpanLink={createSpanLink}
|
||||
focusedSpanId={focusedSpanId}
|
||||
createFocusSpanLink={createFocusSpanLink}
|
||||
datasourceType={datasourceType}
|
||||
datasourceUid={datasourceUid}
|
||||
traceFlameGraphs={traceFlameGraphs}
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
setRedrawListView={setRedrawListView}
|
||||
timeRange={timeRange}
|
||||
app={app}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineRow.Cell>
|
||||
</TimelineRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</TimelineRow.Cell>
|
||||
</TimelineRow>
|
||||
);
|
||||
});
|
||||
|
||||
UnthemedSpanDetailRow.displayName = 'UnthemedSpanDetailRow';
|
||||
|
||||
export default withTheme2(UnthemedSpanDetailRow);
|
||||
|
||||
@@ -18,7 +18,8 @@ const renderMenuItems = (
|
||||
closeMenu: () => void,
|
||||
datasourceType: string
|
||||
) => {
|
||||
links.sort(function (linkA, linkB) {
|
||||
links.sort((linkA, linkB) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return (linkA.title || 'link').toLowerCase().localeCompare((linkB.title || 'link').toLowerCase());
|
||||
});
|
||||
|
||||
@@ -88,7 +89,7 @@ const getStyles = (color: string) => ({
|
||||
border: 'none',
|
||||
background: `${color}10`,
|
||||
borderBottom: `1px solid ${color}CF`,
|
||||
paddingRight: '4px',
|
||||
paddingInline: '4px',
|
||||
}),
|
||||
button: css({
|
||||
background: 'transparent',
|
||||
|
||||
+25
-8
@@ -47,15 +47,32 @@ describe('SpanTreeOffset', () => {
|
||||
});
|
||||
|
||||
describe('.SpanTreeOffset--indentGuide', () => {
|
||||
it('renders only one SpanTreeOffset--indentGuide for entire trace if span has no ancestors', () => {
|
||||
it('renders no indentGuide if span has no ancestors and no children', () => {
|
||||
jest.mocked(spanAncestorIdsSpy).mockReturnValue([]);
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const indentGuides = screen.queryAllByTestId('SpanTreeOffset--indentGuide');
|
||||
expect(indentGuides.length).toBe(0);
|
||||
});
|
||||
|
||||
it('renders only one SpanTreeOffset--indentGuide for entire trace if span has no ancestors but has children', () => {
|
||||
jest.mocked(spanAncestorIdsSpy).mockReturnValue([]);
|
||||
props.span.hasChildren = true;
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const indentGuide = screen.getByTestId('SpanTreeOffset--indentGuide');
|
||||
expect(indentGuide).toBeInTheDocument();
|
||||
expect(indentGuide).toHaveAttribute('data-ancestor-id', specialRootID);
|
||||
});
|
||||
|
||||
it('renders one SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace', () => {
|
||||
it('renders one SpanTreeOffset--indentGuide per ancestor span when span has no children', () => {
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const indentGuides = screen.getAllByTestId('SpanTreeOffset--indentGuide');
|
||||
expect(indentGuides.length).toBe(2);
|
||||
expect(indentGuides[0]).toHaveAttribute('data-ancestor-id', specialRootID);
|
||||
expect(indentGuides[1]).toHaveAttribute('data-ancestor-id', rootSpanID);
|
||||
});
|
||||
|
||||
it('renders one SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace when span has children', () => {
|
||||
props.span.hasChildren = true;
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const indentGuides = screen.getAllByTestId('SpanTreeOffset--indentGuide');
|
||||
expect(indentGuides.length).toBe(3);
|
||||
@@ -65,28 +82,28 @@ describe('SpanTreeOffset', () => {
|
||||
});
|
||||
|
||||
it('adds .is-active to correct indentGuide', () => {
|
||||
props.hoverIndentGuideIds = new Set([parentSpanID]);
|
||||
props.hoverIndentGuideIds = new Set([rootSpanID]);
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const styles = getStyles(createTheme());
|
||||
const activeIndentGuide = document.querySelector(`.${styles.indentGuideActive}`);
|
||||
expect(activeIndentGuide).toBeInTheDocument();
|
||||
expect(activeIndentGuide).toHaveAttribute('data-ancestor-id', parentSpanID);
|
||||
expect(activeIndentGuide).toHaveAttribute('data-ancestor-id', rootSpanID);
|
||||
});
|
||||
|
||||
it('calls props.addHoverIndentGuideId on mouse enter', async () => {
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const span = document.querySelector(`[data-ancestor-id=${parentSpanID}]`);
|
||||
const span = document.querySelector(`[data-ancestor-id=${rootSpanID}]`);
|
||||
await userEvent.hover(span!);
|
||||
expect(props.addHoverIndentGuideId).toHaveBeenCalledTimes(1);
|
||||
expect(props.addHoverIndentGuideId).toHaveBeenCalledWith(parentSpanID);
|
||||
expect(props.addHoverIndentGuideId).toHaveBeenCalledWith(rootSpanID);
|
||||
});
|
||||
|
||||
it('calls props.removeHoverIndentGuideId on mouse leave', async () => {
|
||||
render(<SpanTreeOffset {...props} />);
|
||||
const span = document.querySelector(`[data-ancestor-id=${parentSpanID}]`);
|
||||
const span = document.querySelector(`[data-ancestor-id=${rootSpanID}]`);
|
||||
await userEvent.unhover(span!);
|
||||
expect(props.removeHoverIndentGuideId).toHaveBeenCalledTimes(1);
|
||||
expect(props.removeHoverIndentGuideId).toHaveBeenCalledWith(parentSpanID);
|
||||
expect(props.removeHoverIndentGuideId).toHaveBeenCalledWith(rootSpanID);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+89
-76
@@ -64,6 +64,8 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
label: 'iconWrapper',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
height: '100%',
|
||||
paddingTop: '1px',
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -80,26 +82,27 @@ export type TProps = {
|
||||
visibleSpanIds: string[];
|
||||
};
|
||||
|
||||
export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> {
|
||||
static displayName = 'UnthemedSpanTreeOffset';
|
||||
const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
||||
const {
|
||||
childrenVisible = false,
|
||||
showChildrenIcon = true,
|
||||
onClick,
|
||||
span,
|
||||
theme,
|
||||
visibleSpanIds,
|
||||
hoverIndentGuideIds,
|
||||
addHoverIndentGuideId,
|
||||
removeHoverIndentGuideId,
|
||||
} = props;
|
||||
|
||||
ancestorIds: string[];
|
||||
|
||||
static defaultProps = {
|
||||
childrenVisible: false,
|
||||
showChildrenIcon: true,
|
||||
};
|
||||
|
||||
constructor(props: TProps) {
|
||||
super(props);
|
||||
|
||||
this.ancestorIds = spanAncestorIds(props.span);
|
||||
const ancestorIds = React.useMemo(() => {
|
||||
const ids = spanAncestorIds(span);
|
||||
// Some traces have multiple root-level spans, this connects them all under one guideline and adds the
|
||||
// necessary padding for the collapse icon on root-level spans.
|
||||
this.ancestorIds.push('root');
|
||||
|
||||
this.ancestorIds.reverse();
|
||||
}
|
||||
ids.push('root');
|
||||
ids.reverse();
|
||||
return ids;
|
||||
}, [span]);
|
||||
|
||||
/**
|
||||
* If the mouse leaves to anywhere except another span with the same ancestor id, this span's ancestor id is
|
||||
@@ -109,14 +112,17 @@ export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> {
|
||||
* the element the user is now hovering.
|
||||
* @param {string} ancestorId - The span id that the user was hovering over.
|
||||
*/
|
||||
handleMouseLeave = (event: React.MouseEvent<HTMLSpanElement>, ancestorId: string) => {
|
||||
if (
|
||||
!(event.relatedTarget instanceof HTMLSpanElement) ||
|
||||
_get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId
|
||||
) {
|
||||
this.props.removeHoverIndentGuideId(ancestorId);
|
||||
}
|
||||
};
|
||||
const handleMouseLeave = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLSpanElement>, ancestorId: string) => {
|
||||
if (
|
||||
!(event.relatedTarget instanceof HTMLSpanElement) ||
|
||||
_get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId
|
||||
) {
|
||||
removeHoverIndentGuideId(ancestorId);
|
||||
}
|
||||
},
|
||||
[removeHoverIndentGuideId]
|
||||
);
|
||||
|
||||
/**
|
||||
* If the mouse entered this span from anywhere except another span with the same ancestor id, this span's
|
||||
@@ -126,58 +132,65 @@ export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> {
|
||||
* the last element the user was hovering.
|
||||
* @param {string} ancestorId - The span id that the user is now hovering over.
|
||||
*/
|
||||
handleMouseEnter = (event: React.MouseEvent<HTMLSpanElement>, ancestorId: string) => {
|
||||
if (
|
||||
!(event.relatedTarget instanceof HTMLSpanElement) ||
|
||||
_get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId
|
||||
) {
|
||||
this.props.addHoverIndentGuideId(ancestorId);
|
||||
}
|
||||
};
|
||||
const handleMouseEnter = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLSpanElement>, ancestorId: string) => {
|
||||
if (
|
||||
!(event.relatedTarget instanceof HTMLSpanElement) ||
|
||||
_get(event, 'relatedTarget.dataset.ancestorId') !== ancestorId
|
||||
) {
|
||||
addHoverIndentGuideId(ancestorId);
|
||||
}
|
||||
},
|
||||
[addHoverIndentGuideId]
|
||||
);
|
||||
|
||||
render() {
|
||||
const { childrenVisible, onClick, showChildrenIcon, span, theme, visibleSpanIds } = this.props;
|
||||
const { hasChildren, spanID } = span;
|
||||
const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null;
|
||||
const icon =
|
||||
showChildrenIcon &&
|
||||
hasChildren &&
|
||||
(childrenVisible ? (
|
||||
<Icon name={'angle-down'} data-testid="icon-arrow-down" size={'sm'} />
|
||||
) : (
|
||||
<Icon name={'angle-right'} data-testid="icon-arrow-right" size={'sm'} />
|
||||
));
|
||||
const styles = getStyles(theme);
|
||||
const { hasChildren, spanID } = span;
|
||||
const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null;
|
||||
const icon =
|
||||
showChildrenIcon &&
|
||||
hasChildren &&
|
||||
(childrenVisible ? (
|
||||
<Icon name={'angle-down'} data-testid="icon-arrow-down" size={'sm'} />
|
||||
) : (
|
||||
<Icon name={'angle-right'} data-testid="icon-arrow-right" size={'sm'} />
|
||||
));
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}>
|
||||
{this.ancestorIds.map((ancestorId, index) => (
|
||||
<span
|
||||
key={ancestorId}
|
||||
className={cx(styles.indentGuide, {
|
||||
[styles.indentGuideActive]: this.props.hoverIndentGuideIds.has(ancestorId),
|
||||
[styles.indentGuideThin]:
|
||||
index !== this.ancestorIds.length - 1 && ancestorId !== 'root' && !visibleSpanIds.includes(ancestorId),
|
||||
})}
|
||||
data-ancestor-id={ancestorId}
|
||||
data-testid="SpanTreeOffset--indentGuide"
|
||||
onMouseEnter={(event) => this.handleMouseEnter(event, ancestorId)}
|
||||
onMouseLeave={(event) => this.handleMouseLeave(event, ancestorId)}
|
||||
/>
|
||||
))}
|
||||
{icon && (
|
||||
<span
|
||||
className={styles.iconWrapper}
|
||||
onMouseEnter={(event) => this.handleMouseEnter(event, spanID)}
|
||||
onMouseLeave={(event) => this.handleMouseLeave(event, spanID)}
|
||||
data-testid="icon-wrapper"
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
// If span has no children, don't show the last indent guide
|
||||
const displayedAncestorIds = hasChildren ? ancestorIds : ancestorIds.slice(0, -1);
|
||||
|
||||
return (
|
||||
<span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}>
|
||||
{displayedAncestorIds.map((ancestorId, index) => (
|
||||
<span
|
||||
key={ancestorId}
|
||||
className={cx(styles.indentGuide, {
|
||||
[styles.indentGuideActive]: hoverIndentGuideIds.has(ancestorId),
|
||||
[styles.indentGuideThin]:
|
||||
index !== displayedAncestorIds.length - 1 &&
|
||||
ancestorId !== 'root' &&
|
||||
!visibleSpanIds.includes(ancestorId),
|
||||
})}
|
||||
data-ancestor-id={ancestorId}
|
||||
data-testid="SpanTreeOffset--indentGuide"
|
||||
onMouseEnter={(event) => handleMouseEnter(event, ancestorId)}
|
||||
onMouseLeave={(event) => handleMouseLeave(event, ancestorId)}
|
||||
/>
|
||||
))}
|
||||
{icon && (
|
||||
<span
|
||||
className={cx(styles.iconWrapper, 'icon-wrapper')}
|
||||
onMouseEnter={(event) => handleMouseEnter(event, spanID)}
|
||||
onMouseLeave={(event) => handleMouseLeave(event, spanID)}
|
||||
data-testid="icon-wrapper"
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
UnthemedSpanTreeOffset.displayName = 'UnthemedSpanTreeOffset';
|
||||
|
||||
export default withTheme2(UnthemedSpanTreeOffset);
|
||||
|
||||
Reference in New Issue
Block a user