Files
the-other-dude/frontend/src/components/config/RestoreButton.tsx
Jason Staack b840047e19 feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:30:44 -05:00

141 lines
3.7 KiB
TypeScript

import { useState } from 'react'
import { Loader2 } from 'lucide-react'
import { configApi } from '@/lib/api'
import { toast } from '@/components/ui/toast'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog'
import { RestorePreview } from './RestorePreview'
interface RestoreButtonProps {
tenantId: string
deviceId: string
commitSha: string
backupDate: string
deviceHostname: string
open: boolean
onOpenChange: (open: boolean) => void
onComplete: () => void
}
type RestorePhase = 'preview' | 'pushing' | 'verifying' | 'done'
export function RestoreButton({
tenantId,
deviceId,
commitSha,
backupDate,
deviceHostname,
open,
onOpenChange,
onComplete,
}: RestoreButtonProps) {
const [phase, setPhase] = useState<RestorePhase>('preview')
const handleRestore = async () => {
setPhase('pushing')
// Show verifying state after 30s (halfway through the 60s settle wait)
const verifyTimer = setTimeout(() => {
setPhase('verifying')
}, 30_000)
try {
const result = await configApi.restore(tenantId, deviceId, commitSha)
clearTimeout(verifyTimer)
setPhase('done')
if (result.status === 'committed') {
toast({
title: 'Config restored',
description: result.message,
})
onComplete()
onOpenChange(false)
} else if (result.status === 'reverted') {
toast({
title: 'Restore reverted',
description: result.message,
variant: 'destructive',
})
onComplete()
onOpenChange(false)
} else {
toast({
title: 'Restore failed',
description: result.message,
variant: 'destructive',
})
onOpenChange(false)
}
} catch (err: unknown) {
clearTimeout(verifyTimer)
setPhase('preview')
const message =
err instanceof Error ? err.message : 'Restore operation failed'
toast({
title: 'Restore failed',
description: message,
variant: 'destructive',
})
}
}
const isRunning = phase === 'pushing' || phase === 'verifying'
const handleOpenChange = (nextOpen: boolean) => {
if (isRunning) return
if (!nextOpen) setPhase('preview')
onOpenChange(nextOpen)
}
const statusText = () => {
switch (phase) {
case 'pushing':
return 'Pushing config to device...'
case 'verifying':
return 'Waiting for verification (~60s total)...'
default:
return null
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Restore Config</DialogTitle>
<DialogDescription>
This will push the config from{' '}
<span className="text-text-primary font-medium">{backupDate}</span> to{' '}
<span className="text-text-primary font-medium">{deviceHostname}</span>.
The system will create a safety backup and auto-revert if the device
becomes unreachable.
</DialogDescription>
</DialogHeader>
{phase === 'preview' && (
<RestorePreview
tenantId={tenantId}
deviceId={deviceId}
commitSha={commitSha}
onProceed={() => void handleRestore()}
onCancel={() => handleOpenChange(false)}
/>
)}
{isRunning && (
<div className="flex items-center gap-3 rounded-lg border border-warning/40 bg-warning/10 px-4 py-3 text-sm text-warning">
<Loader2 className="h-4 w-4 animate-spin flex-shrink-0" />
<span>{statusText()}</span>
</div>
)}
</DialogContent>
</Dialog>
)
}