/** * TemplatePushWizard -- multi-step dialog for pushing a template to devices. * Steps: Target Selection -> Variable Input -> Preview -> Confirm & Push -> Progress */ import { useState } from 'react' import { useQuery, useMutation } from '@tanstack/react-query' import { ChevronLeft, ChevronRight, Loader2, AlertTriangle, Play } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { cn } from '@/lib/utils' import { templatesApi, type TemplateResponse, } from '@/lib/templatesApi' import { deviceGroupsApi, metricsApi } from '@/lib/api' import { PushProgressPanel } from './PushProgressPanel' interface TemplatePushWizardProps { open: boolean onClose: () => void tenantId: string template: TemplateResponse } type WizardStep = 'targets' | 'variables' | 'preview' | 'confirm' | 'progress' export function TemplatePushWizard({ open, onClose, tenantId, template }: TemplatePushWizardProps) { const [step, setStep] = useState('targets') const [selectedDeviceIds, setSelectedDeviceIds] = useState>(new Set()) const [variables, setVariables] = useState>(() => { const defaults: Record = {} for (const v of template.variables) { if (v.default) defaults[v.name] = v.default } return defaults }) const [previewDevice, setPreviewDevice] = useState(null) const [previews, setPreviews] = useState>({}) const [rolloutId, setRolloutId] = useState(null) // Fetch devices const { data: devices } = useQuery({ queryKey: ['fleet-devices', tenantId], queryFn: () => metricsApi.fleetSummary(tenantId), enabled: open, }) // Fetch groups const { data: groups } = useQuery({ queryKey: ['device-groups', tenantId], queryFn: () => deviceGroupsApi.list(tenantId), enabled: open, }) // Preview mutation const previewMutation = useMutation({ mutationFn: ({ deviceId }: { deviceId: string }) => templatesApi.preview(tenantId, template.id, deviceId, variables), onSuccess: (data, { deviceId }) => { setPreviews((prev) => ({ ...prev, [deviceId]: data.rendered })) }, }) // Push mutation const pushMutation = useMutation({ mutationFn: () => templatesApi.push(tenantId, template.id, Array.from(selectedDeviceIds), variables), onSuccess: (data) => { setRolloutId(data.rollout_id) setStep('progress') }, }) const selectedDevices = devices?.filter((d) => selectedDeviceIds.has(d.id)) ?? [] // eslint-disable-next-line @typescript-eslint/no-unused-vars const handleGroupSelect = (_groupId: string) => { // For now, just select all online devices. In a real implementation, // we'd load group members from the API. Here we select all devices // as a simplified approach. if (devices) { const onlineIds = new Set(devices.filter((d) => d.status === 'online').map((d) => d.id)) setSelectedDeviceIds(onlineIds) } } const toggleDevice = (deviceId: string) => { setSelectedDeviceIds((prev) => { const next = new Set(prev) if (next.has(deviceId)) { next.delete(deviceId) } else { next.add(deviceId) } return next }) } const goToPreview = () => { setStep('preview') // Trigger preview for first selected device if (selectedDevices.length > 0) { const firstId = selectedDevices[0].id setPreviewDevice(firstId) if (!previews[firstId]) { previewMutation.mutate({ deviceId: firstId }) } } } const selectPreviewDevice = (deviceId: string) => { setPreviewDevice(deviceId) if (!previews[deviceId]) { previewMutation.mutate({ deviceId }) } } const handleClose = () => { setStep('targets') setSelectedDeviceIds(new Set()) setVariables(() => { const defaults: Record = {} for (const v of template.variables) { if (v.default) defaults[v.name] = v.default } return defaults }) setPreviews({}) setRolloutId(null) onClose() } const userVars = template.variables.filter((v) => v.name !== 'device') return ( !o && handleClose()}> Push Template: {template.name} {step !== 'progress' && ( Step {['targets', 'variables', 'preview', 'confirm'].indexOf(step) + 1} of 4 )} {/* Step 1: Target Selection */} {step === 'targets' && (
Select devices to push the template to.
{groups && groups.length > 0 && (
)}
{devices?.map((device) => (
)} {/* Step 2: Variable Input */} {step === 'variables' && (
Provide values for template variables. Built-in device variables are auto-populated per device.
Auto-populated: {'{{ device.hostname }}'}, {'{{ device.ip }}'}, {'{{ device.model }}'}
{userVars.map((v) => (
{v.type === 'boolean' ? (
setVariables((prev) => ({ ...prev, [v.name]: c ? 'true' : 'false' })) } /> {variables[v.name] ?? 'false'}
) : ( setVariables((prev) => ({ ...prev, [v.name]: e.target.value })) } placeholder={ v.type === 'ip' ? '192.168.1.1' : v.type === 'subnet' ? '192.168.1.0/24' : v.type === 'integer' ? '0' : v.default ?? '' } type={v.type === 'integer' ? 'number' : 'text'} className="h-8 text-xs bg-elevated/50 border-border font-mono" /> )}
type: {v.type} {v.default && ` | default: ${v.default}`}
))}
)} {/* Step 3: Preview */} {step === 'preview' && (
Preview the rendered template for each device.
{selectedDevices.map((d) => ( ))}
{previewDevice && previews[previewDevice] && (
                {previews[previewDevice]}
              
)} {previewDevice && !previews[previewDevice] && previewMutation.isPending && (
Loading preview...
)} {previewMutation.isError && (
Preview failed: {previewMutation.error instanceof Error ? previewMutation.error.message : 'Unknown error'}
)}
)} {/* Step 4: Confirm & Push */} {step === 'confirm' && (
This will push configuration to {selectedDeviceIds.size} device(s). Each device will be backed up before changes are applied. If a device becomes unreachable after push, it will automatically revert.
Template: {template.name}
Devices: {selectedDevices.map((d) => d.hostname).join(', ')}
{Object.entries(variables).length > 0 && (
Variables:{' '} {Object.entries(variables) .map(([k, v]) => `${k}=${v}`) .join(', ')}
)}
{pushMutation.isError && (
Push failed: {pushMutation.error instanceof Error ? pushMutation.error.message : 'Unknown error'}
)}
)} {/* Step 5: Progress */} {step === 'progress' && rolloutId && ( )}
) }