11'use client' ;
22
3- import { Plus , MoreVertical , Pencil , Trash2 , Plug } from 'lucide-react' ;
3+ import { Plus , MoreVertical , Pencil , Trash2 , Plug , AlertCircle } from 'lucide-react' ;
44import { Button } from '@/components/ui/button' ;
5+ import { Skeleton } from '@/components/ui/skeleton' ;
56import { Table , TableBody } from '@/components/ui/table' ;
67import {
78 DropdownMenu ,
@@ -142,6 +143,45 @@ function SourceMenu({
142143 ) ;
143144}
144145
146+ /**
147+ * Placeholder occupying the connections column while the connections fetch is in
148+ * flight. Sized to a one-connection row so the source row doesn't jump height
149+ * when the real rows arrive.
150+ */
151+ function ConnectionsLoading ( { sourceId } : { sourceId : string } ) {
152+ return (
153+ < div
154+ className = "flex h-full items-center gap-3 px-6 py-4"
155+ data-testid = { `connections-loading-${ sourceId } ` }
156+ aria-busy = "true"
157+ aria-label = "Loading connections"
158+ >
159+ < Skeleton className = "h-4 w-[45%]" />
160+ < Skeleton className = "h-4 w-[25%]" />
161+ </ div >
162+ ) ;
163+ }
164+
165+ /** The connections fetch failed — say so and offer a retry instead of guessing. */
166+ function ConnectionsError ( { sourceId, onRetry } : { sourceId : string ; onRetry : ( ) => void } ) {
167+ return (
168+ < div className = "flex h-full items-center gap-2 px-6 py-4" >
169+ < AlertCircle className = "h-4 w-4 flex-shrink-0 text-destructive" />
170+ < p className = "text-base text-muted-foreground" >
171+ Couldn't load connections.{ ' ' }
172+ < button
173+ type = "button"
174+ onClick = { onRetry }
175+ data-testid = { `retry-connections-${ sourceId } ` }
176+ className = "cursor-pointer font-medium text-primary underline underline-offset-2 hover:opacity-80"
177+ >
178+ Retry
179+ </ button >
180+ </ p >
181+ </ div >
182+ ) ;
183+ }
184+
145185/**
146186 * One source rendered as a horizontal band for the "Side-by-side" layout:
147187 * a fixed-width left column (source identity + 3-dots menu, which owns the
@@ -172,8 +212,14 @@ export function SourceRow({
172212 onAddConnection,
173213 onEditSource,
174214 onDeleteSource,
215+ connectionsStatus,
216+ onRetryConnections,
175217} : SourceGroupProps ) {
176218 const { source, connections } = group ;
219+ // Zero connections is only meaningful once the fetch has answered — before
220+ // that it's absence of data, not absence of connections. Already-loaded
221+ // connections keep rendering through a background revalidate.
222+ const pending = connections . length === 0 && connectionsStatus !== 'ready' ;
177223
178224 return (
179225 < div
@@ -199,7 +245,13 @@ export function SourceRow({
199245 { /* Right — connections stacked as full-width rows, vertically centered so a
200246 single connection lines up with the source block instead of sitting at the top */ }
201247 < div className = "flex-1 min-w-0 flex flex-col justify-center" >
202- { connections . length === 0 ? (
248+ { pending ? (
249+ connectionsStatus === 'error' ? (
250+ < ConnectionsError sourceId = { source . sourceId } onRetry = { onRetryConnections } />
251+ ) : (
252+ < ConnectionsLoading sourceId = { source . sourceId } />
253+ )
254+ ) : connections . length === 0 ? (
203255 // Empty state — the source is connected but has no connections yet.
204256 // An inline sentence with a clickable "add a connection" that opens the
205257 // connection modal (a button reads as a hard action; this reads as guidance).
0 commit comments