feat(08-02): add snapshot download button to config history timeline
- Add SnapshotResponse interface and getSnapshot API method - Add deviceName prop to ConfigHistorySection - Add download handler that fetches snapshot and triggers .rsc file download - Add Download icon button on each timeline entry with stopPropagation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { History } from 'lucide-react'
|
import { Download, History } from 'lucide-react'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { TableSkeleton } from '@/components/ui/page-skeleton'
|
import { TableSkeleton } from '@/components/ui/page-skeleton'
|
||||||
import { configHistoryApi } from '@/lib/api'
|
import { configHistoryApi } from '@/lib/api'
|
||||||
@@ -9,6 +9,7 @@ import { DiffViewer } from './DiffViewer'
|
|||||||
interface ConfigHistorySectionProps {
|
interface ConfigHistorySectionProps {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
deviceId: string
|
deviceId: string
|
||||||
|
deviceName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeTime(isoDate: string): string {
|
function formatRelativeTime(isoDate: string): string {
|
||||||
@@ -41,8 +42,22 @@ function LineDelta({ added, removed }: { added: number; removed: number }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigHistorySection({ tenantId, deviceId }: ConfigHistorySectionProps) {
|
export function ConfigHistorySection({ tenantId, deviceId, deviceName }: ConfigHistorySectionProps) {
|
||||||
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | null>(null)
|
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
async function handleDownload(snapshotId: string, collectedAt: string) {
|
||||||
|
const snapshot = await configHistoryApi.getSnapshot(tenantId, deviceId, snapshotId)
|
||||||
|
const timestamp = new Date(collectedAt).toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
||||||
|
const filename = `router-${deviceName}-${timestamp}.rsc`
|
||||||
|
const blob = new Blob([snapshot.config_text], { type: 'text/plain' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
const { data: changes, isLoading } = useQuery({
|
const { data: changes, isLoading } = useQuery({
|
||||||
queryKey: ['config-history', tenantId, deviceId],
|
queryKey: ['config-history', tenantId, deviceId],
|
||||||
queryFn: () => configHistoryApi.list(tenantId, deviceId),
|
queryFn: () => configHistoryApi.list(tenantId, deviceId),
|
||||||
@@ -102,6 +117,18 @@ export function ConfigHistorySection({ tenantId, deviceId }: ConfigHistorySectio
|
|||||||
{formatRelativeTime(entry.created_at)}
|
{formatRelativeTime(entry.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Download button */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleDownload(entry.snapshot_id, entry.created_at)
|
||||||
|
}}
|
||||||
|
className="p-1 rounded hover:bg-elevated text-text-muted hover:text-text-primary transition-colors"
|
||||||
|
title="Download .rsc"
|
||||||
|
>
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -991,6 +991,13 @@ export interface DiffResponse {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SnapshotResponse {
|
||||||
|
id: string
|
||||||
|
config_text: string
|
||||||
|
sha256_hash: string
|
||||||
|
collected_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export const configHistoryApi = {
|
export const configHistoryApi = {
|
||||||
list: (tenantId: string, deviceId: string, limit = 50, offset = 0) =>
|
list: (tenantId: string, deviceId: string, limit = 50, offset = 0) =>
|
||||||
api
|
api
|
||||||
@@ -1006,6 +1013,13 @@ export const configHistoryApi = {
|
|||||||
`/api/tenants/${tenantId}/devices/${deviceId}/config/${snapshotId}/diff`,
|
`/api/tenants/${tenantId}/devices/${deviceId}/config/${snapshotId}/diff`,
|
||||||
)
|
)
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
getSnapshot: (tenantId: string, deviceId: string, snapshotId: string) =>
|
||||||
|
api
|
||||||
|
.get<SnapshotResponse>(
|
||||||
|
`/api/tenants/${tenantId}/devices/${deviceId}/config/${snapshotId}`,
|
||||||
|
)
|
||||||
|
.then((r) => r.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── VPN (WireGuard) ────────────────────────────────────────────────────────
|
// ─── VPN (WireGuard) ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -649,7 +649,7 @@ function DeviceDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Configuration History */}
|
{/* Configuration History */}
|
||||||
<ConfigHistorySection tenantId={tenantId} deviceId={deviceId} />
|
<ConfigHistorySection tenantId={tenantId} deviceId={deviceId} deviceName={device.hostname} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
alertsContent={
|
alertsContent={
|
||||||
|
|||||||
Reference in New Issue
Block a user