fix(ui): сохранять недоступные срезы Firewall
This commit is contained in:
1 parent
59440555cd
commit
5e6057842f
11 files changed
+228
-77
No files matched your search
@@ -21,7 +21,7 @@ import {
|
||||
type Server,
|
||||
} from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { firewallResponseSchema, firewallServersSchema, type FirewallBackendServer } from "@/lib/firewall-response"
|
||||
import { firewallResponseSchema, firewallServersSchema, mergeFirewallRows, type FirewallFailure, type FirewallBackendServer } from "@/lib/firewall-response"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { Frame, FramePanel } from "@/components/reui/frame"
|
||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||
@@ -792,7 +792,7 @@ function AddressListsTab({
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground ml-auto">{filtered.length} записей</span>
|
||||
<Button size="sm" onClick={onAdd}><PlusIcon className="size-4" />Добавить</Button>
|
||||
<Button size="sm" disabled={mutationsDisabled} onClick={onAdd}><PlusIcon className="size-4" />Добавить</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -805,7 +805,7 @@ function AddressListsTab({
|
||||
<span className="text-sm font-mono font-medium">{listName}</span>
|
||||
<span className="text-xs text-muted-foreground">({listEntries.length})</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs gap-1" onClick={() => onAddToList(listName)}>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs gap-1" disabled={mutationsDisabled} onClick={() => onAddToList(listName)}>
|
||||
<PlusIcon className="size-3" />Адрес
|
||||
</Button>
|
||||
</div>
|
||||
@@ -834,7 +834,7 @@ function AddressListsTab({
|
||||
</Button>
|
||||
} />
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuItem onClick={() => onEdit(e)}><PencilIcon className="size-4" />Редактировать</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={mutationsDisabled} onClick={() => onEdit(e)}><PencilIcon className="size-4" />Редактировать</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={mutationsDisabled} onClick={() => onToggle(e)}>
|
||||
<PowerIcon className="size-4" />{e.disabled ? "Включить" : "Отключить"}
|
||||
</DropdownMenuItem>
|
||||
@@ -1930,6 +1930,7 @@ function FirewallPageInner() {
|
||||
const [dataLoading, setDataLoading] = useState(false)
|
||||
const [dataError, setDataError] = useState<string | null>(null)
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
const [failures, setFailures] = useState<FirewallFailure[]>([])
|
||||
const loadEpoch = useRef(0)
|
||||
useEffect(() => () => { loadEpoch.current += 1 }, [backendUrl, isLive])
|
||||
const [busy, setBusy] = useState(false)
|
||||
@@ -1980,8 +1981,10 @@ function FirewallPageInner() {
|
||||
const firewall = firewallResponseSchema.safeParse(fw)
|
||||
if (!servers.success || !firewall.success) throw new Error("Сервер вернул некорректные данные Firewall")
|
||||
setLiveServers(servers.data.map(mapBackendToServer))
|
||||
setLiveRules(firewall.data.rules)
|
||||
setLiveAddrLists(firewall.data.addressLists)
|
||||
const enabledIds = new Set(servers.data.filter(server => server.enabled).map(server => String(server.id)))
|
||||
setLiveRules(previous => mergeFirewallRows(previous, firewall.data.rules, firewall.data.failures, enabledIds))
|
||||
setLiveAddrLists(previous => mergeFirewallRows(previous, firewall.data.addressLists, firewall.data.failures, enabledIds, true))
|
||||
setFailures(firewall.data.failures)
|
||||
setHasLoaded(true)
|
||||
} catch (e) {
|
||||
if (epoch !== loadEpoch.current) return
|
||||
@@ -2017,6 +2020,7 @@ function FirewallPageInner() {
|
||||
const restoreRevision = useCallback(async (id: string) => {
|
||||
if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live")
|
||||
if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall")
|
||||
if (failures.some(f => f.serverId === historyServerId)) throw new Error("Обновите полный набор данных устройства перед восстановлением")
|
||||
mutationLock.current = true
|
||||
loadEpoch.current += 1
|
||||
setDataLoading(false)
|
||||
@@ -2036,7 +2040,7 @@ function FirewallPageInner() {
|
||||
mutationLock.current = false
|
||||
setHistoryRestoring(false)
|
||||
}
|
||||
}, [isLive, historyServerId, apiFetch, loadLive, loadRevisions])
|
||||
}, [isLive, historyServerId, apiFetch, loadLive, loadRevisions, failures])
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
@@ -2044,6 +2048,7 @@ function FirewallPageInner() {
|
||||
setLiveRules([])
|
||||
setLiveAddrLists([])
|
||||
setHasLoaded(false)
|
||||
setFailures([])
|
||||
setDataError(null)
|
||||
setDataLoading(false)
|
||||
if (isLive) void loadLive()
|
||||
@@ -2138,6 +2143,8 @@ function FirewallPageInner() {
|
||||
return counts
|
||||
}, [groupRules])
|
||||
|
||||
const scopedFailures = isLive ? failures.filter(f => (selectedServerId === ALL_SERVERS_ID || f.serverId === selectedServerId) && (!f.family || f.family === ipFamily)) : []
|
||||
const firewallIncomplete = scopedFailures.length > 0
|
||||
const totalEnabled = familyRules.filter((r) => r.enabled).length
|
||||
const totalHits = familyRules.reduce((s, r) => s + r.hits, 0)
|
||||
const dropRules = familyRules.filter((r) => r.action === "drop" || r.action === "reject").length
|
||||
@@ -2151,7 +2158,7 @@ function FirewallPageInner() {
|
||||
|
||||
async function toggleRule(id: string) {
|
||||
const r = displayRules.find((x) => x.id === id)
|
||||
if (!r || mutationLock.current || busy || historyRestoring) return
|
||||
if (!r || mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
if (isLive) {
|
||||
const key = ruleKey(r)
|
||||
if (!key) return
|
||||
@@ -2181,13 +2188,13 @@ function FirewallPageInner() {
|
||||
}
|
||||
|
||||
function requestDelete(target: NonNullable<typeof pendingDelete>) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
setDeleteError(null)
|
||||
setPendingDelete(target)
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!pendingDelete || mutationLock.current || busy || historyRestoring) return
|
||||
if (!pendingDelete || mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
const target = pendingDelete
|
||||
mutationLock.current = true
|
||||
loadEpoch.current += 1
|
||||
@@ -2225,7 +2232,7 @@ function FirewallPageInner() {
|
||||
}, [filteredRules])
|
||||
|
||||
async function reorderRules(activeId: string, overId: string) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
const source = isLive ? liveRules : mockRules
|
||||
const next = reorderWithinGroup(source, activeId, overId)
|
||||
if (!next) {
|
||||
@@ -2273,7 +2280,7 @@ function FirewallPageInner() {
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
setFormError(null)
|
||||
if (!requireWriteServer()) return
|
||||
const table = tableOfGroup(chainGroup)
|
||||
@@ -2285,14 +2292,14 @@ function FirewallPageInner() {
|
||||
}
|
||||
|
||||
function openEdit(r: FirewallRule) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
setFormError(null)
|
||||
setEditingRule(r)
|
||||
setRuleSheetOpen(true)
|
||||
}
|
||||
|
||||
async function saveRule(payload: RuleWritePayload) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
const table = (editingRule?.table ?? tableOfGroup(chainGroup) ?? "filter") as FirewallTable
|
||||
const family = editingRule?.id ? ruleFamily(editingRule as FirewallRule) : ipFamily
|
||||
const serverId = editingRule?.serverId ?? requireWriteServer()
|
||||
@@ -2361,7 +2368,7 @@ function FirewallPageInner() {
|
||||
}
|
||||
|
||||
function openAddAddr(list?: string) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
setFormError(null)
|
||||
if (!requireWriteServer()) return
|
||||
setEditingAddr(list ? { list } : null)
|
||||
@@ -2369,14 +2376,14 @@ function FirewallPageInner() {
|
||||
}
|
||||
|
||||
function openEditAddr(e: AddressListEntry) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
setFormError(null)
|
||||
setEditingAddr(e)
|
||||
setAddrSheetOpen(true)
|
||||
}
|
||||
|
||||
async function toggleAddr(e: AddressListEntry) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
if (isLive) {
|
||||
const key = addressKey(e)
|
||||
if (!key) return
|
||||
@@ -2412,7 +2419,7 @@ function FirewallPageInner() {
|
||||
timeout?: string
|
||||
disabled?: boolean
|
||||
}) {
|
||||
if (mutationLock.current || busy || historyRestoring) return
|
||||
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
|
||||
const family = editingAddr?.id ? addressFamily(editingAddr as AddressListEntry) : ipFamily
|
||||
const serverId = editingAddr?.serverId ?? requireWriteServer()
|
||||
if (!serverId) return
|
||||
@@ -2511,7 +2518,7 @@ function FirewallPageInner() {
|
||||
setHistoryOpen(true)
|
||||
void loadRevisions()
|
||||
}}
|
||||
disabled={!isLive || !historyServerId || dataLoading || busy || togglePending || deletePending || movePending || historyRestoring}
|
||||
disabled={!isLive || !historyServerId || dataLoading || busy || togglePending || deletePending || movePending || historyRestoring || failures.some(f => f.serverId === historyServerId)}
|
||||
title={!historyServerId ? "Выберите сервер, чтобы смотреть историю" : "История версий и откат на CHR"}
|
||||
>
|
||||
<HistoryIcon className="size-4" />
|
||||
@@ -2520,7 +2527,7 @@ function FirewallPageInner() {
|
||||
<Button variant="outline" size="sm" onClick={() => setExportOpen(true)}>
|
||||
<CodeXmlIcon className="size-4" />Экспорт .rsc
|
||||
</Button>
|
||||
<Button size="sm" onClick={openAdd}>
|
||||
<Button size="sm" disabled={firewallIncomplete} onClick={openAdd}>
|
||||
<PlusIcon className="size-4" />Новое правило
|
||||
</Button>
|
||||
</>
|
||||
@@ -2550,6 +2557,11 @@ function FirewallPageInner() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{firewallIncomplete && <div role="alert" className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3 text-sm">
|
||||
<p className="font-medium">Не все срезы Firewall получены</p>
|
||||
<p>Сохранены последние данные недоступных срезов; их состояние неизвестно. Обновите данные перед изменениями.</p>
|
||||
{scopedFailures.map((failure, index) => <p key={index}>{[failure.serverName || failure.serverId, failure.family, failure.table, failure.error].filter(Boolean).join(" · ")}</p>)}
|
||||
</div>}
|
||||
{moveStatus && <p role="status" className="text-sm text-muted-foreground">{moveStatus}</p>}
|
||||
{moveError && <p role="alert" className="text-sm text-destructive">{moveError}</p>}
|
||||
{toggleStatus && <p role="status" className="text-sm text-muted-foreground">{toggleStatus}</p>}
|
||||
@@ -2560,28 +2572,28 @@ function FirewallPageInner() {
|
||||
{
|
||||
id: "rules",
|
||||
label: "Всего правил",
|
||||
value: isLive && !hasLoaded ? "—" : familyRules.length,
|
||||
value: isLive && (!hasLoaded || firewallIncomplete) ? "—" : familyRules.length,
|
||||
icon: <ShieldIcon className="size-4" />,
|
||||
iconClassName: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
label: "Активных",
|
||||
value: isLive && !hasLoaded ? "—" : totalEnabled,
|
||||
value: isLive && (!hasLoaded || firewallIncomplete) ? "—" : totalEnabled,
|
||||
icon: <CheckCircleIcon className="size-4" />,
|
||||
iconClassName: "text-success",
|
||||
},
|
||||
{
|
||||
id: "drop",
|
||||
label: "Блокирующих",
|
||||
value: isLive && !hasLoaded ? "—" : dropRules,
|
||||
value: isLive && (!hasLoaded || firewallIncomplete) ? "—" : dropRules,
|
||||
icon: <ShieldOffIcon className="size-4" />,
|
||||
iconClassName: "text-destructive",
|
||||
},
|
||||
{
|
||||
id: "hits",
|
||||
label: "Срабатываний",
|
||||
value: isLive && !hasLoaded ? "—" : fmtHits(totalHits),
|
||||
value: isLive && (!hasLoaded || firewallIncomplete) ? "—" : fmtHits(totalHits),
|
||||
icon: <ListFilterIcon className="size-4" />,
|
||||
iconClassName: "text-info",
|
||||
},
|
||||
@@ -2612,12 +2624,12 @@ function FirewallPageInner() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLive && !hasLoaded ? (
|
||||
{isLive && (!hasLoaded || (firewallIncomplete && (chainGroup === "address-lists" ? familyAddrLists.length === 0 : filteredRules.length === 0))) ? (
|
||||
<p role="status" className="text-sm text-muted-foreground">{dataLoading ? "Загрузка данных Firewall…" : "Данные Firewall пока недоступны. Повторите загрузку."}</p>
|
||||
) : chainGroup === "address-lists" ? (
|
||||
<AddressListsTab
|
||||
entries={familyAddrLists}
|
||||
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring}
|
||||
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring || firewallIncomplete}
|
||||
onAdd={() => openAddAddr()}
|
||||
onAddToList={(list) => openAddAddr(list)}
|
||||
onToggle={(e) => { void toggleAddr(e) }}
|
||||
@@ -2659,11 +2671,11 @@ function FirewallPageInner() {
|
||||
|
||||
<FirewallRulesDataGrid
|
||||
rules={filteredRules}
|
||||
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring}
|
||||
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring || firewallIncomplete}
|
||||
onToggle={(id) => { void toggleRule(id) }}
|
||||
onEdit={openEdit}
|
||||
onDelete={(item) => requestDelete({ kind: "rule", item })}
|
||||
onReorder={canReorder && !togglePending && !deletePending && !movePending && !busy && !historyRestoring ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined}
|
||||
onReorder={canReorder && !togglePending && !deletePending && !movePending && !busy && !historyRestoring && !firewallIncomplete ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined}
|
||||
showServer={showServerCol}
|
||||
/>
|
||||
</DataPageCard>
|
||||
@@ -2758,7 +2770,7 @@ function FirewallPageInner() {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deletePending}>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" disabled={deletePending || togglePending || busy || historyRestoring} onClick={e => { e.preventDefault(); void confirmDelete() }}>Удалить</AlertDialogAction>
|
||||
<AlertDialogAction variant="destructive" disabled={deletePending || togglePending || busy || historyRestoring || firewallIncomplete} onClick={e => { e.preventDefault(); void confirmDelete() }}>Удалить</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
Reference in new issue
Block a user