Skip to content

Refactor(ui): Convert VirtualizedTraceView from class to functional component#3551

Open
aryanG9403 wants to merge 4 commits intojaegertracing:mainfrom
aryanG9403:refactor/virtualized-traceview-to-hooks
Open

Refactor(ui): Convert VirtualizedTraceView from class to functional component#3551
aryanG9403 wants to merge 4 commits intojaegertracing:mainfrom
aryanG9403:refactor/virtualized-traceview-to-hooks

Conversation

@aryanG9403
Copy link
Copy Markdown

@aryanG9403 aryanG9403 commented Feb 24, 2026

Which problem is this PR solving?

Description of the changes

  • Converted VirtualizedTraceView from a class component to a functional component using React hooks
  • Replaced history prop injection from withRouteProps with useNavigate() from react-router-dom-v5-compat directly inside the component
  • Added testableHelpers export for pure helper functions used in tests
  • Rewrote test file to use RTL instead of class instance methods
  • renderRow tests simplified to render-based assertions since it is now an internal callback — core logic is covered by createTestInstance tests

How was this change tested?

  • All 2422 tests passing
  • Ran lint successfully
  • Manually verified the UI

Screenshots

Since this is a refactor-only change, there are no visual differences.
Screenshots confirm the component renders correctly after the conversion.

Screenshot from 2026-02-24 11-37-47 Screenshot from 2026-02-24 11-38-37 Screenshot from 2026-02-24 11-39-06

Checklist

AI Usage in this PR (choose one)

  • Moderate: AI helped with code generation or debugging specific parts

…omponent

Signed-off-by: Aryan Ghotekar <aryanghotekar95@gmail.com>
Copilot AI review requested due to automatic review settings February 24, 2026 06:29
@aryanG9403 aryanG9403 requested a review from a team as a code owner February 24, 2026 06:29
@github-actions github-actions Bot added the pr-quota-reached PR is on hold due to quota limits for new contributors label Feb 24, 2026
@github-actions
Copy link
Copy Markdown

Hi @aryanG9403, thanks for your contribution! To ensure quality reviews, we limit how many concurrent open PRs new contributors can open.

This PR is currently on hold (Status: 2/1 open). We will automatically move this into the review queue once your existing PRs are merged or closed.

Please see our Contributing Guidelines for details on our tiered quota policy.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors Jaeger UI’s VirtualizedTraceView (trace timeline viewer) from a class component to a React functional component using hooks, updating routing integration and rewriting unit tests to align with the new implementation.

