156 lines
4.2 KiB
TypeScript
156 lines
4.2 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type LogLevel = 'info' | 'success' | 'warning' | 'error';
|
|
|
|
export interface LogEntry {
|
|
timestamp: string;
|
|
message: string;
|
|
level: LogLevel;
|
|
}
|
|
|
|
export interface DeployLogViewerProps {
|
|
logs: LogEntry[];
|
|
maxHeight?: string;
|
|
className?: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Level colors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const levelStyles: Record<LogLevel, string> = {
|
|
info: 'text-gray-400',
|
|
success: 'text-emerald-400',
|
|
warning: 'text-amber-400',
|
|
error: 'text-red-400',
|
|
};
|
|
|
|
const levelBadge: Record<LogLevel, string> = {
|
|
info: 'text-gray-500',
|
|
success: 'text-emerald-500',
|
|
warning: 'text-amber-500',
|
|
error: 'text-red-500',
|
|
};
|
|
|
|
const levelPrefix: Record<LogLevel, string> = {
|
|
info: '○',
|
|
success: '✓',
|
|
warning: '⚠',
|
|
error: '✗',
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Component
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function DeployLogViewer({
|
|
logs,
|
|
maxHeight = '320px',
|
|
className,
|
|
}: DeployLogViewerProps) {
|
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Auto-scroll to bottom on new logs
|
|
useEffect(() => {
|
|
if (bottomRef.current) {
|
|
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
}, [logs.length]);
|
|
|
|
const formatTime = (ts: string) => {
|
|
try {
|
|
const d = new Date(ts);
|
|
return d.toLocaleTimeString('en-US', {
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
});
|
|
} catch {
|
|
return '--:--:--';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'relative rounded-xl border border-gray-800 bg-gray-900 overflow-hidden',
|
|
className,
|
|
)}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-2">
|
|
<div className="flex gap-1.5">
|
|
<div className="h-3 w-3 rounded-full bg-red-500/70" />
|
|
<div className="h-3 w-3 rounded-full bg-amber-500/70" />
|
|
<div className="h-3 w-3 rounded-full bg-emerald-500/70" />
|
|
</div>
|
|
<span className="ml-2 text-xs font-medium text-gray-500">
|
|
Deploy Log
|
|
</span>
|
|
<span className="ml-auto text-xs text-gray-600">
|
|
{logs.length} entries
|
|
</span>
|
|
</div>
|
|
|
|
{/* Log content */}
|
|
<div
|
|
ref={containerRef}
|
|
className="overflow-y-auto p-4 font-mono text-sm leading-relaxed"
|
|
style={{ maxHeight }}
|
|
>
|
|
{logs.length === 0 && (
|
|
<div className="flex items-center gap-2 text-gray-600">
|
|
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-gray-600" />
|
|
Waiting for deploy to start…
|
|
</div>
|
|
)}
|
|
|
|
{logs.map((entry, i) => (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
'flex gap-3 py-0.5 transition-opacity duration-300',
|
|
i === logs.length - 1 ? 'opacity-100' : 'opacity-90',
|
|
)}
|
|
>
|
|
{/* Timestamp */}
|
|
<span className="shrink-0 text-gray-600 select-none">
|
|
{formatTime(entry.timestamp)}
|
|
</span>
|
|
|
|
{/* Level indicator */}
|
|
<span
|
|
className={cn('shrink-0 w-4 text-center', levelBadge[entry.level])}
|
|
>
|
|
{levelPrefix[entry.level]}
|
|
</span>
|
|
|
|
{/* Message */}
|
|
<span className={cn(levelStyles[entry.level])}>
|
|
{entry.message}
|
|
</span>
|
|
</div>
|
|
))}
|
|
|
|
{/* Cursor blink at bottom */}
|
|
{logs.length > 0 && (
|
|
<div className="mt-1 flex items-center gap-1 text-gray-600">
|
|
<span className="inline-block h-3.5 w-1.5 animate-pulse bg-indigo-500/70" />
|
|
</div>
|
|
)}
|
|
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|