feat(config-revisions): implement configuration history for Firewall, GRE, and WireGuard
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 2m20s
Docker images / frontend-image (push) Successful in 2m51s
Docker images / updater-image (push) Successful in 44s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 12s
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 2m20s
Docker images / frontend-image (push) Successful in 2m51s
Docker images / updater-image (push) Successful in 44s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 12s
- Added configuration history management for Firewall, GRE, and WireGuard pages, enabling users to view and restore previous configurations. - Introduced new components for displaying configuration history and integrated them into the respective pages. - Enhanced API routes to support fetching and restoring configuration revisions, ensuring data consistency across the application. - Updated state management to handle loading and restoring states effectively, improving user experience during data operations. - Enhanced tests to cover new functionalities and ensure reliability. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -49,12 +49,14 @@ import {
|
||||
PowerIcon, CheckCircleIcon,
|
||||
PlayIcon, SquareIcon, RotateCcwIcon, ZapIcon,
|
||||
CheckCircle2Icon, XCircleIcon, MinusCircleIcon, SkipForwardIcon,
|
||||
SlidersHorizontalIcon, RefreshCwIcon,
|
||||
SlidersHorizontalIcon, RefreshCwIcon, HistoryIcon,
|
||||
} from "lucide-react"
|
||||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||||
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
|
||||
import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-rail"
|
||||
import { toast } from "sonner"
|
||||
import { ConfigHistorySheet } from "@/components/config-history-sheet"
|
||||
import type { ConfigRevisionDto } from "@/lib/config-revisions"
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1947,6 +1949,10 @@ function FirewallPageInner() {
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
const [editingAddr, setEditingAddr] = useState<Partial<AddressListEntry> | null>(null)
|
||||
const [addrSheetOpen, setAddrSheetOpen] = useState(false)
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [revisions, setRevisions] = useState<ConfigRevisionDto[]>([])
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
const [historyRestoring, setHistoryRestoring] = useState(false)
|
||||
|
||||
const loadLive = useCallback(async () => {
|
||||
if (!isLive) return
|
||||
@@ -1970,6 +1976,42 @@ function FirewallPageInner() {
|
||||
}
|
||||
}, [isLive, apiFetch])
|
||||
|
||||
const historyServerId = selectedServerId === ALL_SERVERS_ID ? null : selectedServerId
|
||||
|
||||
const loadRevisions = useCallback(async () => {
|
||||
if (!isLive || !historyServerId) return
|
||||
setHistoryLoading(true)
|
||||
try {
|
||||
const res = await apiFetch<{ revisions: ConfigRevisionDto[] }>(
|
||||
`/api/firewall/revisions?serverId=${encodeURIComponent(historyServerId)}`,
|
||||
)
|
||||
setRevisions(res.revisions)
|
||||
} catch (err) {
|
||||
toast.error("Не удалось загрузить историю", { description: String(err) })
|
||||
setRevisions([])
|
||||
} finally {
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
}, [isLive, historyServerId, apiFetch])
|
||||
|
||||
const restoreRevision = useCallback(async (id: string) => {
|
||||
if (!isLive || !historyServerId) return
|
||||
setHistoryRestoring(true)
|
||||
try {
|
||||
await apiFetch(
|
||||
`/api/firewall/revisions/${encodeURIComponent(id)}/restore`,
|
||||
{ method: "POST", body: JSON.stringify({ serverId: historyServerId }) },
|
||||
)
|
||||
toast.success("Версия применена на роутер")
|
||||
await loadLive()
|
||||
await loadRevisions()
|
||||
} catch (err) {
|
||||
toast.error("Не удалось откатить", { description: String(err) })
|
||||
} finally {
|
||||
setHistoryRestoring(false)
|
||||
}
|
||||
}, [isLive, historyServerId, apiFetch, loadLive, loadRevisions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
queueMicrotask(() => {
|
||||
@@ -2373,6 +2415,19 @@ function FirewallPageInner() {
|
||||
<RefreshCwIcon className={cn("size-4", dataLoading && "animate-spin")} />
|
||||
Обновить
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setHistoryOpen(true)
|
||||
void loadRevisions()
|
||||
}}
|
||||
disabled={!isLive || !historyServerId || dataLoading}
|
||||
title={!historyServerId ? "Выберите сервер, чтобы смотреть историю" : "История версий и откат на CHR"}
|
||||
>
|
||||
<HistoryIcon className="size-4" />
|
||||
История
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setExportOpen(true)}>
|
||||
<CodeXmlIcon className="size-4" />Экспорт .rsc
|
||||
</Button>
|
||||
@@ -2586,6 +2641,17 @@ function FirewallPageInner() {
|
||||
onClose={() => setExportOpen(false)}
|
||||
rules={familyRules}
|
||||
/>
|
||||
|
||||
<ConfigHistorySheet
|
||||
open={historyOpen}
|
||||
onOpenChange={setHistoryOpen}
|
||||
title="История Firewall"
|
||||
itemLabel="объектов"
|
||||
revisions={revisions}
|
||||
loading={historyLoading}
|
||||
restoring={historyRestoring}
|
||||
onRestore={restoreRevision}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user