feat(08-01): add diff viewer component and API client
- Add DiffResponse interface and getDiff method to configHistoryApi - Create DiffViewer component with unified diff rendering - Green highlighting for added lines, red for removed lines - Blue styling for hunk headers, loading skeleton, error state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
72
frontend/src/components/config/DiffViewer.tsx
Normal file
72
frontend/src/components/config/DiffViewer.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { X } from 'lucide-react'
|
||||
import { configHistoryApi } from '@/lib/api'
|
||||
|
||||
interface DiffViewerProps {
|
||||
tenantId: string
|
||||
deviceId: string
|
||||
snapshotId: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function classifyLine(line: string): string {
|
||||
if (line.startsWith('@@')) return 'bg-blue-900/20 text-blue-300'
|
||||
if (line.startsWith('+++') || line.startsWith('---')) return 'text-text-muted'
|
||||
if (line.startsWith('+')) return 'bg-green-900/30 text-green-300'
|
||||
if (line.startsWith('-')) return 'bg-red-900/30 text-red-300'
|
||||
return 'text-text-primary'
|
||||
}
|
||||
|
||||
export function DiffViewer({ tenantId, deviceId, snapshotId, onClose }: DiffViewerProps) {
|
||||
const { data: diff, isLoading, isError } = useQuery({
|
||||
queryKey: ['config-diff', tenantId, deviceId, snapshotId],
|
||||
queryFn: () => configHistoryApi.getDiff(tenantId, deviceId, snapshotId),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Config Diff</h3>
|
||||
{diff && (
|
||||
<span className="text-xs font-mono">
|
||||
<span className="text-success">+{diff.lines_added}</span>
|
||||
<span className="text-text-muted mx-0.5">/</span>
|
||||
<span className="text-error">-{diff.lines_removed}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-elevated transition-colors text-text-muted hover:text-text-primary"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${60 + Math.random() * 40}%` }} />
|
||||
))}
|
||||
</div>
|
||||
) : isError || !diff ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<span className="text-xs text-text-muted">No diff available.</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-auto max-h-[60vh] rounded border border-border bg-background">
|
||||
<div className="font-mono text-xs whitespace-pre">
|
||||
{diff.diff_text.split('\n').map((line, i) => (
|
||||
<div key={i} className={`px-3 py-0.5 ${classifyLine(line)}`}>
|
||||
{line || '\u00A0'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -981,6 +981,16 @@ export interface ConfigChangeEntry {
|
||||
snapshot_id: string
|
||||
}
|
||||
|
||||
export interface DiffResponse {
|
||||
id: string
|
||||
diff_text: string
|
||||
lines_added: number
|
||||
lines_removed: number
|
||||
old_snapshot_id: string
|
||||
new_snapshot_id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export const configHistoryApi = {
|
||||
list: (tenantId: string, deviceId: string, limit = 50, offset = 0) =>
|
||||
api
|
||||
@@ -989,6 +999,13 @@ export const configHistoryApi = {
|
||||
{ params: { limit, offset } },
|
||||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
getDiff: (tenantId: string, deviceId: string, snapshotId: string) =>
|
||||
api
|
||||
.get<DiffResponse>(
|
||||
`/api/tenants/${tenantId}/devices/${deviceId}/config/${snapshotId}/diff`,
|
||||
)
|
||||
.then((r) => r.data),
|
||||
}
|
||||
|
||||
// ─── VPN (WireGuard) ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user