Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion tracingganttchart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"module": "lib/index.js",
"types": "lib/index.d.ts",
"dependencies": {
"color-hash": "^2.0.2"
"color-hash": "^2.0.2",
"react-virtuoso": "^4.12.2"
},
"peerDependencies": {
"@emotion/react": "^11.7.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export function AttributeItem(props: AttributeItemProps): ReactElement {
);
}

function renderAttributeValue(value: otlpcommonv1.AnyValue): string {
export function renderAttributeValue(value: otlpcommonv1.AnyValue): string {
if ('stringValue' in value) return value.stringValue || '<empty string>';
if ('intValue' in value) return value.intValue;
if ('doubleValue' in value) return String(value.doubleValue);
Expand All @@ -177,5 +177,5 @@ function renderAttributeValue(value: otlpcommonv1.AnyValue): string {
const values = value.arrayValue.values;
return values && values.length > 0 ? values.map(renderAttributeValue).join(', ') : '<empty array>';
}
return 'unknown';
return '<unknown type>';
}
88 changes: 59 additions & 29 deletions tracingganttchart/src/TracingGanttChart/GanttTable/GanttTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { Virtuoso, ListRange } from 'react-virtuoso';
import { ReactElement, useMemo, useRef, useState } from 'react';
import { Virtuoso, VirtuosoHandle, ListRange } from 'react-virtuoso';
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react';
import { Box, useTheme } from '@mui/material';
import { Viewport } from '../utils';
import { CustomLinks, TracingGanttChartOptions } from '../../gantt-chart-model';
import { Span, Trace } from '../trace';
import { Span, Trace, forEachSpan } from '../trace';
import { useGanttTableContext } from './GanttTableProvider';
import { GanttTableRow } from './GanttTableRow';
import { GanttTableHeader } from './GanttTableHeader';
Expand All @@ -29,32 +29,72 @@ export interface GanttTableProps {
viewport: Viewport;
selectedSpan?: Span;
onSpanClick: (span: Span) => void;
matchingSpanIds?: string[];
focusedSpanId?: string;
}

export function GanttTable(props: GanttTableProps): ReactElement {
const { options, customLinks, trace, viewport, selectedSpan, onSpanClick } = props;
const { collapsedSpans, setVisibleSpans } = useGanttTableContext();
const { options, customLinks, trace, viewport, selectedSpan, onSpanClick, matchingSpanIds, focusedSpanId } = props;
const { collapsedSpans, setCollapsedSpans, setVisibleSpans } = useGanttTableContext();
const [nameColumnWidth, setNameColumnWidth] = useState<number>(0.25);
const tableRef = useRef<HTMLDivElement>(null);
const virtuosoRef = useRef<VirtuosoHandle>(null);
const theme = useTheme();

// Recursively flatten the span tree to a list of rows, hiding collapsed child spans.
const rows = useMemo(() => {
const rows: Span[] = [];
for (const rootSpan of trace.rootSpans) {
treeToRows(rows, rootSpan, collapsedSpans);
}
forEachSpan(trace.rootSpans, (span) => {
rows.push(span);
if (collapsedSpans.has(span.spanId)) {
return false;
}
});
return rows;
}, [trace.rootSpans, collapsedSpans]);
const matchingSpanIdSet = useMemo(() => new Set(matchingSpanIds ?? []), [matchingSpanIds]);

const selectedSpanIndex = useMemo(() => {
if (!selectedSpan) return undefined;
// Auto-expand collapsed ancestors when focusing a search match
useEffect(() => {
if (!focusedSpanId) return;

for (let i = 0; i < rows.length; i++) {
if (rows[i]?.spanId === selectedSpan.spanId) {
return i;
}
const span = trace.spanById.get(focusedSpanId);
if (!span) return;

const ancestorIds = new Set<string>();
let parent = span.parentSpan;
while (parent) {
ancestorIds.add(parent.spanId);
parent = parent.parentSpan;
}
return undefined;
if (ancestorIds.size > 0) {
setCollapsedSpans((prev) => {
const next = new Set(prev);
let changed = false;
for (const id of ancestorIds) {
if (next.delete(id)) changed = true;
}
return changed ? next : prev;
});
}
}, [focusedSpanId, trace.spanById, setCollapsedSpans]);

// Scroll to focused span when using prev/next buttons in search bar.
useEffect(() => {
if (!focusedSpanId || !virtuosoRef.current) return;

const index = rows.findIndex((r) => r.spanId === focusedSpanId);
if (index >= 0) {
virtuosoRef.current.scrollToIndex({ index, align: 'center' });
}
}, [focusedSpanId, rows]);

// Set the top most index in the Virtuoso table to the selected span
// Required e.g. when navigating from another page.
const initialTopMostSpanIndex = useMemo(() => {
if (!selectedSpan) return 0;
const index = rows.findIndex((r) => r.spanId === selectedSpan.spanId);
return index >= 0 ? index : 0;
}, [rows, selectedSpan]);

const divider = <ResizableDivider parentRef={tableRef} onMove={setNameColumnWidth} />;
Expand All @@ -81,15 +121,18 @@ export function GanttTable(props: GanttTableProps): ReactElement {
>
<GanttTableHeader trace={trace} viewport={viewport} nameColumnWidth={nameColumnWidth} divider={divider} />
<Virtuoso
ref={virtuosoRef}
data={rows}
initialTopMostItemIndex={selectedSpanIndex ?? 0}
initialTopMostItemIndex={initialTopMostSpanIndex}
itemContent={(_, span) => (
<GanttTableRow
options={options}
customLinks={customLinks}
span={span}
viewport={viewport}
selected={span === selectedSpan}
matched={matchingSpanIdSet.has(span.spanId)}
focused={span.spanId === focusedSpanId}
nameColumnWidth={nameColumnWidth}
divider={divider}
onClick={onSpanClick}
Expand All @@ -100,16 +143,3 @@ export function GanttTable(props: GanttTableProps): ReactElement {
</Box>
);
}

/**
* treeToRows recursively transforms the span tree to a list of rows and
* hides collapsed child spans.
*/
function treeToRows(rows: Span[], span: Span, collapsedSpans: string[]): void {
rows.push(span);
if (!collapsedSpans.includes(span.spanId)) {
for (const child of span.childSpans) {
treeToRows(rows, child, collapsedSpans);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
import { createContext, ReactElement, useContext, useState } from 'react';

interface GanttTableContextType {
collapsedSpans: string[];
setCollapsedSpans: (s: string[]) => void;
collapsedSpans: Set<string>;
setCollapsedSpans: React.Dispatch<React.SetStateAction<Set<string>>>;
visibleSpans: string[];
setVisibleSpans: (s: string[]) => void;
/** can be a spanId, an empty string for the root span or undefined for no hover */
Expand All @@ -36,7 +36,7 @@ interface GanttTableProviderProps {

export function GanttTableProvider(props: GanttTableProviderProps): ReactElement {
const { children } = props;
const [collapsedSpans, setCollapsedSpans] = useState<string[]>([]);
const [collapsedSpans, setCollapsedSpans] = useState<Set<string>>(new Set());
const [visibleSpans, setVisibleSpans] = useState<string[]>([]);
const [hoveredParent, setHoveredParent] = useState<string | undefined>(undefined);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ interface GanttTableRowProps {
customLinks?: CustomLinks;
span: Span;
viewport: Viewport;
/** this span is opened in the attribute pane */
selected?: boolean;
/** this span is matched in the search results */
matched?: boolean;
/** this span is focused by clicking prev/next in the search bar */
focused?: boolean;
nameColumnWidth: number;
divider: React.ReactNode;
onClick: (span: Span) => void;
}

export const GanttTableRow = memo(function GanttTableRow(props: GanttTableRowProps) {
const { options, customLinks, span, viewport, selected, nameColumnWidth, divider, onClick } = props;
const { options, customLinks, span, viewport, selected, matched, focused, nameColumnWidth, divider, onClick } = props;
const theme = useTheme();

const handleOnClick = (): void => {
Expand All @@ -41,9 +46,22 @@ export const GanttTableRow = memo(function GanttTableRow(props: GanttTableRowPro
onClick(span);
};

let backgroundColor: string | undefined;
if (selected) {
backgroundColor = theme.palette.action.focus;
} else if (focused) {
backgroundColor = theme.palette.action.selected;
} else if (matched) {
backgroundColor = theme.palette.action.hover;
}

return (
<RowContainer
sx={{ backgroundColor: selected ? theme.palette.action.selected : 'inherit' }}
sx={{
backgroundColor,
// overwrite hover if background color is set (selected, focused or matched)
'&:hover': backgroundColor ? { backgroundColor } : undefined,
}}
direction="row"
onClick={handleOnClick}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ export function SpanIndents(props: SpanIndentsProps): ReactElement {
const handleToggleClick = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
if (collapsedSpans.includes(span.spanId)) {
setCollapsedSpans(collapsedSpans.filter((spanId) => spanId !== span.spanId));
} else {
setCollapsedSpans([...collapsedSpans, span.spanId]);
}
setCollapsedSpans((prev) => {
const next = new Set(prev);
if (next.has(span.spanId)) {
next.delete(span.spanId);
} else {
next.add(span.spanId);
}
return next;
});
},
[span, collapsedSpans, setCollapsedSpans]
[span, setCollapsedSpans]
);

const handleIconMouseEnter = useCallback(() => {
Expand Down Expand Up @@ -78,7 +82,7 @@ export function SpanIndents(props: SpanIndentsProps): ReactElement {
>
{i === spans.length - 1 &&
span.childSpans.length > 0 &&
(collapsedSpans.includes(span.spanId) ? (
(collapsedSpans.has(span.spanId) ? (
<ChevronRightIcon titleAccess="expand" onClick={handleToggleClick} onMouseEnter={handleIconMouseEnter} />
) : (
<ChevronDownIcon titleAccess="collapse" onClick={handleToggleClick} onMouseEnter={handleIconMouseEnter} />
Expand Down
10 changes: 2 additions & 8 deletions tracingganttchart/src/TracingGanttChart/MiniGanttChart/draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { Span, Trace } from '../trace';
import { Span, Trace, forEachSpan } from '../trace';
import { minSpanWidthPx } from '../utils';

const MIN_BAR_HEIGHT = 1;
Expand Down Expand Up @@ -47,13 +47,7 @@ export function drawSpans(
);
ctx.fill();
y += yChange;

for (const childSpan of span.childSpans) {
drawSpan(childSpan);
}
};

for (const rootSpan of trace.rootSpans) {
drawSpan(rootSpan);
}
forEachSpan(trace.rootSpans, drawSpan);
}
Loading
Loading