import type { ReactNode } from 'react'; import { GitBranch, Loader2, AlertCircle } from 'lucide-react'; import type { CheckStatus } from '@/types/imageUpdates'; import { isConfirmedImageUpdate } from '@/types/imageUpdates'; import { Checkbox } from '@/components/ui/checkbox'; import type { Label } from '@/components/label-types'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { sidebarRowActive, sidebarRowBase, sidebarRowCheckboxSlot } from './sidebar-styles'; import { statusText, statusColor } from './stack-status-utils'; import type { StackRowStatus } from './stack-status-utils'; import { updateAvailableLabel } from '@/lib/updateAvailableLabel'; interface StackRowProps { file: string; displayName: string; status: StackRowStatus; // Running/total container counts (set for any stack with containers); consumed only for the partial-stack pill tooltip. running?: number; total?: number; /** Hydration display projection: pending/error force unknown indicators, * stale dims the pill, current/incomplete render normally. */ hydrationDisplay?: 'pending' | 'error' | 'current' | 'stale' | 'incomplete'; isBusy: boolean; isActive: boolean; labels: Label[]; hasUpdate: boolean; /** Outdated service names for the update tooltip; empty keeps the generic label. */ outdatedServices?: string[]; // Last image-update check outcome. Incomplete/failed checks with hasUpdate // use a distinct indicator so they are not mistaken for a confirmed update. checkStatus?: CheckStatus; lastError?: string; hasGitPending: boolean; onSelect: (file: string) => void; kebabSlot: ReactNode; bulkMode?: boolean; isSelected?: boolean; onToggleSelect?: (file: string) => void; } function RowTooltip({ trigger, label }: { trigger: ReactNode; label: string }) { return ( {trigger} {label} ); } function appendErrorDetail(base: string, lastError?: string): string { if (!lastError) return base; return `${base} ${lastError}`; } function partialUpdateTooltip(hasUpdate: boolean, lastError?: string): string { if (hasUpdate) { // Neutral copy: partial + hasUpdate can mean newly detected OR retained; // provenance is not persisted on the wire. return appendErrorDetail( 'The last check was incomplete; an update was detected or retained, but the full stack could not be verified.', lastError, ); } return appendErrorDetail( 'The last image-update check was incomplete; update status could not be fully verified.', lastError, ); } function failedCheckTooltip(hasUpdate: boolean, lastError?: string): string { if (hasUpdate) { return appendErrorDetail( 'Previous update status retained; the last check failed.', lastError, ); } return lastError ? `Update check failed: ${lastError}` : 'Update check failed'; } export function StackRow(props: StackRowProps) { const { file, displayName, status, running, total, isBusy, isActive, hasUpdate, outdatedServices, checkStatus, lastError, hasGitPending, onSelect, kebabSlot, bulkMode = false, isSelected = false, onToggleSelect, hydrationDisplay = 'pending', } = props; const staleEvidence = hydrationDisplay === 'stale'; const confirmedUpdate = isConfirmedImageUpdate({ hasUpdate, checkStatus }); const partialIncomplete = checkStatus === 'partial'; const failedCheck = checkStatus === 'failed'; const handleClick = () => { if (bulkMode) onToggleSelect?.(file); else onSelect(file); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } }; return (
{ e.stopPropagation(); onToggleSelect?.(file); }} aria-hidden={!bulkMode} > {bulkMode && ( )} {/* Status pill. Partial stacks add a hover tooltip with the running/total count. Hydration state shapes the pill: pending stays muted, error turns the unknown indicator warning-colored (distinct from pending), stale dims the retained value so it is never read as current. */} {isBusy ? ( ) : status === 'partial' && running !== undefined && total !== undefined ? ( {statusText(status)}} label={`${running}/${total} running`} /> ) : ( statusText(status) )} {/* Stack name */} {displayName} {/* Trailing: confirmed update > partial incomplete > failed > git pending */} {confirmedUpdate ? ( )} label={updateAvailableLabel(outdatedServices)} /> ) : partialIncomplete ? ( } label={partialUpdateTooltip(hasUpdate, lastError)} /> ) : failedCheck ? ( } label={failedCheckTooltip(hasUpdate, lastError)} /> ) : hasGitPending ? ( } label="Git source update pending" /> ) : null} {/* Kebab: always rightmost. Hover-revealed on desktop; always visible on touch viewports where there is no hover. */}
e.stopPropagation()} > {kebabSlot}
); }