import { useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getTarget, getJobs, updateTarget } 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_bigip: 'F5 BIG-IP', iis: 'IIS', }; function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value}
); } export default function TargetDetailPage() { const { id } = useParams<{ id: string }>(); const queryClient = useQueryClient(); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(''); const [editHostname, setEditHostname] = useState(''); const updateMutation = useMutation({ mutationFn: (data: Partial<{ name: string; hostname: string }>) => updateTarget(id!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['target', id] }); setIsEditing(false); }, }); 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 ( <> { setEditName(target.name); setEditHostname(target.hostname || ''); setIsEditing(true); }} className="px-3 py-1.5 border border-surface-border rounded text-ink text-xs hover:bg-surface-hover transition-colors font-medium" > Edit } />
{/* Target info */}

Target Information

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

Configuration

{target.config && Object.keys(target.config).length > 0 ? (
{Object.entries(target.config).map(([key, val]) => { const sensitiveKeys = ['password', 'secret', 'token', 'key', 'winrm_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, hostname: editHostname }); }} 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" />
setEditHostname(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" />
)} ); }