fix(backups): продолжать наблюдение без повторного создания
This commit is contained in:
1 parent
4aee2a34f1
commit
714af7e872
9 files changed
+265
-74
No files matched your search
+32
-66
@@ -23,9 +23,7 @@ import { can } from "@/lib/auth"
|
||||
import { userApiError } from "@/shared/api/users"
|
||||
import { toFrontendServer } from "@/entities/server/model/mappers"
|
||||
import {
|
||||
createBackupsAsync,
|
||||
deleteBackup,
|
||||
getBackupJob,
|
||||
getBackupScheduleSettings,
|
||||
getBackupStorageSettings,
|
||||
listBackups,
|
||||
@@ -38,6 +36,8 @@ import {
|
||||
} from "@/shared/api/backups"
|
||||
import { ApiClientError, requestJson, requestBlob } from "@/shared/api/http-client"
|
||||
import { toast } from "sonner"
|
||||
import { useBackupJob } from "@/lib/use-backup-job"
|
||||
import { backupJobOutcome } from "@/lib/backup-job-poll"
|
||||
import { backupSchedulePayload, backupStoragePayload } from "@/lib/backup-settings"
|
||||
import { BackupsHistory } from "@/components/backups/backups-history"
|
||||
import {
|
||||
@@ -85,11 +85,10 @@ function BackupsContent() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [advancedFilters, setAdvancedFilters] = useState<FilterQuery>(() => createFilterQuery())
|
||||
const [liveServers, setLiveServers] = useState<Server[]>(isLive ? [] : mockServers)
|
||||
const canCreate = canWrite && canReadServers && liveServers.length > 0 && current("servers") && current("storage")
|
||||
const baseCanCreate = canWrite && canReadServers && liveServers.length > 0 && current("servers") && current("storage")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [opBusy, setOpBusy] = useState(false)
|
||||
const [opError, setOpError] = useState<string | null>(null)
|
||||
const [backupJobId, setBackupJobId] = useState<string | null>(null)
|
||||
const [scheduleSaveBusy, setScheduleSaveBusy] = useState(false)
|
||||
const [testBusy, setTestBusy] = useState(false)
|
||||
const [syncBusy, setSyncBusy] = useState(false)
|
||||
@@ -165,6 +164,12 @@ function BackupsContent() {
|
||||
return () => { alive.current = false; reader.current?.abort(); settingsController.current?.abort() }
|
||||
}, [loadLive])
|
||||
|
||||
const job = useBackupJob(backendUrl, isLive, canRead, baseCanCreate && !opBusy && !scheduleSaveBusy, loadLive)
|
||||
const checkpoint = job.checkpoint
|
||||
const jobTerminal = !!checkpoint?.job && ["done", "failed"].includes(checkpoint.job.status)
|
||||
const backupJobId = checkpoint && !jobTerminal ? checkpoint.request.requestId : null
|
||||
const canCreate = baseCanCreate && job.hydrated && !checkpoint && !job.storageError
|
||||
|
||||
function startSettingsMutation() {
|
||||
if (!alive.current || settingsLock.current || opBusy || backupJobId) return null
|
||||
settingsLock.current = true
|
||||
@@ -246,60 +251,12 @@ function BackupsContent() {
|
||||
}
|
||||
|
||||
async function handleManualBackup() {
|
||||
if (!canCreate || manualServers.size === 0 || opBusy) return
|
||||
setOpError(null)
|
||||
try {
|
||||
const res = await createBackupsAsync(backendUrl, {
|
||||
serverIds: [...manualServers],
|
||||
notes: manualNotes || undefined,
|
||||
})
|
||||
setBackupJobId(res.jobId)
|
||||
setManualOpen(false)
|
||||
setManualServers(new Set())
|
||||
setManualNotes("")
|
||||
setTab("history")
|
||||
} catch (e) {
|
||||
setOpError(e instanceof Error ? e.message : "Ошибка создания бэкапа")
|
||||
}
|
||||
if (checkpoint) { await job.run(); return }
|
||||
if (!canCreate || !manualServers.size || settingsLock.current || opBusy) return
|
||||
if ([...manualServers].some(id => !liveServers.some(server => server.id === id))) { setOpError("Выбранный сервер отсутствует; обновите выбор"); return }
|
||||
await job.run({ serverIds: [...manualServers], notes: manualNotes || undefined })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!backupJobId) return
|
||||
toast.info("Бэкап выполняется в фоне...")
|
||||
let cancelled = false
|
||||
const timer = setInterval(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const job = await getBackupJob(backendUrl, backupJobId)
|
||||
if (cancelled) return
|
||||
if (job.status === "done") {
|
||||
clearInterval(timer)
|
||||
setBackupJobId(null)
|
||||
if (job.failures.length > 0) {
|
||||
setOpError(`Часть бэкапов не создалась: ${job.failures.map((f) => `${f.serverId}: ${f.error}`).join("; ")}`)
|
||||
} else {
|
||||
toast.success("Бэкап успешно завершён")
|
||||
}
|
||||
await loadLive()
|
||||
} else if (job.status === "failed") {
|
||||
clearInterval(timer)
|
||||
setBackupJobId(null)
|
||||
setOpError(job.failures.map((f) => `${f.serverId}: ${f.error}`).join("; ") || "Ошибка фоновой задачи бэкапа")
|
||||
await loadLive()
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(timer)
|
||||
setBackupJobId(null)
|
||||
setOpError(e instanceof Error ? e.message : "Ошибка опроса задачи бэкапа")
|
||||
}
|
||||
})()
|
||||
}, 1200)
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [backendUrl, backupJobId, loadLive])
|
||||
|
||||
useEffect(() => {
|
||||
if (opError) toast.error(opError)
|
||||
}, [opError])
|
||||
@@ -377,6 +334,7 @@ function BackupsContent() {
|
||||
: "default"
|
||||
|
||||
function openCreate(all: boolean) {
|
||||
if (checkpoint) { setManualStep(3); setManualOpen(true); return }
|
||||
if (!canCreate) return
|
||||
setManualServers(all ? new Set(liveServers.map((s) => s.id)) : new Set())
|
||||
setManualNotes("")
|
||||
@@ -395,7 +353,7 @@ function BackupsContent() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openCreate(true)}
|
||||
disabled={!canCreate || loading}
|
||||
disabled={!canCreate || loading || job.busy}
|
||||
>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
Снять со всех
|
||||
@@ -404,10 +362,10 @@ function BackupsContent() {
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => openCreate(false)}
|
||||
disabled={!canCreate || loading}
|
||||
disabled={checkpoint ? !canRead : !canCreate || loading || job.busy}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Новый бэкап
|
||||
{checkpoint ? "Задача резервного копирования" : "Новый бэкап"}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -428,6 +386,7 @@ function BackupsContent() {
|
||||
</Alert>)}
|
||||
|
||||
{opError && <Alert variant="warning" aria-label="Операция резервных копий"><AlertTitle>Операция не подтверждена</AlertTitle><AlertDescription>{opError}</AlertDescription></Alert>}
|
||||
{(job.error || job.storageError) && <Alert variant="warning" aria-label="Задача резервного копирования"><AlertTitle>{jobTerminal ? "Задача завершена с ограничениями" : "Результат задачи пока неизвестен"}</AlertTitle><AlertDescription>{job.error || job.storageError}</AlertDescription></Alert>}
|
||||
<KpiStatGrid
|
||||
aria-label="Сводка бэкапов"
|
||||
items={[
|
||||
@@ -465,11 +424,11 @@ function BackupsContent() {
|
||||
<Alert variant={alertVariant}>
|
||||
<ClockIcon />
|
||||
<AlertTitle>
|
||||
{backupJobId ? "Задача выполняется" : schedSummary}
|
||||
{backupJobId ? (checkpoint?.accepted ? "Задача наблюдается" : "Результат запроса пока неизвестен") : schedSummary}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{backupJobId
|
||||
? "Снимок конфигурации снимается в фоне. Список обновится после завершения."
|
||||
? "Закрытие формы не отменяет задачу. Проверка продолжает чтение того же ID."
|
||||
: statusDetail}
|
||||
</AlertDescription>
|
||||
<AlertAction>
|
||||
@@ -532,8 +491,8 @@ function BackupsContent() {
|
||||
scheduleKnown={known("schedule")}
|
||||
storageKnown={known("storage")}
|
||||
storageDirty={storageIsDirty}
|
||||
scheduleWritable={canWrite && canReadServers && current("schedule") && current("servers")}
|
||||
storageWritable={canWrite && current("storage")}
|
||||
scheduleWritable={canWrite && canReadServers && current("schedule") && current("servers") && !backupJobId}
|
||||
storageWritable={canWrite && current("storage") && !backupJobId}
|
||||
allEnabled={allEnabled}
|
||||
onAllEnabledChange={value => { scheduleDirty.current = true; setAllEnabled(value) }}
|
||||
onTest={handleTestStorage}
|
||||
@@ -554,7 +513,7 @@ function BackupsContent() {
|
||||
step={manualStep}
|
||||
onStepChange={setManualStep}
|
||||
servers={liveServers}
|
||||
selected={manualServers}
|
||||
selected={checkpoint ? new Set(checkpoint.request.serverIds.map(String)) : manualServers}
|
||||
onToggle={(id) => {
|
||||
setManualServers((prev) => {
|
||||
const next = new Set(prev)
|
||||
@@ -565,10 +524,17 @@ function BackupsContent() {
|
||||
}}
|
||||
onSelectAll={() => setManualServers(new Set(liveServers.map((s) => s.id)))}
|
||||
onClear={() => setManualServers(new Set())}
|
||||
notes={manualNotes}
|
||||
notes={checkpoint?.request.notes ?? manualNotes}
|
||||
onNotesChange={setManualNotes}
|
||||
destinationLabel={destinationLabel}
|
||||
busy={backupJobId !== null}
|
||||
busy={job.busy}
|
||||
locked={!!checkpoint}
|
||||
terminal={jobTerminal}
|
||||
canObserve={canRead}
|
||||
jobId={checkpoint?.request.requestId}
|
||||
jobStatus={checkpoint?.job ? ({ queued: "В очереди", running: "Выполняется", done: "Завершена", failed: "Прервана" })[checkpoint.job.status] + ": " + checkpoint.job.completed + "/" + checkpoint.job.total : checkpoint?.accepted ? "Запрос принят, результат ещё неизвестен" : "Результат запроса ещё неизвестен"}
|
||||
result={checkpoint?.job && jobTerminal ? "Создано " + backupJobOutcome(checkpoint.job).created + " из " + checkpoint.job.total : undefined}
|
||||
onReset={() => { if (job.reset()) { setManualServers(new Set()); setManualNotes(""); setManualStep(1) } }}
|
||||
onSubmit={handleManualBackup}
|
||||
/>
|
||||
|
||||
|
||||
Reference in new issue
Block a user