orchai/src/components/projects/ProjectDashboard.tsx

134 lines
4.5 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import { getProject, deleteProject, listTrackers, listProcessedTickets } from "../../lib/api";
import type { Project, WatchedTracker, ProcessedTicket } from "../../lib/types";
import TrackerList from "../trackers/TrackerList";
export default function ProjectDashboard() {
const { projectId } = useParams();
const navigate = useNavigate();
const [project, setProject] = useState<Project | null>(null);
const [trackers, setTrackers] = useState<WatchedTracker[]>([]);
const [tickets, setTickets] = useState<ProcessedTicket[]>([]);
async function loadData() {
if (!projectId) return;
const [proj, trks, tkts] = await Promise.all([
getProject(projectId),
listTrackers(projectId),
listProcessedTickets(projectId),
]);
setProject(proj);
setTrackers(trks);
setTickets(tkts);
}
useEffect(() => {
loadData();
}, [projectId]);
async function handleDelete() {
if (!projectId) return;
if (!window.confirm(`Delete project "${project?.name}"?`)) return;
await deleteProject(projectId);
window.dispatchEvent(new Event("orchai:refresh-projects"));
navigate("/");
}
function statusBadgeClass(status: string): string {
switch (status) {
case "Pending":
return "bg-yellow-100 text-yellow-700";
case "Done":
return "bg-green-100 text-green-700";
case "Error":
return "bg-red-100 text-red-700";
default:
return "bg-blue-100 text-blue-700";
}
}
if (!project) {
return <div className="p-8 text-gray-400">Loading...</div>;
}
const recentTickets = tickets.slice(-10).reverse();
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold">{project.name}</h2>
<div className="flex gap-2">
<Link
to={`/projects/${project.id}/edit`}
className="px-3 py-1 bg-gray-200 rounded text-sm hover:bg-gray-300"
>
Edit
</Link>
<button
onClick={handleDelete}
className="px-3 py-1 bg-red-100 text-red-700 rounded text-sm hover:bg-red-200"
>
Delete
</button>
</div>
</div>
<div className="bg-white rounded-lg border border-gray-200 p-4 space-y-3">
<div>
<span className="text-sm text-gray-500">Path:</span>
<span className="ml-2 text-sm font-mono">{project.path}</span>
</div>
{project.cloned_from && (
<div>
<span className="text-sm text-gray-500">Cloned from:</span>
<span className="ml-2 text-sm font-mono">{project.cloned_from}</span>
</div>
)}
<div>
<span className="text-sm text-gray-500">Base branch:</span>
<span className="ml-2 text-sm font-mono">{project.base_branch}</span>
</div>
<div>
<span className="text-sm text-gray-500">Created:</span>
<span className="ml-2 text-sm">{new Date(project.created_at).toLocaleDateString()}</span>
</div>
</div>
<div className="mt-8">
<h3 className="text-lg font-semibold mb-4">Watched Trackers</h3>
<TrackerList trackers={trackers} projectId={project.id} onRefresh={loadData} />
</div>
<div className="mt-8">
<h3 className="text-lg font-semibold mb-4">Recent Tickets</h3>
{recentTickets.length === 0 ? (
<div className="text-sm text-gray-400">No tickets processed yet.</div>
) : (
<div className="space-y-3">
{recentTickets.map((ticket) => (
<div
key={ticket.id}
className="bg-white rounded-lg border border-gray-200 p-4 flex items-center justify-between gap-4"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400 font-mono">#{ticket.artifact_id}</span>
<span className="text-sm font-medium truncate">{ticket.artifact_title}</span>
</div>
</div>
<span
className={`text-xs px-2 py-0.5 rounded-full font-medium shrink-0 ${statusBadgeClass(ticket.status)}`}
>
{ticket.status}
</span>
</div>
))}
</div>
)}
</div>
</div>
);
}