All files / features / projects ProjectsPage.tsx

41.81% Statements 46/110
31.66% Branches 19/60
45.16% Functions 14/31
61.64% Lines 45/73

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            126x   126x   54x 54x 54x 54x 54x 54x 54x           54x 18x 18x 18x         54x 18x 18x 18x         42x 42x 42x 42x 42x 42x 42x                         54x                         54x 54x         54x           42x         42x               3x                          
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>
  )
}