import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getTargets, createTarget, deleteTarget } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import StatusBadge from '../components/StatusBadge'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { Target } from '../api/types'; const typeLabels: Record = { nginx: 'NGINX', f5_bigip: 'F5 BIG-IP', iis: 'IIS', apache: 'Apache', haproxy: 'HAProxy', }; const TARGET_TYPES = [ { value: 'nginx', label: 'NGINX', description: 'Deploy to NGINX web server via file write + config validation + reload' }, { value: 'apache', label: 'Apache httpd', description: 'Separate cert/chain/key files, apachectl configtest, graceful reload' }, { value: 'haproxy', label: 'HAProxy', description: 'Combined PEM file (cert+chain+key), optional validate, reload' }, { value: 'f5_bigip', label: 'F5 BIG-IP', description: 'iControl REST via proxy agent (V3 implementation)' }, { value: 'iis', label: 'IIS', description: 'Windows IIS via agent-local PowerShell or proxy WinRM (V3 implementation)' }, ]; const CONFIG_FIELDS: Record = { nginx: [ { key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/nginx/ssl/cert.pem', required: true }, { key: 'key_path', label: 'Key Path', placeholder: '/etc/nginx/ssl/key.pem', required: true }, { key: 'chain_path', label: 'Chain Path', placeholder: '/etc/nginx/ssl/chain.pem' }, { key: 'reload_cmd', label: 'Reload Command', placeholder: 'nginx -t && systemctl reload nginx' }, ], apache: [ { key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/apache2/ssl/cert.pem', required: true }, { key: 'key_path', label: 'Key Path', placeholder: '/etc/apache2/ssl/key.pem', required: true }, { key: 'chain_path', label: 'Chain Path', placeholder: '/etc/apache2/ssl/chain.pem' }, { key: 'reload_cmd', label: 'Reload Command', placeholder: 'apachectl configtest && apachectl graceful' }, ], haproxy: [ { key: 'pem_path', label: 'Combined PEM Path', placeholder: '/etc/haproxy/certs/combined.pem', required: true }, { key: 'reload_cmd', label: 'Reload Command', placeholder: 'systemctl reload haproxy' }, { key: 'validate_cmd', label: 'Validate Command (optional)', placeholder: 'haproxy -c -f /etc/haproxy/haproxy.cfg' }, ], f5_bigip: [ { key: 'management_ip', label: 'Management IP', placeholder: '192.168.1.100', required: true }, { key: 'partition', label: 'Partition', placeholder: 'Common' }, { key: 'proxy_agent_id', label: 'Proxy Agent ID', placeholder: 'agent-f5-proxy' }, ], iis: [ { key: 'site_name', label: 'IIS Site Name', placeholder: 'Default Web Site', required: true }, { key: 'binding_ip', label: 'Binding IP', placeholder: '*' }, { key: 'binding_port', label: 'Binding Port', placeholder: '443' }, { key: 'cert_store', label: 'Certificate Store', placeholder: 'My' }, ], }; function CreateTargetWizard({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { const [step, setStep] = useState<'type' | 'config' | 'review'>('type'); const [targetType, setTargetType] = useState(''); const [name, setName] = useState(''); const [hostname, setHostname] = useState(''); const [agentId, setAgentId] = useState(''); const [config, setConfig] = useState>({}); const [error, setError] = useState(''); const mutation = useMutation({ mutationFn: () => createTarget({ name, type: targetType, hostname, agent_id: agentId, config: Object.fromEntries(Object.entries(config).filter(([, v]) => v)), }), onSuccess: () => onSuccess(), onError: (err: Error) => setError(err.message), }); const fields = CONFIG_FIELDS[targetType] || []; const canProceedToReview = name && targetType && fields.filter(f => f.required).every(f => config[f.key]); return (
e.stopPropagation()}> {/* Step indicators */}
{['Select Type', 'Configure', 'Review'].map((label, i) => { const stepNames = ['type', 'config', 'review'] as const; const currentIdx = stepNames.indexOf(step); const isActive = i === currentIdx; const isDone = i < currentIdx; return (
{isDone ? '✓' : i + 1}
{label} {i < 2 &&
}
); })}
{error &&
{error}
} {/* Step 1: Select Type */} {step === 'type' && (

Select Target Type

{TARGET_TYPES.map(t => ( ))}
)} {/* Step 2: Configure */} {step === 'config' && (

Configure {typeLabels[targetType] || targetType} Target

setName(e.target.value)} className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500" placeholder="web-server-1" />
setHostname(e.target.value)} className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500" placeholder="web1.example.com" />
setAgentId(e.target.value)} className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500" placeholder="agent-web1" />
{fields.map(f => (
setConfig(c => ({ ...c, [f.key]: e.target.value }))} className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500" placeholder={f.placeholder} />
))}
)} {/* Step 3: Review */} {step === 'review' && (

Review Target

Name {name}
Type {typeLabels[targetType] || targetType}
{hostname && (
Hostname {hostname}
)} {agentId && (
Agent {agentId}
)} {Object.entries(config).filter(([, v]) => v).map(([k, v]) => (
{k.replace(/_/g, ' ')} {v}
))}
)}
); } export default function TargetsPage() { const queryClient = useQueryClient(); const [showCreate, setShowCreate] = useState(false); const { data, isLoading, error, refetch } = useQuery({ queryKey: ['targets'], queryFn: () => getTargets(), }); const deleteMutation = useMutation({ mutationFn: deleteTarget, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['targets'] }), }); const columns: Column[] = [ { key: 'name', label: 'Target', render: (t) => (
{t.name}
{t.id}
), }, { key: 'type', label: 'Type', render: (t) => ( {typeLabels[t.type] || t.type} ), }, { key: 'hostname', label: 'Hostname', render: (t) => {t.hostname || '\u2014'}, }, { key: 'agent', label: 'Agent', render: (t) => {t.agent_id || '\u2014'}, }, { key: 'status', label: 'Status', render: (t) => , }, { key: 'created', label: 'Created', render: (t) => {formatDateTime(t.created_at)}, }, { key: 'actions', label: '', render: (t) => ( ), }, ]; return ( <> setShowCreate(true)} className="btn btn-primary text-xs"> + New Target } />
{error ? ( refetch()} /> ) : ( )}
{showCreate && ( setShowCreate(false)} onSuccess={() => { setShowCreate(false); queryClient.invalidateQueries({ queryKey: ['targets'] }); }} /> )} ); }