Skip to content

refactor(ui): decompose MonitorView into maintainable modules#161

Merged
streamer45 merged 7 commits intovideofrom
devin/1773521888-monitor-view-decomposition
Mar 15, 2026
Merged

refactor(ui): decompose MonitorView into maintainable modules#161
streamer45 merged 7 commits intovideofrom
devin/1773521888-monitor-view-decomposition

Conversation

@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot commented Mar 14, 2026

Summary

Decomposes the 3,685-line MonitorView.tsx into focused modules, reducing it to ~1,860 lines (~50% smaller). This is a pure refactor — no behavior, styling, or logic changes. All code was moved verbatim except for one rename (StatusDotConnectionStatusDot) to avoid a naming collision in the shared styles file.

Phase 1 — Pure utility extractions (zero React dependencies):

  • utils/pipelineDiff.ts — staged-vs-live pipeline diff helpers
  • utils/pipelineGraph.ts — ReactFlow node/edge building, YAML generation, slow-timeout helpers
  • utils/nodeIssues.ts — node health summary utilities

Phase 2 — Component & style extractions:

  • components/monitor/MonitorView.styles.ts — all ~50 styled-components
  • components/monitor/SessionItem.tsxSessionItem, SessionInfoChip, SessionUptime, InlineCopyButton
  • components/monitor/TopControls.tsx — staging controls with custom memo comparator
  • components/monitor/ConnectionStatus.tsx — WebSocket connection indicator
  • components/monitor/LeftPanel.tsx — session list sidebar with search & nodes library tab
  • components/monitor/Legend.tsx — node-state color legend overlay

Phase 3 — Hook extractions:

  • hooks/useMonitorPreview.ts — MoQ preview connection management (stream store selectors, teardown on session deselect, pipeline-aware gateway/broadcast config)
  • hooks/useAutoLayout.ts — auto-layout + fit-view logic (applyAutoLayout, handleAutoLayout, two effects, needsAutoLayout/needsFit state)
  • hooks/useNodeStatesSubscription.ts — throttled Zustand→ReactFlow patching bridge (bypasses React render cycle for perf)

The remaining MonitorViewContent (~1,860 lines) retains session management, staging, topology effect, YAML handling, and the JSX render tree.

Local smoke test

Tested locally with the backend + Vite dev UI. Created a session from the Stream view, then verified the Monitor view:

Monitor View with active session showing pipeline graph, session list, staging controls, and legend

Review & Testing Checklist for Human

  • Verify StatusDotConnectionStatusDot rename: The connection-status dot was renamed to avoid collision with the session status dot. Confirm ConnectionStatus.tsx and MonitorView.styles.ts use the new name consistently and it renders the same green/red pulsing dot.
  • Verify Phase 3 hook extractions: useAutoLayout and useNodeStatesSubscription were lifted out of MonitorViewContent. Confirm React hook call order is preserved and the topoEffectRanRef coordination point between the subscription hook and the topology effect works correctly.
  • Spot-check extracted components match originals: Compare SessionItem.tsx and TopControls.tsx against the git diff to confirm no logic was accidentally altered (particularly the areTopControlPropsEqual custom memo comparator and the SessionInfoChip click-outside handler).
  • Smoke-test Monitor View end-to-end: Open the Monitor view with at least one active session → verify session list renders, session selection works, pipeline graph displays, staging mode enter/discard/commit works, and the output preview panel appears when connected.

Notes

  • TypeScript compilation passes (only pre-existing test error in useCompositorLayers.monitor-flow.test.ts).
  • All UI tests pass. Formatting verified clean.

Link to Devin session: https://staging.itsdev.in/sessions/db99bbd5d38849e0bed287f4ea6da283
Requested by: @streamer45


Staging: Open in Devin

streamkit-devin and others added 4 commits March 14, 2026 21:01
Phase 1 of MonitorView decomposition:
- Extract pipeline diff helpers to utils/pipelineDiff.ts
- Extract pipeline graph helpers to utils/pipelineGraph.ts
- Extract node issue utilities to utils/nodeIssues.ts
- Extract styled components to components/monitor/MonitorView.styles.ts

MonitorView.tsx reduced from 3685 to 2988 lines (~700 lines extracted).

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Phase 2 of MonitorView decomposition:
- Extract SessionItem, SessionInfoChip, SessionUptime, InlineCopyButton
  to components/monitor/SessionItem.tsx
- Extract TopControls to components/monitor/TopControls.tsx
- Extract ConnectionStatus to components/monitor/ConnectionStatus.tsx
- Extract LeftPanel to components/monitor/LeftPanel.tsx

MonitorView.tsx reduced from 2988 to 2347 lines (~640 more lines extracted).
Total reduction from original 3685 to 2347 lines (36% smaller).

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 3 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment on lines +66 to +76
// Compare changes array length and validation errors
const prevChanges = prevProps.stagingData?.changes ?? [];
const nextChanges = nextProps.stagingData?.changes ?? [];
const prevErrors = prevProps.stagingData?.validationErrors ?? [];
const nextErrors = nextProps.stagingData?.validationErrors ?? [];

if (prevChanges.length !== nextChanges.length) return false;
if (prevErrors.length !== nextErrors.length) return false;

// If lengths are same and other props haven't changed, don't re-render
return true;
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot Mar 14, 2026

Choose a reason for hiding this comment

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

🚩 Custom memo comparator only checks array lengths, not content

The areTopControlPropsEqual comparator at ui/src/components/monitor/TopControls.tsx:66-76 only compares changes.length and validationErrors.length, not the actual array contents. This means if a staged change is replaced (e.g., an add_node becomes a remove_node) while keeping the total count the same, the TopControls component won't re-render, and the +N / -M / ~K counters shown in the UI could display stale values. This is pre-existing behavior from the original MonitorView.tsx and was not introduced by this PR, but it's worth noting as a potential source of subtle staleness bugs.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — this is pre-existing behavior carried over verbatim from the original MonitorView.tsx. The length-only comparison was an intentional trade-off in the original code (noted in the comment: "If lengths are same and other props haven't changed, don't re-render"). In practice, staging changes are append-only (adds accumulate, removes accumulate) so replacement without a length change is unlikely, but it's a valid observation for a future improvement. Not addressing in this refactor PR.

Phase 2d + Phase 3a of MonitorView decomposition:
- Extract Legend to components/monitor/Legend.tsx
- Extract useMonitorPreview hook to hooks/useMonitorPreview.ts
  (encapsulates MoQ preview connection, teardown, and pipeline-aware config)

MonitorView.tsx reduced from 2347 to 2206 lines.

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration staging-devin-ai-integration bot changed the title refactor(ui): decompose MonitorView into maintainable modules (Phases 1-2) refactor(ui): decompose MonitorView into maintainable modules Mar 14, 2026
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 8 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment on lines +69 to +76
const prevErrors = prevProps.stagingData?.validationErrors ?? [];
const nextErrors = nextProps.stagingData?.validationErrors ?? [];

if (prevChanges.length !== nextChanges.length) return false;
if (prevErrors.length !== nextErrors.length) return false;

// If lengths are same and other props haven't changed, don't re-render
return true;
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot Mar 15, 2026

Choose a reason for hiding this comment

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

🟡 Custom memo comparison only checks changes.length, missing composition changes

The areTopControlPropsEqual comparator at lines 72-83 only checks prevChanges.length !== nextChanges.length to decide whether to re-render. However, the component's render output at lines 146-182 breaks down changes by type (add_node/add_connection → added, remove_node/remove_connection → removed, update_params → modified) and displays per-type counts like "+2 -1 ~1". If the composition of changes shifts (e.g., an update_params entry is removed because params were reverted, and an add_connection entry is added) without the total length changing, the memo returns true and the component displays a stale change summary. This can happen when React batches multiple Zustand store updates from debounced param changes (ui/src/stores/stagingStore.ts:399-479) firing concurrently with immediate staging operations.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c700b5a — the comparator now also counts entries with type === 'error' so a warning→error swap at the same array length triggers a re-render.

…iew timer cleanup

- TopControls: compare blocking error count (type === 'error') in addition
  to array length so a warning→error swap at same length triggers re-render
- MonitorView.styles: fix ConnectionStatusContainer background to differ
  between connected (overlay-medium) and disconnected (danger tint) states
- useAutoLayout: track fitView setTimeout via fitTimerRef and cancel on
  unmount for proper cleanup

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 9 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment on lines +108 to +117
{sessions.length >= 5 && (
<SearchWrapper>
<SessionSearchInput
type="text"
placeholder="Search sessions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</SearchWrapper>
)}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 Stale search filter hides sessions when session count drops below threshold

The search input is only rendered when sessions.length >= 5 (line 108), but searchQuery state persists even after the input is hidden. If the session count drops from ≥5 to <5 while a non-empty search query is active, the filteredSessions memo at lines 73-83 continues to filter by the stale query. This causes the session list to show "No matching sessions" with no visible search input to clear the filter, making existing sessions invisible to the user.

Suggested change
{sessions.length >= 5 && (
<SearchWrapper>
<SessionSearchInput
type="text"
placeholder="Search sessions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</SearchWrapper>
)}
{sessions.length >= 5 ? (
<SearchWrapper>
<SessionSearchInput
type="text"
placeholder="Search sessions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</SearchWrapper>
) : (
searchQuery && setSearchQuery('') || null
)}
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +64 to +70
useEffect(() => {
const prev = prevSelectedSessionIdRef.current;
prevSelectedSessionIdRef.current = selectedSessionId;
if (prev && !selectedSessionId && previewStatus !== 'disconnected') {
previewDisconnect();
}
}, [selectedSessionId, previewStatus, previewDisconnect]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 Preview connection not torn down when switching sessions

In useMonitorPreview.ts:64-70, the cleanup effect only disconnects the preview when selectedSessionId transitions from a non-null value to null. If the user switches directly from session A to session B (both non-null), the old MoQ preview connection remains active. The handleStartPreview callback updates serverUrl and outputBroadcast in the stream store, but the existing connection is not affected by those changes. The next call to previewConnect() (ui/src/stores/streamStore.ts:222) sees status === 'connected' and returns early without reconnecting. This means the preview would continue showing session A's output even after switching to session B. The Preview button disappears once connected (!isPreviewConnected guard at TopControls.tsx:112), so the user has no obvious way to reconnect. This may be intentional (the preview is a secondary feature and the user would need to navigate away), but it could also be a UX gap worth investigating.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

@streamer45 streamer45 merged commit e477b03 into video Mar 15, 2026
1 check passed
@streamer45 streamer45 deleted the devin/1773521888-monitor-view-decomposition branch March 15, 2026 09:04
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.

2 participants