/** * BulkDeployDialog -- Multi-device certificate deployment dialog. * * Shows a checkbox list of devices without deployed certs, with Select All / Deselect All. * On deploy, calls bulkDeploy API and shows progress + results summary. */ import { useState } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Layers, Loader2, CheckCircle, XCircle, Check, } from 'lucide-react' import { certificatesApi } from '@/lib/certificatesApi' import { devicesApi, type DeviceResponse } from '@/lib/api' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { toast } from '@/components/ui/toast' import { cn } from '@/lib/utils' type BulkStep = 'select' | 'deploying' | 'results' interface BulkResult { success: number failed: number errors: Array<{ device_id: string; error: string }> } interface BulkDeployDialogProps { open: boolean onClose: () => void tenantId: string } export function BulkDeployDialog({ open, onClose, tenantId, }: BulkDeployDialogProps) { const queryClient = useQueryClient() const [selected, setSelected] = useState>(new Set()) const [step, setStep] = useState('select') const [result, setResult] = useState(null) // Fetch devices const { data: deviceList = [] } = useQuery({ queryKey: ['devices-for-cert', tenantId], queryFn: async () => { const result = await devicesApi.list(tenantId) return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[]) }, enabled: !!tenantId && open, }) // Fetch existing device certs to filter const { data: existingCerts = [] } = useQuery({ queryKey: ['deviceCerts', tenantId], queryFn: () => certificatesApi.getDeviceCerts(undefined, tenantId), enabled: !!tenantId && open, }) const deployedDeviceIds = new Set( existingCerts .filter((c) => c.status === 'deployed' || c.status === 'deploying') .map((c) => c.device_id), ) const availableDevices = (deviceList as DeviceResponse[]).filter( (d) => !deployedDeviceIds.has(d.id), ) const toggleDevice = (id: string) => { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) } const selectAll = () => { setSelected(new Set(availableDevices.map((d) => d.id))) } const deselectAll = () => { setSelected(new Set()) } const handleDeploy = async () => { if (selected.size === 0) return setStep('deploying') try { const responses = await certificatesApi.bulkDeploy(Array.from(selected), tenantId) const succeeded = responses.filter((r) => r.success).length const failed = responses.filter((r) => !r.success) const bulkResult: BulkResult = { success: succeeded, failed: failed.length, errors: failed.map((f) => ({ device_id: f.device_id, error: f.error ?? 'Unknown error', })), } setResult(bulkResult) setStep('results') void queryClient.invalidateQueries({ queryKey: ['deviceCerts'] }) if (failed.length === 0) { toast({ title: `${succeeded} certificate(s) deployed successfully` }) } else { toast({ title: `${succeeded} deployed, ${failed.length} failed`, variant: 'destructive', }) } } catch (e: unknown) { const err = e as { response?: { data?: { detail?: string } } } setResult({ success: 0, failed: selected.size, errors: [ { device_id: 'bulk', error: err?.response?.data?.detail || 'Bulk deployment failed', }, ], }) setStep('results') toast({ title: 'Bulk deployment failed', variant: 'destructive' }) } } const handleClose = () => { onClose() setSelected(new Set()) setStep('select') setResult(null) } return ( !v && handleClose()}> Bulk Certificate Deployment
{step === 'select' && ( <>

Select devices to sign and deploy TLS certificates in batch.

{availableDevices.length === 0 ? (

All devices have certificates

Every device already has a deployed certificate.

) : ( <> {/* Select All / Deselect All */}
{selected.size} of {availableDevices.length} selected
{/* Device list */}
{availableDevices.map((d: DeviceResponse) => ( ))}
)} )} {step === 'deploying' && (

Deploying certificates...

Signing and deploying to {selected.size} device {selected.size !== 1 ? 's' : ''}. This may take a moment.

)} {step === 'results' && result && (
{/* Summary */}

{result.success}

Succeeded

0 ? 'border-error/30 bg-error/5' : 'border-border bg-surface', )} > 0 ? 'text-error' : 'text-text-muted', )} />

0 ? 'text-error' : 'text-text-muted', )} > {result.failed}

Failed

{/* Error details */} {result.errors.length > 0 && (

Failed deployments:

{result.errors.map((err, i) => (
{err.error}
))}
)}
)}
) }