import * as React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import type { Credential, CredentialWithSecret } from '@sentryagent/idp-sdk'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/ui/dialog'; import { getClient } from '@/lib/client'; /** Truncates a string to a maximum length with ellipsis. */ function truncate(value: string, maxLen = 16): string { return value.length > maxLen ? `${value.slice(0, maxLen)}…` : value; } /** Formats an ISO timestamp to a short local date string. */ function formatDate(iso: string): string { return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } interface NewSecretBoxProps { secret: string; onDismiss: () => void; } /** * Displays a newly issued client secret exactly once. * Provides a copy button and a dismiss button. */ function NewSecretBox({ secret, onDismiss }: NewSecretBoxProps): React.JSX.Element { const [copied, setCopied] = React.useState(false); const handleCopy = React.useCallback(async (): Promise => { await navigator.clipboard.writeText(secret); setCopied(true); setTimeout(() => { setCopied(false); }, 2000); }, [secret]); return (

New client secret — copy it now. It will not be shown again.

{secret}
); } type DialogAction = { type: 'rotate'; credentialId: string } | { type: 'revoke'; credentialId: string }; /** * Credentials page — lists all credentials for an agent with rotate/revoke actions. * Route: /dashboard/agents/:agentId/credentials */ export default function Credentials(): React.JSX.Element { const { agentId } = useParams<{ agentId: string }>(); const navigate = useNavigate(); const [credentials, setCredentials] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [actionLoading, setActionLoading] = React.useState(false); const [dialog, setDialog] = React.useState(null); const [newSecret, setNewSecret] = React.useState(null); const fetchCredentials = React.useCallback(async (): Promise => { if (!agentId) return; setLoading(true); setError(null); try { const result = await getClient().credentials.listCredentials(agentId); setCredentials(result.data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load credentials.'); } finally { setLoading(false); } }, [agentId]); React.useEffect(() => { void fetchCredentials(); }, [fetchCredentials]); const handleGenerate = React.useCallback(async (): Promise => { if (!agentId) return; setActionLoading(true); setError(null); try { const result = await getClient().credentials.generateCredential(agentId, {}); setNewSecret(result); await fetchCredentials(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate credential.'); } finally { setActionLoading(false); } }, [agentId, fetchCredentials]); const handleConfirm = React.useCallback(async (): Promise => { if (!dialog || !agentId) return; setActionLoading(true); setDialog(null); setError(null); try { if (dialog.type === 'rotate') { const result = await getClient().credentials.rotateCredential(agentId, dialog.credentialId); setNewSecret(result); } else { await getClient().credentials.revokeCredential(agentId, dialog.credentialId); } await fetchCredentials(); } catch (err) { setError(err instanceof Error ? err.message : `Failed to ${dialog.type} credential.`); } finally { setActionLoading(false); } }, [dialog, agentId, fetchCredentials]); const dialogConfig = React.useMemo(() => { if (!dialog) return null; if (dialog.type === 'rotate') { return { title: 'Rotate credential?', description: 'The existing secret will be invalidated immediately. You will receive a new secret — store it securely.', confirmLabel: 'Rotate', variant: 'destructive' as const, }; } return { title: 'Revoke credential?', description: 'This will permanently revoke the credential. This cannot be undone.', confirmLabel: 'Revoke', variant: 'destructive' as const, }; }, [dialog]); return (
{/* Back navigation */}

Credentials

{error && (
{error}
)} {/* New secret display — shown once */} {newSecret !== null && ( { setNewSecret(null); }} /> )} {/* Credentials table */}
{['Credential ID', 'Status', 'Created', 'Actions'].map((col) => ( ))} {loading ? ( Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 4 }).map((__, j) => ( ))} )) ) : credentials.length === 0 ? ( ) : credentials.map((cred) => ( ))}
{col}
No credentials found. Generate one above.
{truncate(cred.credentialId, 24)} {cred.status} {formatDate(cred.createdAt)} {cred.status === 'active' && (
)}
{/* Confirm dialog */} {dialog !== null && dialogConfig !== null && ( { void handleConfirm(); }} onCancel={() => { setDialog(null); }} /> )}
); }