refactor(settings): move Jira connection test from admin Jira tab to Profile tab
Aegis CI / lint-and-test (push) Waiting to run
Snyk Security Scan / Python vulnerabilities (backend) (push) Waiting to run
Snyk Security Scan / npm vulnerabilities (frontend) (push) Waiting to run
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Waiting to run

This commit is contained in:
kitos
2026-06-18 15:27:19 +02:00
parent 986682aad1
commit 263823f290
+51 -57
View File
@@ -845,6 +845,8 @@ function ProfileSection() {
const [showTempoToken, setShowTempoToken] = useState(false); const [showTempoToken, setShowTempoToken] = useState(false);
const [tempoTestResult, setTempoTestResult] = useState<string | null>(null); const [tempoTestResult, setTempoTestResult] = useState<string | null>(null);
const [tempoTestError, setTempoTestError] = useState<string | null>(null); const [tempoTestError, setTempoTestError] = useState<string | null>(null);
const [jiraTestResult, setJiraTestResult] = useState<{ connectedAs: string; url: string } | null>(null);
const [jiraTestError, setJiraTestError] = useState<string | null>(null);
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
// Initialise editable fields from server on first successful load // Initialise editable fields from server on first successful load
@@ -871,6 +873,23 @@ function ProfileSection() {
onError: () => setToast({ msg: "Failed to save", type: "error" }), onError: () => setToast({ msg: "Failed to save", type: "error" }),
}); });
const jiraTestMut = useMutation({
mutationFn: testJiraConnection,
onSuccess: (data) => {
if (data.status === "ok") {
setJiraTestResult({ connectedAs: data.connected_as ?? "", url: data.jira_url ?? "" });
setJiraTestError(null);
} else {
setJiraTestError((data as { message?: string }).message ?? "Connection failed");
setJiraTestResult(null);
}
},
onError: (err: Error) => {
setJiraTestError(err.message || "Connection failed");
setJiraTestResult(null);
},
});
const tempoTestMut = useMutation({ const tempoTestMut = useMutation({
mutationFn: testTempoConnection, mutationFn: testTempoConnection,
onSuccess: (data) => { onSuccess: (data) => {
@@ -1041,6 +1060,38 @@ function ProfileSection() {
</p> </p>
</div> </div>
</div> </div>
{/* Test Jira connection */}
<div className="mt-4 rounded-lg border border-gray-700 bg-gray-800/50 p-3 space-y-2">
<button
onClick={() => {
setJiraTestResult(null);
setJiraTestError(null);
jiraTestMut.mutate();
}}
disabled={jiraTestMut.isPending || !me?.jira_token_set}
className="flex items-center gap-2 rounded-lg border border-cyan-700 px-3 py-1.5 text-sm font-medium text-cyan-400 hover:bg-cyan-900/30 disabled:opacity-50 transition-colors"
>
{jiraTestMut.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube className="h-4 w-4" />
)}
Test Jira Connection
</button>
{jiraTestResult && (
<div className="flex items-center gap-2 rounded-md bg-emerald-900/30 border border-emerald-800/50 px-3 py-2 text-sm text-emerald-300">
<CheckCircle className="h-4 w-4 shrink-0" />
Connected as: {jiraTestResult.connectedAs}
</div>
)}
{jiraTestError && (
<div className="flex items-center gap-2 rounded-md bg-red-900/30 border border-red-800/50 px-3 py-2 text-sm text-red-300">
<XCircle className="h-4 w-4 shrink-0" />
{jiraTestError}
</div>
)}
</div>
</div>} </div>}
{/* ── Tempo Integration ─────────────────────────────────── */} {/* ── Tempo Integration ─────────────────────────────────── */}
@@ -1153,8 +1204,6 @@ function ProfileSection() {
function JiraConfigSection() { function JiraConfigSection() {
const qc = useQueryClient(); const qc = useQueryClient();
const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null); const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null);
const [testResult, setTestResult] = useState<{ connectedAs: string; url: string } | null>(null);
const [testError, setTestError] = useState<string | null>(null);
const { data: cfg, isLoading } = useQuery({ const { data: cfg, isLoading } = useQuery({
queryKey: ["jira-config"], queryKey: ["jira-config"],
@@ -1174,24 +1223,6 @@ function JiraConfigSection() {
onError: () => setToast({ msg: "Failed to save Jira configuration", type: "error" }), onError: () => setToast({ msg: "Failed to save Jira configuration", type: "error" }),
}); });
const testMut = useMutation({
mutationFn: testJiraConnection,
onSuccess: (data) => {
// Backend always returns HTTP 200; status field tells us if it worked
if (data.status === "ok") {
setTestResult({ connectedAs: data.connected_as ?? "", url: data.jira_url ?? "" });
setTestError(null);
} else {
setTestError((data as { message?: string }).message ?? "Connection failed");
setTestResult(null);
}
},
onError: (err: Error) => {
setTestError(err.message || "Connection failed");
setTestResult(null);
},
});
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
@@ -1284,43 +1315,6 @@ function JiraConfigSection() {
</button> </button>
</div> </div>
{/* Test Jira connection */}
<div className="mt-4 rounded-lg border border-gray-700 bg-gray-800/50 p-4 space-y-3">
<p className="text-sm font-medium text-gray-300">Test Jira Connection</p>
<div className="rounded-md bg-blue-900/20 border border-blue-800/50 px-3 py-2 text-xs text-blue-300">
Uses your personal Jira API token (configured in the Profile tab)
</div>
<button
onClick={() => {
setTestResult(null);
setTestError(null);
testMut.mutate();
}}
disabled={testMut.isPending}
className="flex items-center gap-2 rounded-lg border border-cyan-700 px-4 py-2 text-sm font-medium text-cyan-400 hover:bg-cyan-900/30 disabled:opacity-50 transition-colors"
>
{testMut.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube className="h-4 w-4" />
)}
Test Jira Connection
</button>
{testResult && (
<div className="flex items-center gap-2 rounded-md bg-emerald-900/30 border border-emerald-800/50 px-3 py-2 text-sm text-emerald-300">
<CheckCircle className="h-4 w-4 shrink-0" />
Connected as: {testResult.connectedAs}
</div>
)}
{testError && (
<div className="flex items-center gap-2 rounded-md bg-red-900/30 border border-red-800/50 px-3 py-2 text-sm text-red-300">
<XCircle className="h-4 w-4 shrink-0" />
{testError}
</div>
)}
</div>
</div> </div>
</> </>
); );