Extract types, constants, helpers, and UI pieces (shared LoadingSkeleton/ EmptyState/StatusBadge/CopyButton, SSOConfigFormModal, DeleteConfirmModal, ConnectionTestPanel, SSOConfigCard, SSOUsersTable, SSOInfoSection) into _components/ and _types.ts to bring page.tsx from 1482 LOC to 339 LOC (under the 500 hard cap). Behavior preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
CheckCircle2,
|
|
ExternalLink,
|
|
Loader2,
|
|
RefreshCw,
|
|
TestTube,
|
|
XCircle,
|
|
} from 'lucide-react'
|
|
import type { ConnectionTestResult, SSOConfig } from '../_types'
|
|
import { API_BASE } from './constants'
|
|
import { apiFetch, getTenantId } from './helpers'
|
|
|
|
export function ConnectionTestPanel({ config }: { config: SSOConfig }) {
|
|
const [testing, setTesting] = useState(false)
|
|
const [result, setResult] = useState<ConnectionTestResult | null>(null)
|
|
|
|
const runTest = async () => {
|
|
setTesting(true)
|
|
setResult(null)
|
|
try {
|
|
const data = await apiFetch<ConnectionTestResult>(`/configs/${config.id}/test`, {
|
|
method: 'POST',
|
|
})
|
|
setResult(data)
|
|
} catch (err: unknown) {
|
|
setResult({
|
|
success: false,
|
|
message: err instanceof Error ? err.message : 'Verbindungstest fehlgeschlagen',
|
|
})
|
|
} finally {
|
|
setTesting(false)
|
|
}
|
|
}
|
|
|
|
const openLoginFlow = () => {
|
|
const loginUrl = `${API_BASE}/configs/${config.id}/login?tenant_id=${getTenantId()}`
|
|
window.open(loginUrl, '_blank', 'width=600,height=700')
|
|
}
|
|
|
|
return (
|
|
<div className="border border-slate-200 rounded-lg p-4 bg-slate-50">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h4 className="text-sm font-medium text-slate-700 flex items-center gap-2">
|
|
<TestTube className="w-4 h-4" />
|
|
Verbindungstest
|
|
</h4>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={runTest}
|
|
disabled={testing}
|
|
className="px-3 py-1.5 text-xs text-purple-600 border border-purple-300 rounded-lg hover:bg-purple-50 transition-colors flex items-center gap-1 disabled:opacity-50"
|
|
>
|
|
{testing ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
|
Discovery testen
|
|
</button>
|
|
<button
|
|
onClick={openLoginFlow}
|
|
className="px-3 py-1.5 text-xs text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-1"
|
|
>
|
|
<ExternalLink className="w-3 h-3" />
|
|
Login testen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{result && (
|
|
<div className={`p-3 rounded-lg text-sm ${
|
|
result.success
|
|
? 'bg-green-50 border border-green-200 text-green-700'
|
|
: 'bg-red-50 border border-red-200 text-red-700'
|
|
}`}>
|
|
<div className="flex items-center gap-2 font-medium mb-1">
|
|
{result.success ? (
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
) : (
|
|
<XCircle className="w-4 h-4" />
|
|
)}
|
|
{result.message}
|
|
</div>
|
|
{result.details && (
|
|
<div className="mt-2 space-y-1 text-xs">
|
|
<div className="flex items-center gap-2">
|
|
{result.details.issuer_reachable ? (
|
|
<CheckCircle2 className="w-3 h-3 text-green-500" />
|
|
) : (
|
|
<XCircle className="w-3 h-3 text-red-500" />
|
|
)}
|
|
Issuer erreichbar
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{result.details.jwks_available ? (
|
|
<CheckCircle2 className="w-3 h-3 text-green-500" />
|
|
) : (
|
|
<XCircle className="w-3 h-3 text-red-500" />
|
|
)}
|
|
JWKS verfuegbar
|
|
</div>
|
|
{result.details.authorization_endpoint && (
|
|
<div className="flex items-center gap-2 text-slate-500 font-mono truncate">
|
|
Authorization: {result.details.authorization_endpoint}
|
|
</div>
|
|
)}
|
|
{result.details.token_endpoint && (
|
|
<div className="flex items-center gap-2 text-slate-500 font-mono truncate">
|
|
Token: {result.details.token_endpoint}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|