feat(ui): regrouper la configuration Tuleap et Graylog

This commit is contained in:
thibaud-lclr 2026-04-21 15:36:34 +02:00
parent d300b64603
commit ee9d00efa4
3 changed files with 129 additions and 6 deletions

View file

@ -517,9 +517,9 @@ export default function ProjectDashboard() {
to={`/projects/${project.id}/graylog`} to={`/projects/${project.id}/graylog`}
className="rounded-lg border border-gray-200 bg-white p-4 hover:border-gray-300" className="rounded-lg border border-gray-200 bg-white p-4 hover:border-gray-300"
> >
<div className="text-sm font-semibold text-gray-900">Graylog</div> <div className="text-sm font-semibold text-gray-900">Intégrations</div>
<div className="mt-1 text-xs text-gray-500"> <div className="mt-1 text-xs text-gray-500">
Configure le polling Graylog et surveille les sujets scorés. Configure Tuleap et Graylog au même endroit.
</div> </div>
</Link> </Link>
</div> </div>

View file

@ -1,12 +1,15 @@
import { FormEvent, useCallback, useEffect, useState } from "react"; import { FormEvent, useCallback, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { import {
getTuleapCredentials,
getGraylogCredentials, getGraylogCredentials,
listAgents, listAgents,
listGraylogDetections, listGraylogDetections,
listGraylogSubjects, listGraylogSubjects,
manualGraylogPoll, manualGraylogPoll,
setTuleapCredentials,
setGraylogCredentials, setGraylogCredentials,
testTuleapConnection,
testGraylogConnection, testGraylogConnection,
} from "../../lib/api"; } from "../../lib/api";
import { getErrorMessage } from "../../lib/errors"; import { getErrorMessage } from "../../lib/errors";
@ -15,6 +18,7 @@ import type {
GraylogCredentialsSafe, GraylogCredentialsSafe,
GraylogDetection, GraylogDetection,
GraylogSubject, GraylogSubject,
TuleapCredentialsSafe,
} from "../../lib/types"; } from "../../lib/types";
import { import {
backLinkClass, backLinkClass,
@ -30,10 +34,14 @@ export default function ProjectGraylog() {
const { projectId } = useParams<{ projectId: string }>(); const { projectId } = useParams<{ projectId: string }>();
const [agents, setAgents] = useState<Agent[]>([]); const [agents, setAgents] = useState<Agent[]>([]);
const [tuleapCredentials, setTuleapCredentialsState] = useState<TuleapCredentialsSafe | null>(null);
const [credentials, setCredentials] = useState<GraylogCredentialsSafe | null>(null); const [credentials, setCredentials] = useState<GraylogCredentialsSafe | null>(null);
const [subjects, setSubjects] = useState<GraylogSubject[]>([]); const [subjects, setSubjects] = useState<GraylogSubject[]>([]);
const [detections, setDetections] = useState<GraylogDetection[]>([]); const [detections, setDetections] = useState<GraylogDetection[]>([]);
const [tuleapUrl, setTuleapUrl] = useState("");
const [tuleapUsername, setTuleapUsername] = useState("");
const [tuleapPassword, setTuleapPassword] = useState("");
const [baseUrl, setBaseUrl] = useState(""); const [baseUrl, setBaseUrl] = useState("");
const [apiToken, setApiToken] = useState(""); const [apiToken, setApiToken] = useState("");
const [analystAgentId, setAnalystAgentId] = useState(""); const [analystAgentId, setAnalystAgentId] = useState("");
@ -45,12 +53,18 @@ export default function ProjectGraylog() {
const [scoreThreshold, setScoreThreshold] = useState(70); const [scoreThreshold, setScoreThreshold] = useState(70);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [savingTuleap, setSavingTuleap] = useState(false);
const [testingTuleap, setTestingTuleap] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const analysts = agents.filter((agent) => agent.role === "analyst"); const analysts = agents.filter((agent) => agent.role === "analyst");
const developers = agents.filter((agent) => agent.role === "developer"); const developers = agents.filter((agent) => agent.role === "developer");
const hasProjectScopedTuleapCredentials =
Boolean(projectId) && tuleapCredentials?.project_id === projectId;
const usingGlobalTuleapFallback =
Boolean(projectId) && Boolean(tuleapCredentials) && tuleapCredentials?.project_id !== projectId;
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
if (!projectId) return; if (!projectId) return;
@ -59,18 +73,29 @@ export default function ProjectGraylog() {
setError(null); setError(null);
try { try {
const [agentList, creds, subjectList, detectionList] = await Promise.all([ const [agentList, tuleapCreds, creds, subjectList, detectionList] = await Promise.all([
listAgents(), listAgents(),
getTuleapCredentials(projectId),
getGraylogCredentials(projectId), getGraylogCredentials(projectId),
listGraylogSubjects(projectId), listGraylogSubjects(projectId),
listGraylogDetections(projectId), listGraylogDetections(projectId),
]); ]);
setAgents(agentList); setAgents(agentList);
setTuleapCredentialsState(tuleapCreds);
setCredentials(creds); setCredentials(creds);
setSubjects(subjectList); setSubjects(subjectList);
setDetections(detectionList); setDetections(detectionList);
if (tuleapCreds) {
setTuleapUrl(tuleapCreds.tuleap_url);
setTuleapUsername(tuleapCreds.username);
} else {
setTuleapUrl("");
setTuleapUsername("");
}
setTuleapPassword("");
if (creds) { if (creds) {
setBaseUrl(creds.base_url); setBaseUrl(creds.base_url);
setAnalystAgentId(creds.analyst_agent_id); setAnalystAgentId(creds.analyst_agent_id);
@ -92,6 +117,43 @@ export default function ProjectGraylog() {
void refresh(); void refresh();
}, [refresh]); }, [refresh]);
async function handleSaveTuleap(event: FormEvent) {
event.preventDefault();
if (!projectId) return;
setSavingTuleap(true);
setError(null);
setSuccess(null);
try {
const saved = await setTuleapCredentials(projectId, tuleapUrl, tuleapUsername, tuleapPassword);
setTuleapCredentialsState(saved);
setTuleapPassword("");
setSuccess("Configuration Tuleap sauvegardée.");
} catch (err: unknown) {
setError(getErrorMessage(err));
} finally {
setSavingTuleap(false);
}
}
async function handleTestTuleap() {
if (!projectId) return;
setTestingTuleap(true);
setError(null);
setSuccess(null);
try {
const message = await testTuleapConnection(projectId);
setSuccess(message);
} catch (err: unknown) {
setError(getErrorMessage(err));
} finally {
setTestingTuleap(false);
}
}
async function handleSave(event: FormEvent) { async function handleSave(event: FormEvent) {
event.preventDefault(); event.preventDefault();
if (!projectId) return; if (!projectId) return;
@ -162,7 +224,7 @@ export default function ProjectGraylog() {
Back Back
</Link> </Link>
)} )}
<h2 className={pageTitleClass}>Graylog</h2> <h2 className={pageTitleClass}>Intégrations</h2>
</div> </div>
{error && ( {error && (
@ -176,8 +238,69 @@ export default function ProjectGraylog() {
</div> </div>
)} )}
<form onSubmit={handleSaveTuleap} className={`space-y-3 ${cardContentClass}`}>
<h3 className="text-sm font-semibold text-gray-900">Configuration Tuleap</h3>
{usingGlobalTuleapFallback && (
<div className={noticeClass("warning")}>
Aucun credential Tuleap spécifique au projet n'est configuré. Le fallback global est utilisé.
</div>
)}
<div className="text-xs text-gray-500">
{hasProjectScopedTuleapCredentials
? "Credentials Tuleap projet actifs."
: "Credentials utilisés pour les trackers Tuleap du projet."}
</div>
<input
className={inputClass}
type="url"
value={tuleapUrl}
onChange={(event) => setTuleapUrl(event.target.value)}
placeholder="https://tuleap.example.com"
required
/>
<input
className={inputClass}
type="text"
value={tuleapUsername}
onChange={(event) => setTuleapUsername(event.target.value)}
placeholder="Username Tuleap"
required
/>
<input
className={inputClass}
type="password"
value={tuleapPassword}
onChange={(event) => setTuleapPassword(event.target.value)}
placeholder="Mot de passe Tuleap"
required
/>
<div className="flex gap-2">
<button
type="submit"
disabled={savingTuleap}
className={buttonClass({ variant: "primary" })}
>
{savingTuleap ? "Sauvegarde..." : "Sauvegarder"}
</button>
<button
type="button"
onClick={() => void handleTestTuleap()}
disabled={testingTuleap || !tuleapCredentials}
className={buttonClass({ variant: "secondary" })}
>
{testingTuleap ? "Test..." : "Tester la connexion"}
</button>
</div>
</form>
<form onSubmit={handleSave} className={`space-y-3 ${cardContentClass}`}> <form onSubmit={handleSave} className={`space-y-3 ${cardContentClass}`}>
<h3 className="text-sm font-semibold text-gray-900">Configuration</h3> <h3 className="text-sm font-semibold text-gray-900">Configuration Graylog</h3>
<input <input
className={inputClass} className={inputClass}

View file

@ -82,7 +82,7 @@ export default function ProjectModules() {
to={`/projects/${projectId}/graylog`} to={`/projects/${projectId}/graylog`}
className={`mt-2 ${buttonClass({ variant: "secondary", size: "xs" })}`} className={`mt-2 ${buttonClass({ variant: "secondary", size: "xs" })}`}
> >
Configurer Configurer les intégrations
</Link> </Link>
)} )}
</div> </div>