fix(backups): сохранять расписание и хранилище независимо
This commit is contained in:
1 parent
5ce21e9a47
commit
8e01666e0e
12 files changed
+286
-104
No files matched your search
@@ -20,11 +20,11 @@ export type StorageProvider = "local" | "s3"
|
||||
export type BackupScheduleForm = {
|
||||
enabled: boolean
|
||||
frequency: BackupFreq
|
||||
hour: number
|
||||
minute: number
|
||||
weekDay: number
|
||||
monthDay: number
|
||||
keepCount: number
|
||||
hour: number | string
|
||||
minute: number | string
|
||||
weekDay: number | string
|
||||
monthDay: number | string
|
||||
keepCount: number | string
|
||||
format: "rsc" | "backup"
|
||||
}
|
||||
|
||||
@@ -93,6 +93,13 @@ export function BackupsSettings({
|
||||
onSelectAll,
|
||||
onClearServers,
|
||||
onSave,
|
||||
onSaveStorage,
|
||||
scheduleKnown = true,
|
||||
storageKnown = true,
|
||||
scheduleWritable = true,
|
||||
storageWritable = true,
|
||||
allEnabled = false,
|
||||
onAllEnabledChange,
|
||||
onTest,
|
||||
onSync,
|
||||
saveBusy,
|
||||
@@ -109,6 +116,13 @@ export function BackupsSettings({
|
||||
onToggleServer: (id: string) => void
|
||||
onSelectAll: () => void
|
||||
onClearServers: () => void
|
||||
onSaveStorage: () => void
|
||||
scheduleKnown?: boolean
|
||||
storageKnown?: boolean
|
||||
scheduleWritable?: boolean
|
||||
storageWritable?: boolean
|
||||
allEnabled?: boolean
|
||||
onAllEnabledChange: (value: boolean) => void
|
||||
onSave: () => void
|
||||
onTest: () => void
|
||||
onSync: () => void
|
||||
@@ -121,16 +135,17 @@ export function BackupsSettings({
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{!scheduleKnown ? <p role="status">Расписание пока неизвестно</p> : <fieldset disabled={!scheduleWritable || saveBusy || testBusy || syncBusy} className="min-w-0">
|
||||
<OpsPanel title="Расписание" description="Автоматический съём конфигурации" contentClassName="px-5 py-5 flex flex-col gap-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Автоматический бэкап</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Создавать бэкапы по расписанию</p>
|
||||
</div>
|
||||
<FormToggle checked={schedule.enabled} onChange={(v) => onScheduleChange("enabled", v)} />
|
||||
<FormToggle ariaLabel="Автоматический бэкап" checked={schedule.enabled} onChange={(v) => onScheduleChange("enabled", v)} />
|
||||
</div>
|
||||
|
||||
<div className={cn("flex flex-col gap-4", !schedule.enabled && "opacity-40 pointer-events-none")}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField label="Частота">
|
||||
<SegmentedControl
|
||||
value={schedule.frequency}
|
||||
@@ -172,8 +187,9 @@ export function BackupsSettings({
|
||||
min={1}
|
||||
max={28}
|
||||
className="font-mono w-24"
|
||||
aria-label="День месяца"
|
||||
value={schedule.monthDay}
|
||||
onChange={(e) => onScheduleChange("monthDay", Math.min(28, Math.max(1, Number(e.target.value))))}
|
||||
onChange={(e) => onScheduleChange("monthDay", e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
@@ -185,10 +201,12 @@ export function BackupsSettings({
|
||||
min={0}
|
||||
max={23}
|
||||
className="font-mono w-20 text-center"
|
||||
value={String(schedule.hour).padStart(2, "0")}
|
||||
onChange={(e) => onScheduleChange("hour", Math.min(23, Math.max(0, Number(e.target.value))))}
|
||||
aria-label="Час запуска"
|
||||
value={schedule.hour}
|
||||
onChange={(e) => onScheduleChange("hour", e.target.value)}
|
||||
/>
|
||||
<span className="text-muted-foreground font-mono text-lg">:</span>
|
||||
<Input aria-label="Минута запуска" type="number" min={0} max={59} className="font-mono w-20 text-center" value={schedule.minute} onChange={e => onScheduleChange("minute", e.target.value)} />
|
||||
<div className="flex gap-1">
|
||||
{[0, 15, 30, 45].map((m) => (
|
||||
<button
|
||||
@@ -213,26 +231,25 @@ export function BackupsSettings({
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={90}
|
||||
max={365}
|
||||
className="font-mono w-24"
|
||||
aria-label="Хранить копий"
|
||||
value={schedule.keepCount}
|
||||
onChange={(e) => onScheduleChange("keepCount", Math.max(1, Number(e.target.value)))}
|
||||
onChange={(e) => onScheduleChange("keepCount", e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Формат файла</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Снимается текстовый экспорт RouterOS</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" size="sm">.rsc</Badge>
|
||||
<Badge variant="warning-light" size="sm">.backup скоро</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<FormField label="Формат файла" hint="Плановый binary .backup пока недоступен">
|
||||
<select aria-label="Формат файла" className="h-9 rounded-md border px-3 bg-background" value={schedule.format} onChange={e => onScheduleChange("format", e.target.value as "rsc" | "backup")}>
|
||||
<option value="rsc">Текстовый экспорт .rsc</option>
|
||||
<option value="backup" disabled>Binary .backup — недоступен</option>
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
</OpsPanel>
|
||||
</fieldset>}
|
||||
|
||||
{!storageKnown ? <p role="status">Хранилище пока неизвестно</p> : <fieldset disabled={!storageWritable || saveBusy || testBusy || syncBusy} className="min-w-0">
|
||||
<OpsPanel
|
||||
title="Хранилище"
|
||||
description="Локальный диск приложения или S3-compatible бакет"
|
||||
@@ -353,7 +370,9 @@ export function BackupsSettings({
|
||||
</div>
|
||||
)}
|
||||
</OpsPanel>
|
||||
</fieldset>}
|
||||
|
||||
<fieldset disabled={!scheduleKnown || !scheduleWritable || saveBusy || testBusy || syncBusy} className="min-w-0 lg:col-span-2">
|
||||
<OpsPanel
|
||||
className="lg:col-span-2"
|
||||
title="Серверы для бэкапа"
|
||||
@@ -374,6 +393,7 @@ export function BackupsSettings({
|
||||
}
|
||||
contentClassName="px-5 py-5 flex flex-col gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm"><Checkbox aria-label="Все включённые серверы" checked={allEnabled} onCheckedChange={value => onAllEnabledChange(value === true)} /><span>Все включённые серверы, включая добавленные позже</span></div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||||
{servers.map((s) => {
|
||||
const checked = selectedServers.has(s.id)
|
||||
@@ -388,6 +408,7 @@ export function BackupsSettings({
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={allEnabled}
|
||||
checked={checked}
|
||||
onCheckedChange={() => onToggleServer(s.id)}
|
||||
aria-label={`Выбрать ${s.name}`}
|
||||
@@ -407,12 +428,14 @@ export function BackupsSettings({
|
||||
Выбрано {selectedServers.size} из {servers.length} серверов
|
||||
</p>
|
||||
</OpsPanel>
|
||||
</fieldset>
|
||||
|
||||
<div className="lg:col-span-2 flex items-center gap-3">
|
||||
<Button type="button" onClick={onSave} className="gap-2" disabled={saveBusy}>
|
||||
<div className="lg:col-span-2 flex flex-wrap items-center gap-3">
|
||||
<Button type="button" onClick={onSave} className="gap-2" disabled={!scheduleKnown || !scheduleWritable || saveBusy || testBusy || syncBusy}>
|
||||
{saveBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : null}
|
||||
Сохранить настройки
|
||||
Сохранить расписание
|
||||
</Button>
|
||||
<Button type="button" onClick={onSaveStorage} disabled={!storageKnown || !storageWritable || saveBusy || testBusy || syncBusy}>Сохранить хранилище</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user