release-tracker/src/app/releases/[id]/page.tsx

227 lines
7.7 KiB
TypeScript

import Link from 'next/link';
import { notFound, redirect } from 'next/navigation';
import { ArrowLeft, Edit, Play, Archive, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Separator } from '@/components/ui/separator';
import { getReleaseById, activateRelease, archiveRelease } from '@/lib/actions/releases';
import { getReleaseStepsGroupedByCluster, getStepStats } from '@/lib/actions/customer-steps';
import { listCustomers } from '@/lib/actions/customers';
import { ReleaseMatrixClient } from '@/components/releases/release-matrix-client';
interface ReleaseDetailPageProps {
params: Promise<{
id: string;
}>;
}
const typeColors: Record<string, string> = {
onboarding: 'bg-purple-100 text-purple-800',
release: 'bg-blue-100 text-blue-800',
hotfix: 'bg-red-100 text-red-800',
};
const statusColors: Record<string, string> = {
draft: 'bg-slate-100 text-slate-800',
active: 'bg-green-100 text-green-800',
archived: 'bg-gray-100 text-gray-800',
};
function getTypeColor(type: string) {
return typeColors[type] || 'bg-slate-100 text-slate-800';
}
function getStatusColor(status: string) {
return statusColors[status] || 'bg-slate-100 text-slate-800';
}
export const dynamic = 'force-dynamic';
export default async function ReleaseDetailPage({ params }: ReleaseDetailPageProps) {
const { id } = await params;
const releaseId = parseInt(id);
const release = await getReleaseById(releaseId);
if (!release) {
notFound();
}
// Get steps grouped by cluster
const stepsByCluster = release.status === 'active'
? await getReleaseStepsGroupedByCluster(releaseId)
: {};
const stats = release.status === 'active'
? await getStepStats(releaseId)
: { total: 0, done: 0, skipped: 0, pending: 0, reverted: 0, percentage: 0 };
const customers = await listCustomers();
async function handleActivate() {
'use server';
await activateRelease(releaseId);
redirect(`/releases/${releaseId}`);
}
async function handleArchive() {
'use server';
await archiveRelease(releaseId);
redirect(`/releases/${releaseId}`);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<Link href="/releases">
<Button variant="outline" size="icon">
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<div className="flex items-center gap-3">
<h1 className="text-3xl font-bold text-slate-900">{release.name}</h1>
<Badge className={getTypeColor(release.type || '')}>{release.type}</Badge>
<Badge className={getStatusColor(release.status || '')}>{release.status}</Badge>
</div>
{release.versionNumber && (
<p className="text-slate-600 mt-1">Version: {release.versionNumber}</p>
)}
</div>
</div>
<div className="flex gap-2">
<Link href={`/releases/${releaseId}/edit`}>
<Button variant="outline" size="sm">
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
</Link>
{release.status === 'draft' && (
<form action={handleActivate}>
<Button size="sm" type="submit">
<Play className="w-4 h-4 mr-2" />
Activate
</Button>
</form>
)}
{release.status === 'active' && (
<form action={handleArchive}>
<Button variant="outline" size="sm" type="submit">
<Archive className="w-4 h-4 mr-2" />
Archive
</Button>
</form>
)}
</div>
</div>
{/* Release Info */}
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="text-sm font-medium text-slate-500">Release Date</label>
<p className="text-slate-900">
{release.releaseDate
? new Date(release.releaseDate).toLocaleDateString()
: 'Not set'}
</p>
</div>
<div>
<label className="text-sm font-medium text-slate-500">Total Customers</label>
<p className="text-slate-900">{customers.length}</p>
</div>
{release.status === 'active' && (
<div>
<label className="text-sm font-medium text-slate-500">Progress</label>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${stats.percentage}%` }}
/>
</div>
<span className="text-sm font-medium">{stats.percentage}%</span>
</div>
</div>
)}
</div>
{release.description && (
<>
<Separator className="my-4" />
<div>
<label className="text-sm font-medium text-slate-500">Description</label>
<p className="text-slate-900 mt-1 whitespace-pre-wrap">{release.description}</p>
</div>
</>
)}
</CardContent>
</Card>
{/* Steps Management (for draft) */}
{release.status === 'draft' && (
<Card>
<CardHeader>
<CardTitle>Template Steps</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="font-medium mb-3">Deploy Steps ({release.templates.filter(t => t.category === 'deploy').length})</h3>
<Link href={`/releases/${releaseId}/steps`}>
<Button variant="outline" size="sm">
<Plus className="w-4 h-4 mr-2" />
Manage Deploy Steps
</Button>
</Link>
</div>
<div>
<h3 className="font-medium mb-3">Verify Steps ({release.templates.filter(t => t.category === 'verify').length})</h3>
<Link href={`/releases/${releaseId}/steps`}>
<Button variant="outline" size="sm">
<Plus className="w-4 h-4 mr-2" />
Manage Verify Steps
</Button>
</Link>
</div>
</div>
</CardContent>
</Card>
)}
{/* Matrix View (for active) */}
{release.status === 'active' && (
<Tabs defaultValue="deploy">
<TabsList>
<TabsTrigger value="deploy">
Deploy ({stats.done + stats.skipped}/{stats.total})
</TabsTrigger>
<TabsTrigger value="verify">
Verify
</TabsTrigger>
</TabsList>
<TabsContent value="deploy" className="mt-4">
<ReleaseMatrixClient
stepsByCluster={stepsByCluster}
category="deploy"
releaseId={releaseId}
/>
</TabsContent>
<TabsContent value="verify" className="mt-4">
<ReleaseMatrixClient
stepsByCluster={stepsByCluster}
category="verify"
releaseId={releaseId}
/>
</TabsContent>
</Tabs>
)}
</div>
);
}