import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
import {
cancelBuild,
getBuildLogs,
listBuilds,
type BuildEntity,
type BuildStatus,
} from '@/lib/api-client';
import { useBuildUpdates } from '@/lib/use-build-updates';
import { formatRelative } from '@/lib/time';
import {
Check,
X,
Loader2,
Clock,
RefreshCw,
FileText,
Box,
Sparkles,
Filter,
Circle,
HardDrive,
} from 'lucide-react';
const statusOptions: Array<{ value: '' | BuildStatus; label: string }> = [
{ value: '', label: 'All statuses' },
{ value: 'pending', label: 'Pending' },
{ value: 'running', label: 'Running' },
{ value: 'success', label: 'Success' },
{ value: 'failed', label: 'Failed' },
{ value: 'cancelled', label: 'Cancelled' },
];
const demoBuilds: BuildEntity[] = [
{
id: 'build-demo-01',
projectId: 'project-demo',
serviceId: 'service-api',
status: 'success',
progress: 100,
startedAt: new Date(Date.now() - 20 * 60_000).toISOString(),
completedAt: new Date(Date.now() - 18 * 60_000).toISOString(),
imageName: 'ghcr.io/containr/api',
imageTag: 'sha-1f2e3d4',
size: 156_000_000,
log: '[demo] Build finished successfully.',
metadata: { branch: 'main' },
},
{
id: 'build-demo-02',
projectId: 'project-demo',
serviceId: 'service-worker',
status: 'running',
progress: 54,
startedAt: new Date(Date.now() - 2 * 60_000).toISOString(),
imageName: 'ghcr.io/containr/worker',
imageTag: 'sha-9a8b7c6',
size: 0,
log: '[demo] Building image layers...',
metadata: { branch: 'feature/queue' },
},
];
function StatusBadge({ status }: { status: BuildStatus }) {
const config = {
success: { color: 'var(--success)', bg: 'var(--success-soft)', Icon: Check, animate: false },
failed: { color: 'var(--error)', bg: 'var(--error-soft)', Icon: X, animate: false },
running: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: Loader2, animate: true },
pending: { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Clock, animate: false },
cancelled: { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: X, animate: false },
}[status] || { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Circle, animate: false };
const { Icon } = config;
return (
{status}
);
}
function bytesToHumanReadable(bytes: number): string {
if (bytes <= 0) return '—';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let index = 0;
while (value >= 1024 && index < units.length - 1) {
value /= 1024;
index += 1;
}
return `${value.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}
export function BuildsPage() {
const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
const isDemoMode = searchParams.get('demo') === '1';
const [projectFilter, setProjectFilter] = useState('');
const [serviceFilter, setServiceFilter] = useState('');
const [statusFilter, setStatusFilter] = useState<'' | BuildStatus>('');
const [limit, setLimit] = useState(50);
const [selectedBuild, setSelectedBuild] = useState(null);
const buildsQuery = useQuery({
queryKey: ['builds-page', { projectFilter, serviceFilter, statusFilter, limit }],
enabled: !isDemoMode,
queryFn: () =>
listBuilds({
projectId: projectFilter || undefined,
serviceId: serviceFilter || undefined,
status: statusFilter || undefined,
page: 1,
limit,
}),
});
const builds = useMemo(
() => (isDemoMode ? demoBuilds : buildsQuery.data?.builds ?? []),
[isDemoMode, buildsQuery.data?.builds],
);
const subscribedBuildIds = useMemo(
() => (isDemoMode ? [] : builds.map((build) => build.id)),
[isDemoMode, builds],
);
const liveStatus = useBuildUpdates(subscribedBuildIds, ({ channel }) => {
queryClient.invalidateQueries({ queryKey: ['builds-page'] });
queryClient.invalidateQueries({ queryKey: ['usage-builds'] });
if (selectedBuild && channel === `build:${selectedBuild.id}`) {
queryClient.invalidateQueries({ queryKey: ['build-logs', selectedBuild.id] });
}
});
const cancelMutation = useMutation({
mutationFn: (buildId: string) => cancelBuild(buildId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['builds-page'] });
queryClient.invalidateQueries({ queryKey: ['usage-builds'] });
},
});
const logsQuery = useQuery({
queryKey: ['build-logs', selectedBuild?.id],
enabled: Boolean(selectedBuild) && !isDemoMode,
queryFn: () => getBuildLogs(selectedBuild!.id),
});
// Stats
const stats = useMemo(() => {
const running = builds.filter(b => b.status === 'running' || b.status === 'pending').length;
const success = builds.filter(b => b.status === 'success').length;
const failed = builds.filter(b => b.status === 'failed').length;
return { running, success, failed, total: builds.length };
}, [builds]);
return (
{/* Header */}
Build Pipeline_
Monitor build progress and manage jobs
{liveStatus === 'live' ? 'Live' : liveStatus === 'offline' ? 'Reconnecting...' : 'Polling'}
{/* Demo Mode Banner */}
{isDemoMode && (
Demo mode active — using sample data
)}
{/* Stats Overview */}
{stats.total}
Total Builds
{stats.running}
In Progress
{stats.success}
Successful
{/* Filters */}
{/* Build Table */}
{!isDemoMode && buildsQuery.isLoading ? (
) : null}
{!isDemoMode && buildsQuery.isError ? (
Failed to load builds
{buildsQuery.error instanceof Error ? buildsQuery.error.message : 'Unknown error'}
) : null}
{builds.length === 0 && !buildsQuery.isLoading ? (
No builds match current filters
) : null}
{builds.length > 0 ? (
{builds.map((build) => (
setSelectedBuild(build)}
>
{build.status === 'success' && }
{build.status === 'failed' && }
{build.status === 'running' && }
{build.status === 'pending' && }
{build.status === 'cancelled' && }
{build.id}
{build.startedAt ? formatRelative(build.startedAt) : '—'}
{/* Image info */}
Image
{build.imageName || '—'}:{build.imageTag || 'latest'}
{/* Progress bar */}
{(build.status === 'running' || build.status === 'pending') && (
Progress
{build.progress}%
)}
{/* Size and service */}
{bytesToHumanReadable(build.size)}
{build.serviceId || '—'}
{/* Error message if present */}
{build.error && (
)}
))}
) : null}
{/* Error Toast */}
{cancelMutation.isError && (
{cancelMutation.error instanceof Error ? cancelMutation.error.message : 'Failed to cancel build.'}
)}
{/* Logs Modal */}
{selectedBuild && (
setSelectedBuild(null)} />
Build Logs
{selectedBuild.id}
{isDemoMode
? selectedBuild.log || '[demo] No logs available.'
: logsQuery.isLoading
? 'Loading logs...'
: logsQuery.isError
? 'Failed to load logs.'
: logsQuery.data || '(empty logs)'}
)}
);
}