Replace react-virtualized with @tanstack/react-virtual in JobOutput - #489
Conversation
8bda50f to
0869fcc
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the Job Output UI to replace the legacy react-virtualized implementation with @tanstack/react-virtual, aiming to remove React-19-incompatible patterns (e.g., string refs) and improve long-output performance/stability via updated measurement and scrolling behavior.
Changes:
- Migrates
JobOutput.jsvirtualization fromreact-virtualized(AutoSizer/List/CellMeasurer/InfiniteLoader) to@tanstack/react-virtual(useVirtualizer,measureElement, customrangeExtractor+ load-on-range effect). - Adds flexbox
min-height: 0constraints for the output wrapper/scroll container to ensure the scroll viewport constrains correctly in long jobs. - Updates UI dependencies and licensing to include
@tanstack/react-virtualand removereact-virtualized.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| licenses/ui/tanstack-react-virtual.txt | Adds/updates MIT license text for the new virtualization dependency. |
| awx/ui/src/screens/Job/JobOutput/JobOutput.js | Replaces the virtualization stack and adjusts scrolling/measurement behavior to support long/variable-height output efficiently. |
| awx/ui/package.json | Adds @tanstack/react-virtual and removes react-virtualized. |
| awx/ui/package-lock.json | Locks the new TanStack virtualizer dependency and removes react-virtualized transitive deps. |
Files not reviewed (1)
- awx/ui/package-lock.json: Generated file
| const scrollToEnd = useCallback(() => { | ||
| scrollToRow(-1); | ||
| let timeout; | ||
| if (isFollowModeEnabled) { | ||
| setTimeout(() => scrollToRow(-1), 100); | ||
| } | ||
| return () => clearTimeout(timeout); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isFollowModeEnabled]); |
| "@patternfly/react-core": "4.278.1", | ||
| "@patternfly/react-icons": "4.93.7", | ||
| "@patternfly/react-table": "4.113.7", | ||
| "@tanstack/react-virtual": "^3.14.3", |
|
Pushed a fix for both issues here. The overlapping/garbled rows on a running playbook were caused by the per row measure callback. It called rowVirtualizer.measure(), which resets the whole size cache back to the 25px estimate. A ResizeObserver only re-fires for rows whose box actually changes, so rows that had already settled got stuck at the estimate, and since streaming events fire that callback constantly every measured height kept collapsing and the absolutely positioned rows piled up on top of each other. react-virtual's measureElement already re-measures a row when its content height changes, so that callback is now a no-op. I also stopped deferring the measureElement ResizeObserver to requestAnimationFrame (that left streaming re-renders painting rows at the stale estimate for a frame) and instead silence the benign "ResizeObserver loop completed" notice with a window error handler so the CRA dev overlay still does not treat it as fatal. I verified this in a browser against a live job that streams a few hundred varied height rows: rows stay correctly positioned the whole time the job runs (no overlap once the first paint settles), and there are no ResizeObserver errors surfaced. I also fixed the scrollToEnd timer that Copilot flagged: the setTimeout handle was never assigned to the timeout variable, so the clearTimeout was a no-op. The handle is now kept in a ref and cleared before scheduling the next one and on unmount, and scrollToRow reads the row count from a ref so the memoized scrollToEnd does not act on a stale count. Full Job test suite stays green (19 suites, 164 tests) and lint is clean. |
ad5ee88 to
d53e04c
Compare
|
Rebased onto current main to clear the package-lock.json conflict that #488 (enzyme removal) introduced. The two commits (the tanstack swap and the live-output overlap fix) are intact; Job suite green, lint clean. Mergeable now. |
|
I am still getting a few errors with it |
63c7822 to
9f2daf8
Compare
|
@cigamit both items in your output are now sorted:
I've also refreshed the PR description to reflect the final state of the branch. Ready for another look. |
|
This fixes it for me. |
|
Thanks. Adopted your patch. Pushed as I verified it before/after in a headless Chrome against a live
The "before" run reproduced your screenshot exactly. Overlap appears as rows stream in and persists after streaming settles. After the fix it stays correctly positioned the whole run, with no ResizeObserver overlay errors. Job suite green (19 suites, 164 tests), lint clean. |
react-virtualized is unmaintained and blocks the React 17 to 19 upgrade. Swap the JobOutput virtualization over to @tanstack/react-virtual: - Replace the AutoSizer + Grid + CellMeasurer + InfiniteLoader stack with useVirtualizer and measureElement, an absolutely positioned row list, and a manual infinite-load driver keyed off the rendered range. - Preserve the selection-aware overscan (computeOverscanIndices) so an active text selection is not unmounted, wired through a custom rangeExtractor. - Keep the scrollToEnd / follow-mode behavior, storing the follow-up timer in a ref so it is actually cleared (the previous local let was never assigned, making the clearTimeout a no-op) and reading the row count from a ref so the memoized callback does not act on a stale count. - Do not call rowVirtualizer.measure() on content, load, or width changes. measure() clears the entire itemSizeCache back to the 25px estimateSize, and measureElement's per-row ResizeObserver only re-fires for rows whose box actually changes, so already-settled rows stay stuck at the estimate and the absolutely positioned multi-line rows overlap (garbled output during a running playbook). measureElement already re-measures each row on mount, reflow, and height change, so no global reset is needed. - Swallow the benign 'ResizeObserver loop completed' notice at the window error level so CRA's dev overlay does not treat it as fatal. Suppress react-hooks/incompatible-library at the single useVirtualizer call site, since it returns functions the React Compiler cannot memoize.
7fb2d53 to
dd520f4
Compare
|
Everything looks good now. |


