fix(webapp): synchronize run page highlight when hovering spans#2998
fix(webapp): synchronize run page highlight when hovering spans#2998
Conversation
|
WalkthroughThis change adds hover-aware interaction and scroll-aware hover deferral to the runs route. It introduces Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:
- Around line 724-729: The scroll timeout handler currently only applies
pendingHoverIdRef.current when it is not null, which prevents clearing the hover
when the pointer left during scroll; update the timeout callback in the
component where scrollTimeoutRef, pendingHoverIdRef, setIsScrolling, and
setHoveredNodeId are used so that after setIsScrolling(false) you always call
setHoveredNodeId(pendingHoverIdRef.current) (apply the pending value
unconditionally) and then clear pendingHoverIdRef.current as needed to avoid
stale state.
🧹 Nitpick comments (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (1)
697-701: Clean up the scroll timeout on unmount.This avoids a late
setStateafter unmount and keeps the timeout lifecycle tidy.Suggested fix
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null); const pendingHoverIdRef = useRef<string | null>(null); + + useEffect(() => { + return () => { + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + scrollTimeoutRef.current = null; + } + }; + }, []);Also applies to: 712-731
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead
**/*.{ts,tsx}: Always import tasks from@trigger.dev/sdk, never use@trigger.dev/sdk/v3or deprecatedclient.defineJobpattern
Every Trigger.dev task must be exported and have a uniqueidproperty with no timeouts in the run function
Files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use zod for validation in packages/core and apps/webapp
Files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use function declarations instead of default exports
Import from
@trigger.dev/coreusing subpaths only, never import from root
Files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
apps/webapp/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Access all environment variables through the
envexport ofenv.server.tsinstead of directly accessingprocess.envin the Trigger.dev webapp
Files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
apps/webapp/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
apps/webapp/**/*.{ts,tsx}: When importing from@trigger.dev/corein the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webappAccess environment variables via
envexport fromapps/webapp/app/env.server.ts, never useprocess.envdirectly
Files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}
📄 CodeRabbit inference engine (AGENTS.md)
Format code using Prettier before committing
Files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
🧠 Learnings (2)
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.
Applied to files:
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
🧬 Code graph analysis (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (1)
apps/webapp/app/utils/cn.ts (1)
cn(77-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (25)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
- GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
- GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
- GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
- GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
- GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
- GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
- GitHub Check: typecheck / typecheck
- GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
- GitHub Check: sdk-compat / Bun Runtime
- GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
- GitHub Check: sdk-compat / Cloudflare Workers
🔇 Additional comments (3)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (3)
847-867: Hover styling and handlers are consistent across tree and timeline.Nice synchronization for visual feedback between both panes.
Also applies to: 1199-1219
925-931: Scroll synchronization wiring looks good.Keeping the two scroll containers in lockstep makes sense here.
Also applies to: 1329-1334
958-962: Hover/scroll prop plumbing is consistent end-to-end.The added props are wired cleanly through the component tree.
Also applies to: 1027-1054
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
...es/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
Outdated
Show resolved
Hide resolved
nicktrn
left a comment
There was a problem hiding this comment.
PR Review: Synchronize run page highlight on hover
Overall: The feature goal is good — syncing hover highlights between the tree panel and timeline panel is a clear UX improvement. However, the current implementation has a high-severity bug, significant performance concerns, and several code quality issues that should be addressed before merging.
HIGH — Stale hover after mousewheel scroll
When scrolling via mousewheel while the cursor hovers over a row, the rows move under the mouse but onMouseEnter/onMouseLeave events do not fire for the new rows appearing beneath the cursor. The pendingHoverIdRef retains the ID of the row that was under the mouse when scrolling started. When the 150ms debounce expires, that stale ID is applied — highlighting a row the user isn't pointing at.
Similarly, when virtualized rows are recycled during scroll (TanStack Virtual unmounts/mounts row components), no onMouseLeave fires for the unmounted row, leaving pendingHoverIdRef stale.
Suggestion: When scroll ends, set hoveredNodeId to null instead of applying the ref value. Let the next natural mouse event re-establish hover:
scrollTimeoutRef.current = setTimeout(() => {
setIsScrolling(false);
setHoveredNodeId(null);
pendingHoverIdRef.current = null;
}, 150);HIGH — Performance: every hover re-renders the entire tree + timeline
hoveredNodeId is React state in TasksTreeView. Every setHoveredNodeId call re-renders the parent, which re-renders both TreeView components (tree + timeline). Key factors:
TreeViewis not wrapped inReact.memorenderNodecallbacks are inline arrow functions, recreated every render- Virtualizer uses
overscan: 50, so ~70-80 rows re-render per panel per hover event (~140-160 total) - Timeline rows are expensive (multiple
Timeline.Point,motion.div, Framer Motion animations)
Suggestion: Consider a CSS/DOM-based approach that avoids React re-renders entirely. Add data-node-id attributes to rows and toggle a data-hovered attribute via refs + direct DOM manipulation. The hover highlight is purely visual — it doesn't need to participate in React's render cycle. If that feels too imperative, an intermediate option is to add pointer-events: none to the scroll container during scroll (one state toggle on the container, not per-node), and keep CSS :hover for the actual highlight.
MEDIUM — CSS hover replaced with JS hover adds latency
The PR replaces native CSS :hover pseudo-classes (hover:bg-grid-dimmed) with React state-driven styling. CSS :hover is handled on the compositor thread and is effectively instant. The JS approach adds at minimum one frame of latency (~16ms) through the React event → setState → reconciliation → paint pipeline, and more under load. Users will notice the highlight trailing the mouse when scanning quickly across rows.
LOW — setHoveredNodeId prop passed but unused in TimelineView
TimelineViewProps includes setHoveredNodeId, but TimelineView only uses handleHoverChange (which wraps setHoveredNodeId with scroll-aware logic). The raw setter shouldn't be exposed — it bypasses scroll suppression and is dead code.
LOW — handleHoverChange dependency on isScrolling state destabilizes callback identity
const handleHoverChange = useCallback((nodeId) => {
pendingHoverIdRef.current = nodeId;
if (!isScrolling) {
setHoveredNodeId(nodeId);
}
}, [isScrolling]); // changes every time isScrolling togglesWhen isScrolling toggles, handleHoverChange gets a new identity. This would bust any React.memo on child components receiving it. If isScrolling were tracked as a ref instead of state, the callback would be stable ([] deps).
LOW — Duplicated hover styling logic
The hover class logic is copy-pasted in both the tree renderNode and timeline renderNode:
state.selected
? isHovered ? "bg-grid-bright" : "bg-grid-dimmed"
: isHovered ? "bg-grid-dimmed" : "bg-transparent"Could be extracted into a helper like getRowBgClass(selected, hovered).
LOW — Indentation bug in handleScroll
scrollTimeoutRef.current = setTimeout(() => {
setIsScrolling(false);
setHoveredNodeId(pendingHoverIdRef.current); // 8 spaces, should be 6
}, 150);LOW — Unused parameter
handleScroll takes scrollTop: number but never reads it. Should be (_scrollTop: number) => void or () => void.
LOW — NodeJS.Timeout type in browser context
useRef<NodeJS.Timeout | null> — in a browser environment this should be ReturnType<typeof setTimeout> for correctness.
Closes #
✅ Checklist
Testing
Checked in the interface
Changelog
Synchronized row between spans and timeline on hover
Screenshots