Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 45x 45x 24x 24x 24x 24x 24x 24x 24x 24x 8x 8x 8x 24x 8x 8x 8x 18x 18x 18x 18x 18x 18x 18x 24x 24x 24x 18x 1x | import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useProjects, useDeleteProject } from './projectsApi'
import { DataTable, type Column } from '@/shared/components/DataTable'
import { ConfirmDialog } from '@/shared/components/ConfirmDialog'
import { Button } from '@/components/ui/button'
function relativeTime(dateStr: string | undefined): string {
if (!dateStr) return '\u2014'
const diff = Date.now() - new Date(dateStr).getTime()
I const mins = Math.floor(diff / 60000)
if (mins < 1) return 'now'
if (mins < 60) return `${mins} min ago`
I const hours = Math.floor(mins / 60)
I if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
E if (days < 7) return `${days}d ago`
return new Date(dateStr).toLocaleDateString()
}
function statusIcon(status: string | undefined): string {
switch (status) {
case 'succeeded': return '\u2705'
case 'failed': return '\u274C'
case 'running': return '\u26A0\uFE0F'
default: return '\u26AA'
}
}
function statusColor(status: string | undefined): string {
switch (status) {
case 'succeeded': return 'text-green-500'
case 'failed': return 'text-red-500'
case 'running': return 'text-yellow-500'
default: return 'text-muted-foreground'
}
}
export function ProjectsPage() {
const navigate = useNavigate()
const { data, isLoading, error, refetch } = useProjects()
const deleteProject = useDeleteProject()
const [deleteId, setDeleteId] = useState<string | null>(null)
const projects = data?.data ?? []
const columns: Column[] = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'pipeline_count', label: 'Pipelines' },
{
key: 'last_run_at',
label: 'Last Run',
render: (item) => (
<span className="text-sm text-muted-foreground">
{relativeTime(item.last_run_at as string | undefined)}
</span>
),
},
{
key: 'last_run_status',
label: 'Status',
render: (item) => {
const status = item.last_run_status as string | undefined
return (
<span className={statusColor(status)}>
{statusIcon(status)}
</span>
)
},
},
]
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Projects</h1>
<Button onClick={() => navigate('/projects/new')}>New Project</Button>
</div>
<DataTable
columns={columns}
data={projects as unknown as Record<string, unknown>[]}
isLoading={isLoading}
error={error ?? null}
onRetry={() => refetch()}
emptyMessage="No projects configured"
emptyAction={
<Button variant="outline" onClick={() => navigate('/projects/new')}>
Create your first project
</Button>
}
onRowClick={(item) => navigate(`/projects/${item.id as string}`)}
onSort={(key) => console.log('sort', key)}
/>
<ConfirmDialog
open={!!deleteId}
title="Delete project"
message="Are you sure you want to delete this project? This will also delete all associated pipelines."
onConfirm={() => {
if (deleteId) deleteProject.mutate(deleteId)
setDeleteId(null)
}}
onCancel={() => setDeleteId(null)}
/>
</div>
)
}
|