SUMMARY
Replace the unmaintained
react-virtualized(9.22.6, last released 2020) with@tanstack/react-virtual(^3.14.3) in the job output view.react-virtualized'sGridleans on legacy patterns React 19 drops (string refs), so this is React-19 readiness done early. The new library is React-17-compatible, so it can land independently of the React 18 bump.Scope is contained to
JobOutput.js+package.json/lockfile + the license file. No test files change.API mapping:
AutoSizer→ a flex scroll container measured by the virtualizerCellMeasurer/CellMeasurerCache→measureElement(ResizeObserver) +estimateSizeInfiniteLoader→ a guarded load-on-range effect (primitive deps + in-flight dedupe + skip-when-no-viewport)List/overscanIndicesGetter→getVirtualItems()rows + arangeExtractorwrapping the preservedcomputeOverscanIndicesscrollToRow/recompute→scrollToIndex/rowVirtualizer.measure()Fixes that browser testing on real jobs surfaced
These bugs are invisible on short/uniform output (what unit tests and a quick manual check exercise) and only appear on real jobs, so they're called out explicitly:
min-height: 0on the scroll container + itsOutputWrapperparent. Flex items default tomin-height: autoand won't shrink below their content height. Withoutmin-height: 0the column grows to the full content height,clientHeightequalsscrollHeight, and the virtualizer treats every row as visible — on a long job it renders the whole list and pegs the CPU in ameasureElementre-measure loop.react-virtualized'sAutoSizerset an explicit pixel height, somainnever hit this.Overlapping / garbled rows during live runs. The per-row measure callback called
rowVirtualizer.measure(), which resets the entire size cache back to the 25px estimate. AResizeObserveronly re-fires for rows whose box actually changes, so rows that had already settled stayed stuck at the estimate; with streaming events firing that callback constantly, measured heights kept collapsing and the absolutely-positioned rows piled up on top of each other. react-virtual'smeasureElementalready re-measures a row when its content height changes, so that callback is now a no-op.ResizeObserver dev-overlay noise.
measureElement's ResizeObserver can shift layout within a frame, so the browser emits the benignResizeObserver loop completed with undelivered notificationsnotice, which CRA's dev overlay treats as fatal. Rather than defer the observer torequestAnimationFrame(which left streaming re-renders painting rows at the stale estimate for a frame), awindowerror handler silences just that one notice so the overlay no longer fires.scrollToEndtimer leak (Copilot-flagged). ThesetTimeouthandle was never assigned, so theclearTimeoutwas a no-op. The handle is now kept in a ref, cleared before scheduling the next one and on unmount, andscrollToRowreads the row count from a ref so the memoizedscrollToEnddoesn't act on a stale count.react-hooks/incompatible-librarylint warning.useVirtualizer()returns functions the React Compiler can't memoize, whicheslint-plugin-react-hooks7.x flags. This component predates React Compiler — the same ruleset the codebase already opts out of ineslint.config.mjs. Suppressed inline at the single call site with an explanatory comment, leaving the rule active everywhere else.ISSUE TYPE
COMPONENT NAME
ASCENDER VERSION
ADDITIONAL INFORMATION
npm --prefix awx/ui run test(Job suites): 19 suites / 164 tests green;JobOutput11 suites / 109 tests green.npm --prefix awx/ui run lintclean.licenses/ui/react-virtualized.txt→licenses/ui/tanstack-react-virtual.txt(MIT);test_licenses.pypasses in-container.Browser verification (headless Chrome against the dev server)
Measured with CDP
Performance.getMetrics(ScriptDuration/LayoutCountdeltas) and DOM inspection. Three job shapes that stress different paths:ScriptDurationdeltas0.000) — matchesmain; scroll-to-first/last work.ScriptDuration≈ 0.1–0.2 / 3s), a single one-time spike at completion, then fully idle; no overlay; 0 ResizeObserver / page errors.The pre-
min-heightbuild pegged the CPU continuously on the long job, the pre-fix build piled overlapping rows on the live job, and the pre-overlay-handler build raised the blocking overlay — all confirmed and re-verified fixed.