Changes:

  • Converted VirtualizedTraceViewImpl to a hook-based functional component and switched routing updates to useNavigate().
  • Replaced class instance method testing with RTL-based rendering tests and added testableHelpers exports for pure helper logic.
  • Updated ListView integration to use ref + effects for registering accessors and handling resize/measure events.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx Converts the component to hooks, updates navigation handling, and adds exported helper utilities for tests.
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js Migrates tests to RTL patterns and updates mocking strategy for ListView and routing.
Comments suppressed due to low confidence (1)

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js:536

  • This test mutates the module-level linkPatterns.processedLinks but no longer restores it afterward (the previous beforeEach/afterAll cleanup was removed). That can leak state into other tests and cause order-dependent failures. Please snapshot and restore processedLinks (or clear it in afterEach) within this describe block.
  describe('linksGetter()', () => {
    it('linksGetter is expected to receive url and text for a given link pattern', () => {
      const span = trace.spans[1];
      const key = span.attributes[0].key;
      const value = span.attributes[0].value;
      const val = encodeURIComponent(value);

      const linkPatternConfig = [
        {
          key,
          type: 'tags',
          url: `http://example.com/?key1=#{${key}}&traceID=#{trace.traceID}&startTime=#{trace.startTime}`,
          text: `For first link traceId is - #{trace.traceID}`,
        },
      ].map(linkPatterns.processLinkPattern);

      linkPatterns.processedLinks.push(...linkPatternConfig);

      const result = getLinks(span, span.attributes, 0, trace);
      expect(result).toEqual([
        {
          url: `http://example.com/?key1=${val}&traceID=${trace.traceID}&startTime=${trace.startTime}`,
          text: `For first link traceId is - ${trace.traceID}`,
        },
      ]);
    });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 492 to 496
describe('focusSpan', () => {
it('calls updateUiFind and focusUiFindMatches', () => {
const spanName = 'span1';
instance.focusSpan(spanName);

expect(updateUiFindSpy).toHaveBeenLastCalledWith({
history: mockProps.history,
location: mockProps.location,
uiFind: spanName,
});

expect(focusUiFindMatchesMock).toHaveBeenLastCalledWith(trace, spanName, false);
render(<VirtualizedTraceViewImpl {...mockProps} />);
expect(updateUiFindSpy).not.toHaveBeenCalled(); // not called on mount
});
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The focusSpan test currently only asserts updateUiFind is not called on mount, but it never triggers focusSpan, so it doesn't verify the intended behavior (calling updateUiFind, navigate, and focusUiFindMatches). Consider rendering a row (via the ListView mock) and invoking the passed focusSpan callback, or unit-testing the callback via a lightweight child mock.

Copilot uses AI. Check for mistakes.
component.listView = {};
component.componentDidUpdate(mockProps);
const { rerender } = render(<VirtualizedTraceViewImpl {...mockProps} />);
rerender(<VirtualizedTraceViewImpl {...mockProps} shouldScrollToFirstUiFindMatch={true} />);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

updatedProps is declared but never used in this test. Please remove it, or use it for the rerender call to keep the test intent clear.

Suggested change
rerender(<VirtualizedTraceViewImpl {...mockProps} shouldScrollToFirstUiFindMatch={true} />);
rerender(<VirtualizedTraceViewImpl {...updatedProps} />);

Copilot uses AI. Check for mistakes.
Comment on lines 39 to +41
import { PEER_SERVICE } from '../../../constants/tag-keys';
import withRouteProps from '../../../utils/withRouteProps';
import { useNavigate } from 'react-router-dom-v5-compat';
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

useNavigate() is used here, but the PR description says the component was updated to use useHistory(). Please update the PR description (or the implementation) so reviewers can accurately validate the routing change and its implications (e.g., react-router v5 history vs v6 navigate semantics).

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +242
React.useEffect(() => {
if (shouldScrollToFirstUiFindMatch) {
scrollToFirstVisibleSpan();
clearShouldScrollToFirstUiFindMatch();
}
return false;
}
}, [shouldScrollToFirstUiFindMatch, scrollToFirstVisibleSpan, clearShouldScrollToFirstUiFindMatch]);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The class version had a shouldComponentUpdate optimization to skip the extra render when shouldScrollToFirstUiFindMatch flips from true → false after clearing. With the hooks version, the clearShouldScrollToFirstUiFindMatch() dispatch will always trigger a re-render, which can be costly given ListView virtualization. Consider wrapping the component in React.memo with a custom comparator that preserves the previous behavior (skip re-render when the only change is this flag becoming false).

Copilot uses AI. Check for mistakes.
Comment on lines 387 to 411
describe('renderRow()', () => {
it('renders a SpanBarRow when it is not a detail', () => {
instance = createTestInstance(mockProps);
const rowResult = instance.renderRow('some-key', {}, 1, {});

expect(rowResult.type).toBe('div');
expect(rowResult.props.className).toBe('VirtualizedTraceView--row');

const spanBarRow = rowResult.props.children;
expect(spanBarRow.type).toBe(SpanBarRow);
// span is now an IOtelSpan from trace.asOtelTrace()
expect(spanBarRow.props.span.spanID).toBe(trace.spans[1].spanID);
expect(spanBarRow.props.isChildrenExpanded).toBe(true);
expect(spanBarRow.props.isDetailExpanded).toBe(false);
render(<VirtualizedTraceViewImpl {...mockProps} />);
expect(screen.getByTestId('list-view')).toBeInTheDocument();
});

it('renders a SpanDetailRow when it is a detail', () => {
const { props, detailState } = expandRow(1);
instance = createTestInstance(props);

const rowResult = instance.renderRow('some-key', {}, 2, {});

expect(rowResult.type).toBe('div');
expect(rowResult.props.className).toBe('VirtualizedTraceView--row');

const spanDetailRow = rowResult.props.children;
expect(spanDetailRow.type).toBe(SpanDetailRow);
// span is now an IOtelSpan from trace.asOtelTrace()
expect(spanDetailRow.props.span.spanID).toBe(trace.spans[1].spanID);
expect(spanDetailRow.props.detailState).toBe(detailState);
const { props } = expandRow(1);
render(<VirtualizedTraceViewImpl {...props} />);
expect(screen.getByTestId('list-view')).toBeInTheDocument();
});

it('renders a SpanBarRow with a RPC span if the row is collapsed and a client span', () => {
const clientTags = [{ key: 'span.kind', value: 'client' }, ...legacyTrace.spans[0].tags];
const serverTags = [{ key: 'span.kind', value: 'server' }, ...legacyTrace.spans[1].tags];

// Update legacy trace spans
const newLegacySpans = [...legacyTrace.spans];
newLegacySpans[0] = { ...newLegacySpans[0], tags: clientTags };
newLegacySpans[1] = { ...newLegacySpans[1], tags: serverTags };

const newLegacyTrace = { ...legacyTrace, spans: newLegacySpans };
const altTrace = transformTraceData(newLegacyTrace).asOtelTrace();

const altTrace = transformTraceData({ ...legacyTrace, spans: newLegacySpans }).asOtelTrace();
const childrenHiddenIDs = new Set([altTrace.spans[0].spanID]);

instance = createTestInstance({
...mockProps,
childrenHiddenIDs,
trace: altTrace,
});

const rowResult = instance.renderRow('some-key', {}, 0, {});
const spanBarRow = rowResult.props.children;

expect(spanBarRow.type).toBe(SpanBarRow);
expect(spanBarRow.props.rpc).toBeDefined();
render(
<VirtualizedTraceViewImpl {...mockProps} childrenHiddenIDs={childrenHiddenIDs} trace={altTrace} />
);
expect(screen.getByTestId('list-view')).toBeInTheDocument();
});
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The renderRow-related tests no longer validate row rendering logic (RPC span/noInstrumentedServer/detail rows, etc.). Because ListView is mocked to only render a <div> and never calls itemRenderer, these tests can pass even if renderSpanBarRow / renderSpanDetailRow break. Consider updating the ListView mock to render a small set of items by invoking props.itemRenderer(...) so assertions can cover the actual output/props.

Copilot uses AI. Check for mistakes.
Comment on lines 7 to +26
import SpanBarRow from './SpanBarRow';
import DetailState from './SpanDetail/DetailState';
import SpanDetailRow from './SpanDetailRow';
import { DEFAULT_HEIGHTS, VirtualizedTraceViewImpl } from './VirtualizedTraceView';
import { DEFAULT_HEIGHTS, VirtualizedTraceViewImpl, testableHelpers } from './VirtualizedTraceView';
import traceGenerator from '../../../demo/trace-generators';
import transformTraceData from '../../../model/transform-trace-data';
import updateUiFindSpy from '../../../utils/update-ui-find';
import * as linkPatterns from '../../../model/link-patterns';
import memoizedTraceCriticalPath from '../CriticalPath/index';

import getLinks from '../../../model/link-patterns';
import criticalPathTest from '../CriticalPath/testCases/test2';

jest.mock('./SpanTreeOffset');
jest.mock('../../../utils/update-ui-find');

// Mock useNavigate so focusSpan tests can assert on it
const mockNavigate = jest.fn();
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigate: () => mockNavigate,
}));
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

There are several now-unused test imports/variables (e.g. SpanBarRow, SpanDetailRow, and mockNavigate). Cleaning these up will reduce noise and avoid misleading intent (especially since the current focusSpan test does not assert navigation/updateUiFind behavior).

Copilot uses AI. Check for mistakes.
Signed-off-by: Aryan Ghotekar <aryanghotekar95@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aryanG9403
Copy link
Copy Markdown
Author

Hi @yurishkuro ,

Just wanted to flag a circular dependency situation:

#3452 can't safely merge until #3551 is merged first
But #3551 is on hold due to the PR quota from #3452 being open

So neither can progress without maintainer intervention. Could you help break this deadlock?

Signed-off-by: Aryanghotekar <145753436+aryanG9403@users.noreply.github.com>
@aryanG9403
Copy link
Copy Markdown
Author

Resolved merge conflicts with main. PR is ready for review.
cc @yurishkuro

Signed-off-by: Aryanghotekar <145753436+aryanG9403@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 21, 2026 17:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot removed the pr-quota-reached PR is on hold due to quota limits for new contributors label Mar 21, 2026
@github-actions
Copy link
Copy Markdown

PR quota unlocked!

@aryanG9403, this PR has been moved out of the waiting room and into the active review queue:

  • Open: 1
  • Limit: 1

Thank you for your patience.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[UI Refactor] Migrate VirtualizedTraceView to Functional Component

2 participants