Files
nuc/nuc-portal/src/components/DeploymentSkeleton.tsx
Alejandro Gutiérrez 9a0881e852 Add NUC Portal - infrastructure dashboard
Next.js 16 dashboard for managing NUC services via Coolify API.
Features service cards with health indicators, deployment dashboard
with live log streaming, S3-backed preview images, SSE real-time
updates, and dark mode support. 18 services across 7 categories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:17:32 +01:00

261 lines
9.8 KiB
TypeScript

'use client';
import Link from 'next/link';
import { Icon } from './Icons';
/**
* Skeleton component for deployment detail page
* Matches the layout of DeploymentDashboard for a seamless loading experience
*/
export function DeploymentSkeleton() {
return (
<div className="space-y-6 animate-pulse">
{/* Header section with app name and actions */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-slate-200 dark:bg-stone-800" />
<div>
<div className="h-6 w-40 bg-slate-200 dark:bg-stone-800 rounded mb-2" />
<div className="h-4 w-24 bg-slate-100 dark:bg-stone-800/50 rounded" />
</div>
</div>
{/* Action buttons skeleton */}
<div className="flex items-center gap-2">
<div className="h-9 w-20 bg-slate-200 dark:bg-stone-800 rounded-lg" />
<div className="h-9 w-20 bg-slate-200 dark:bg-stone-800 rounded-lg" />
<div className="h-9 w-20 bg-slate-900/20 dark:bg-stone-100/20 rounded-lg" />
</div>
</div>
{/* Tab navigation skeleton */}
<div className="border-b border-slate-200 dark:border-stone-800">
<div className="flex gap-6 pb-3">
{['Deployment', 'Logs', 'Resources', 'Source'].map((tab, i) => (
<div
key={tab}
className={`h-5 rounded ${
i === 0
? 'w-24 bg-slate-300 dark:bg-stone-700'
: 'w-16 bg-slate-200 dark:bg-stone-800'
}`}
/>
))}
</div>
</div>
{/* Main content card skeleton */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-200 dark:border-stone-700/50 shadow-sm overflow-hidden">
{/* Card header */}
<div className="flex items-center justify-between p-4 border-b border-slate-100 dark:border-stone-800">
<div className="h-5 w-36 bg-slate-200 dark:bg-stone-800 rounded" />
<div className="flex items-center gap-2">
<div className="h-8 w-16 bg-slate-200 dark:bg-stone-800 rounded-lg" />
<div className="h-8 w-16 bg-slate-200 dark:bg-stone-800 rounded-lg" />
<div className="h-8 w-16 bg-slate-900/20 dark:bg-stone-100/20 rounded-lg" />
</div>
</div>
{/* Main content: Preview + Metadata Grid */}
<div className="p-6">
<div className="flex gap-6">
{/* Preview thumbnail skeleton */}
<div className="flex-shrink-0 w-80">
<div className="aspect-[16/10] bg-slate-100 dark:bg-stone-800 rounded-lg flex items-center justify-center">
<Icon name="image" size={48} className="text-slate-300 dark:text-stone-700" />
</div>
</div>
{/* Metadata grid skeleton */}
<div className="flex-1 grid grid-cols-2 gap-x-8 gap-y-5">
{/* Created */}
<MetadataRowSkeleton />
{/* Status */}
<MetadataRowSkeleton hasIndicator />
{/* Health */}
<MetadataRowSkeleton hasIndicator />
{/* Duration */}
<MetadataRowSkeleton />
{/* Environment */}
<MetadataRowSkeleton hasBadge />
{/* Domains */}
<MetadataRowSkeleton />
{/* Source */}
<MetadataRowSkeleton hasSecondLine />
</div>
</div>
</div>
{/* Collapsible sections skeleton */}
{['Deployment Settings', 'Build Logs', 'Container Stats', 'Deployment Summary'].map(
(section, i) => (
<div
key={section}
className="border-t border-slate-100 dark:border-stone-800 p-4 flex items-center gap-3"
>
<div className="w-4 h-4 bg-slate-200 dark:bg-stone-800 rounded" />
<div className="w-4 h-4 bg-slate-200 dark:bg-stone-800 rounded" />
<div className="h-5 bg-slate-200 dark:bg-stone-800 rounded" style={{ width: `${section.length * 8}px` }} />
{i === 1 && (
<div className="ml-auto flex items-center gap-2">
<div className="h-4 w-12 bg-slate-200 dark:bg-stone-800 rounded" />
<div className="w-2 h-2 bg-slate-300 dark:bg-stone-700 rounded-full" />
</div>
)}
</div>
)
)}
{/* Action Cards Grid skeleton */}
<div className="p-6 border-t border-slate-100 dark:border-stone-800">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<ActionCardSkeleton key={i} />
))}
</div>
</div>
</div>
{/* Footer link skeleton */}
<div className="flex justify-end">
<div className="h-5 w-32 bg-slate-200 dark:bg-stone-800 rounded" />
</div>
</div>
);
}
/**
* Skeleton for metadata rows
*/
function MetadataRowSkeleton({
hasIndicator = false,
hasBadge = false,
hasSecondLine = false,
}: {
hasIndicator?: boolean;
hasBadge?: boolean;
hasSecondLine?: boolean;
}) {
return (
<div>
<div className="h-3 w-16 bg-slate-100 dark:bg-stone-800/50 rounded mb-2" />
<div className="flex items-center gap-2">
{hasIndicator && (
<div className="w-2 h-2 bg-slate-300 dark:bg-stone-700 rounded-full" />
)}
<div className="h-5 w-28 bg-slate-200 dark:bg-stone-800 rounded" />
{hasBadge && (
<div className="h-5 w-16 bg-slate-100 dark:bg-stone-800/50 rounded" />
)}
</div>
{hasSecondLine && (
<div className="h-4 w-48 bg-slate-100 dark:bg-stone-800/50 rounded mt-1" />
)}
</div>
);
}
/**
* Skeleton for action cards
*/
function ActionCardSkeleton() {
return (
<div className="p-4 rounded-lg border border-slate-200 dark:border-stone-700">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-5 h-5 bg-slate-200 dark:bg-stone-800 rounded" />
<div className="h-5 w-24 bg-slate-200 dark:bg-stone-800 rounded" />
</div>
</div>
<div className="h-4 w-full bg-slate-100 dark:bg-stone-800/50 rounded" />
</div>
);
}
/**
* Error state component for deployment page
*/
export function DeploymentError({
error,
uuid,
}: {
error: string;
uuid: string;
}) {
return (
<div className="flex flex-col items-center justify-center py-24">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20 mb-4">
<Icon name="alert-circle" size={32} className="text-red-500" />
</div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-stone-100 mb-2">
{error}
</h2>
<p className="text-slate-500 dark:text-stone-500 mb-6 text-center max-w-md">
{error === 'Deployment not found' ? (
<>
The deployment with UUID &quot;<code className="font-mono text-sm bg-slate-100 dark:bg-stone-800 px-1 py-0.5 rounded">{uuid}</code>&quot; could not be found.
It may have been deleted or never existed.
</>
) : (
<>
Unable to load deployment details. This could be due to a network issue
or the deployment service being temporarily unavailable.
</>
)}
</p>
<div className="flex gap-3">
<button
onClick={() => window.location.reload()}
className="flex items-center gap-2 px-4 py-2 text-slate-600 dark:text-stone-400 border border-slate-200 dark:border-stone-700 rounded-lg hover:bg-slate-50 dark:hover:bg-stone-800 transition-colors"
>
<Icon name="refresh-cw" size={16} />
Retry
</button>
<Link
href="/?tab=deployments"
className="flex items-center gap-2 px-4 py-2 bg-slate-900 dark:bg-stone-100 text-white dark:text-stone-900 rounded-lg hover:bg-slate-800 dark:hover:bg-stone-200 transition-colors"
>
<Icon name="arrow-left" size={16} />
Back to Deployments
</Link>
</div>
</div>
);
}
/**
* Empty state component for when no deployment data exists
*/
export function DeploymentEmpty({ uuid }: { uuid: string }) {
return (
<div className="flex flex-col items-center justify-center py-24">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-slate-100 dark:bg-stone-800 mb-4">
<Icon name="box" size={32} className="text-slate-400 dark:text-stone-500" />
</div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-stone-100 mb-2">
No deployment data
</h2>
<p className="text-slate-500 dark:text-stone-500 mb-6 text-center max-w-md">
The deployment &quot;<code className="font-mono text-sm bg-slate-100 dark:bg-stone-800 px-1 py-0.5 rounded">{uuid.substring(0, 9)}</code>&quot; exists
but has no data available yet. It may still be initializing.
</p>
<div className="flex gap-3">
<button
onClick={() => window.location.reload()}
className="flex items-center gap-2 px-4 py-2 text-slate-600 dark:text-stone-400 border border-slate-200 dark:border-stone-700 rounded-lg hover:bg-slate-50 dark:hover:bg-stone-800 transition-colors"
>
<Icon name="refresh-cw" size={16} />
Refresh
</button>
<Link
href="/?tab=deployments"
className="flex items-center gap-2 px-4 py-2 bg-slate-900 dark:bg-stone-100 text-white dark:text-stone-900 rounded-lg hover:bg-slate-800 dark:hover:bg-stone-200 transition-colors"
>
<Icon name="arrow-left" size={16} />
Back to Deployments
</Link>
</div>
</div>
);
}