feat: updated project dashboard with tracker list and recent tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere 2026-04-13 14:50:13 +02:00
parent eb8908e434
commit e98f7d59de
4 changed files with 69 additions and 8 deletions

View file

@ -82,7 +82,7 @@ fn load_or_generate_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std:
} else {
let mut key = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut key);
std::fs::write(path, &key)?;
std::fs::write(path, key)?;
Ok(key)
}
}

View file

@ -35,6 +35,7 @@ fn from_row(row: &rusqlite::Row) -> rusqlite::Result<ProcessedTicket> {
})
}
#[allow(dead_code)]
const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, artifact_id, artifact_title, artifact_data, \
status, analyst_report, developer_report, worktree_path, branch_name, \
detected_at, processed_at FROM processed_tickets";
@ -91,6 +92,7 @@ impl ProcessedTicket {
Ok(count > 0)
}
#[allow(dead_code)]
pub fn list_by_tracker(conn: &Connection, tracker_id: &str) -> Result<Vec<ProcessedTicket>> {
let sql = format!(
"{} WHERE tracker_id = ?1 ORDER BY detected_at DESC",
@ -115,6 +117,7 @@ impl ProcessedTicket {
rows.collect()
}
#[allow(dead_code)]
pub fn get_by_id(conn: &Connection, id: &str) -> Result<ProcessedTicket> {
let sql = format!("{} WHERE id = ?1", SELECT_ALL_COLS);
conn.query_row(&sql, params![id], from_row)

View file

@ -31,6 +31,7 @@ impl TuleapClient {
}
}
#[allow(dead_code)]
pub async fn test_connection(&self) -> Result<(), String> {
let url = format!("{}/api/projects?limit=1", self.base_url);
let resp = self

View file

@ -1,17 +1,30 @@
import { useEffect, useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import { getProject, deleteProject } from "../../lib/api";
import type { Project } from "../../lib/types";
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(() => {
if (projectId) {
getProject(projectId).then(setProject);
}
loadData();
}, [projectId]);
async function handleDelete() {
@ -23,10 +36,25 @@ export default function ProjectDashboard() {
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">
@ -68,8 +96,37 @@ export default function ProjectDashboard() {
</div>
</div>
<div className="mt-8 text-gray-400 text-sm">
Tracker surveillance and ticket processing will be available in the next update.
<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>
);