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:
parent
eb8908e434
commit
e98f7d59de
4 changed files with 69 additions and 8 deletions
|
|
@ -82,7 +82,7 @@ fn load_or_generate_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std:
|
||||||
} else {
|
} else {
|
||||||
let mut key = [0u8; 32];
|
let mut key = [0u8; 32];
|
||||||
rand::rngs::OsRng.fill_bytes(&mut key);
|
rand::rngs::OsRng.fill_bytes(&mut key);
|
||||||
std::fs::write(path, &key)?;
|
std::fs::write(path, key)?;
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, \
|
const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, artifact_id, artifact_title, artifact_data, \
|
||||||
status, analyst_report, developer_report, worktree_path, branch_name, \
|
status, analyst_report, developer_report, worktree_path, branch_name, \
|
||||||
detected_at, processed_at FROM processed_tickets";
|
detected_at, processed_at FROM processed_tickets";
|
||||||
|
|
@ -91,6 +92,7 @@ impl ProcessedTicket {
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn list_by_tracker(conn: &Connection, tracker_id: &str) -> Result<Vec<ProcessedTicket>> {
|
pub fn list_by_tracker(conn: &Connection, tracker_id: &str) -> Result<Vec<ProcessedTicket>> {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"{} WHERE tracker_id = ?1 ORDER BY detected_at DESC",
|
"{} WHERE tracker_id = ?1 ORDER BY detected_at DESC",
|
||||||
|
|
@ -115,6 +117,7 @@ impl ProcessedTicket {
|
||||||
rows.collect()
|
rows.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn get_by_id(conn: &Connection, id: &str) -> Result<ProcessedTicket> {
|
pub fn get_by_id(conn: &Connection, id: &str) -> Result<ProcessedTicket> {
|
||||||
let sql = format!("{} WHERE id = ?1", SELECT_ALL_COLS);
|
let sql = format!("{} WHERE id = ?1", SELECT_ALL_COLS);
|
||||||
conn.query_row(&sql, params![id], from_row)
|
conn.query_row(&sql, params![id], from_row)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ impl TuleapClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn test_connection(&self) -> Result<(), String> {
|
pub async fn test_connection(&self) -> Result<(), String> {
|
||||||
let url = format!("{}/api/projects?limit=1", self.base_url);
|
let url = format!("{}/api/projects?limit=1", self.base_url);
|
||||||
let resp = self
|
let resp = self
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
import { getProject, deleteProject } from "../../lib/api";
|
import { getProject, deleteProject, listTrackers, listProcessedTickets } from "../../lib/api";
|
||||||
import type { Project } from "../../lib/types";
|
import type { Project, WatchedTracker, ProcessedTicket } from "../../lib/types";
|
||||||
|
import TrackerList from "../trackers/TrackerList";
|
||||||
|
|
||||||
export default function ProjectDashboard() {
|
export default function ProjectDashboard() {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [project, setProject] = useState<Project | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (projectId) {
|
loadData();
|
||||||
getProject(projectId).then(setProject);
|
|
||||||
}
|
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
|
|
@ -23,10 +36,25 @@ export default function ProjectDashboard() {
|
||||||
navigate("/");
|
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) {
|
if (!project) {
|
||||||
return <div className="p-8 text-gray-400">Loading...</div>;
|
return <div className="p-8 text-gray-400">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recentTickets = tickets.slice(-10).reverse();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
|
@ -68,8 +96,37 @@ export default function ProjectDashboard() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 text-gray-400 text-sm">
|
<div className="mt-8">
|
||||||
Tracker surveillance and ticket processing will be available in the next update.
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue