feat: frontend types, API wrappers, and Settings page for Tuleap credentials

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere 2026-04-13 14:43:03 +02:00
parent b20ecc5493
commit 43a3483fd0
5 changed files with 296 additions and 1 deletions

View file

@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import AppLayout from "./components/layout/AppLayout";
import ProjectForm from "./components/projects/ProjectForm";
import ProjectDashboard from "./components/projects/ProjectDashboard";
import SettingsPage from "./components/settings/SettingsPage";
function EmptyState() {
return (
@ -20,6 +21,7 @@ function App() {
<Route path="/projects/new" element={<ProjectForm />} />
<Route path="/projects/:projectId" element={<ProjectDashboard />} />
<Route path="/projects/:projectId/edit" element={<ProjectForm />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>

View file

@ -58,6 +58,12 @@ export default function Sidebar() {
<p className="px-3 py-2 text-sm text-gray-500">No projects yet</p>
)}
</nav>
<div className="p-2 border-t border-gray-700">
<Link to="/settings" className="block px-3 py-2 rounded text-sm text-gray-300 hover:bg-gray-800 hover:text-white">
Settings
</Link>
</div>
</aside>
);
}

View file

@ -0,0 +1,174 @@
import { useState, useEffect } from "react";
import {
getTuleapCredentials,
setTuleapCredentials,
deleteTuleapCredentials,
testTuleapConnection,
} from "../../lib/api";
import type { TuleapCredentialsSafe } from "../../lib/types";
export default function SettingsPage() {
const [tuleapUrl, setTuleapUrl] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [existing, setExisting] = useState<TuleapCredentialsSafe | null>(null);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
getTuleapCredentials().then((creds) => {
if (creds) {
setExisting(creds);
setTuleapUrl(creds.tuleap_url);
setUsername(creds.username);
}
});
}, []);
function clearMessages() {
setError(null);
setSuccess(null);
}
async function handleSave(e: React.FormEvent) {
e.preventDefault();
clearMessages();
setSaving(true);
try {
const creds = await setTuleapCredentials(tuleapUrl, username, password);
setExisting(creds);
setPassword("");
setSuccess("Credentials saved.");
} catch (err: unknown) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setSaving(false);
}
}
async function handleTest() {
clearMessages();
setTesting(true);
try {
const msg = await testTuleapConnection();
setSuccess(msg);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setTesting(false);
}
}
async function handleDelete() {
if (!window.confirm("Delete Tuleap credentials? This cannot be undone.")) return;
clearMessages();
setDeleting(true);
try {
await deleteTuleapCredentials();
setExisting(null);
setTuleapUrl("");
setUsername("");
setPassword("");
setSuccess("Credentials deleted.");
} catch (err: unknown) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setDeleting(false);
}
}
return (
<div className="max-w-lg mx-auto p-8">
<h2 className="text-xl font-bold mb-6">Settings</h2>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h3 className="text-base font-semibold mb-4">Tuleap credentials</h3>
<form onSubmit={handleSave} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tuleap URL
</label>
<input
type="url"
value={tuleapUrl}
onChange={(e) => setTuleapUrl(e.target.value)}
required
placeholder="https://tuleap.example.com"
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Username
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={existing ? "Leave empty to keep current" : ""}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{error && (
<div className="text-red-600 text-sm bg-red-50 border border-red-200 rounded p-2">
{error}
</div>
)}
{success && (
<div className="text-green-700 text-sm bg-green-50 border border-green-200 rounded p-2">
{success}
</div>
)}
<div className="flex gap-2 flex-wrap">
<button
type="submit"
disabled={saving}
className="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "Saving..." : "Save"}
</button>
<button
type="button"
onClick={handleTest}
disabled={testing || !existing}
className="px-4 py-2 bg-gray-200 rounded text-sm hover:bg-gray-300 disabled:opacity-50"
>
{testing ? "Testing..." : "Test connection"}
</button>
{existing && (
<button
type="button"
onClick={handleDelete}
disabled={deleting}
className="px-4 py-2 bg-red-600 text-white rounded text-sm hover:bg-red-700 disabled:opacity-50"
>
{deleting ? "Deleting..." : "Delete"}
</button>
)}
</div>
</form>
</div>
</div>
);
}

View file

@ -1,5 +1,13 @@
import { invoke } from "@tauri-apps/api/core";
import type { Project } from "./types";
import type {
Project,
TuleapCredentialsSafe,
AgentConfig,
FilterGroup,
WatchedTracker,
TrackerField,
ProcessedTicket,
} from "./types";
export async function createProject(
name: string,
@ -32,3 +40,45 @@ export async function updateProject(
export async function deleteProject(id: string): Promise<void> {
return invoke("delete_project", { id });
}
// Credentials
export async function setTuleapCredentials(tuleapUrl: string, username: string, password: string): Promise<TuleapCredentialsSafe> {
return invoke("set_tuleap_credentials", { tuleapUrl, username, password });
}
export async function getTuleapCredentials(): Promise<TuleapCredentialsSafe | null> {
return invoke("get_tuleap_credentials");
}
export async function deleteTuleapCredentials(): Promise<void> {
return invoke("delete_tuleap_credentials");
}
export async function testTuleapConnection(): Promise<string> {
return invoke("test_tuleap_connection");
}
// Trackers
export async function addTracker(projectId: string, trackerId: number, trackerLabel: string, pollingInterval: number, agentConfig: AgentConfig, filters: FilterGroup[]): Promise<WatchedTracker> {
return invoke("add_tracker", { projectId, trackerId, trackerLabel, pollingInterval, agentConfig, filters });
}
export async function listTrackers(projectId: string): Promise<WatchedTracker[]> {
return invoke("list_trackers", { projectId });
}
export async function updateTracker(id: string, pollingInterval: number, agentConfig: AgentConfig, filters: FilterGroup[], enabled: boolean): Promise<void> {
return invoke("update_tracker", { id, pollingInterval, agentConfig, filters, enabled });
}
export async function removeTracker(id: string): Promise<void> {
return invoke("remove_tracker", { id });
}
export async function getTrackerFields(trackerId: number): Promise<TrackerField[]> {
return invoke("get_tracker_fields", { trackerId });
}
// Tickets & Polling
export async function listProcessedTickets(projectId: string): Promise<ProcessedTicket[]> {
return invoke("list_processed_tickets", { projectId });
}
export async function manualPoll(trackerId: string): Promise<ProcessedTicket[]> {
return invoke("manual_poll", { trackerId });
}
export async function getQueueStatus(projectId: string): Promise<ProcessedTicket[]> {
return invoke("get_queue_status", { projectId });
}

View file

@ -6,3 +6,66 @@ export interface Project {
base_branch: string;
created_at: string;
}
export interface TuleapCredentialsSafe {
id: string;
tuleap_url: string;
username: string;
}
export interface AgentConfig {
analyst_command: string;
analyst_args: string[];
developer_command: string;
developer_args: string[];
}
export interface Filter {
field: string;
operator: string;
value: string[];
}
export interface FilterGroup {
conditions: Filter[];
}
export interface TrackerField {
field_id: number;
label: string;
field_type: string;
values: FieldValue[];
}
export interface FieldValue {
id: number;
label: string;
}
export interface WatchedTracker {
id: string;
project_id: string;
tracker_id: number;
tracker_label: string;
polling_interval: number;
agent_config: AgentConfig;
filters: FilterGroup[];
enabled: boolean;
last_polled_at: string | null;
created_at: string;
}
export interface ProcessedTicket {
id: string;
tracker_id: string;
artifact_id: number;
artifact_title: string;
artifact_data: string;
status: string;
analyst_report: string | null;
developer_report: string | null;
worktree_path: string | null;
branch_name: string | null;
detected_at: string;
processed_at: string | null;
}