Add explicit edit actions for existing pipeline cards
Build and Push Docker Image / build (push) Successful in 10m16s Details

This commit is contained in:
Matt 2026-02-14 13:04:27 +01:00
parent c88f540633
commit 2a374195c4
1 changed files with 229 additions and 216 deletions

View File

@ -1,244 +1,257 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { import {
Plus, Plus,
Layers, Layers,
GitBranch, GitBranch,
Calendar, Calendar,
Workflow, Workflow,
Pencil,
Settings2,
} from 'lucide-react' } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
import { useEdition } from '@/contexts/edition-context' import { useEdition } from '@/contexts/edition-context'
const statusConfig = { const statusConfig = {
DRAFT: { DRAFT: {
label: 'Draft', label: 'Draft',
bgClass: 'bg-gray-100 text-gray-700', bgClass: 'bg-gray-100 text-gray-700',
dotClass: 'bg-gray-500', dotClass: 'bg-gray-500',
}, },
ACTIVE: { ACTIVE: {
label: 'Active', label: 'Active',
bgClass: 'bg-emerald-100 text-emerald-700', bgClass: 'bg-emerald-100 text-emerald-700',
dotClass: 'bg-emerald-500', dotClass: 'bg-emerald-500',
}, },
CLOSED: { CLOSED: {
label: 'Closed', label: 'Closed',
bgClass: 'bg-blue-100 text-blue-700', bgClass: 'bg-blue-100 text-blue-700',
dotClass: 'bg-blue-500', dotClass: 'bg-blue-500',
}, },
ARCHIVED: { ARCHIVED: {
label: 'Archived', label: 'Archived',
bgClass: 'bg-muted text-muted-foreground', bgClass: 'bg-muted text-muted-foreground',
dotClass: 'bg-muted-foreground', dotClass: 'bg-muted-foreground',
}, },
} as const } as const
export default function PipelineListPage() { export default function PipelineListPage() {
const { currentEdition } = useEdition() const { currentEdition } = useEdition()
const programId = currentEdition?.id const programId = currentEdition?.id
const { data: pipelines, isLoading } = trpc.pipeline.list.useQuery( const { data: pipelines, isLoading } = trpc.pipeline.list.useQuery(
{ programId: programId! }, { programId: programId! },
{ enabled: !!programId } { enabled: !!programId }
) )
if (!programId) { if (!programId) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold">Pipelines</h1> <h1 className="text-xl font-bold">Pipelines</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Select an edition to view pipelines Select an edition to view pipelines
</p> </p>
</div> </div>
</div> </div>
<Card> <Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center"> <CardContent className="flex flex-col items-center justify-center py-12 text-center">
<Calendar className="h-12 w-12 text-muted-foreground/50" /> <Calendar className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No Edition Selected</p> <p className="mt-2 font-medium">No Edition Selected</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Select an edition from the sidebar to view its pipelines Select an edition from the sidebar to view its pipelines
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
) )
} }
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold">Pipelines</h1> <h1 className="text-xl font-bold">Pipelines</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Manage evaluation pipelines for {currentEdition?.name} Manage evaluation pipelines for {currentEdition?.name}
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link href={`/admin/rounds/new-pipeline?programId=${programId}` as Route}> <Link href={`/admin/rounds/new-pipeline?programId=${programId}` as Route}>
<Button size="sm"> <Button size="sm">
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Create Pipeline Create Pipeline
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
{/* Loading */} {/* Loading */}
{isLoading && ( {isLoading && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<Card key={i}> <Card key={i}>
<CardHeader> <CardHeader>
<Skeleton className="h-5 w-32" /> <Skeleton className="h-5 w-32" />
<Skeleton className="h-4 w-20 mt-1" /> <Skeleton className="h-4 w-20 mt-1" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Skeleton className="h-4 w-full" /> <Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4 mt-2" /> <Skeleton className="h-4 w-3/4 mt-2" />
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
)} )}
{/* Empty State */} {/* Empty State */}
{!isLoading && (!pipelines || pipelines.length === 0) && ( {!isLoading && (!pipelines || pipelines.length === 0) && (
<Card className="border-2 border-dashed"> <Card className="border-2 border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16 text-center"> <CardContent className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-full bg-primary/10 p-4 mb-4"> <div className="rounded-full bg-primary/10 p-4 mb-4">
<Workflow className="h-10 w-10 text-primary" /> <Workflow className="h-10 w-10 text-primary" />
</div> </div>
<h3 className="text-lg font-semibold mb-2">No Pipelines Yet</h3> <h3 className="text-lg font-semibold mb-2">No Pipelines Yet</h3>
<p className="text-sm text-muted-foreground max-w-md mb-6"> <p className="text-sm text-muted-foreground max-w-md mb-6">
Pipelines organize your project evaluation workflow into tracks and stages. Pipelines organize your project evaluation workflow into tracks and stages.
Create your first pipeline to get started with managing project evaluations. Create your first pipeline to get started with managing project evaluations.
</p> </p>
<Link href={`/admin/rounds/new-pipeline?programId=${programId}` as Route}> <Link href={`/admin/rounds/new-pipeline?programId=${programId}` as Route}>
<Button> <Button>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Create Your First Pipeline Create Your First Pipeline
</Button> </Button>
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{/* Pipeline Cards */} {/* Pipeline Cards */}
{pipelines && pipelines.length > 0 && ( {pipelines && pipelines.length > 0 && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{pipelines.map((pipeline) => { {pipelines.map((pipeline) => {
const status = pipeline.status as keyof typeof statusConfig const status = pipeline.status as keyof typeof statusConfig
const config = statusConfig[status] || statusConfig.DRAFT const config = statusConfig[status] || statusConfig.DRAFT
const description = (pipeline.settingsJson as Record<string, unknown> | null)?.description as string | undefined const description = (pipeline.settingsJson as Record<string, unknown> | null)?.description as string | undefined
return ( return (
<Link <Card key={pipeline.id} className="group hover:shadow-md transition-shadow h-full flex flex-col">
key={pipeline.id}
href={`/admin/rounds/pipeline/${pipeline.id}` as Route}
className="block"
>
<Card className="group hover:shadow-md transition-shadow cursor-pointer h-full flex flex-col">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<CardTitle className="text-base leading-tight mb-1"> <CardTitle className="text-base leading-tight mb-1">
{pipeline.name} <Link href={`/admin/rounds/pipeline/${pipeline.id}` as Route} className="hover:underline">
{pipeline.name}
</Link>
</CardTitle> </CardTitle>
<p className="font-mono text-xs text-muted-foreground truncate"> <p className="font-mono text-xs text-muted-foreground truncate">
{pipeline.slug} {pipeline.slug}
</p> </p>
</div> </div>
<Badge <Badge
variant="secondary" variant="secondary"
className={cn( className={cn(
'text-[10px] shrink-0 flex items-center gap-1.5', 'text-[10px] shrink-0 flex items-center gap-1.5',
config.bgClass config.bgClass
)} )}
> >
<span className={cn('h-1.5 w-1.5 rounded-full', config.dotClass)} /> <span className={cn('h-1.5 w-1.5 rounded-full', config.dotClass)} />
{config.label} {config.label}
</Badge> </Badge>
</div> </div>
{/* Description */} {/* Description */}
{description && ( {description && (
<p className="text-xs text-muted-foreground line-clamp-2 mt-2"> <p className="text-xs text-muted-foreground line-clamp-2 mt-2">
{description} {description}
</p> </p>
)} )}
</CardHeader> </CardHeader>
<CardContent className="mt-auto"> <CardContent className="mt-auto">
{/* Track Indicator - Simplified visualization */} {/* Track Indicator - Simplified visualization */}
<div className="mb-3 pb-3 border-b"> <div className="mb-3 pb-3 border-b">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1.5"> <div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1.5">
<Layers className="h-3.5 w-3.5" /> <Layers className="h-3.5 w-3.5" />
<span className="font-medium"> <span className="font-medium">
{pipeline._count.tracks === 0 {pipeline._count.tracks === 0
? 'No tracks' ? 'No tracks'
: pipeline._count.tracks === 1 : pipeline._count.tracks === 1
? '1 track' ? '1 track'
: `${pipeline._count.tracks} tracks`} : `${pipeline._count.tracks} tracks`}
</span> </span>
</div> </div>
{pipeline._count.tracks > 0 && ( {pipeline._count.tracks > 0 && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{Array.from({ length: Math.min(pipeline._count.tracks, 5) }).map((_, i) => ( {Array.from({ length: Math.min(pipeline._count.tracks, 5) }).map((_, i) => (
<div <div
key={i} key={i}
className="h-6 flex-1 rounded border border-border bg-muted/30 flex items-center justify-center" className="h-6 flex-1 rounded border border-border bg-muted/30 flex items-center justify-center"
> >
<div className="h-1 w-1 rounded-full bg-muted-foreground/40" /> <div className="h-1 w-1 rounded-full bg-muted-foreground/40" />
</div> </div>
))} ))}
{pipeline._count.tracks > 5 && ( {pipeline._count.tracks > 5 && (
<span className="text-[10px] text-muted-foreground ml-1"> <span className="text-[10px] text-muted-foreground ml-1">
+{pipeline._count.tracks - 5} +{pipeline._count.tracks - 5}
</span> </span>
)} )}
</div> </div>
)} )}
</div> </div>
{/* Stats */} {/* Stats */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-1.5 text-muted-foreground"> <div className="flex items-center gap-1.5 text-muted-foreground">
<GitBranch className="h-3.5 w-3.5" /> <GitBranch className="h-3.5 w-3.5" />
<span>Routing rules</span> <span>Routing rules</span>
</div> </div>
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
{pipeline._count.routingRules} {pipeline._count.routingRules}
</span> </span>
</div> </div>
<div className="flex items-center justify-between text-xs text-muted-foreground"> <div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Updated {formatDistanceToNow(new Date(pipeline.updatedAt))} ago</span> <span>Updated {formatDistanceToNow(new Date(pipeline.updatedAt))} ago</span>
</div> </div>
</div> </div>
<div className="mt-3 flex items-center gap-2">
<Link href={`/admin/rounds/pipeline/${pipeline.id}/edit` as Route} className="flex-1">
<Button size="sm" variant="outline" className="w-full">
<Pencil className="h-3.5 w-3.5 mr-1.5" />
Edit
</Button>
</Link>
<Link href={`/admin/rounds/pipeline/${pipeline.id}/advanced` as Route} className="flex-1">
<Button size="sm" variant="outline" className="w-full">
<Settings2 className="h-3.5 w-3.5 mr-1.5" />
Advanced
</Button>
</Link>
</div>
</CardContent> </CardContent>
</Card> </Card>
</Link>
) )
})} })}
</div> </div>
)} )}
</div> </div>
) )
} }