orchai/src/components/projects/ProjectGraylog.tsx

482 lines
15 KiB
TypeScript

import { FormEvent, useCallback, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import {
getTuleapCredentials,
getGraylogCredentials,
listAgents,
listGraylogDetections,
listGraylogSubjects,
manualGraylogPoll,
setTuleapCredentials,
setGraylogCredentials,
testTuleapConnection,
testGraylogConnection,
} from "../../lib/api";
import { getErrorMessage } from "../../lib/errors";
import type {
Agent,
GraylogCredentialsSafe,
GraylogDetection,
GraylogSubject,
TuleapCredentialsSafe,
} from "../../lib/types";
import {
backLinkClass,
buttonClass,
cardContentClass,
inputClass,
noticeClass,
pageStackClass,
pageTitleClass,
} from "../ui/primitives";
export default function ProjectGraylog() {
const { projectId } = useParams<{ projectId: string }>();
const [agents, setAgents] = useState<Agent[]>([]);
const [tuleapCredentials, setTuleapCredentialsState] = useState<TuleapCredentialsSafe | null>(null);
const [credentials, setCredentials] = useState<GraylogCredentialsSafe | null>(null);
const [subjects, setSubjects] = useState<GraylogSubject[]>([]);
const [detections, setDetections] = useState<GraylogDetection[]>([]);
const [tuleapUrl, setTuleapUrl] = useState("");
const [tuleapUsername, setTuleapUsername] = useState("");
const [tuleapPassword, setTuleapPassword] = useState("");
const [baseUrl, setBaseUrl] = useState("");
const [apiToken, setApiToken] = useState("");
const [analystAgentId, setAnalystAgentId] = useState("");
const [developerAgentId, setDeveloperAgentId] = useState("");
const [reviewerAgentId, setReviewerAgentId] = useState("");
const [streamId, setStreamId] = useState("");
const [queryFilter, setQueryFilter] = useState("level:(critical OR error OR warning)");
const [pollingIntervalMinutes, setPollingIntervalMinutes] = useState(10);
const [lookbackMinutes, setLookbackMinutes] = useState(30);
const [scoreThreshold, setScoreThreshold] = useState(70);
const [loading, setLoading] = useState(false);
const [savingTuleap, setSavingTuleap] = useState(false);
const [testingTuleap, setTestingTuleap] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const analysts = agents.filter((agent) => agent.role === "analyst");
const developers = agents.filter((agent) => agent.role === "developer");
const reviewers = agents.filter((agent) => agent.role === "reviewer");
const hasProjectScopedTuleapCredentials =
Boolean(projectId) && tuleapCredentials?.project_id === projectId;
const usingGlobalTuleapFallback =
Boolean(projectId) && Boolean(tuleapCredentials) && tuleapCredentials?.project_id !== projectId;
const refresh = useCallback(async () => {
if (!projectId) return;
setLoading(true);
setError(null);
try {
const [agentList, tuleapCreds, creds, subjectList, detectionList] = await Promise.all([
listAgents(),
getTuleapCredentials(projectId),
getGraylogCredentials(projectId),
listGraylogSubjects(projectId),
listGraylogDetections(projectId),
]);
setAgents(agentList);
setTuleapCredentialsState(tuleapCreds);
setCredentials(creds);
setSubjects(subjectList);
setDetections(detectionList);
if (tuleapCreds) {
setTuleapUrl(tuleapCreds.tuleap_url);
setTuleapUsername(tuleapCreds.username);
} else {
setTuleapUrl("");
setTuleapUsername("");
}
setTuleapPassword("");
if (creds) {
setBaseUrl(creds.base_url);
setAnalystAgentId(creds.analyst_agent_id);
setDeveloperAgentId(creds.developer_agent_id);
setReviewerAgentId(creds.reviewer_agent_id);
setStreamId(creds.stream_id ?? "");
setQueryFilter(creds.query_filter);
setPollingIntervalMinutes(creds.polling_interval_minutes);
setLookbackMinutes(creds.lookback_minutes);
setScoreThreshold(creds.score_threshold);
}
} catch (err: unknown) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
void 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) {
event.preventDefault();
if (!projectId) return;
setSaving(true);
setError(null);
setSuccess(null);
try {
const saved = await setGraylogCredentials(
projectId,
baseUrl,
apiToken,
analystAgentId,
developerAgentId,
reviewerAgentId,
streamId.trim() || null,
queryFilter,
pollingIntervalMinutes,
lookbackMinutes,
scoreThreshold
);
setCredentials(saved);
setApiToken("");
setSuccess("Configuration Graylog sauvegardée.");
await refresh();
} catch (err: unknown) {
setError(getErrorMessage(err));
} finally {
setSaving(false);
}
}
async function handleTestConnection() {
if (!projectId) return;
setError(null);
setSuccess(null);
try {
const message = await testGraylogConnection(projectId);
setSuccess(message);
} catch (err: unknown) {
setError(getErrorMessage(err));
}
}
async function handleManualPoll() {
if (!projectId) return;
setError(null);
setSuccess(null);
try {
const triggeredCount = await manualGraylogPoll(projectId);
setSuccess(`Polling Graylog manuel terminé: ${triggeredCount} sujet(s) déclenché(s).`);
await refresh();
} catch (err: unknown) {
setError(getErrorMessage(err));
}
}
return (
<div className={pageStackClass}>
<div>
{projectId && (
<Link to={`/projects/${projectId}`} className={backLinkClass}>
Back
</Link>
)}
<h2 className={pageTitleClass}>Intégrations</h2>
</div>
{error && (
<div className={noticeClass("error")}>
{error}
</div>
)}
{success && (
<div className={noticeClass("success")}>
{success}
</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}`}>
<h3 className="text-sm font-semibold text-gray-900">Configuration Graylog</h3>
<input
className={inputClass}
value={baseUrl}
onChange={(event) => setBaseUrl(event.target.value)}
placeholder="https://graylog.example.com"
required
/>
<input
className={inputClass}
value={apiToken}
onChange={(event) => setApiToken(event.target.value)}
placeholder={
credentials
? "Laisser vide pour conserver le token actuel"
: "Token API Graylog"
}
required={!credentials}
/>
<div className="grid gap-3 md:grid-cols-3">
<select
className={inputClass}
value={analystAgentId}
onChange={(event) => setAnalystAgentId(event.target.value)}
required
>
<option value="">Analyst agent</option>
{analysts.map((agent) => (
<option key={agent.id} value={agent.id}>
{agent.name}
</option>
))}
</select>
<select
className={inputClass}
value={developerAgentId}
onChange={(event) => setDeveloperAgentId(event.target.value)}
required
>
<option value="">Developer agent</option>
{developers.map((agent) => (
<option key={agent.id} value={agent.id}>
{agent.name}
</option>
))}
</select>
<select
className={inputClass}
value={reviewerAgentId}
onChange={(event) => setReviewerAgentId(event.target.value)}
required
>
<option value="">Reviewer agent</option>
{reviewers.map((agent) => (
<option key={agent.id} value={agent.id}>
{agent.name}
</option>
))}
</select>
</div>
<div className="grid gap-3 md:grid-cols-4">
<input
className={inputClass}
value={streamId}
onChange={(event) => setStreamId(event.target.value)}
placeholder="stream_id (optionnel)"
/>
<input
className={`${inputClass} md:col-span-3`}
value={queryFilter}
onChange={(event) => setQueryFilter(event.target.value)}
placeholder="Filtre query Graylog"
/>
</div>
<div className="grid gap-3 md:grid-cols-3">
<input
type="number"
className={inputClass}
value={pollingIntervalMinutes}
onChange={(event) => setPollingIntervalMinutes(Number(event.target.value))}
min={1}
/>
<input
type="number"
className={inputClass}
value={lookbackMinutes}
onChange={(event) => setLookbackMinutes(Number(event.target.value))}
min={1}
/>
<input
type="number"
className={inputClass}
value={scoreThreshold}
onChange={(event) => setScoreThreshold(Number(event.target.value))}
min={1}
max={100}
/>
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={saving}
className={buttonClass({ variant: "primary" })}
>
{saving ? "Sauvegarde..." : "Sauvegarder"}
</button>
<button
type="button"
onClick={() => void handleTestConnection()}
className={buttonClass({ variant: "secondary" })}
>
Tester la connexion
</button>
<button
type="button"
onClick={() => void handleManualPoll()}
className={buttonClass({ variant: "neutralDark" })}
>
Poll manuel
</button>
</div>
</form>
<div className={cardContentClass}>
<h3 className="mb-3 text-sm font-semibold text-gray-900">Sujets détectés</h3>
{loading ? (
<div className="text-sm text-gray-500">Chargement...</div>
) : subjects.length === 0 ? (
<div className="text-sm text-gray-400">Aucun sujet détecté.</div>
) : (
<div className="space-y-2">
{subjects.map((subject) => (
<div key={subject.id} className="rounded border border-gray-100 p-3">
<div className="text-sm font-medium text-gray-900">{subject.source}</div>
<div className="text-xs text-gray-600">{subject.normalized_message}</div>
<div className="mt-1 text-xs text-gray-500">
Score: {subject.last_score} | Statut: {subject.status} | Last seen: {" "}
{new Date(subject.last_seen_at).toLocaleString()}
</div>
</div>
))}
</div>
)}
</div>
<div className={cardContentClass}>
<h3 className="mb-3 text-sm font-semibold text-gray-900">Dernières détections</h3>
{detections.length === 0 ? (
<div className="text-sm text-gray-400">Aucune détection enregistrée.</div>
) : (
<div className="space-y-2">
{detections.slice(0, 20).map((detection) => (
<div
key={detection.id}
className="rounded border border-gray-100 p-3 text-xs text-gray-700"
>
score={detection.score} total={detection.total_count} triggered={" "}
{String(detection.triggered)} at={" "}
{new Date(detection.created_at).toLocaleString()}
</div>
))}
</div>
)}
</div>
</div>
);
}