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:
parent
b20ecc5493
commit
43a3483fd0
5 changed files with 296 additions and 1 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
174
src/components/settings/SettingsPage.tsx
Normal file
174
src/components/settings/SettingsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue