Skip to content

Commit 5cba75a

Browse files
authored
Split view for diff (#19)
* Split view for diff * Fix wrapping problem
1 parent afaf250 commit 5cba75a

File tree

3 files changed

+234
-18
lines changed

3 files changed

+234
-18
lines changed

frontend/app/components/diff-viewer.tsx

Lines changed: 229 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import type React from "react"
44

55
import { useEffect, useState } from "react"
66
import { Button } from "~/components/ui/button"
7-
import {X, ChevronDown, ChevronRight, Shell, CheckCircle2} from "lucide-react"
7+
import {X, ChevronDown, ChevronRight, Shell, CheckCircle2, ArrowLeftRight} from "lucide-react"
88
import axios from "axios";
99
import {Switch} from "~/components/ui/switch";
1010
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
1111
import { Badge } from "~/components/ui/badge"
12-
import Markdown from 'react-markdown'
1312

1413
interface FileData {
1514
name: string
@@ -105,6 +104,7 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
105104
const [activeTab, setActiveTab] = useState("diff")
106105
const [isLargeDiff, setIsLargeDiff] = useState(false)
107106
const [showLargeDiff, setShowLargeDiff] = useState(false)
107+
const [isSideBySide, setIsSideBySide] = useState(false)
108108

109109
useEffect(() => {
110110
let result
@@ -306,6 +306,187 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
306306
)
307307
}
308308

309+
const renderSideBySideDiffTree = (node: DiffNode, level = 0) => {
310+
const indent = level * 20
311+
const isExpanded = expandedNodes.has(node.path)
312+
const hasChildren = node.children && node.children.length > 0
313+
314+
// Determine if this is a primitive value or an object/array
315+
const isPrimitive =
316+
(typeof node.value1 !== "object" || node.value1 === null || Array.isArray(node.value1)) &&
317+
(typeof node.value2 !== "object" || node.value2 === null || Array.isArray(node.value2))
318+
319+
const isArray = Array.isArray(node.value1) || Array.isArray(node.value2)
320+
321+
// Determine if this node or any of its children have changes
322+
const hasChanges =
323+
node.type !== "unchanged" ||
324+
(node.children &&
325+
node.children.some(
326+
(child) =>
327+
child.type !== "unchanged" ||
328+
(child.children && child.children.some((grandchild) => grandchild.type !== "unchanged")),
329+
))
330+
331+
// Skip rendering unchanged nodes based on settings
332+
if ((showUnchanged || (level === 1 && !hasChanges)) && node.type === "unchanged") {
333+
return null
334+
}
335+
336+
// Determine what to show on each side
337+
const showLeft = node.type !== "added"
338+
const showRight = node.type !== "removed"
339+
340+
return (
341+
<div key={node.path} className="relative">
342+
{/* Render the node itself */}
343+
<div className="flex items-start">
344+
{/* Left side (old) */}
345+
<div
346+
className={`break-all flex-1 flex items-start rounded-sm ${
347+
node.type === "removed" ? "bg-red-50" : node.type === "changed" ? "bg-amber-50" : ""
348+
}`}
349+
style={{ visibility: showLeft ? "visible" : "hidden" }}
350+
>
351+
<div style={{ paddingLeft: `${indent}px` }} className="flex items-start w-full">
352+
{/* Expand/collapse button for objects/arrays */}
353+
{hasChildren && (
354+
<button onClick={(e) => toggleNode(node.path, e)} className="mr-1 p-1 hover:bg-gray-200 rounded">
355+
{isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
356+
</button>
357+
)}
358+
359+
{/* Key name */}
360+
<div
361+
className={`font-mono py-1 pr-2 flex-shrink-0 ${
362+
node.type === "removed" ? "text-red-800" : node.type === "changed" ? "text-amber-800" : ""
363+
}`}
364+
>
365+
{node.type === "removed" && "- "}
366+
{node.key}
367+
{hasChildren && !isPrimitive && ": {"}
368+
{!hasChildren && !isPrimitive && node.type === "unchanged" && ": {}"}
369+
</div>
370+
371+
{/* Value for primitive types */}
372+
{isPrimitive && (
373+
<div className="flex flex-col w-full">
374+
{(node.type === "removed" || node.type === "changed" || node.type === "unchanged") && !isArray && (
375+
<div
376+
className={`${
377+
node.type === "removed"
378+
? "text-red-800"
379+
: node.type === "changed"
380+
? "text-red-800 bg-red-50"
381+
: ""
382+
} py-1 px-2 rounded font-mono`}
383+
>
384+
{formatValue(node.value1)}
385+
</div>
386+
)}
387+
</div>
388+
)}
389+
</div>
390+
</div>
391+
392+
{/* Divider */}
393+
<div className="w-4"></div>
394+
395+
{/* Right side (new) */}
396+
<div
397+
className={`break-all flex-1 flex items-start rounded-sm ${
398+
node.type === "added" ? "bg-green-50" : node.type === "changed" ? "bg-amber-50" : ""
399+
}`}
400+
style={{ visibility: showRight ? "visible" : "hidden" }}
401+
>
402+
<div style={{ paddingLeft: `${indent}px` }} className="flex items-start w-full">
403+
{/* Expand/collapse button for objects/arrays */}
404+
{hasChildren && (
405+
<button onClick={(e) => toggleNode(node.path, e)} className="mr-1 p-1 hover:bg-gray-200 rounded">
406+
{isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
407+
</button>
408+
)}
409+
410+
{/* Key name */}
411+
<div
412+
className={`font-mono py-1 pr-2 flex-shrink-0 ${
413+
node.type === "added" ? "text-green-800" : node.type === "changed" ? "text-amber-800" : ""
414+
}`}
415+
>
416+
{node.type === "added" && "+ "}
417+
{node.key}
418+
{hasChildren && !isPrimitive && ": {"}
419+
{!hasChildren && !isPrimitive && node.type === "unchanged" && ": {}"}
420+
</div>
421+
422+
{/* Value for primitive types */}
423+
{isPrimitive && (
424+
<div className="flex flex-col w-full">
425+
{(node.type === "added" || node.type === "changed" || node.type === "unchanged") && !isArray && (
426+
<div
427+
className={`${
428+
node.type === "added"
429+
? "text-green-800"
430+
: node.type === "changed"
431+
? "text-green-800 bg-green-50"
432+
: ""
433+
} py-1 px-2 rounded font-mono`}
434+
>
435+
{formatValue(node.type === "added" || node.type === "unchanged" ? node.value2 : node.value2)}
436+
</div>
437+
)}
438+
</div>
439+
)}
440+
</div>
441+
</div>
442+
</div>
443+
444+
{/* Render children if expanded */}
445+
{hasChildren && isExpanded && (
446+
<div>
447+
{node.children!.map((child, index) =>
448+
renderSideBySideDiffTree(child, level + 1),
449+
)}
450+
{!isPrimitive && (
451+
<div className="flex">
452+
{/* Left side closing brace */}
453+
<div className="flex-1">
454+
{showLeft && (
455+
<div
456+
className={`font-mono py-1 ${
457+
node.type === "removed" ? "text-red-800" : node.type === "changed" ? "text-amber-800" : ""
458+
}`}
459+
style={{ paddingLeft: `${indent}px` }}
460+
>
461+
{node.type === "removed" && "- "}
462+
{"}"}
463+
</div>
464+
)}
465+
</div>
466+
{/* Divider */}
467+
<div className="w-4"></div>
468+
{/* Right side closing brace */}
469+
<div className="flex-1">
470+
{showRight && (
471+
<div
472+
className={`font-mono py-1 ${
473+
node.type === "added" ? "text-green-800" : node.type === "changed" ? "text-amber-800" : ""
474+
}`}
475+
style={{ paddingLeft: `${indent}px` }}
476+
>
477+
{node.type === "added" && "+ "}
478+
{"}"}
479+
</div>
480+
)}
481+
</div>
482+
</div>
483+
)}
484+
</div>
485+
)}
486+
</div>
487+
)
488+
}
489+
309490

310491
const renderApiChangesView = () => {
311492
if (!apiChanges) {
@@ -742,9 +923,25 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
742923
</div>
743924
) : !!diffTree ? (
744925
<div className="text-sm">
745-
{diffTree.children?.map((child, _) =>
746-
renderDiffTree(child, 1),
747-
)}
926+
<div className="text-sm">
927+
{/* Render header for side-by-side view */}
928+
{isSideBySide && (
929+
<div className="flex mb-2 font-medium text-sm">
930+
<div className="flex-1 px-2 py-1 bg-gray-100 rounded-t">Old Version</div>
931+
<div className="w-4"></div>
932+
<div className="flex-1 px-2 py-1 bg-gray-100 rounded-t">New Version</div>
933+
</div>
934+
)}
935+
936+
{/* Render the appropriate diff view based on the toggle */}
937+
{isSideBySide
938+
? diffTree.children?.map((child, index) =>
939+
renderSideBySideDiffTree(child, 1),
940+
)
941+
: diffTree.children?.map((child, index) =>
942+
renderDiffTree(child, 1),
943+
)}
944+
</div>
748945
</div>
749946
) : (
750947
<div className="flex items-center justify-center h-full">
@@ -861,19 +1058,34 @@ export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
8611058
)}
8621059
</div>
8631060
<div className="p-4 border-t flex justify-between items-center">
864-
{activeTab === "diff" && (
1061+
8651062
<div className="flex items-center space-x-2">
866-
<Switch
867-
checked={showUnchanged}
868-
onCheckedChange={(e) => setShowUnchanged(e)}
869-
id="hide-unchanged"
870-
/>
871-
<label htmlFor="hide-unchanged" className="text-sm font-medium cursor-pointer">
872-
Hide unchanged nodes
873-
</label>
874-
</div>)}
875-
{(activeTab === "metrics" || activeTab === "api" || activeTab === "ai") && <div></div>}
876-
<Button onClick={onClose}>Close</Button>
1063+
{activeTab === "diff" && (
1064+
<>
1065+
<Switch
1066+
checked={showUnchanged}
1067+
onCheckedChange={(e) => setShowUnchanged(e)}
1068+
id="hide-unchanged"
1069+
/>
1070+
<label htmlFor="hide-unchanged" className="text-sm font-medium cursor-pointer">
1071+
Hide unchanged nodes
1072+
</label>
1073+
</>
1074+
)}
1075+
</div>
1076+
1077+
{/* View toggle button */}
1078+
<div className="flex justify-end mb-4 gap-2">
1079+
{(activeTab === "diff") && <Button
1080+
onClick={() => setIsSideBySide(!isSideBySide)}
1081+
>
1082+
<ArrowLeftRight className="h-4 w-4" />
1083+
{isSideBySide ? "Unified View" : "Side-by-Side View"}
1084+
</Button>
1085+
}
1086+
1087+
<Button onClick={onClose}>Close</Button>
1088+
</div>
8771089
</div>
8781090
</div>
8791091
</div>

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
<dependency>
6565
<groupId>com.apiprotector</groupId>
6666
<artifactId>api-protector-core</artifactId>
67-
<version>1.0.2-SNAPSHOT</version>
67+
<version>1.0.3-SNAPSHOT</version>
6868
</dependency>
6969
</dependencies>
7070

src/main/resources/application.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
spring.application.name=apiprotector
2+
server.tomcat.max-http-form-post-size=40MB
3+
spring.servlet.multipart.max-file-size=40MB
4+
spring.servlet.multipart.max-request-size=40MB
5+
26

37
gemini.api.key=${GEMINI_API_KEY}
48
gemini.api.baseurl=https://generativelanguage.googleapis.com/v1beta/models/

0 commit comments

Comments
 (0)