From dda00fbd23785510ccf7a00ae273ed4c683a359c Mon Sep 17 00:00:00 2001 From: Jason Staack Date: Thu, 12 Mar 2026 23:20:24 -0500 Subject: [PATCH] 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 --- frontend/src/components/config/DiffViewer.tsx | 72 +++++++++++++++++++ frontend/src/lib/api.ts | 17 +++++ 2 files changed, 89 insertions(+) create mode 100644 frontend/src/components/config/DiffViewer.tsx diff --git a/frontend/src/components/config/DiffViewer.tsx b/frontend/src/components/config/DiffViewer.tsx new file mode 100644 index 0000000..c64af73 --- /dev/null +++ b/frontend/src/components/config/DiffViewer.tsx @@ -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 ( +
+ {/* Header */} +
+
+

Config Diff

+ {diff && ( + + +{diff.lines_added} + / + -{diff.lines_removed} + + )} +
+ +
+ + {/* Content */} + {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ ) : isError || !diff ? ( +
+ No diff available. +
+ ) : ( +
+
+ {diff.diff_text.split('\n').map((line, i) => ( +
+ {line || '\u00A0'} +
+ ))} +
+
+ )} +
+ ) +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 40f04c9..787ba88 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -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( + `/api/tenants/${tenantId}/devices/${deviceId}/config/${snapshotId}/diff`, + ) + .then((r) => r.data), } // ─── VPN (WireGuard) ────────────────────────────────────────────────────────