217 lines
8.7 KiB
TypeScript
217 lines
8.7 KiB
TypeScript
"use client"
|
|
|
|
import type { Server } from "@/lib/data"
|
|
import { FormField } from "@/components/form-kit"
|
|
import { StatusBadge } from "@/components/status-badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
SheetDescription,
|
|
SheetFooter,
|
|
SheetClose,
|
|
} from "@/components/ui/sheet"
|
|
import {
|
|
Stepper,
|
|
StepperContent,
|
|
StepperIndicator,
|
|
StepperItem,
|
|
StepperNav,
|
|
StepperPanel,
|
|
StepperSeparator,
|
|
StepperTitle,
|
|
StepperTrigger,
|
|
} from "@/components/reui/stepper"
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export function BackupCreateSheet({
|
|
open,
|
|
onOpenChange,
|
|
step,
|
|
onStepChange,
|
|
servers,
|
|
selected,
|
|
onToggle,
|
|
onSelectAll,
|
|
onClear,
|
|
notes,
|
|
onNotesChange,
|
|
destinationLabel,
|
|
busy,
|
|
onSubmit,
|
|
locked = false,
|
|
terminal = false,
|
|
canObserve = true,
|
|
jobId,
|
|
jobStatus,
|
|
result,
|
|
error,
|
|
onReset,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
step: number
|
|
onStepChange: (step: number) => void
|
|
servers: Server[]
|
|
selected: Set<string>
|
|
onToggle: (id: string) => void
|
|
onSelectAll: () => void
|
|
onClear: () => void
|
|
notes: string
|
|
onNotesChange: (value: string) => void
|
|
destinationLabel: string
|
|
busy: boolean
|
|
locked?: boolean
|
|
terminal?: boolean
|
|
canObserve?: boolean
|
|
jobId?: string
|
|
jobStatus?: string
|
|
result?: string
|
|
error?: string | null
|
|
onReset: () => void
|
|
onSubmit: () => void
|
|
}) {
|
|
return (
|
|
<Sheet open={open} onOpenChange={(v) => { onOpenChange(v); if (!v) onStepChange(1) }}>
|
|
<SheetContent side="right" className="data-[side=right]:w-full data-[side=right]:sm:max-w-md min-w-0 flex flex-col gap-0 p-0">
|
|
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
|
<SheetTitle>{locked ? "Задача резервного копирования" : "Новый бэкап"}</SheetTitle>
|
|
<SheetDescription>{locked ? "Подтверждённые файлы и ошибки выполнения" : "Снять конфигурацию вручную с выбранных серверов"}</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
{locked && <div role="status" className="px-6 py-3 text-sm [overflow-wrap:anywhere]"><p>ID: {jobId}</p><p>{jobStatus}</p>{result && <p>{result}</p>}<p className="text-xs text-muted-foreground">Закрытие формы не отменяет задачу. Повторная проверка не создаёт новые копии.</p></div>}
|
|
{error && <div className="px-6 pb-3"><Alert variant="warning" aria-label="Результат задачи"><AlertTitle>{terminal ? "Результат с ограничениями" : "Запрос не подтверждён"}</AlertTitle><AlertDescription className="[overflow-wrap:anywhere]">{error}</AlertDescription></Alert></div>}
|
|
<fieldset disabled={busy || locked} className="flex-1 min-h-0 flex flex-col">
|
|
<Stepper value={step} onValueChange={onStepChange} className="flex-1 flex flex-col min-h-0 px-6 py-5">
|
|
<StepperNav className="mb-5">
|
|
<StepperItem step={1}>
|
|
<StepperTrigger>
|
|
<StepperIndicator>1</StepperIndicator>
|
|
<StepperTitle className="sr-only">Серверы</StepperTitle>
|
|
</StepperTrigger>
|
|
<StepperSeparator />
|
|
</StepperItem>
|
|
<StepperItem step={2}>
|
|
<StepperTrigger>
|
|
<StepperIndicator>2</StepperIndicator>
|
|
<StepperTitle className="sr-only">Заметка</StepperTitle>
|
|
</StepperTrigger>
|
|
<StepperSeparator />
|
|
</StepperItem>
|
|
<StepperItem step={3}>
|
|
<StepperTrigger>
|
|
<StepperIndicator>3</StepperIndicator>
|
|
<StepperTitle className="sr-only">Подтверждение</StepperTitle>
|
|
</StepperTrigger>
|
|
</StepperItem>
|
|
</StepperNav>
|
|
<StepperPanel className="flex-1 overflow-y-auto">
|
|
<StepperContent value={1} className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<p className="text-sm font-medium">Выберите серверы</p>
|
|
<div className="flex items-center gap-2">
|
|
<button type="button" onClick={onSelectAll} className="text-xs text-primary hover:underline">
|
|
Все
|
|
</button>
|
|
<span className="text-border">·</span>
|
|
<button
|
|
type="button"
|
|
onClick={onClear}
|
|
className="text-xs text-muted-foreground hover:text-foreground hover:underline"
|
|
>
|
|
Сбросить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{servers.map((s) => {
|
|
const checked = selected.has(s.id)
|
|
return (
|
|
<label
|
|
key={s.id}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-3 rounded-lg border p-3 text-left transition-colors",
|
|
checked ? "border-primary/40 bg-primary/5" : "border-border hover:bg-muted/40",
|
|
)}
|
|
>
|
|
<Checkbox
|
|
checked={checked}
|
|
onCheckedChange={() => onToggle(s.id)}
|
|
aria-label={`Выбрать ${s.name}`}
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium">{s.name}</p>
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<span className="text-xs font-mono text-muted-foreground">{s.host}</span>
|
|
<StatusBadge status={s.status} />
|
|
</div>
|
|
</div>
|
|
{s.status === "offline" && (
|
|
<span className="text-xs text-muted-foreground">недоступен</span>
|
|
)}
|
|
</label>
|
|
)
|
|
})}
|
|
</StepperContent>
|
|
<StepperContent value={2} className="flex flex-col gap-4">
|
|
<FormField label="Заметка">
|
|
<Input
|
|
placeholder="Например: перед обновлением BGP"
|
|
aria-label="Заметка резервной копии"
|
|
maxLength={500}
|
|
value={notes}
|
|
onChange={(e) => onNotesChange(e.target.value)}
|
|
/>
|
|
</FormField>
|
|
</StepperContent>
|
|
<StepperContent value={3} className="flex flex-col gap-3 text-sm">
|
|
<p className="text-muted-foreground">
|
|
{locked ? "Запрошены резервные копии для" : "Будет создан бэкап для"} <strong className="text-foreground">{selected.size}</strong> серверов.
|
|
</p>
|
|
{!locked && <p className="text-muted-foreground">
|
|
Куда сохранится: <strong className="text-foreground">{destinationLabel}</strong>
|
|
</p>}
|
|
{notes ? <p className="text-muted-foreground">Заметка: {notes}</p> : null}
|
|
</StepperContent>
|
|
</StepperPanel>
|
|
</Stepper>
|
|
</fieldset>
|
|
</div>
|
|
|
|
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row flex-wrap gap-2">
|
|
<SheetClose render={<Button variant="outline" className="flex-1" />}>Закрыть</SheetClose>
|
|
{!locked && step > 1 && (
|
|
<Button type="button" variant="outline" className="flex-1" onClick={() => onStepChange(step - 1)}>
|
|
Назад
|
|
</Button>
|
|
)}
|
|
{locked ? <><Button type="button" disabled={!canObserve || busy || terminal} onClick={onSubmit}>Проверить задачу</Button>{terminal && <Button type="button" onClick={onReset}>Новая задача</Button>}</> : step < 3 ? (
|
|
<Button
|
|
type="button"
|
|
className="flex-1"
|
|
disabled={step === 1 && selected.size === 0}
|
|
onClick={() => onStepChange(step + 1)}
|
|
>
|
|
Далее
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="button"
|
|
className="flex-1"
|
|
disabled={selected.size === 0 || busy}
|
|
onClick={onSubmit}
|
|
>
|
|
Снять бэкап ({selected.size})
|
|
</Button>
|
|
)}
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|