Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collapsable trace tree #312

Merged
merged 4 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all 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: 3 additions & 0 deletions app-server/src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum Feature {
Storage,
/// Build all containers. If false, only lite part is used: app-server, postgres, frontend
FullBuild,
/// Machine manager to spin up and manage machines
MachineManager,
}

pub fn is_feature_enabled(feature: Feature) -> bool {
Expand All @@ -27,5 +29,6 @@ pub fn is_feature_enabled(feature: Feature) -> bool {
.expect("ENVIRONMENT must be set")
.as_str(),
),
Feature::MachineManager => env::var("MACHINE_MANAGER_URL_GRPC").is_ok(),
}
}
2 changes: 1 addition & 1 deletion app-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ fn main() -> anyhow::Result<()> {
};

let machine_manager: Arc<dyn MachineManager> =
if is_feature_enabled(Feature::FullBuild) {
if is_feature_enabled(Feature::MachineManager) {
let machine_manager_url_grpc = env::var("MACHINE_MANAGER_URL_GRPC")
.expect("MACHINE_MANAGER_URL_GRPC must be set");
let machine_manager_client = Arc::new(
Expand Down
62 changes: 44 additions & 18 deletions frontend/components/traces/span-card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChevronDown, ChevronRight } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';

import { getDurationString } from '@/lib/flow/utils';
Expand All @@ -18,6 +19,8 @@ interface SpanCardProps {
depth: number;
selectedSpan?: Span | null;
onSpanSelect?: (span: Span) => void;
collapsedSpans: Set<string>;
onToggleCollapse?: (spanId: string) => void;
}

export function SpanCard({
Expand All @@ -27,19 +30,23 @@ export function SpanCard({
onSpanSelect,
containerWidth,
depth,
selectedSpan
selectedSpan,
collapsedSpans,
onToggleCollapse
}: SpanCardProps) {
const [isSelected, setIsSelected] = useState(false);
const [segmentHeight, setSegmentHeight] = useState(0);
const ref = useRef<HTMLDivElement>(null);

const childrenSpans = childSpans[span.spanId];

const hasChildren = childrenSpans && childrenSpans.length > 0;

useEffect(() => {
if (ref.current) {
setSegmentHeight(ref.current.getBoundingClientRect().y - parentY);
}
}, [parentY]);
}, [parentY, collapsedSpans]);

useEffect(() => {
setIsSelected(selectedSpan?.spanId === span.spanId);
Expand Down Expand Up @@ -100,24 +107,43 @@ export function SpanCard({
}}
/>
)}
{hasChildren && (
<button
className="z-40 p-1 hover:bg-muted transition-all text-muted-foreground rounded-sm"
onClick={(e) => {
e.stopPropagation();
onToggleCollapse?.(span.spanId);
}}
>
{collapsedSpans.has(span.spanId) ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
)}
</div>
</div>
<div className="flex flex-col">
{childrenSpans &&
childrenSpans.map((child, index) => (
<div className="pl-6 relative" key={index}>
<SpanCard
span={child}
childSpans={childSpans}
parentY={ref.current?.getBoundingClientRect().y || 0}
onSpanSelect={onSpanSelect}
containerWidth={containerWidth}
selectedSpan={selectedSpan}
depth={depth + 1}
/>
</div>
))}
</div>
{!collapsedSpans.has(span.spanId) && (
<div className="flex flex-col">
{childrenSpans &&
childrenSpans.map((child, index) => (
<div className="pl-6 relative" key={index}>
<SpanCard
span={child}
childSpans={childSpans}
parentY={ref.current?.getBoundingClientRect().y || 0}
onSpanSelect={onSpanSelect}
containerWidth={containerWidth}
selectedSpan={selectedSpan}
collapsedSpans={collapsedSpans}
onToggleCollapse={onToggleCollapse}
depth={depth + 1}
/>
</div>
))}
</div>
)}
</div>
);
}
37 changes: 20 additions & 17 deletions frontend/components/traces/timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

import { getDuration } from '@/lib/flow/utils';
import { Span } from '@/lib/traces/types';
Expand All @@ -8,6 +8,7 @@ import { SPAN_TYPE_TO_COLOR } from '@/lib/traces/utils';
interface TimelineProps {
spans: Span[];
childSpans: { [key: string]: Span[] };
collapsedSpans: Set<string>;
}

interface SegmentEvent {
Expand All @@ -25,28 +26,30 @@ interface Segment {

const HEIGHT = 32;

export default function Timeline({ spans, childSpans }: TimelineProps) {
export default function Timeline({ spans, childSpans, collapsedSpans }: TimelineProps) {
const [segments, setSegments] = useState<Segment[]>([]);
const [timeIntervals, setTimeIntervals] = useState<string[]>([]);
const ref = useRef<HTMLDivElement>(null);

const traverse = (
span: Span,
childSpans: { [key: string]: Span[] },
orderedSpands: Span[]
) => {
if (!span) {
return;
}
const traverse = useCallback(
(span: Span, childSpans: { [key: string]: Span[] }, orderedSpands: Span[]) => {
if (!span) {
return;
}
orderedSpands.push(span);

orderedSpands.push(span);
if (collapsedSpans.has(span.spanId)) {
return;
}

if (childSpans[span.spanId]) {
for (const child of childSpans[span.spanId]) {
traverse(child, childSpans, orderedSpands);
if (childSpans[span.spanId]) {
for (const child of childSpans[span.spanId]) {
traverse(child, childSpans, orderedSpands);
}
}
}
};
},
[collapsedSpans]
);

useEffect(() => {
if (!ref.current || childSpans === null) {
Expand Down Expand Up @@ -142,7 +145,7 @@ export default function Timeline({ spans, childSpans }: TimelineProps) {
}

setSegments(segments);
}, [spans, childSpans]);
}, [spans, childSpans, collapsedSpans]);

return (
<div className="flex flex-col h-full w-full" ref={ref}>
Expand Down
32 changes: 25 additions & 7 deletions frontend/components/traces/trace-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export default function TraceView({ traceId, onClose }: TraceViewProps) {
: null
);

// Add new state for collapsed spans
const [collapsedSpans, setCollapsedSpans] = useState<Set<string>>(new Set());

useEffect(() => {
if (!trace) {
return;
Expand Down Expand Up @@ -134,7 +137,7 @@ export default function TraceView({ traceId, onClose }: TraceViewProps) {
traceTreePanel.current!.getBoundingClientRect().width + 1
);
}
}, [containerWidth, selectedSpan, traceTreePanel.current]);
}, [containerWidth, selectedSpan, traceTreePanel.current, collapsedSpans]);

return (
<div className="flex flex-col h-full w-full overflow-clip">
Expand All @@ -159,11 +162,10 @@ export default function TraceView({ traceId, onClose }: TraceViewProps) {
variant={'outline'}
onClick={() => {
setSelectedSpan(null);
searchParams.delete('spanId');
router.push(`${pathName}?${searchParams.toString()}`);
setTimelineWidth(
container.current!.getBoundingClientRect().width
);
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using setTimeout with a delay of 10ms is not a reliable way to ensure state updates before URL changes. Consider using a callback or a state effect to handle this synchronously.

searchParams.delete('spanId');
router.push(`${pathName}?${searchParams.toString()}`);
}, 10);
}}
>
Show timeline
Expand Down Expand Up @@ -234,6 +236,18 @@ export default function TraceView({ traceId, onClose }: TraceViewProps) {
depth={1}
selectedSpan={selectedSpan}
containerWidth={timelineWidth}
collapsedSpans={collapsedSpans}
onToggleCollapse={(spanId) => {
setCollapsedSpans((prev) => {
const next = new Set(prev);
if (next.has(spanId)) {
next.delete(spanId);
} else {
next.add(spanId);
}
return next;
});
}}
onSpanSelect={(span) => {
setSelectedSpan(span);
setTimelineWidth(
Expand All @@ -253,7 +267,11 @@ export default function TraceView({ traceId, onClose }: TraceViewProps) {
</td>
{!selectedSpan && !searchParams.get('spanId') && (
<td className="flex flex-grow w-full p-0">
<Timeline spans={spans} childSpans={childSpans} />
<Timeline
spans={spans}
childSpans={childSpans}
collapsedSpans={collapsedSpans}
/>
</td>
)}
</tr>
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.2.1",
"postgres": "^3.4.5",
"posthog-js": "^1.202.2",
"posthog-js": "^1.205.0",
"posthog-node": "^4.3.2",
"re-resizable": "^6.10.3",
"react": "^18.3.1",
Expand Down
Loading
Loading