feat: improve tracker editing, sorting, and live activity
This commit is contained in:
parent
abaf86a6ec
commit
a981e189c5
11 changed files with 564 additions and 55 deletions
|
|
@ -2,14 +2,15 @@ use crate::error::AppError;
|
||||||
use crate::models::credential::TuleapCredentials;
|
use crate::models::credential::TuleapCredentials;
|
||||||
use crate::models::ticket::ProcessedTicket;
|
use crate::models::ticket::ProcessedTicket;
|
||||||
use crate::models::tracker::WatchedTracker;
|
use crate::models::tracker::WatchedTracker;
|
||||||
use crate::services::{crypto, filter_engine};
|
use crate::services::{crypto, filter_engine, notifier};
|
||||||
use crate::services::tuleap_client::TuleapClient;
|
use crate::services::tuleap_client::TuleapClient;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use tauri::State;
|
use tauri::{Emitter, State};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn manual_poll(
|
pub async fn manual_poll(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
tracker_id: String,
|
tracker_id: String,
|
||||||
) -> Result<Vec<ProcessedTicket>, AppError> {
|
) -> Result<Vec<ProcessedTicket>, AppError> {
|
||||||
let (tracker, client) = {
|
let (tracker, client) = {
|
||||||
|
|
@ -36,10 +37,32 @@ pub async fn manual_poll(
|
||||||
(tracker, client)
|
(tracker, client)
|
||||||
}; // lock dropped here
|
}; // lock dropped here
|
||||||
|
|
||||||
let artifacts = client
|
let _ = app_handle.emit(
|
||||||
.get_artifacts(tracker.tracker_id)
|
"polling-started",
|
||||||
.await
|
serde_json::json!({
|
||||||
.map_err(AppError::from)?;
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"source": "manual",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let artifacts = match client.get_artifacts(tracker.tracker_id).await {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"polling-error",
|
||||||
|
serde_json::json!({
|
||||||
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"source": "manual",
|
||||||
|
"error": e,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return Err(AppError::from(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let filtered = filter_engine::apply_filters(&artifacts, &tracker.filters);
|
let filtered = filter_engine::apply_filters(&artifacts, &tracker.filters);
|
||||||
|
|
||||||
|
|
@ -80,6 +103,40 @@ pub async fn manual_poll(
|
||||||
WatchedTracker::update_last_polled(&db, &tracker.id)?;
|
WatchedTracker::update_last_polled(&db, &tracker.id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !newly_inserted.is_empty() {
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"new-tickets-detected",
|
||||||
|
serde_json::json!({
|
||||||
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"count": newly_inserted.len(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for ticket in &newly_inserted {
|
||||||
|
notifier::notify_new_ticket(
|
||||||
|
&state.db,
|
||||||
|
&app_handle,
|
||||||
|
&tracker.project_id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
&ticket.artifact_title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"polling-finished",
|
||||||
|
serde_json::json!({
|
||||||
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"source": "manual",
|
||||||
|
"new_tickets_count": newly_inserted.len(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(newly_inserted)
|
Ok(newly_inserted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::models::credential::TuleapCredentials;
|
use crate::models::credential::TuleapCredentials;
|
||||||
use crate::models::ticket::ProcessedTicket;
|
use crate::models::ticket::ProcessedTicket;
|
||||||
use crate::models::tracker::{AgentConfig, FilterGroup, WatchedTracker};
|
use crate::models::tracker::{AgentConfig, FilterGroup, TrackerUpdate, WatchedTracker};
|
||||||
use crate::services::crypto;
|
use crate::services::crypto;
|
||||||
use crate::services::tuleap_client::TuleapClient;
|
use crate::services::tuleap_client::TuleapClient;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
@ -73,17 +73,14 @@ pub fn list_trackers(
|
||||||
pub fn update_tracker(
|
pub fn update_tracker(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
id: String,
|
id: String,
|
||||||
polling_interval: i32,
|
update: TrackerUpdate,
|
||||||
agent_config: AgentConfig,
|
|
||||||
filters: Vec<FilterGroup>,
|
|
||||||
enabled: bool,
|
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let db = state
|
let db = state
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
WatchedTracker::update(&db, &id, polling_interval, agent_config, filters, enabled)?;
|
WatchedTracker::update(&db, &id, update)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,10 +101,18 @@ pub async fn get_tracker_fields(
|
||||||
tracker_id: i32,
|
tracker_id: i32,
|
||||||
) -> Result<Vec<crate::services::tuleap_client::TrackerField>, AppError> {
|
) -> Result<Vec<crate::services::tuleap_client::TrackerField>, AppError> {
|
||||||
let client = build_tuleap_client(&state)?;
|
let client = build_tuleap_client(&state)?;
|
||||||
let fields = client
|
let mut fields = client
|
||||||
.get_tracker_fields(tracker_id)
|
.get_tracker_fields(tracker_id)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
fields.sort_by(|a, b| a.label.to_lowercase().cmp(&b.label.to_lowercase()));
|
||||||
|
for field in &mut fields {
|
||||||
|
field
|
||||||
|
.values
|
||||||
|
.sort_by(|a, b| a.label.to_lowercase().cmp(&b.label.to_lowercase()));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(fields)
|
Ok(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,16 @@ pub struct WatchedTracker {
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TrackerUpdate {
|
||||||
|
pub tracker_id: i32,
|
||||||
|
pub tracker_label: String,
|
||||||
|
pub polling_interval: i32,
|
||||||
|
pub agent_config: AgentConfig,
|
||||||
|
pub filters: Vec<FilterGroup>,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<WatchedTracker> {
|
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<WatchedTracker> {
|
||||||
let agent_config_json: String = row.get(5)?;
|
let agent_config_json: String = row.get(5)?;
|
||||||
let filters_json: String = row.get(6)?;
|
let filters_json: String = row.get(6)?;
|
||||||
|
|
@ -125,23 +135,24 @@ impl WatchedTracker {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(conn: &Connection, id: &str, update: TrackerUpdate) -> Result<()> {
|
||||||
conn: &Connection,
|
let agent_config_json = serde_json::to_string(&update.agent_config)
|
||||||
id: &str,
|
|
||||||
polling_interval: i32,
|
|
||||||
agent_config: AgentConfig,
|
|
||||||
filters: Vec<FilterGroup>,
|
|
||||||
enabled: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let agent_config_json = serde_json::to_string(&agent_config)
|
|
||||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||||
let filters_json = serde_json::to_string(&filters)
|
let filters_json = serde_json::to_string(&update.filters)
|
||||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||||
let enabled_int = if enabled { 1i32 } else { 0i32 };
|
let enabled_int = if update.enabled { 1i32 } else { 0i32 };
|
||||||
|
|
||||||
let affected = conn.execute(
|
let affected = conn.execute(
|
||||||
"UPDATE watched_trackers SET polling_interval = ?1, agent_config_json = ?2, filters_json = ?3, enabled = ?4 WHERE id = ?5",
|
"UPDATE watched_trackers SET tracker_id = ?1, tracker_label = ?2, polling_interval = ?3, agent_config_json = ?4, filters_json = ?5, enabled = ?6 WHERE id = ?7",
|
||||||
params![polling_interval, agent_config_json, filters_json, enabled_int, id],
|
params![
|
||||||
|
update.tracker_id,
|
||||||
|
update.tracker_label,
|
||||||
|
update.polling_interval,
|
||||||
|
agent_config_json,
|
||||||
|
filters_json,
|
||||||
|
enabled_int,
|
||||||
|
id
|
||||||
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if affected == 0 {
|
if affected == 0 {
|
||||||
|
|
@ -258,10 +269,14 @@ mod tests {
|
||||||
WatchedTracker::update(
|
WatchedTracker::update(
|
||||||
&conn,
|
&conn,
|
||||||
&t2.id,
|
&t2.id,
|
||||||
t2.polling_interval,
|
TrackerUpdate {
|
||||||
sample_agent_config(),
|
tracker_id: t2.tracker_id,
|
||||||
vec![],
|
tracker_label: t2.tracker_label.clone(),
|
||||||
false,
|
polling_interval: t2.polling_interval,
|
||||||
|
agent_config: sample_agent_config(),
|
||||||
|
filters: vec![],
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -318,10 +333,23 @@ mod tests {
|
||||||
}],
|
}],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
WatchedTracker::update(&conn, &created.id, 60, sample_agent_config(), new_filters, false)
|
WatchedTracker::update(
|
||||||
.expect("update should succeed");
|
&conn,
|
||||||
|
&created.id,
|
||||||
|
TrackerUpdate {
|
||||||
|
tracker_id: 11,
|
||||||
|
tracker_label: "Updated tracker".to_string(),
|
||||||
|
polling_interval: 60,
|
||||||
|
agent_config: sample_agent_config(),
|
||||||
|
filters: new_filters,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("update should succeed");
|
||||||
|
|
||||||
let updated = WatchedTracker::get_by_id(&conn, &created.id).unwrap();
|
let updated = WatchedTracker::get_by_id(&conn, &created.id).unwrap();
|
||||||
|
assert_eq!(updated.tracker_id, 11);
|
||||||
|
assert_eq!(updated.tracker_label, "Updated tracker");
|
||||||
assert_eq!(updated.polling_interval, 60);
|
assert_eq!(updated.polling_interval, 60);
|
||||||
assert!(!updated.enabled);
|
assert!(!updated.enabled);
|
||||||
assert_eq!(updated.filters[0].conditions[0].field, "priority");
|
assert_eq!(updated.filters[0].conditions[0].field, "priority");
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,9 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-started",
|
"ticket-processing-started",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"ticket_id": ticket.id,
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
"step": "analyst",
|
"step": "analyst",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -216,7 +218,12 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
);
|
);
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-error",
|
"ticket-processing-error",
|
||||||
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
serde_json::json!({
|
||||||
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
|
"error": e
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +242,11 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
.map_err(|e| format!("update_status: {}", e))?;
|
.map_err(|e| format!("update_status: {}", e))?;
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-done",
|
"ticket-processing-done",
|
||||||
serde_json::json!({ "ticket_id": ticket.id }),
|
serde_json::json!({
|
||||||
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
notifier::notify_analysis_done(
|
notifier::notify_analysis_done(
|
||||||
db,
|
db,
|
||||||
|
|
@ -272,7 +283,12 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
);
|
);
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-error",
|
"ticket-processing-error",
|
||||||
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
serde_json::json!({
|
||||||
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
|
"error": e
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +307,9 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-started",
|
"ticket-processing-started",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"ticket_id": ticket.id,
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
"step": "developer",
|
"step": "developer",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -324,7 +342,12 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
);
|
);
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-error",
|
"ticket-processing-error",
|
||||||
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
serde_json::json!({
|
||||||
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
|
"error": e
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
@ -340,7 +363,11 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
|
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-done",
|
"ticket-processing-done",
|
||||||
serde_json::json!({ "ticket_id": ticket.id }),
|
serde_json::json!({
|
||||||
|
"project_id": &project.id,
|
||||||
|
"ticket_id": &ticket.id,
|
||||||
|
"artifact_id": ticket.artifact_id,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
notifier::notify_fix_ready(
|
notifier::notify_fix_ready(
|
||||||
db,
|
db,
|
||||||
|
|
|
||||||
|
|
@ -102,11 +102,31 @@ async fn poll_single_tracker(
|
||||||
tracker: &WatchedTracker,
|
tracker: &WatchedTracker,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
) {
|
) {
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"polling-started",
|
||||||
|
serde_json::json!({
|
||||||
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"source": "scheduled",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 1. Fetch artifacts
|
// 1. Fetch artifacts
|
||||||
let artifacts = match client.get_artifacts(tracker.tracker_id).await {
|
let artifacts = match client.get_artifacts(tracker.tracker_id).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("poller: failed to fetch artifacts for tracker {}: {}", tracker.id, e);
|
eprintln!("poller: failed to fetch artifacts for tracker {}: {}", tracker.id, e);
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"polling-error",
|
||||||
|
serde_json::json!({
|
||||||
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"source": "scheduled",
|
||||||
|
"error": e,
|
||||||
|
}),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -169,8 +189,9 @@ async fn poll_single_tracker(
|
||||||
if let Err(e) = app_handle.emit(
|
if let Err(e) = app_handle.emit(
|
||||||
"new-tickets-detected",
|
"new-tickets-detected",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"tracker_id": tracker.id,
|
"project_id": &tracker.project_id,
|
||||||
"tracker_label": tracker.tracker_label,
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
"count": new_tickets.len(),
|
"count": new_tickets.len(),
|
||||||
}),
|
}),
|
||||||
) {
|
) {
|
||||||
|
|
@ -188,4 +209,15 @@ async fn poll_single_tracker(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"polling-finished",
|
||||||
|
serde_json::json!({
|
||||||
|
"project_id": &tracker.project_id,
|
||||||
|
"tracker_id": &tracker.id,
|
||||||
|
"tracker_label": &tracker.tracker_label,
|
||||||
|
"source": "scheduled",
|
||||||
|
"new_tickets_count": new_tickets.len(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ function App() {
|
||||||
<Route path="/projects/:projectId/tickets" element={<TicketList />} />
|
<Route path="/projects/:projectId/tickets" element={<TicketList />} />
|
||||||
<Route path="/projects/:projectId/edit" element={<ProjectForm />} />
|
<Route path="/projects/:projectId/edit" element={<ProjectForm />} />
|
||||||
<Route path="/projects/:projectId/trackers/new" element={<TrackerConfig />} />
|
<Route path="/projects/:projectId/trackers/new" element={<TrackerConfig />} />
|
||||||
|
<Route path="/projects/:projectId/trackers/:trackerConfigId/edit" element={<TrackerConfig />} />
|
||||||
<Route path="/tickets/:ticketId" element={<TicketDetail />} />
|
<Route path="/tickets/:ticketId" element={<TicketDetail />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,57 @@
|
||||||
import { useEffect, useState } from "react";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
import { getProject, deleteProject, listTrackers, listProcessedTickets } from "../../lib/api";
|
import { getProject, deleteProject, listTrackers, listProcessedTickets } from "../../lib/api";
|
||||||
import type { Project, WatchedTracker, ProcessedTicket } from "../../lib/types";
|
import type { Project, WatchedTracker, ProcessedTicket } from "../../lib/types";
|
||||||
import TrackerList from "../trackers/TrackerList";
|
import TrackerList from "../trackers/TrackerList";
|
||||||
|
|
||||||
|
type ActivityLevel = "info" | "success" | "error";
|
||||||
|
|
||||||
|
interface ActivityItem {
|
||||||
|
id: string;
|
||||||
|
level: ActivityLevel;
|
||||||
|
message: string;
|
||||||
|
at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PollingPayload {
|
||||||
|
project_id: string;
|
||||||
|
tracker_id: string;
|
||||||
|
tracker_label: string;
|
||||||
|
source: "manual" | "scheduled";
|
||||||
|
new_tickets_count?: number;
|
||||||
|
error?: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TicketProcessingPayload {
|
||||||
|
project_id: string;
|
||||||
|
ticket_id: string;
|
||||||
|
artifact_id: number;
|
||||||
|
step?: "analyst" | "developer";
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
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 [trackers, setTrackers] = useState<WatchedTracker[]>([]);
|
||||||
const [tickets, setTickets] = useState<ProcessedTicket[]>([]);
|
const [tickets, setTickets] = useState<ProcessedTicket[]>([]);
|
||||||
|
const [activity, setActivity] = useState<ActivityItem[]>([]);
|
||||||
|
const [activePolls, setActivePolls] = useState<Record<string, string>>({});
|
||||||
|
const [activeAgents, setActiveAgents] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
function appendActivity(level: ActivityLevel, message: string) {
|
||||||
|
const item: ActivityItem = {
|
||||||
|
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setActivity((prev) => [item, ...prev].slice(0, 30));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
@ -27,6 +69,153 @@ export default function ProjectDashboard() {
|
||||||
loadData();
|
loadData();
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) return;
|
||||||
|
|
||||||
|
let unlistenFns: Array<() => void> = [];
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
try {
|
||||||
|
const [unlistenPollingStarted, unlistenPollingFinished, unlistenPollingError, unlistenTicketsDetected, unlistenTicketStarted, unlistenTicketDone, unlistenTicketError] =
|
||||||
|
await Promise.all([
|
||||||
|
listen<PollingPayload>("polling-started", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
|
||||||
|
setActivePolls((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[payload.tracker_id]: payload.tracker_label,
|
||||||
|
}));
|
||||||
|
appendActivity(
|
||||||
|
"info",
|
||||||
|
`Polling ${payload.source === "manual" ? "manuel" : "auto"} lancé sur "${payload.tracker_label}".`
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
listen<PollingPayload>("polling-finished", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
|
||||||
|
setActivePolls((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[payload.tracker_id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
appendActivity(
|
||||||
|
"success",
|
||||||
|
`Polling terminé sur "${payload.tracker_label}" (${payload.new_tickets_count ?? 0} nouveau(x) ticket(s)).`
|
||||||
|
);
|
||||||
|
void loadData();
|
||||||
|
}),
|
||||||
|
listen<PollingPayload>("polling-error", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
|
||||||
|
setActivePolls((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[payload.tracker_id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
appendActivity(
|
||||||
|
"error",
|
||||||
|
`Erreur de polling sur "${payload.tracker_label}": ${payload.error ?? "erreur inconnue"}.`
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
listen<PollingPayload>("new-tickets-detected", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
appendActivity(
|
||||||
|
"success",
|
||||||
|
`${payload.count ?? 0} nouveau(x) ticket(s) détecté(s) dans "${payload.tracker_label}".`
|
||||||
|
);
|
||||||
|
void loadData();
|
||||||
|
}),
|
||||||
|
listen<TicketProcessingPayload>("ticket-processing-started", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
|
||||||
|
setActiveAgents((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[payload.ticket_id]: payload.step ?? "processing",
|
||||||
|
}));
|
||||||
|
appendActivity(
|
||||||
|
"info",
|
||||||
|
`Agent ${payload.step ?? "processing"} lancé pour le ticket #${payload.artifact_id}.`
|
||||||
|
);
|
||||||
|
void loadData();
|
||||||
|
}),
|
||||||
|
listen<TicketProcessingPayload>("ticket-processing-done", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
|
||||||
|
setActiveAgents((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[payload.ticket_id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
appendActivity(
|
||||||
|
"success",
|
||||||
|
`Pipeline agent terminé pour le ticket #${payload.artifact_id}.`
|
||||||
|
);
|
||||||
|
void loadData();
|
||||||
|
}),
|
||||||
|
listen<TicketProcessingPayload>("ticket-processing-error", (event) => {
|
||||||
|
const payload = event.payload;
|
||||||
|
if (payload.project_id !== projectId) return;
|
||||||
|
|
||||||
|
setActiveAgents((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[payload.ticket_id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
appendActivity(
|
||||||
|
"error",
|
||||||
|
`Erreur agent sur le ticket #${payload.artifact_id}: ${payload.error ?? "erreur inconnue"}.`
|
||||||
|
);
|
||||||
|
void loadData();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
unlistenPollingStarted();
|
||||||
|
unlistenPollingFinished();
|
||||||
|
unlistenPollingError();
|
||||||
|
unlistenTicketsDetected();
|
||||||
|
unlistenTicketStarted();
|
||||||
|
unlistenTicketDone();
|
||||||
|
unlistenTicketError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlistenFns = [
|
||||||
|
unlistenPollingStarted,
|
||||||
|
unlistenPollingFinished,
|
||||||
|
unlistenPollingError,
|
||||||
|
unlistenTicketsDetected,
|
||||||
|
unlistenTicketStarted,
|
||||||
|
unlistenTicketDone,
|
||||||
|
unlistenTicketError,
|
||||||
|
];
|
||||||
|
} catch (err) {
|
||||||
|
appendActivity(
|
||||||
|
"error",
|
||||||
|
`Écoute des événements live indisponible: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
for (const unlisten of unlistenFns) {
|
||||||
|
unlisten();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
if (!window.confirm(`Delete project "${project?.name}"?`)) return;
|
if (!window.confirm(`Delete project "${project?.name}"?`)) return;
|
||||||
|
|
@ -54,6 +243,8 @@ export default function ProjectDashboard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const recentTickets = tickets.slice(-10).reverse();
|
const recentTickets = tickets.slice(-10).reverse();
|
||||||
|
const activePollList = useMemo(() => Object.entries(activePolls), [activePolls]);
|
||||||
|
const activeAgentList = useMemo(() => Object.values(activeAgents), [activeAgents]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
|
|
@ -101,6 +292,55 @@ export default function ProjectDashboard() {
|
||||||
<TrackerList trackers={trackers} projectId={project.id} onRefresh={loadData} />
|
<TrackerList trackers={trackers} projectId={project.id} onRefresh={loadData} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Live Activity</h3>
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 p-4 space-y-4">
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<div className="text-sm rounded-full bg-blue-50 text-blue-700 px-3 py-1">
|
||||||
|
Polling en cours: {activePollList.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm rounded-full bg-purple-50 text-purple-700 px-3 py-1">
|
||||||
|
Agents actifs: {activeAgentList.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(activePollList.length > 0 || activeAgentList.length > 0) && (
|
||||||
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
|
{activePollList.map(([trackerId, label]) => (
|
||||||
|
<div key={`poll-${trackerId}`}>Polling: {label}</div>
|
||||||
|
))}
|
||||||
|
{activeAgentList.map((step, i) => (
|
||||||
|
<div key={`agent-${i}`}>Agent: {step}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activity.length === 0 ? (
|
||||||
|
<div className="text-sm text-gray-400">No live activity yet.</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||||
|
{activity.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`rounded border px-3 py-2 text-sm ${
|
||||||
|
item.level === "error"
|
||||||
|
? "border-red-200 bg-red-50 text-red-700"
|
||||||
|
: item.level === "success"
|
||||||
|
? "border-green-200 bg-green-50 text-green-700"
|
||||||
|
: "border-blue-200 bg-blue-50 text-blue-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>{item.message}</div>
|
||||||
|
<div className="text-[11px] opacity-70 mt-0.5">
|
||||||
|
{new Date(item.at).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold">Recent Tickets</h3>
|
<h3 className="text-lg font-semibold">Recent Tickets</h3>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { addTracker, getTrackerFields } from "../../lib/api";
|
import { useEffect } from "react";
|
||||||
|
import { addTracker, getTrackerFields, listTrackers, updateTracker } from "../../lib/api";
|
||||||
import { getErrorMessage } from "../../lib/errors";
|
import { getErrorMessage } from "../../lib/errors";
|
||||||
import type { FilterGroup, TrackerField, AgentConfig } from "../../lib/types";
|
import type { FilterGroup, TrackerField, AgentConfig } from "../../lib/types";
|
||||||
import FilterBuilder from "./FilterBuilder";
|
import FilterBuilder from "./FilterBuilder";
|
||||||
|
|
||||||
export default function TrackerConfig() {
|
export default function TrackerConfig() {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId, trackerConfigId } = useParams<{ projectId: string; trackerConfigId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const isEditing = Boolean(trackerConfigId);
|
||||||
|
|
||||||
const [trackerId, setTrackerId] = useState<number | "">("");
|
const [trackerId, setTrackerId] = useState<number | "">("");
|
||||||
const [trackerLabel, setTrackerLabel] = useState("");
|
const [trackerLabel, setTrackerLabel] = useState("");
|
||||||
|
|
@ -18,8 +20,56 @@ export default function TrackerConfig() {
|
||||||
const [filters, setFilters] = useState<FilterGroup[]>([]);
|
const [filters, setFilters] = useState<FilterGroup[]>([]);
|
||||||
const [analystCommand, setAnalystCommand] = useState("claude");
|
const [analystCommand, setAnalystCommand] = useState("claude");
|
||||||
const [developerCommand, setDeveloperCommand] = useState("claude");
|
const [developerCommand, setDeveloperCommand] = useState("claude");
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [initializing, setInitializing] = useState(false);
|
||||||
|
|
||||||
|
function sortTrackerFields(input: TrackerField[]): TrackerField[] {
|
||||||
|
return input
|
||||||
|
.map((field) => ({
|
||||||
|
...field,
|
||||||
|
values: [...field.values].sort((a, b) =>
|
||||||
|
a.label.localeCompare(b.label, "fr", { sensitivity: "base" })
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.label.localeCompare(b.label, "fr", { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadTrackerForEdit() {
|
||||||
|
if (!projectId || !trackerConfigId) return;
|
||||||
|
setInitializing(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const trackers = await listTrackers(projectId);
|
||||||
|
const tracker = trackers.find((t) => t.id === trackerConfigId);
|
||||||
|
if (!tracker) {
|
||||||
|
setError("Tracker not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTrackerId(tracker.tracker_id);
|
||||||
|
setTrackerLabel(tracker.tracker_label);
|
||||||
|
setPollingInterval(tracker.polling_interval);
|
||||||
|
setFilters(tracker.filters);
|
||||||
|
setAnalystCommand(tracker.agent_config.analyst_command);
|
||||||
|
setDeveloperCommand(tracker.agent_config.developer_command);
|
||||||
|
setEnabled(tracker.enabled);
|
||||||
|
|
||||||
|
const trackerFields = await getTrackerFields(tracker.tracker_id);
|
||||||
|
setFields(sortTrackerFields(trackerFields));
|
||||||
|
setFieldsLoaded(true);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setError(getErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setInitializing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadTrackerForEdit();
|
||||||
|
}, [projectId, trackerConfigId]);
|
||||||
|
|
||||||
async function handleLoadFields() {
|
async function handleLoadFields() {
|
||||||
if (!trackerId) return;
|
if (!trackerId) return;
|
||||||
|
|
@ -27,7 +77,7 @@ export default function TrackerConfig() {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await getTrackerFields(Number(trackerId));
|
const result = await getTrackerFields(Number(trackerId));
|
||||||
setFields(result);
|
setFields(sortTrackerFields(result));
|
||||||
setFieldsLoaded(true);
|
setFieldsLoaded(true);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
|
@ -50,7 +100,26 @@ export default function TrackerConfig() {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addTracker(projectId, Number(trackerId), trackerLabel, pollingInterval, agentConfig, filters);
|
if (isEditing && trackerConfigId) {
|
||||||
|
await updateTracker(
|
||||||
|
trackerConfigId,
|
||||||
|
Number(trackerId),
|
||||||
|
trackerLabel,
|
||||||
|
pollingInterval,
|
||||||
|
agentConfig,
|
||||||
|
filters,
|
||||||
|
enabled
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await addTracker(
|
||||||
|
projectId,
|
||||||
|
Number(trackerId),
|
||||||
|
trackerLabel,
|
||||||
|
pollingInterval,
|
||||||
|
agentConfig,
|
||||||
|
filters
|
||||||
|
);
|
||||||
|
}
|
||||||
navigate(`/projects/${projectId}`);
|
navigate(`/projects/${projectId}`);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
|
@ -61,9 +130,15 @@ export default function TrackerConfig() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto p-8">
|
<div className="max-w-2xl mx-auto p-8">
|
||||||
<h2 className="text-xl font-bold mb-6">Add tracker</h2>
|
<h2 className="text-xl font-bold mb-6">
|
||||||
|
{isEditing ? "Edit tracker" : "Add tracker"}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{initializing && (
|
||||||
|
<div className="text-sm text-gray-500">Loading tracker...</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Basic fields */}
|
{/* Basic fields */}
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4 space-y-4">
|
<div className="bg-white rounded-lg border border-gray-200 p-4 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -122,6 +197,17 @@ export default function TrackerConfig() {
|
||||||
className="w-40 border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-40 border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<label className="flex items-center gap-2 text-sm text-gray-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => setEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Tracker enabled
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter builder */}
|
{/* Filter builder */}
|
||||||
|
|
@ -172,10 +258,10 @@ export default function TrackerConfig() {
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading || initializing}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
|
className="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? "Adding..." : "Add tracker"}
|
{loading ? "Saving..." : isEditing ? "Save tracker" : "Add tracker"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { manualPoll, updateTracker, removeTracker } from "../../lib/api";
|
import { manualPoll, updateTracker, removeTracker } from "../../lib/api";
|
||||||
import type { WatchedTracker } from "../../lib/types";
|
import type { WatchedTracker } from "../../lib/types";
|
||||||
|
|
@ -9,12 +10,17 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TrackerList({ trackers, projectId, onRefresh }: Props) {
|
export default function TrackerList({ trackers, projectId, onRefresh }: Props) {
|
||||||
|
const [pollingIds, setPollingIds] = useState<string[]>([]);
|
||||||
|
|
||||||
async function handlePollNow(tracker: WatchedTracker) {
|
async function handlePollNow(tracker: WatchedTracker) {
|
||||||
try {
|
try {
|
||||||
|
setPollingIds((prev) => [...prev, tracker.id]);
|
||||||
await manualPoll(tracker.id);
|
await manualPoll(tracker.id);
|
||||||
onRefresh();
|
onRefresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Poll failed:", err);
|
console.error("Poll failed:", err);
|
||||||
|
} finally {
|
||||||
|
setPollingIds((prev) => prev.filter((id) => id !== tracker.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +28,8 @@ export default function TrackerList({ trackers, projectId, onRefresh }: Props) {
|
||||||
try {
|
try {
|
||||||
await updateTracker(
|
await updateTracker(
|
||||||
tracker.id,
|
tracker.id,
|
||||||
|
tracker.tracker_id,
|
||||||
|
tracker.tracker_label,
|
||||||
tracker.polling_interval,
|
tracker.polling_interval,
|
||||||
tracker.agent_config,
|
tracker.agent_config,
|
||||||
tracker.filters,
|
tracker.filters,
|
||||||
|
|
@ -79,10 +87,17 @@ export default function TrackerList({ trackers, projectId, onRefresh }: Props) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handlePollNow(tracker)}
|
onClick={() => handlePollNow(tracker)}
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded text-xs hover:bg-blue-700"
|
disabled={pollingIds.includes(tracker.id)}
|
||||||
|
className="px-3 py-1 bg-blue-600 text-white rounded text-xs hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Poll now
|
{pollingIds.includes(tracker.id) ? "Polling..." : "Poll now"}
|
||||||
</button>
|
</button>
|
||||||
|
<Link
|
||||||
|
to={`/projects/${projectId}/trackers/${tracker.id}/edit`}
|
||||||
|
className="px-3 py-1 bg-gray-200 text-gray-700 rounded text-xs hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleToggleEnabled(tracker)}
|
onClick={() => handleToggleEnabled(tracker)}
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,26 @@ export async function addTracker(projectId: string, trackerId: number, trackerLa
|
||||||
export async function listTrackers(projectId: string): Promise<WatchedTracker[]> {
|
export async function listTrackers(projectId: string): Promise<WatchedTracker[]> {
|
||||||
return invoke("list_trackers", { projectId });
|
return invoke("list_trackers", { projectId });
|
||||||
}
|
}
|
||||||
export async function updateTracker(id: string, pollingInterval: number, agentConfig: AgentConfig, filters: FilterGroup[], enabled: boolean): Promise<void> {
|
export async function updateTracker(
|
||||||
return invoke("update_tracker", { id, pollingInterval, agentConfig, filters, enabled });
|
id: string,
|
||||||
|
trackerId: number,
|
||||||
|
trackerLabel: string,
|
||||||
|
pollingInterval: number,
|
||||||
|
agentConfig: AgentConfig,
|
||||||
|
filters: FilterGroup[],
|
||||||
|
enabled: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
return invoke("update_tracker", {
|
||||||
|
id,
|
||||||
|
update: {
|
||||||
|
tracker_id: trackerId,
|
||||||
|
tracker_label: trackerLabel,
|
||||||
|
polling_interval: pollingInterval,
|
||||||
|
agent_config: agentConfig,
|
||||||
|
filters,
|
||||||
|
enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
export async function removeTracker(id: string): Promise<void> {
|
export async function removeTracker(id: string): Promise<void> {
|
||||||
return invoke("remove_tracker", { id });
|
return invoke("remove_tracker", { id });
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ export function getErrorMessage(err: unknown): string {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(err);
|
return JSON.stringify(err);
|
||||||
} catch {
|
} catch {
|
||||||
// fallthrough to String below
|
return "Erreur inattendue";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(err);
|
return String(err ?? "Erreur inattendue");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue