Add routing map preview for GitHub and Linear#13
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a read-only routing map preview to the dashboard’s routing editor (enabled for GitHub and Linear) by introducing an @xyflow/react-based visualization that renders routing rules as a source → event → rule → destination graph.
Changes:
- Add an optional
showRoutingMapflag toRoutingEditorand enable it for GitHub and Linear tabs. - Introduce
RoutingMap+buildRoutingGraphto convert routing config into React Flow nodes/edges and render a preview. - Add React Flow styles and dashboard CSS to style the routing map UI.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| dashboard/src/tabs/Linear.tsx | Enables routing map preview for Linear by passing showRoutingMap. |
| dashboard/src/tabs/GitHub.tsx | Enables routing map preview for GitHub by passing showRoutingMap. |
| dashboard/src/components/RoutingEditor.tsx | Adds showRoutingMap prop and renders RoutingMap above the rules section. |
| dashboard/src/components/RoutingMap.tsx | New React Flow-based component for rendering the routing graph preview. |
| dashboard/src/components/routingGraph.ts | New graph builder to map routing config into nodes/edges (events, rules, defaults, destinations). |
| dashboard/src/main.tsx | Imports React Flow base stylesheet globally. |
| dashboard/src/styles.css | Adds styling for routing map container, nodes, edges, minimap, and controls. |
| dashboard/package.json | Adds @xyflow/react dependency. |
| dashboard/pnpm-lock.yaml | Locks @xyflow/react and transitive dependencies. |
Files not reviewed (1)
- dashboard/pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| defaultViewport={{ x: 80, y: 70, zoom: 0.92 }} | ||
| minZoom={0.35} | ||
| maxZoom={1.35} | ||
| nodesDraggable |
| if (routing.rules.length === 0) { | ||
| addNode("no-rules", "drop", "No rules", "unmatched events use defaults", 0); | ||
| addEdge(sourceId, "no-rules"); | ||
| } |
Greptile SummaryThis PR adds
Confidence Score: 3/5Not safe to merge as-is — all three integration tabs lose their routing configuration UI. The dashboard/src/tabs/GitHub.tsx, dashboard/src/tabs/Gitlab.tsx, and dashboard/src/tabs/Linear.tsx — all three replaced the routing editor with a read-only panel. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Tab as GitHub/Linear/GitLab Tab
participant RMP as RoutingMapPanel
participant RM as RoutingMap
participant RG as routingGraph.ts
participant API as /api/apps/{app}/routing
Tab->>RMP: render(appName, eventOptions)
RMP->>API: useSWR(routing)
RMP->>API: useSWR(chats) [shouldRetryOnError:false]
RMP->>API: useSWR(users) [shouldRetryOnError:false]
API-->>RMP: RoutingConfig / chats / users
RMP->>RM: render(routing, eventOptions, chats, users)
RM->>RG: buildRoutingGraph(args) [useMemo]
RG-->>RM: "{ nodes, edges }"
RM->>RM: map to flowNodes/flowEdges [useMemo]
RM->>RM: setGraphNodes/setGraphEdges [useEffect]
RM-->>Tab: ReactFlow canvas (read-only)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Tab as GitHub/Linear/GitLab Tab
participant RMP as RoutingMapPanel
participant RM as RoutingMap
participant RG as routingGraph.ts
participant API as /api/apps/{app}/routing
Tab->>RMP: render(appName, eventOptions)
RMP->>API: useSWR(routing)
RMP->>API: useSWR(chats) [shouldRetryOnError:false]
RMP->>API: useSWR(users) [shouldRetryOnError:false]
API-->>RMP: RoutingConfig / chats / users
RMP->>RM: render(routing, eventOptions, chats, users)
RM->>RG: buildRoutingGraph(args) [useMemo]
RG-->>RM: "{ nodes, edges }"
RM->>RM: map to flowNodes/flowEdges [useMemo]
RM->>RM: setGraphNodes/setGraphEdges [useEffect]
RM-->>Tab: ReactFlow canvas (read-only)
Reviews (2): Last reviewed commit: "Add routing map panel for GitHub and Lin..." | Re-trigger Greptile |
| const [graphNodes, setGraphNodes, onNodesChange] = | ||
| useNodesState<RoutingGraphNode>([]); | ||
| const [graphEdges, setGraphEdges, onEdgesChange] = | ||
| useEdgesState<RoutingGraphEdge>([]); | ||
|
|
||
| useEffect(() => { | ||
| setGraphNodes(flowNodes); | ||
| }, [flowNodes, setGraphNodes]); | ||
|
|
||
| useEffect(() => { | ||
| setGraphEdges(flowEdges); | ||
| }, [flowEdges, setGraphEdges]); |
There was a problem hiding this comment.
Blank-canvas flash on every mount
useNodesState([]) / useEdgesState([]) initialise with empty arrays. Because RoutingMap only renders after the if (!edit) return <Spinner /> guard in RoutingEditor, flowNodes and flowEdges are already non-empty on the very first render — but React Flow still receives the empty initial state on that first pass and is populated only once the two useEffects fire. This produces a visible blank grey canvas between the spinner disappearing and the graph appearing. Initialize with the computed values directly so the first render is already populated. The useEffects are still needed for subsequent updates (user edits, chat/user data arriving), but the initial flash is eliminated.
| className: | ||
| edge.data?.kind === "default" | ||
| ? "routing-map-edge default" | ||
| : "routing-map-edge", | ||
| style: | ||
| edge.data?.kind === "default" | ||
| ? { strokeDasharray: "6 5" } | ||
| : undefined, |
There was a problem hiding this comment.
The
strokeDasharray for default edges is set twice: via the inline style prop here and via the .routing-map-edge.default CSS rule in styles.css. Inline styles take precedence over class rules in SVG, so the CSS rule is dead code. Remove the duplicate inline assignment to keep the styling canonical to CSS.
| className: | |
| edge.data?.kind === "default" | |
| ? "routing-map-edge default" | |
| : "routing-map-edge", | |
| style: | |
| edge.data?.kind === "default" | |
| ? { strokeDasharray: "6 5" } | |
| : undefined, | |
| className: | |
| edge.data?.kind === "default" | |
| ? "routing-map-edge default" | |
| : "routing-map-edge", |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
AprilNEA
left a comment
There was a problem hiding this comment.
The graph-building logic in routingGraph.ts is solid — normalization, dedup, and row allocation all check out. But the wiring contradicts the PR description and removes existing functionality, so this can't merge as-is.
The intended shape per your own description seems to be: keep RoutingEditor in all three tabs, pass showRoutingMap for GitHub/Linear only, and drop RoutingMapPanel. Details inline.
| <h2>GitHub</h2> | ||
| <LarkBinding appName="github" /> | ||
| <RoutingEditor appName="github" eventOptions={GITHUB_EVENTS} /> | ||
| <RoutingMapPanel appName="github" eventOptions={GITHUB_EVENTS} /> |
There was a problem hiding this comment.
Blocking: RoutingEditor is replaced wholesale by the read-only RoutingMapPanel (same in Gitlab.tsx and Linear.tsx), so after merge there is no way to add/remove rules, edit default destinations, the reviewer user map, or alert labels — no Save button anywhere. This contradicts the PR description ("add read-only RoutingMap component", "keep GitLab unchanged via explicit showRoutingMap flag"): GitLab is changed too, and nothing is additive. Looks like a bad rebase or mistaken commit — this should be <RoutingEditor appName="github" eventOptions={GITHUB_EVENTS} showRoutingMap />.
| <h2>GitLab</h2> | ||
| <LarkBinding appName="gitlab" /> | ||
| <RoutingEditor appName="gitlab" eventOptions={GITLAB_EVENTS} /> | ||
| <RoutingMapPanel appName="gitlab" eventOptions={GITLAB_EVENTS} /> |
There was a problem hiding this comment.
Per the PR description GitLab should be unchanged — restore RoutingEditor here without showRoutingMap.
| showUserMap={false} | ||
| showAlertLabels={false} | ||
| /> | ||
| <RoutingMapPanel appName="linear" eventOptions={LINEAR_EVENTS} /> |
There was a problem hiding this comment.
Restore RoutingEditor with its previous showUserMap={false} showAlertLabels={false} props plus showRoutingMap — the read-only panel drops all editing (rules, defaults, save).
| * label-triggered alerts (e.g. Linear). */ | ||
| showAlertLabels?: boolean; | ||
| /** Show the read-only routing map preview (default `false`). */ | ||
| showRoutingMap?: boolean; |
There was a problem hiding this comment.
showRoutingMap is dead code at this head: no file imports RoutingEditor anymore, so this flag and the embedded map section are unreachable. Wiring the tabs back to RoutingEditor (see tab comments) makes this the live path.
|
|
||
| export type RoutingGraphDestinationKind = "chat" | "dm"; | ||
|
|
||
| export interface RoutingGraphDestination { |
There was a problem hiding this comment.
These duplicate the wire types already defined in RoutingEditor.tsx (Destination/Rule/RoutingConfig/EventOption/Chat/User, all mirroring lark_kit::routing::Config). Two parallel type sets only stay compatible by structural luck and will drift when the backend shape changes — export one shared set (from RoutingEditor.tsx or a small shared module) and import it here.
| eventOptions, | ||
| }: RoutingMapPanelProps) { | ||
| const url = `/api/apps/${appName}/routing`; | ||
| const { data: routing, error } = useSWR<RoutingGraphConfig>(url); |
There was a problem hiding this comment.
This re-implements RoutingEditor's SWR fetching of /routing, /chats, /users and its error/spinner handling. Once the map is embedded in the editor behind showRoutingMap, this component is redundant — delete it.
| ); | ||
|
|
||
| rule.events.forEach((event) => { | ||
| addEdge(eventIds.get(event) ?? eventNodeId(event, 0), ruleId); |
There was a problem hiding this comment.
The ?? eventNodeId(event, 0) fallback would emit an edge pointing at a nonexistent node (React Flow silently drops it). It's currently unreachable — eventValues is built from every normalized rule's events, so eventIds.get can't miss. Remove the fallback rather than keeping a dead branch that hides a future bug.
Summary
Verification