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}
);
}