Trace View: UI improvements (#114156)
* Fix unclear child span relationship * Trace view UI improvements * Fix tests
This commit is contained in:
+1
-1
@@ -157,7 +157,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
label: 'ServiceNameAndLinks',
|
label: 'ServiceNameAndLinks',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginBottom: '16px',
|
marginBottom: theme.spacing(1),
|
||||||
}),
|
}),
|
||||||
operationName: css({
|
operationName: css({
|
||||||
label: 'SpanDetailOperationName',
|
label: 'SpanDetailOperationName',
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
label: 'detailWrapper',
|
label: 'detailWrapper',
|
||||||
flex: '1',
|
flex: '1',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
backgroundColor: theme.colors.background.canvas,
|
||||||
|
border: `1px solid ${theme.colors.border.weak}`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -165,6 +167,7 @@ const UnthemedSpanDetailRow = React.memo<SpanDetailRowProps>((props) => {
|
|||||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||||
visibleSpanIds={visibleSpanIds}
|
visibleSpanIds={visibleSpanIds}
|
||||||
|
removeLastIndentGuide={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.detailWrapper}>
|
<div className={styles.detailWrapper}>
|
||||||
|
|||||||
+13
-7
@@ -47,11 +47,12 @@ describe('SpanTreeOffset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('.SpanTreeOffset--indentGuide', () => {
|
describe('.SpanTreeOffset--indentGuide', () => {
|
||||||
it('renders no indentGuide if span has no ancestors and no children', () => {
|
it('renders only the trace-level indentGuide if span has no ancestors and no children', () => {
|
||||||
jest.mocked(spanAncestorIdsSpy).mockReturnValue([]);
|
jest.mocked(spanAncestorIdsSpy).mockReturnValue([]);
|
||||||
render(<SpanTreeOffset {...props} />);
|
render(<SpanTreeOffset {...props} />);
|
||||||
const indentGuides = screen.queryAllByTestId('SpanTreeOffset--indentGuide');
|
const indentGuides = screen.queryAllByTestId('SpanTreeOffset--indentGuide');
|
||||||
expect(indentGuides.length).toBe(0);
|
expect(indentGuides.length).toBe(1);
|
||||||
|
expect(indentGuides[0]).toHaveAttribute('data-ancestor-id', specialRootID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders only one SpanTreeOffset--indentGuide for entire trace if span has no ancestors but has children', () => {
|
it('renders only one SpanTreeOffset--indentGuide for entire trace if span has no ancestors but has children', () => {
|
||||||
@@ -66,9 +67,10 @@ describe('SpanTreeOffset', () => {
|
|||||||
it('renders one SpanTreeOffset--indentGuide per ancestor span when span has no children', () => {
|
it('renders one SpanTreeOffset--indentGuide per ancestor span when span has no children', () => {
|
||||||
render(<SpanTreeOffset {...props} />);
|
render(<SpanTreeOffset {...props} />);
|
||||||
const indentGuides = screen.getAllByTestId('SpanTreeOffset--indentGuide');
|
const indentGuides = screen.getAllByTestId('SpanTreeOffset--indentGuide');
|
||||||
expect(indentGuides.length).toBe(2);
|
expect(indentGuides.length).toBe(3);
|
||||||
expect(indentGuides[0]).toHaveAttribute('data-ancestor-id', specialRootID);
|
expect(indentGuides[0]).toHaveAttribute('data-ancestor-id', specialRootID);
|
||||||
expect(indentGuides[1]).toHaveAttribute('data-ancestor-id', rootSpanID);
|
expect(indentGuides[1]).toHaveAttribute('data-ancestor-id', rootSpanID);
|
||||||
|
expect(indentGuides[2]).toHaveAttribute('data-ancestor-id', parentSpanID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders one SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace when span has children', () => {
|
it('renders one SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace when span has children', () => {
|
||||||
@@ -112,16 +114,20 @@ describe('SpanTreeOffset', () => {
|
|||||||
props = { ...props, span: { ...props.span, hasChildren: true } };
|
props = { ...props, span: { ...props.span, hasChildren: true } };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render icon if props.span.hasChildren is false', () => {
|
it('renders placeholder content when props.span.hasChildren is false', () => {
|
||||||
props.span.hasChildren = false;
|
props.span.hasChildren = false;
|
||||||
render(<SpanTreeOffset {...props} />);
|
render(<SpanTreeOffset {...props} />);
|
||||||
expect(screen.queryByTestId('icon-wrapper')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('icon-arrow-right')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('icon-arrow-down')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('icon-wrapper')).toHaveTextContent('-');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render icon if props.span.hasChildren is true and showChildrenIcon is false', () => {
|
it('renders placeholder content when props.span.hasChildren is true but showChildrenIcon is false', () => {
|
||||||
props.showChildrenIcon = false;
|
props.showChildrenIcon = false;
|
||||||
render(<SpanTreeOffset {...props} />);
|
render(<SpanTreeOffset {...props} />);
|
||||||
expect(screen.queryByTestId('icon-wrapper')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('icon-arrow-right')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('icon-arrow-down')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('icon-wrapper')).toHaveTextContent('-');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders arrow-right if props.span.hasChildren is true and props.childrenVisible is false', () => {
|
it('renders arrow-right if props.span.hasChildren is true and props.childrenVisible is false', () => {
|
||||||
|
|||||||
+19
-18
@@ -66,6 +66,8 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
|||||||
right: 0,
|
right: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
paddingTop: '1px',
|
paddingTop: '1px',
|
||||||
|
width: '1rem',
|
||||||
|
textAlign: 'center',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ export type TProps = {
|
|||||||
removeHoverIndentGuideId: (spanID: string) => void;
|
removeHoverIndentGuideId: (spanID: string) => void;
|
||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
visibleSpanIds: string[];
|
visibleSpanIds: string[];
|
||||||
|
removeLastIndentGuide?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
||||||
@@ -93,6 +96,7 @@ const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
|||||||
hoverIndentGuideIds,
|
hoverIndentGuideIds,
|
||||||
addHoverIndentGuideId,
|
addHoverIndentGuideId,
|
||||||
removeHoverIndentGuideId,
|
removeHoverIndentGuideId,
|
||||||
|
removeLastIndentGuide = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const ancestorIds = React.useMemo(() => {
|
const ancestorIds = React.useMemo(() => {
|
||||||
@@ -101,8 +105,12 @@ const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
|||||||
// necessary padding for the collapse icon on root-level spans.
|
// necessary padding for the collapse icon on root-level spans.
|
||||||
ids.push('root');
|
ids.push('root');
|
||||||
ids.reverse();
|
ids.reverse();
|
||||||
|
|
||||||
|
if (removeLastIndentGuide) {
|
||||||
|
ids.pop();
|
||||||
|
}
|
||||||
return ids;
|
return ids;
|
||||||
}, [span]);
|
}, [span, removeLastIndentGuide]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the mouse leaves to anywhere except another span with the same ancestor id, this span's ancestor id is
|
* If the mouse leaves to anywhere except another span with the same ancestor id, this span's ancestor id is
|
||||||
@@ -156,20 +164,15 @@ const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
|||||||
));
|
));
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
// If span has no children, don't show the last indent guide
|
|
||||||
const displayedAncestorIds = hasChildren ? ancestorIds : ancestorIds.slice(0, -1);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}>
|
<span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}>
|
||||||
{displayedAncestorIds.map((ancestorId, index) => (
|
{ancestorIds.map((ancestorId, index) => (
|
||||||
<span
|
<span
|
||||||
key={ancestorId}
|
key={ancestorId}
|
||||||
className={cx(styles.indentGuide, {
|
className={cx(styles.indentGuide, {
|
||||||
[styles.indentGuideActive]: hoverIndentGuideIds.has(ancestorId),
|
[styles.indentGuideActive]: hoverIndentGuideIds.has(ancestorId),
|
||||||
[styles.indentGuideThin]:
|
[styles.indentGuideThin]:
|
||||||
index !== displayedAncestorIds.length - 1 &&
|
index !== ancestorIds.length - 1 && ancestorId !== 'root' && !visibleSpanIds.includes(ancestorId),
|
||||||
ancestorId !== 'root' &&
|
|
||||||
!visibleSpanIds.includes(ancestorId),
|
|
||||||
})}
|
})}
|
||||||
data-ancestor-id={ancestorId}
|
data-ancestor-id={ancestorId}
|
||||||
data-testid="SpanTreeOffset--indentGuide"
|
data-testid="SpanTreeOffset--indentGuide"
|
||||||
@@ -177,16 +180,14 @@ const UnthemedSpanTreeOffset = React.memo<TProps>((props) => {
|
|||||||
onMouseLeave={(event) => handleMouseLeave(event, ancestorId)}
|
onMouseLeave={(event) => handleMouseLeave(event, ancestorId)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{icon && (
|
<span
|
||||||
<span
|
className={cx(styles.iconWrapper, 'icon-wrapper')}
|
||||||
className={cx(styles.iconWrapper, 'icon-wrapper')}
|
onMouseEnter={(event) => icon && handleMouseEnter(event, spanID)}
|
||||||
onMouseEnter={(event) => handleMouseEnter(event, spanID)}
|
onMouseLeave={(event) => icon && handleMouseLeave(event, spanID)}
|
||||||
onMouseLeave={(event) => handleMouseLeave(event, spanID)}
|
data-testid="icon-wrapper"
|
||||||
data-testid="icon-wrapper"
|
>
|
||||||
>
|
{icon || (!removeLastIndentGuide && '-')}
|
||||||
{icon}
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => {
|
|||||||
listStyle: 'none',
|
listStyle: 'none',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
fontSize: theme.typography.size.sm,
|
||||||
...(divider
|
...(divider
|
||||||
? {
|
? {
|
||||||
marginRight: '-8px',
|
marginRight: '-8px',
|
||||||
@@ -49,7 +50,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => {
|
|||||||
}),
|
}),
|
||||||
LabeledListLabel: css({
|
LabeledListLabel: css({
|
||||||
label: 'LabeledListLabel',
|
label: 'LabeledListLabel',
|
||||||
color: theme.isLight ? '#999' : '#666',
|
color: theme.colors.text.secondary,
|
||||||
marginRight: '0.25rem',
|
marginRight: '0.25rem',
|
||||||
}),
|
}),
|
||||||
LabeledListValue: css({
|
LabeledListValue: css({
|
||||||
@@ -66,7 +67,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => {
|
|||||||
LabeledListServiceLine: css({
|
LabeledListServiceLine: css({
|
||||||
label: 'LabeledListServiceLine',
|
label: 'LabeledListServiceLine',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: '1.25rem',
|
width: '1rem',
|
||||||
height: '0.35rem',
|
height: '0.35rem',
|
||||||
marginRight: '0.5rem',
|
marginRight: '0.5rem',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
@@ -95,7 +96,7 @@ export default function LabeledList(props: LabeledListProps) {
|
|||||||
{label === 'Service:' && (
|
{label === 'Service:' && (
|
||||||
<div className={styles.LabeledListServiceLine} style={{ backgroundColor: color }} />
|
<div className={styles.LabeledListServiceLine} style={{ backgroundColor: color }} />
|
||||||
)}
|
)}
|
||||||
{icon && <Icon name={icon} className={styles.LabeledListIcon} />}
|
{icon && <Icon name={icon} className={styles.LabeledListIcon} size="sm" />}
|
||||||
<span className={styles.LabeledListLabel}>{label}</span>
|
<span className={styles.LabeledListLabel}>{label}</span>
|
||||||
<strong className={styles.LabeledListValue}>{value}</strong>
|
<strong className={styles.LabeledListValue}>{value}</strong>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user