import { useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getTarget, getJobs, updateTarget, testTargetConnection } from '../api/client'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { Job } from '../api/types'; const typeLabels: Record = { NGINX: 'NGINX', Apache: 'Apache', HAProxy: 'HAProxy', Traefik: 'Traefik', Caddy: 'Caddy', F5: 'F5 BIG-IP', IIS: 'IIS', Envoy: 'Envoy', Postfix: 'Postfix', Dovecot: 'Dovecot', SSH: 'SSH', WinCertStore: 'Windows Cert Store', JavaKeystore: 'Java Keystore', KubernetesSecrets: 'Kubernetes Secrets', }; function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value}
); } function TestStatusIndicator({ status, testedAt }: { status?: string; testedAt?: string }) { if (!status || status === 'untested') { return Not tested; } const styles: Record = { success: 'bg-emerald-100 text-emerald-700', failed: 'bg-red-100 text-red-700', }; const labels: Record = { success: 'Connected', failed: 'Failed', }; return ( {labels[status] || status} {testedAt && {formatDateTime(testedAt)}} ); } function SourceBadge({ source }: { source?: string }) { if (!source || source === 'database') { return GUI; } if (source === 'env') { return Env Var; } return {source}; } export default function TargetDetailPage() { const { id } = useParams<{ id: string }>(); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(''); const updateMutation = useTrackedMutation({ mutationFn: (data: Partial<{ name: string }>) => updateTarget(id!, data), invalidates: [['target', id]], onSuccess: () => { setIsEditing(false); }, }); const testMutation = useTrackedMutation({ mutationFn: () => testTargetConnection(id!), invalidates: [['target', id]], }); const { data: target, isLoading, error, refetch } = useQuery({ queryKey: ['target', id], queryFn: () => getTarget(id!), enabled: !!id, }); // Deployment jobs for this target const { data: jobsData } = useQuery({ queryKey: ['jobs', { target_id: id, type: 'Deployment' }], queryFn: () => getJobs({ target_id: id! }), enabled: !!id, }); if (error) { return ( <> refetch()} /> ); } if (isLoading || !target) { return ( <>
Loading target...
); } const jobColumns: Column[] = [ { key: 'id', label: 'Job', render: (j) => ( {j.id} ), }, { key: 'status', label: 'Status', render: (j) => }, { key: 'cert', label: 'Certificate', render: (j) => ( {j.certificate_id} )}, { key: 'completed', label: 'Completed', render: (j) => {formatDateTime(j.completed_at)} }, { key: 'verification', label: 'Verification', render: (j) => { if (!j.verification_status) return ; const styles: Record = { success: 'bg-emerald-100 text-emerald-700', failed: 'bg-red-100 text-red-700', pending: 'bg-yellow-100 text-yellow-700', skipped: 'bg-gray-100 text-gray-600', }; const labels: Record = { success: 'Verified', failed: 'Failed', pending: 'Pending', skipped: 'Skipped', }; return ( {labels[j.verification_status] || j.verification_status} ); }, }, ]; return ( <> } /> {/* Test connection result banner */} {testMutation.isSuccess && (
Agent connection test passed — agent is online and responsive.
)} {testMutation.isError && (
Connection test failed: {(testMutation.error as Error).message}
)}
{/* Target info */}

Target Information

{target.id}} /> } /> } /> } /> {target.agent_id && ( {target.agent_id} } /> )} {target.updated_at && }
{/* Config */}

Configuration

{target.config && Object.keys(target.config).length > 0 ? (
{Object.entries(target.config).map(([key, val]) => { const sensitiveKeys = ['password', 'secret', 'token', 'key', 'passphrase', 'winrm_password', 'keystore_password']; const isSensitive = sensitiveKeys.some(s => key.toLowerCase().includes(s)); const displayVal = isSensitive && val ? '********' : String(val); return ( {displayVal} } /> ); })}
) : (
No configuration data
)}
{/* Deployment history */}

Deployment History {jobsData ? `(${jobsData.total})` : ''}

{/* Edit Modal */} {isEditing && (
setIsEditing(false)}>
e.stopPropagation()}>

Edit Target

{updateMutation.isError && (
{(updateMutation.error as Error).message}
)}
{ e.preventDefault(); updateMutation.mutate({ name: editName }); }} className="space-y-4">
setEditName(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" />
)} ); }