fix(collectors): сохранять только изменённые настройки с учётом прав
This commit is contained in:
1 parent
117aa9a21a
commit
cb377fb60c
6 files changed
+193
-208
No files matched your search
+100
-206
@@ -24,8 +24,9 @@ import {
|
||||
type SchedulerStatusDto,
|
||||
type SchedulerRunRowDto,
|
||||
} from "@/lib/scheduler-settings"
|
||||
import { getTrafficCollectorSettings, getServersApiPingSettings, getUptimeCollectorSettings, getInternetPathSettings } from "@/shared/api/collector-settings"
|
||||
import { getCertificateRenewSettings } from "@/shared/api/certificates"
|
||||
import { getTrafficCollectorSettings, getServersApiPingSettings, getUptimeCollectorSettings, getInternetPathSettings, putTrafficCollectorSettings, putServersApiPingSettings, putUptimeCollectorSettings, putInternetPathSettings } from "@/shared/api/collector-settings"
|
||||
import { can } from "@/lib/auth"
|
||||
import { getCertificateRenewSettings, putCertificateRenewSettings } from "@/shared/api/certificates"
|
||||
import { getSchedulerStatus, listSchedulerRuns } from "@/shared/api/scheduler"
|
||||
import { requestJson, ApiClientError } from "@/shared/api/http-client"
|
||||
import {
|
||||
@@ -82,17 +83,6 @@ function extractApiError(reason: unknown): string {
|
||||
return String(reason)
|
||||
}
|
||||
|
||||
function collectSettledErrors(results: PromiseSettledResult<unknown>[], labels: string[]): string[] {
|
||||
const errors: string[] = []
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
const result = results[i]
|
||||
if (result.status === "rejected") {
|
||||
errors.push(`${labels[i]}: ${extractApiError(result.reason)}`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
function fmtMs(ms: number): string {
|
||||
if (ms < 1000) return `${ms} мс`
|
||||
const s = ms / 1000
|
||||
@@ -519,17 +509,8 @@ function RunRowDetail({ r }: { r: SchedulerRunRowDto }) {
|
||||
)
|
||||
}
|
||||
|
||||
type SchedulerToggleOverrides = {
|
||||
trafficEnabled?: boolean
|
||||
serversApiEnabled?: boolean
|
||||
resourcesEnabled?: boolean
|
||||
pingEnabled?: boolean
|
||||
speedEnabled?: boolean
|
||||
internetPathEnabled?: boolean
|
||||
certRenewEnabled?: boolean
|
||||
}
|
||||
|
||||
const SETTINGS_LABELS = { traffic: "Трафик", rest: "Серверы REST API", uptime: "Uptime", internet: "Internet path", cert: "Сертификаты" } as const
|
||||
const SETTINGS_PERMISSIONS = { traffic: "mm:traffic:write", rest: "mm:servers:write", uptime: "mm:uptime:write", internet: "mm:network:write", cert: "mm:certificates:write" } as const
|
||||
type SettingsKey = keyof typeof SETTINGS_LABELS
|
||||
type SettingsRead = { known: boolean; error: string | null; receivedAt: string | null }
|
||||
const INITIAL_SETTINGS: Record<SettingsKey, SettingsRead> = Object.fromEntries(Object.keys(SETTINGS_LABELS).map(key => [key, { known: false, error: null, receivedAt: null }])) as Record<SettingsKey, SettingsRead>
|
||||
@@ -551,7 +532,12 @@ function DataCollectionContent() {
|
||||
const [internetPathCollector, setInternetPathCollector] = useState<Awaited<ReturnType<typeof getInternetPathSettings>> | null>(null)
|
||||
const [settingsReads, setSettingsReads] = useState(INITIAL_SETTINGS)
|
||||
const dirtySettings = useRef(new Set<SettingsKey>())
|
||||
const settingsReady = Object.values(settingsReads).every(value => value.known && !value.error)
|
||||
const dirtyFields = useRef<Record<SettingsKey, Set<string>>>({ traffic: new Set(), rest: new Set(), uptime: new Set(), internet: new Set(), cert: new Set() })
|
||||
const [dirtyCount, setDirtyCount] = useState(0)
|
||||
const [saveMessage, setSaveMessage] = useState<string | null>(null)
|
||||
const mutationLock = useRef(false)
|
||||
const writable = (key: SettingsKey) => isLive && can(SETTINGS_PERMISSIONS[key]) && settingsReads[key].known && !settingsReads[key].error
|
||||
const markDirty = (key: SettingsKey, field: string) => { dirtySettings.current.add(key); dirtyFields.current[key].add(field); setDirtyCount(Object.values(dirtyFields.current).reduce((count, fields) => count + fields.size, 0)) }
|
||||
const [trafficIntervalDraft, setTrafficIntervalDraft] = useState("30")
|
||||
const [trafficRetentionDraft, setTrafficRetentionDraft] = useState("14")
|
||||
const [uptimeResourceIntervalDraft, setUptimeResourceIntervalDraft] = useState("300")
|
||||
@@ -597,7 +583,7 @@ function DataCollectionContent() {
|
||||
else next.delete(id)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
}, [setOpenRunIds])
|
||||
|
||||
const schedulerJobsByKey = useMemo(() => {
|
||||
const jobs = schedulerStatus?.jobs ?? []
|
||||
@@ -605,7 +591,7 @@ function DataCollectionContent() {
|
||||
}, [schedulerStatus?.jobs])
|
||||
|
||||
const loadCollectors = useCallback(async () => {
|
||||
if (!isLive || !reader.current.active) return
|
||||
if (!isLive || !reader.current.active || mutationLock.current) return
|
||||
const revision = ++reader.current.revision
|
||||
reader.current.controller?.abort()
|
||||
const controller = new AbortController(); reader.current.controller = controller
|
||||
@@ -666,140 +652,80 @@ function DataCollectionContent() {
|
||||
}
|
||||
}, [backendUrl, isLive, runFilterJobKey])
|
||||
|
||||
const persistSchedulerDrafts = useCallback(async (overrides: SchedulerToggleOverrides = {}) => {
|
||||
if (!settingsReady) throw new Error("Сначала получите подтверждённые настройки всех сборщиков")
|
||||
const trafficEnabled = overrides.trafficEnabled ?? draftTrafficEnabled
|
||||
const serversApiEnabled = overrides.serversApiEnabled ?? draftServersApiEnabled
|
||||
const resourcesEnabled = overrides.resourcesEnabled ?? draftResourcesEnabled
|
||||
const pingEnabled = overrides.pingEnabled ?? draftPingEnabled
|
||||
const speedEnabled = overrides.speedEnabled ?? draftSpeedEnabled
|
||||
const internetPathEnabled = overrides.internetPathEnabled ?? draftInternetPathEnabled
|
||||
const certRenewEnabled = overrides.certRenewEnabled ?? draftCertRenewEnabled
|
||||
const writeSettings = useCallback(async (key: SettingsKey, patch: Record<string, number | boolean>) => {
|
||||
if (!isLive || !reader.current.active || !can(SETTINGS_PERMISSIONS[key]) || !settingsReads[key].known || settingsReads[key].error) throw new Error("Настройки не получены или недостаточно прав для записи")
|
||||
const data = key === "traffic" ? await putTrafficCollectorSettings(backendUrl, patch)
|
||||
: key === "rest" ? await putServersApiPingSettings(backendUrl, patch)
|
||||
: key === "uptime" ? await putUptimeCollectorSettings(backendUrl, patch)
|
||||
: key === "internet" ? await putInternetPathSettings(backendUrl, patch)
|
||||
: await putCertificateRenewSettings(backendUrl, patch)
|
||||
if (Object.entries(patch).some(([field, value]) => (data as Record<string, unknown>)[field] !== value)) throw new Error("Сервер подтвердил другие настройки. Обновите данные перед повторной попыткой")
|
||||
if (!reader.current.active) return
|
||||
if (key === "traffic") setTrafficCollector(data as Awaited<ReturnType<typeof getTrafficCollectorSettings>>)
|
||||
if (key === "rest") setServersApiCollector(data as Awaited<ReturnType<typeof getServersApiPingSettings>>)
|
||||
if (key === "uptime") setUptimeCollector(data as Awaited<ReturnType<typeof getUptimeCollectorSettings>>)
|
||||
if (key === "internet") setInternetPathCollector(data as Awaited<ReturnType<typeof getInternetPathSettings>>)
|
||||
setSettingsReads(prev => ({ ...prev, [key]: { known: true, error: null, receivedAt: new Date().toISOString() } }))
|
||||
}, [backendUrl, isLive, settingsReads])
|
||||
|
||||
const tInt = Math.max(5, Number.parseInt(trafficIntervalDraft, 10) || 30)
|
||||
const tRet = Math.max(1, Number.parseInt(trafficRetentionDraft, 10) || 14)
|
||||
const uRes = Math.max(5, Number.parseInt(uptimeResourceIntervalDraft, 10) || 300)
|
||||
const uPing = Math.max(5, Number.parseInt(uptimeIntervalDraft, 10) || 15)
|
||||
const uSpd = Math.max(10, Number.parseInt(uptimeSpeedIntervalDraft, 10) || 60)
|
||||
const uRet = Math.max(1, Number.parseInt(uptimeRetentionDraft, 10) || 14)
|
||||
const sApiInt = Math.max(10, Number.parseInt(serversApiIntervalDraft, 10) || 120)
|
||||
const ipInt = Math.max(30, Number.parseInt(internetPathIntervalDraft, 10) || 300)
|
||||
const certRenewInt = Math.max(300, Number.parseInt(certRenewIntervalDraft, 10) || 21600)
|
||||
const renewBeforeDays = Math.max(1, Math.min(90, Number.parseInt(renewBeforeDaysDraft, 10) || 30))
|
||||
|
||||
const saveResults = await Promise.allSettled([
|
||||
apiFetch("/api/traffic/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: trafficEnabled,
|
||||
intervalSec: tInt,
|
||||
retentionDays: tRet,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/servers-api-ping/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: serversApiEnabled,
|
||||
intervalSec: sApiInt,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/uptime/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
resourcesEnabled,
|
||||
pingEnabled,
|
||||
speedEnabled,
|
||||
intervalSec: uRes,
|
||||
probeIntervalSec: uPing,
|
||||
speedIntervalSec: uSpd,
|
||||
retentionDays: uRet,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/internet-path/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: internetPathEnabled,
|
||||
intervalSec: ipInt,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/certificates/renew-settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: certRenewEnabled,
|
||||
intervalSec: certRenewInt,
|
||||
renewBeforeDays,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
const saveErrors = collectSettledErrors(
|
||||
saveResults,
|
||||
["трафик", "серверы REST API", "uptime", "internet path", "сертификаты"],
|
||||
)
|
||||
if (saveErrors.length > 0) {
|
||||
throw new Error(`Не все настройки сохранились: ${saveErrors.join("; ")}`)
|
||||
const persistSchedulerDrafts = async () => {
|
||||
if (mutationLock.current || !reader.current.active) return
|
||||
const drafts: Record<SettingsKey, Record<string, string>> = {
|
||||
traffic: { intervalSec: trafficIntervalDraft, retentionDays: trafficRetentionDraft }, rest: { intervalSec: serversApiIntervalDraft },
|
||||
uptime: { intervalSec: uptimeResourceIntervalDraft, probeIntervalSec: uptimeIntervalDraft, speedIntervalSec: uptimeSpeedIntervalDraft, retentionDays: uptimeRetentionDraft },
|
||||
internet: { intervalSec: internetPathIntervalDraft }, cert: { intervalSec: certRenewIntervalDraft, renewBeforeDays: renewBeforeDaysDraft },
|
||||
}
|
||||
}, [
|
||||
settingsReady,
|
||||
apiFetch,
|
||||
certRenewIntervalDraft,
|
||||
draftCertRenewEnabled,
|
||||
draftInternetPathEnabled,
|
||||
draftPingEnabled,
|
||||
draftResourcesEnabled,
|
||||
draftServersApiEnabled,
|
||||
draftSpeedEnabled,
|
||||
draftTrafficEnabled,
|
||||
internetPathIntervalDraft,
|
||||
renewBeforeDaysDraft,
|
||||
serversApiIntervalDraft,
|
||||
trafficIntervalDraft,
|
||||
trafficRetentionDraft,
|
||||
uptimeIntervalDraft,
|
||||
uptimeResourceIntervalDraft,
|
||||
uptimeRetentionDraft,
|
||||
uptimeSpeedIntervalDraft,
|
||||
])
|
||||
const plan: { key: SettingsKey; patch: Record<string, number> }[] = []
|
||||
try {
|
||||
for (const key of Object.keys(drafts) as SettingsKey[]) {
|
||||
if (!dirtyFields.current[key].size) continue
|
||||
if (!writable(key)) throw new Error("Настройки не получены или недостаточно прав: " + SETTINGS_LABELS[key])
|
||||
const patch: Record<string, number> = {}
|
||||
for (const field of dirtyFields.current[key]) {
|
||||
const raw = drafts[key][field]; const value = Number(raw)
|
||||
const min = field === "retentionDays" || field === "renewBeforeDays" ? 1 : key === "cert" ? 300 : key === "internet" ? 30 : key === "rest" || field === "speedIntervalSec" ? 10 : 5
|
||||
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < min || (field === "renewBeforeDays" && value > 90)) throw new Error(SETTINGS_LABELS[key] + ": укажите целое значение от " + min + (field === "renewBeforeDays" ? " до 90" : ""))
|
||||
patch[field] = value
|
||||
}
|
||||
plan.push({ key, patch })
|
||||
}
|
||||
if (!plan.length) return
|
||||
mutationLock.current = true; reader.current.revision++; reader.current.controller?.abort(); setPageBusy(false); setSchedulerSaveBusy(true); setCollectorError(null); setSaveMessage(null)
|
||||
const successes: string[] = [], errors: string[] = []
|
||||
for (const { key, patch } of plan) {
|
||||
if (!reader.current.active) break
|
||||
try {
|
||||
await writeSettings(key, patch)
|
||||
if (!reader.current.active) return
|
||||
for (const field of Object.keys(patch)) dirtyFields.current[key].delete(field)
|
||||
if (!dirtyFields.current[key].size) dirtySettings.current.delete(key)
|
||||
successes.push(SETTINGS_LABELS[key])
|
||||
} catch (error) { if (reader.current.active) errors.push(SETTINGS_LABELS[key] + ": " + extractApiError(error)) }
|
||||
}
|
||||
if (!reader.current.active) return
|
||||
setDirtyCount(Object.values(dirtyFields.current).reduce((count, fields) => count + fields.size, 0))
|
||||
if (successes.length) setSaveMessage("Сохранение подтверждено: " + successes.join(", "))
|
||||
if (errors.length) setCollectorError(errors.join("; "))
|
||||
} catch (error) { if (reader.current.active) setCollectorError(extractApiError(error)) }
|
||||
finally { mutationLock.current = false; if (reader.current.active) setSchedulerSaveBusy(false) }
|
||||
}
|
||||
|
||||
const handleJobEnabledChange = useCallback(async (jobKey: (typeof SCHEDULER_JOB_KEYS)[number], nextEnabled: boolean) => {
|
||||
if (!settingsReady) return
|
||||
const overrides: SchedulerToggleOverrides = {}
|
||||
if (jobKey === "traffic") {
|
||||
setDraftTrafficEnabled(nextEnabled)
|
||||
overrides.trafficEnabled = nextEnabled
|
||||
} else if (jobKey === "servers_rest_ping") {
|
||||
setDraftServersApiEnabled(nextEnabled)
|
||||
overrides.serversApiEnabled = nextEnabled
|
||||
} else if (jobKey === "uptime_resources") {
|
||||
setDraftResourcesEnabled(nextEnabled)
|
||||
overrides.resourcesEnabled = nextEnabled
|
||||
} else if (jobKey === "uptime_ping") {
|
||||
setDraftPingEnabled(nextEnabled)
|
||||
overrides.pingEnabled = nextEnabled
|
||||
} else if (jobKey === "uptime_speed") {
|
||||
setDraftSpeedEnabled(nextEnabled)
|
||||
overrides.speedEnabled = nextEnabled
|
||||
} else if (jobKey === "certificates_renew") {
|
||||
setDraftCertRenewEnabled(nextEnabled)
|
||||
overrides.certRenewEnabled = nextEnabled
|
||||
} else if (jobKey === "internet_path") {
|
||||
setDraftInternetPathEnabled(nextEnabled)
|
||||
overrides.internetPathEnabled = nextEnabled
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
setCollectorError(null)
|
||||
setSchedulerSaveBusy(true)
|
||||
const targets = {
|
||||
traffic: ["traffic", "enabled", setDraftTrafficEnabled], servers_rest_ping: ["rest", "enabled", setDraftServersApiEnabled],
|
||||
uptime_resources: ["uptime", "resourcesEnabled", setDraftResourcesEnabled], uptime_ping: ["uptime", "pingEnabled", setDraftPingEnabled], uptime_speed: ["uptime", "speedEnabled", setDraftSpeedEnabled],
|
||||
internet_path: ["internet", "enabled", setDraftInternetPathEnabled], certificates_renew: ["cert", "enabled", setDraftCertRenewEnabled],
|
||||
} as const
|
||||
if (!(jobKey in targets) || mutationLock.current || !reader.current.active) return
|
||||
const [key, field, accept] = targets[jobKey as keyof typeof targets]
|
||||
mutationLock.current = true; reader.current.revision++; reader.current.controller?.abort(); setPageBusy(false); setSchedulerSaveBusy(true); setCollectorError(null); setSaveMessage(null)
|
||||
try {
|
||||
await persistSchedulerDrafts(overrides)
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось сохранить")
|
||||
} finally {
|
||||
setSchedulerSaveBusy(false)
|
||||
}
|
||||
}, [loadCollectors, persistSchedulerDrafts, settingsReady])
|
||||
await writeSettings(key, { [field]: nextEnabled })
|
||||
if (!reader.current.active) return
|
||||
accept(nextEnabled); setSaveMessage("Сохранение подтверждено: " + SETTINGS_LABELS[key])
|
||||
} catch (error) { if (reader.current.active) setCollectorError(extractApiError(error)) }
|
||||
finally { mutationLock.current = false; if (reader.current.active) setSchedulerSaveBusy(false) }
|
||||
}, [writeSettings])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) return
|
||||
@@ -817,9 +743,10 @@ function DataCollectionContent() {
|
||||
}
|
||||
}
|
||||
|
||||
const schedulerGridRows = useMemo<SchedulerJobGridRow[]>(() => {
|
||||
const schedulerGridRows: SchedulerJobGridRow[] = (() => {
|
||||
return SCHEDULER_JOB_KEYS.map((jobKey) => {
|
||||
const j = schedulerJobsByKey[jobKey]
|
||||
const settingsKey: SettingsKey = jobKey === "traffic" ? "traffic" : jobKey === "servers_rest_ping" ? "rest" : jobKey.startsWith("uptime_") ? "uptime" : jobKey === "certificates_renew" ? "cert" : "internet"
|
||||
const fixedSchedule = jobKey === "gre_bgp" || jobKey === "alert_engine" || jobKey === "backups" || jobKey === "geoip_update"
|
||||
const enabled = fixedSchedule
|
||||
? Boolean(j?.enabled ?? true)
|
||||
@@ -882,7 +809,6 @@ function DataCollectionContent() {
|
||||
? 21600
|
||||
: 300
|
||||
|
||||
const settingsKey: SettingsKey = jobKey === "traffic" ? "traffic" : jobKey === "servers_rest_ping" ? "rest" : jobKey.startsWith("uptime_") ? "uptime" : jobKey === "certificates_renew" ? "cert" : "internet"
|
||||
const settingsKnown = fixedSchedule ? Boolean(j) : settingsReads[settingsKey].known
|
||||
return {
|
||||
id: jobKey,
|
||||
@@ -894,7 +820,7 @@ function DataCollectionContent() {
|
||||
enabled,
|
||||
intervalValue: settingsKnown ? intervalValue : "",
|
||||
intervalReadOnly: fixedSchedule,
|
||||
intervalDisabled: !settingsKnown || schedulerSaveBusy || (!enabled && !fixedSchedule),
|
||||
intervalDisabled: !settingsKnown || schedulerSaveBusy || (!fixedSchedule && !writable(settingsKey)),
|
||||
defaultInterval,
|
||||
job: j,
|
||||
onEnabledChange: fixedSchedule
|
||||
@@ -902,7 +828,7 @@ function DataCollectionContent() {
|
||||
: (nextEnabled) => {
|
||||
void handleJobEnabledChange(jobKey, nextEnabled)
|
||||
},
|
||||
onIntervalChange: (value: string) => { dirtySettings.current.add(settingsKey); onIntervalChange(value) },
|
||||
onIntervalChange: (value: string) => { markDirty(settingsKey, jobKey === "uptime_ping" ? "probeIntervalSec" : jobKey === "uptime_speed" ? "speedIntervalSec" : "intervalSec"); onIntervalChange(value) },
|
||||
onRunNow: async () => {
|
||||
setRunNowJobKey(jobKey)
|
||||
setCollectorError(null)
|
||||
@@ -918,33 +844,10 @@ function DataCollectionContent() {
|
||||
}
|
||||
},
|
||||
runNowLoading: runNowJobKey === jobKey,
|
||||
saveBusy: schedulerSaveBusy || !settingsReady,
|
||||
saveBusy: schedulerSaveBusy || !writable(settingsKey),
|
||||
}
|
||||
})
|
||||
}, [
|
||||
settingsReady,
|
||||
apiFetch,
|
||||
certRenewIntervalDraft,
|
||||
draftCertRenewEnabled,
|
||||
draftInternetPathEnabled,
|
||||
draftPingEnabled,
|
||||
draftResourcesEnabled,
|
||||
draftServersApiEnabled,
|
||||
draftSpeedEnabled,
|
||||
draftTrafficEnabled,
|
||||
handleJobEnabledChange,
|
||||
internetPathIntervalDraft,
|
||||
loadCollectors,
|
||||
runNowJobKey,
|
||||
settingsReads,
|
||||
schedulerJobsByKey,
|
||||
schedulerSaveBusy,
|
||||
serversApiIntervalDraft,
|
||||
trafficIntervalDraft,
|
||||
uptimeIntervalDraft,
|
||||
uptimeResourceIntervalDraft,
|
||||
uptimeSpeedIntervalDraft,
|
||||
])
|
||||
})()
|
||||
|
||||
const enabledJobsCount = schedulerStatus?.jobs.filter(job => job.enabled).length ?? 0
|
||||
const schedulerJobCount = schedulerStatus?.jobs.length ?? 0
|
||||
@@ -997,13 +900,13 @@ function DataCollectionContent() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!isLive || pageBusy}
|
||||
disabled={!isLive || pageBusy || schedulerSaveBusy}
|
||||
onClick={() => { void loadCollectors() }}
|
||||
>
|
||||
<RefreshCwIcon className={cn("size-4", pageBusy && "animate-spin")} />
|
||||
Обновить
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={!isLive || pageBusy} onClick={() => { void handleRefreshScheduler() }}>
|
||||
<Button variant="outline" size="sm" disabled={!isLive || pageBusy || schedulerSaveBusy} onClick={() => { void handleRefreshScheduler() }}>
|
||||
Перечитать таймеры
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1047,10 +950,11 @@ function DataCollectionContent() {
|
||||
{value.error ? <Alert variant="warning" className="mt-1"><AlertCircleIcon aria-hidden="true" /><AlertDescription>{value.error}{value.known ? ". Сохранены последние подтверждённые данные." : ""}</AlertDescription></Alert> : null}
|
||||
</div>
|
||||
))}
|
||||
{isLive && saveMessage ? <p role="status" className="text-sm text-muted-foreground">{saveMessage}</p> : null}
|
||||
{isLive && collectorError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>Ошибка загрузки статуса коллекторов</AlertTitle>
|
||||
<AlertTitle>Операция не завершена</AlertTitle>
|
||||
<AlertDescription>{collectorError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -1100,8 +1004,8 @@ function DataCollectionContent() {
|
||||
<Input
|
||||
value={settingsReads.traffic.known ? trafficRetentionDraft : ""}
|
||||
aria-label="Хранение трафика"
|
||||
disabled={!settingsReads.traffic.known || schedulerSaveBusy}
|
||||
onChange={(e) => { dirtySettings.current.add("traffic"); setTrafficRetentionDraft(e.target.value) }}
|
||||
disabled={!writable("traffic") || schedulerSaveBusy}
|
||||
onChange={(e) => { markDirty("traffic", "retentionDays"); setTrafficRetentionDraft(e.target.value) }}
|
||||
className="h-8 text-sm"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
@@ -1111,8 +1015,8 @@ function DataCollectionContent() {
|
||||
<Input
|
||||
value={settingsReads.uptime.known ? uptimeRetentionDraft : ""}
|
||||
aria-label="Хранение uptime"
|
||||
disabled={!settingsReads.uptime.known || schedulerSaveBusy}
|
||||
onChange={(e) => { dirtySettings.current.add("uptime"); setUptimeRetentionDraft(e.target.value) }}
|
||||
disabled={!writable("uptime") || schedulerSaveBusy}
|
||||
onChange={(e) => { markDirty("uptime", "retentionDays"); setUptimeRetentionDraft(e.target.value) }}
|
||||
className="h-8 text-sm"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
@@ -1123,10 +1027,10 @@ function DataCollectionContent() {
|
||||
value={settingsReads.cert.known ? renewBeforeDaysDraft : ""}
|
||||
aria-label="Обновлять сертификаты за дней"
|
||||
|
||||
onChange={(e) => { dirtySettings.current.add("cert"); setRenewBeforeDaysDraft(e.target.value) }}
|
||||
onChange={(e) => { markDirty("cert", "renewBeforeDays"); setRenewBeforeDaysDraft(e.target.value) }}
|
||||
className="h-8 text-sm"
|
||||
inputMode="numeric"
|
||||
disabled={!settingsReads.cert.known || !draftCertRenewEnabled || schedulerSaveBusy}
|
||||
disabled={!writable("cert") || schedulerSaveBusy}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground mt-1">
|
||||
Вкл/выкл автообновления MM — также на странице «Сертификаты». Не включайте вместе со встроенным ACME RouterOS.
|
||||
@@ -1138,19 +1042,8 @@ function DataCollectionContent() {
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={schedulerSaveBusy || !settingsReady}
|
||||
onClick={async () => {
|
||||
setSchedulerSaveBusy(true)
|
||||
setCollectorError(null)
|
||||
try {
|
||||
await persistSchedulerDrafts()
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось сохранить")
|
||||
} finally {
|
||||
setSchedulerSaveBusy(false)
|
||||
}
|
||||
}}
|
||||
disabled={schedulerSaveBusy || dirtyCount === 0}
|
||||
onClick={() => { void persistSchedulerDrafts() }}
|
||||
>
|
||||
{schedulerSaveBusy ? <LoaderCircleIcon className="size-4 animate-spin mr-2" /> : null}
|
||||
Сохранить настройки планировщика
|
||||
@@ -1228,6 +1121,7 @@ function DataCollectionContent() {
|
||||
id="run-filter"
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-xs min-w-[180px]"
|
||||
value={runFilterJobKey}
|
||||
disabled={schedulerSaveBusy}
|
||||
onChange={(e) => { setRunFilterJobKey(e.target.value); setOpenRunIds(new Set()); setSchedulerRuns([]); setRunsKnown(false); setRunsError(null); setRunsReceivedAt(null) }}
|
||||
>
|
||||
<option value="">Все</option>
|
||||
|
||||
Reference in new issue
Block a user