From 995742ff7af7c0755f541e9f2524bcae5c2265b0 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 1 Jan 2025 18:52:21 +0200 Subject: [PATCH 01/39] check for multi edges on create instead per render --- app/components/graphView.tsx | 21 ------------ app/components/model.ts | 66 +++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index b754fd82..66d75a2d 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -221,20 +221,7 @@ export default function GraphView({ if (!start.x || !start.y || !end.x || !end.y) return - const sameNodesLinks = graph.Elements.links.filter(l => (l.source.id === start.id && l.target.id === end.id) || (l.target.id === start.id && l.source.id === end.id)) - const index = sameNodesLinks.findIndex(l => l.id === link.id) || 0 - const even = index % 2 === 0 - let curve - if (start.id === end.id) { - if (even) { - curve = Math.floor(-(index / 2)) - 3 - } else { - curve = Math.floor((index + 1) / 2) + 2 - } - - link.curve = curve * 0.1 - const radius = NODE_SIZE * link.curve * 6.2; const angleOffset = -Math.PI / 4; // 45 degrees offset for text alignment const textX = start.x + radius * Math.cos(angleOffset); @@ -244,14 +231,6 @@ export default function GraphView({ ctx.translate(textX, textY); ctx.rotate(-angleOffset); } else { - if (even) { - curve = Math.floor(-(index / 2)) - } else { - curve = Math.floor((index + 1) / 2) - } - - link.curve = curve * 0.1 - const midX = (start.x + end.x) / 2 + (end.y - start.y) * (link.curve / 2); const midY = (start.y + end.y) / 2 + (start.x - end.x) * (link.curve / 2); diff --git a/app/components/model.ts b/app/components/model.ts index 44fd658d..23e18959 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -52,7 +52,7 @@ const COLORS_ORDER = [ "#80E6E6", ] -export function getCategoryColorValue(index: number): string { +export function getCategoryColorValue(index: number = 0): string { return COLORS_ORDER[index % COLORS_ORDER.length] } @@ -176,13 +176,41 @@ export class Graph { return } - let sourceId = edgeData.src_node; - let destinationId = edgeData.dest_node + let source = this.nodesMap.get(edgeData.src_node) + let target = this.nodesMap.get(edgeData.dest_node) + + if (!source) { + source = { + id: edgeData.src_node, + name: edgeData.src_node, + color: getCategoryColorValue(), + category: "", + expand: false, + visible: true, + collapsed, + isPath: !!path, + isPathSelected: path?.start?.id === edgeData.src_node || path?.end?.id === edgeData.src_node + } + } + + if (!target) { + target = { + id: edgeData.dest_node, + name: edgeData.dest_node, + color: getCategoryColorValue(), + category: "", + expand: false, + visible: true, + collapsed, + isPath: !!path, + isPathSelected: path?.start?.id === edgeData.dest_node || path?.end?.id === edgeData.dest_node + } + } link = { id: edgeData.id, - source: sourceId, - target: destinationId, + source, + target, label: edgeData.relation, visible: true, expand: false, @@ -196,6 +224,34 @@ export class Graph { newElements.links.push(link) }) + newElements.links.forEach(link => { + const start = link.source + const end = link.target + const sameNodesLinks = this.Elements.links.filter(l => (l.source.id === start.id && l.target.id === end.id) || (l.target.id === start.id && l.source.id === end.id)) + const index = sameNodesLinks.findIndex(l => l.id === link.id) || 0 + const even = index % 2 === 0 + let curve + + if (start.id === end.id) { + if (even) { + curve = Math.floor(-(index / 2)) - 3 + } else { + curve = Math.floor((index + 1) / 2) + 2 + } + } else { + console.log(link.curve) + if (even) { + curve = Math.floor(-(index / 2)) + } else { + curve = Math.floor((index + 1) / 2) + } + + } + + link.curve = curve * 0.1 + }) + + return newElements } From 3cf7586f51fa75d3208ecf3354a8d27d43cbc5a9 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 6 Jan 2025 14:43:58 +0200 Subject: [PATCH 02/39] commit --- tailwind.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tailwind.config.js b/tailwind.config.js index 14d51792..060cc1d9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -79,6 +79,7 @@ module.exports = { animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + 'spin': 'spin 1s linear', }, }, }, From bde842e8ae84c045672a787ce59a49a6f4d52f66 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 6 Jan 2025 18:06:11 +0200 Subject: [PATCH 03/39] commit --- app/components/graphView.tsx | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 66d75a2d..f1030806 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -78,18 +78,7 @@ export default function GraphView({ setSelectedObjects([]) } - const handelNodeClick = (node: Node, evt: MouseEvent) => { - if (isShowPath) { - setPath(prev => { - if (!prev?.start?.name || (prev.end?.name && prev.end?.name !== "")) { - return ({ start: { id: Number(node.id), name: node.name } }) - } else { - return ({ end: { id: Number(node.id), name: node.name }, start: prev.start }) - } - }) - return - } - + const handelNodeRightClick = (node: Node, evt: MouseEvent) => { if (evt.ctrlKey) { if (selectedObjects.some(obj => obj.id === node.id)) { setSelectedObjects(selectedObjects.filter(obj => obj.id !== node.id)) @@ -111,8 +100,20 @@ export default function GraphView({ setSelectedPathId(link.id) } - const handelNodeRightClick = async (node: Node) => { + const handelNodeClick = async (node: Node) => { + if (isShowPath) { + setPath(prev => { + if (!prev?.start?.name || (prev.end?.name && prev.end?.name !== "")) { + return ({ start: { id: Number(node.id), name: node.name } }) + } else { + return ({ end: { id: Number(node.id), name: node.name }, start: prev.start }) + } + }) + return + } + const expand = !node.expand + if (expand) { const elements = await onFetchNode([node.id]) @@ -121,6 +122,7 @@ export default function GraphView({ title: `No neighbors found`, description: `No neighbors found`, }) + return } } else { From 6efd86154ee55bc0a5c44e451aaf1d30db8a11a1 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 8 Jan 2025 13:56:00 +0200 Subject: [PATCH 04/39] expand only on the seconde click --- app/components/graphView.tsx | 54 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index f1030806..4eb81d44 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -61,7 +61,7 @@ export default function GraphView({ }: Props) { const parentRef = useRef(null) - + const lastClick = useRef(new Date()) useEffect(() => { setCooldownTime(4000) setCooldownTicks(undefined) @@ -101,7 +101,35 @@ export default function GraphView({ } const handelNodeClick = async (node: Node) => { - if (isShowPath) { + const now = new Date() + + const isDoubleClick = now.getTime() - lastClick.current.getTime() < 1000 + lastClick.current = now + + if (isDoubleClick) { + const expand = !node.expand + + if (expand) { + const elements = await onFetchNode([node.id]) + + if (elements.nodes.length === 0) { + toast({ + title: `No neighbors found`, + description: `No neighbors found`, + }) + + return + } + } else { + deleteNeighbors([node]); + } + + node.expand = expand + + setSelectedObj(undefined) + setData({ ...graph.Elements }) + + } else if (isShowPath) { setPath(prev => { if (!prev?.start?.name || (prev.end?.name && prev.end?.name !== "")) { return ({ start: { id: Number(node.id), name: node.name } }) @@ -111,28 +139,6 @@ export default function GraphView({ }) return } - - const expand = !node.expand - - if (expand) { - const elements = await onFetchNode([node.id]) - - if (elements.nodes.length === 0) { - toast({ - title: `No neighbors found`, - description: `No neighbors found`, - }) - - return - } - } else { - deleteNeighbors([node]); - } - - node.expand = expand - - setSelectedObj(undefined) - setData({ ...graph.Elements }) } return ( From 9e2cf1ffca0e53967046dbec16b3f0c048f3f271 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 8 Jan 2025 15:23:46 +0200 Subject: [PATCH 05/39] fix zoom on search and in path --- app/components/chat.tsx | 18 ++++++++++++-- app/components/code-graph.tsx | 44 ++++++++++++++++++----------------- app/components/model.ts | 5 ++-- app/page.tsx | 1 + 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index f11f2476..5a861f8f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -9,6 +9,7 @@ import { cn } from "@/lib/utils"; import { TypeAnimation } from "react-type-animation"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { prepareArg } from "../utils"; +import { NodeObject } from "react-force-graph-2d"; type PathData = { nodes: any[] @@ -84,6 +85,7 @@ interface Props { isPathResponse: boolean | undefined setIsPathResponse: (isPathResponse: boolean | undefined) => void setData: Dispatch> + chartRef: any } const SUGGESTIONS = [ @@ -105,7 +107,7 @@ const RemoveLastPath = (messages: Message[]) => { return messages } -export function Chat({ repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData }: Props) { +export function Chat({ repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData, chartRef }: Props) { // Holds the messages in the chat const [messages, setMessages] = useState([]); @@ -131,7 +133,6 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons const p = paths.find((path) => [...path.links, ...path.nodes].some((e: any) => e.id === selectedPathId)) if (!p) return - handelSetSelectedPath(p) }, [selectedPathId]) @@ -154,6 +155,9 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons }, [isPathResponse]) const handelSetSelectedPath = (p: PathData) => { + const chart = chartRef.current + + if (!chart) return setSelectedPath(prev => { if (prev) { if (isPathResponse && paths.some((path) => [...path.nodes, ...path.links].every((e: any) => [...prev.nodes, ...prev.links].some((e: any) => e.id === e.id)))) { @@ -206,6 +210,9 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons }); } setData({ ...graph.Elements }) + setTimeout(() => { + chart.zoomToFit(1000, 150, (n: NodeObject) => p.nodes.some(node => node.id === n.id)); + }, 0) } // A function that handles the change event of the url input box @@ -262,6 +269,10 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons } const handleSubmit = async () => { + const chart = chartRef.current + + if (!chart) return + setSelectedPath(undefined) if (!path?.start?.id || !path.end?.id) return @@ -297,6 +308,9 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons setPath(undefined) setIsPathResponse(true) setData({ ...graph.Elements }) + setTimeout(() => { + chart.zoomToFit(1000, 150, (n: NodeObject) => formattedPaths.some(p => p.nodes.some(node => node.id === n.id))); + }, 0) } const getTip = (disabled = false) => diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 83d27c88..0704da1f 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -14,6 +14,7 @@ import { Checkbox } from '@/components/ui/checkbox'; import dynamic from 'next/dynamic'; import { Position } from "./graphView"; import { prepareArg } from '../utils'; +import { NodeObject } from "react-force-graph-2d"; const GraphView = dynamic(() => import('./graphView')); @@ -208,34 +209,35 @@ export function CodeGraph({ nodes.forEach((node) => { node.expand = expand }) - + setSelectedObj(undefined) setData({ ...graph.Elements }) } const handelSearchSubmit = (node: any) => { - const n = { name: node.properties.name, id: node.id } - - let chartNode = graph.Elements.nodes.find(n => n.id == node.id) - - if (!chartNode?.visible) { - if (!chartNode) { - chartNode = graph.extend({ nodes: [node], edges: [] }).nodes[0] - } else { - chartNode.visible = true - setCooldownTicks(undefined) - setCooldownTime(1000) - } - graph.visibleLinks(true, [chartNode.id]) - } - - setSearchNode(n) - setData({ ...graph.Elements }) - const chart = chartRef.current - + if (chart) { - chart.centerAt(chartNode.x, chartNode.y, 1000); + const n = { name: node.properties.name, id: node.id } + + let chartNode = graph.Elements.nodes.find(n => n.id == node.id) + + if (!chartNode?.visible) { + if (!chartNode) { + chartNode = graph.extend({ nodes: [node], edges: [] }).nodes[0] + } else { + chartNode.visible = true + setCooldownTicks(undefined) + setCooldownTime(1000) + } + graph.visibleLinks(true, [chartNode!.id]) + setData({ ...graph.Elements }) + } + + setSearchNode(n) + setTimeout(() => { + chart.zoomToFit(1000, 150, (n: NodeObject) => n.id === chartNode!.id); + }, 0) } } diff --git a/app/components/model.ts b/app/components/model.ts index 23e18959..080f7984 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -191,6 +191,7 @@ export class Graph { isPath: !!path, isPathSelected: path?.start?.id === edgeData.src_node || path?.end?.id === edgeData.src_node } + this.nodesMap.set(edgeData.src_node, source) } if (!target) { @@ -205,6 +206,7 @@ export class Graph { isPath: !!path, isPathSelected: path?.start?.id === edgeData.dest_node || path?.end?.id === edgeData.dest_node } + this.nodesMap.set(edgeData.dest_node, target) } link = { @@ -228,7 +230,7 @@ export class Graph { const start = link.source const end = link.target const sameNodesLinks = this.Elements.links.filter(l => (l.source.id === start.id && l.target.id === end.id) || (l.target.id === start.id && l.source.id === end.id)) - const index = sameNodesLinks.findIndex(l => l.id === link.id) || 0 + const index = sameNodesLinks.findIndex(l => l.id === link.id) ?? 0 const even = index % 2 === 0 let curve @@ -239,7 +241,6 @@ export class Graph { curve = Math.floor((index + 1) / 2) + 2 } } else { - console.log(link.curve) if (even) { curve = Math.floor(-(index / 2)) } else { diff --git a/app/page.tsx b/app/page.tsx index a58f1a56..ef7fa374 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -281,6 +281,7 @@ export default function Home() { Date: Sun, 19 Jan 2025 11:39:14 +0200 Subject: [PATCH 06/39] commit --- app/components/chat.tsx | 6 ++--- app/components/code-graph.tsx | 12 +++++----- app/components/elementMenu.tsx | 12 +++++----- app/components/graphView.tsx | 43 ++++++++++++++++++++++++---------- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 5a861f8f..6e2c127d 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -133,7 +133,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons const p = paths.find((path) => [...path.links, ...path.nodes].some((e: any) => e.id === selectedPathId)) if (!p) return - handelSetSelectedPath(p) + handleSetSelectedPath(p) }, [selectedPathId]) // Scroll to the bottom of the chat on new message @@ -154,7 +154,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons setPaths([]) }, [isPathResponse]) - const handelSetSelectedPath = (p: PathData) => { + const handleSetSelectedPath = (p: PathData) => { const chart = chartRef.current if (!chart) return @@ -439,7 +439,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons setIsPathResponse(undefined) } - handelSetSelectedPath(p) + handleSetSelectedPath(p) }} >

#{i + 1}

diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 0704da1f..7a99062d 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -82,7 +82,7 @@ export function CodeGraph({ const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Delete') { if (selectedObj && selectedObjects.length === 0) return - handelRemove([...selectedObjects.map(obj => obj.id), selectedObj?.id].filter(id => id !== undefined)); + handleRemove([...selectedObjects.map(obj => obj.id), selectedObj?.id].filter(id => id !== undefined)); } }; @@ -214,7 +214,7 @@ export function CodeGraph({ setData({ ...graph.Elements }) } - const handelSearchSubmit = (node: any) => { + const handleSearchSubmit = (node: any) => { const chart = chartRef.current if (chart) { @@ -241,7 +241,7 @@ export function CodeGraph({ } } - const handelRemove = (ids: number[]) => { + const handleRemove = (ids: number[]) => { graph.Elements.nodes.forEach(node => { if (!ids.includes(node.id)) return node.visible = false @@ -274,7 +274,7 @@ export function CodeGraph({ value={searchNode.name} onValueChange={({ name }) => setSearchNode({ name })} icon={} - handleSubmit={handelSearchSubmit} + handleSubmit={handleSearchSubmit} node={searchNode} /> @@ -354,10 +354,10 @@ export function CodeGraph({ setPath(path) setSelectedObj(undefined) }} - handleRemove={handelRemove} + handleRemove={handleRemove} position={position} url={url} - handelExpand={handleExpand} + handleExpand={handleExpand} parentRef={containerRef} /> void; position: Position | undefined; url: string; - handelExpand: (nodes: Node[], expand: boolean) => void; + handleExpand: (nodes: Node[], expand: boolean) => void; parentRef: RefObject; } -export default function ElementMenu({ obj, objects, setPath, handleRemove, position, url, handelExpand, parentRef }: Props) { +export default function ElementMenu({ obj, objects, setPath, handleRemove, position, url, handleExpand, parentRef }: Props) { const [currentObj, setCurrentObj] = useState(); const [containerWidth, setContainerWidth] = useState(0); @@ -68,13 +68,13 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit @@ -114,13 +114,13 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 4eb81d44..33d6f63d 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -1,7 +1,7 @@ import ForceGraph2D from 'react-force-graph-2d'; import { Graph, GraphData, Link, Node } from './model'; -import { Dispatch, RefObject, SetStateAction, useEffect, useRef } from 'react'; +import { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'; import { toast } from '@/components/ui/use-toast'; import { Path } from '../page'; @@ -61,7 +61,23 @@ export default function GraphView({ }: Props) { const parentRef = useRef(null) - const lastClick = useRef(new Date()) + const lastClick = useRef<{ date: Date, name: string }>({ date: new Date(), name: "" }) + const [parentWidth, setParentWidth] = useState(0) + const [parentHeight, setParentHeight] = useState(0) + + useEffect(() => { + if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return + chartRef.current.d3Force('link').id((link: any) => link.id).distance(50) + chartRef.current.d3Force('charge').strength(-300) + chartRef.current.d3Force('center').strength(0.05) + }, [chartRef, data.links.length, data.nodes.length]) + + useEffect(() => { + if (!parentRef.current) return + setParentWidth(parentRef.current.clientWidth) + setParentHeight(parentRef.current.clientHeight) + }, [parentRef.current?.clientWidth, parentRef.current?.clientHeight]) + useEffect(() => { setCooldownTime(4000) setCooldownTicks(undefined) @@ -78,7 +94,7 @@ export default function GraphView({ setSelectedObjects([]) } - const handelNodeRightClick = (node: Node, evt: MouseEvent) => { + const handleNodeRightClick = (node: Node, evt: MouseEvent) => { if (evt.ctrlKey) { if (selectedObjects.some(obj => obj.id === node.id)) { setSelectedObjects(selectedObjects.filter(obj => obj.id !== node.id)) @@ -94,18 +110,19 @@ export default function GraphView({ setPosition({ x: evt.clientX, y: evt.clientY }) } - const handelLinkClick = (link: Link, evt: MouseEvent) => { + const handleLinkClick = (link: Link, evt: MouseEvent) => { unsetSelectedObjects(evt) if (!isPathResponse || link.id === selectedPathId) return setSelectedPathId(link.id) } - const handelNodeClick = async (node: Node) => { + const handleNodeClick = async (node: Node) => { const now = new Date() + const { date, name } = lastClick.current + + const isDoubleClick = now.getTime() - date.getTime() < 1000 && name === node.name + lastClick.current = { date: now, name: node.name } - const isDoubleClick = now.getTime() - lastClick.current.getTime() < 1000 - lastClick.current = now - if (isDoubleClick) { const expand = !node.expand @@ -145,8 +162,8 @@ export default function GraphView({
setPosition(prev => { return prev && { x: prev.x + translate.x * chartRef.current.zoom(), y: prev.y + translate.y * chartRef.current.zoom() } })} - onNodeRightClick={handelNodeRightClick} - onLinkClick={handelLinkClick} + onNodeRightClick={handleNodeRightClick} + onLinkClick={handleLinkClick} onBackgroundRightClick={unsetSelectedObjects} onBackgroundClick={unsetSelectedObjects} onZoom={() => unsetSelectedObjects()} From 27b592021a24b130ebb1f9e31d0d33bdd8354fb6 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Sun, 19 Jan 2025 12:16:34 +0200 Subject: [PATCH 07/39] add image capture on canvas --- app/components/code-graph.tsx | 54 ++++++++++++++++++++++++++++++++++- package-lock.json | 45 +++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7a99062d..e7ff56d6 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -3,7 +3,7 @@ import { GraphData, Node } from "./model"; import { GraphContext } from "./provider"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { GitFork, Search, X } from "lucide-react"; +import { Download, GitFork, Search, X } from "lucide-react"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; import { toast } from '@/components/ui/use-toast'; @@ -15,6 +15,7 @@ import dynamic from 'next/dynamic'; import { Position } from "./graphView"; import { prepareArg } from '../utils'; import { NodeObject } from "react-force-graph-2d"; +import html2canvas from 'html2canvas'; const GraphView = dynamic(() => import('./graphView')); @@ -252,6 +253,51 @@ export function CodeGraph({ setData({ ...graph.Elements }) } + const handleDownloadImage = async () => { + try { + if (!containerRef.current) { + toast({ + variant: "destructive", + title: "Error", + description: "Graph container not found", + }); + return; + } + + // Wait for any pending renders/animations to complete + await new Promise(resolve => setTimeout(resolve, 500)); + + const canvas = await html2canvas(containerRef.current, { + useCORS: true, + allowTaint: true, + logging: false, + backgroundColor: '#ffffff', + scale: 10, // Increase quality + onclone: (clonedDoc) => { + // Ensure the cloned element maintains proper dimensions + const clonedElement = clonedDoc.querySelector('[data-name="canvas-info-panel"]') as HTMLElement; + if (clonedElement) { + clonedElement.style.position = 'absolute'; + clonedElement.style.bottom = '0'; + } + } + }); + + const dataURL = canvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = dataURL; + link.download = `${graphName || 'graph'}.png`; + link.click(); + } catch (error) { + console.error('Error downloading graph image:', error); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to download image. Please try again.", + }); + } + }; + return (
@@ -345,6 +391,12 @@ export function CodeGraph({ className="pointer-events-auto" chartRef={chartRef} /> +
= 0.6.0" + } + }, "node_modules/bezier-js": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", @@ -3642,6 +3651,14 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5238,6 +5255,18 @@ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -7711,6 +7740,14 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -7999,6 +8036,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b9cf75f9..e8674026 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", + "html2canvas": "^1.4.1", "lucide-react": "^0.441.0", "next": "^15.1.2", "playwright": "^1.49.1", From 08ef5ec4aa298cdc839adf37b918ae557e9ee8b5 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Sun, 19 Jan 2025 12:19:49 +0200 Subject: [PATCH 08/39] change img to svg --- app/components/code-graph.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index e7ff56d6..53cee0c5 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -283,10 +283,10 @@ export function CodeGraph({ } }); - const dataURL = canvas.toDataURL('image/png'); + const dataURL = canvas.toDataURL('image/svg+xml'); const link = document.createElement('a'); link.href = dataURL; - link.download = `${graphName || 'graph'}.png`; + link.download = `${graphName || 'graph'}.svg`; link.click(); } catch (error) { console.error('Error downloading graph image:', error); From 04c5ab642ce02c2e7051b5a956eb19c3845a7703 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Sun, 19 Jan 2025 17:20:49 +0200 Subject: [PATCH 09/39] commit --- app/components/code-graph.tsx | 38 +++++++++-------------------------- package-lock.json | 6 ++++++ package.json | 1 + 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 53cee0c5..7fca147d 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -217,12 +217,12 @@ export function CodeGraph({ const handleSearchSubmit = (node: any) => { const chart = chartRef.current - + if (chart) { const n = { name: node.properties.name, id: node.id } let chartNode = graph.Elements.nodes.find(n => n.id == node.id) - + if (!chartNode?.visible) { if (!chartNode) { chartNode = graph.extend({ nodes: [node], edges: [] }).nodes[0] @@ -234,7 +234,7 @@ export function CodeGraph({ graph.visibleLinks(true, [chartNode!.id]) setData({ ...graph.Elements }) } - + setSearchNode(n) setTimeout(() => { chart.zoomToFit(1000, 150, (n: NodeObject) => n.id === chartNode!.id); @@ -255,45 +255,27 @@ export function CodeGraph({ const handleDownloadImage = async () => { try { - if (!containerRef.current) { + const canvas = document.querySelector('.force-graph-container canvas') as HTMLCanvasElement; + if (!canvas) { toast({ - variant: "destructive", title: "Error", - description: "Graph container not found", + description: "Canvas not found", + variant: "destructive", }); return; } - // Wait for any pending renders/animations to complete - await new Promise(resolve => setTimeout(resolve, 500)); - - const canvas = await html2canvas(containerRef.current, { - useCORS: true, - allowTaint: true, - logging: false, - backgroundColor: '#ffffff', - scale: 10, // Increase quality - onclone: (clonedDoc) => { - // Ensure the cloned element maintains proper dimensions - const clonedElement = clonedDoc.querySelector('[data-name="canvas-info-panel"]') as HTMLElement; - if (clonedElement) { - clonedElement.style.position = 'absolute'; - clonedElement.style.bottom = '0'; - } - } - }); - - const dataURL = canvas.toDataURL('image/svg+xml'); + const dataURL = canvas.toDataURL('image/webp'); const link = document.createElement('a'); link.href = dataURL; - link.download = `${graphName || 'graph'}.svg`; + link.download = `${graphName}.webp`; link.click(); } catch (error) { console.error('Error downloading graph image:', error); toast({ - variant: "destructive", title: "Error", description: "Failed to download image. Please try again.", + variant: "destructive", }); } }; diff --git a/package-lock.json b/package-lock.json index 8858b426..f0a0eed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-tooltip": "^1.1.4", "@types/react-gtm-module": "^2.0.4", "autoprefixer": "^10.4.20", + "canvas2svg": "^1.0.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "html2canvas": "^1.4.1", @@ -3466,6 +3467,11 @@ "node": ">=12" } }, + "node_modules/canvas2svg": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/canvas2svg/-/canvas2svg-1.0.16.tgz", + "integrity": "sha512-r3ryHprzDOtAsFuczw+/DKkLR3XexwIlJWnJ+71I9QF7V9scYaV5JZgYDoCUlYtT3ARnOpDcm/hDNZYbWMRHqA==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/package.json b/package.json index e8674026..776bc54d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-tooltip": "^1.1.4", "@types/react-gtm-module": "^2.0.4", "autoprefixer": "^10.4.20", + "canvas2svg": "^1.0.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "html2canvas": "^1.4.1", From 39142bbec640665ea27c38e451ae28463faebf01 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 11:51:39 +0200 Subject: [PATCH 10/39] commit --- app/components/code-graph.tsx | 9 +-- app/components/dataPanel.tsx | 127 ++++++++++++++++++++++----------- app/components/elementMenu.tsx | 81 ++++++++++++--------- app/components/graphView.tsx | 23 ++++-- app/components/model.ts | 2 - package-lock.json | 39 +++++++--- package.json | 1 + 7 files changed, 189 insertions(+), 93 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7a99062d..2dc2d44e 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -1,5 +1,5 @@ import { Dispatch, RefObject, SetStateAction, useContext, useEffect, useRef, useState } from "react"; -import { GraphData, Node } from "./model"; +import { GraphData, Link, Node } from "./model"; import { GraphContext } from "./provider"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; @@ -21,7 +21,7 @@ const GraphView = dynamic(() => import('./graphView')); interface Props { data: GraphData, setData: Dispatch>, - onFetchGraph: (graphName: string) => void, + onFetchGraph: (graphName: string) => Promise, onFetchNode: (nodeIds: number[]) => Promise, options: string[] setOptions: Dispatch> @@ -55,7 +55,7 @@ export function CodeGraph({ let graph = useContext(GraphContext) const [url, setURL] = useState(""); - const [selectedObj, setSelectedObj] = useState(); + const [selectedObj, setSelectedObj] = useState(); const [selectedObjects, setSelectedObjects] = useState([]); const [position, setPosition] = useState(); const [graphName, setGraphName] = useState(""); @@ -145,9 +145,10 @@ export function CodeGraph({ } run() + }, [graphName]) - function handleSelectedValue(value: string) { + async function handleSelectedValue(value: string) { setGraphName(value) onFetchGraph(value) } diff --git a/app/components/dataPanel.tsx b/app/components/dataPanel.tsx index 07269cd1..21469cb5 100644 --- a/app/components/dataPanel.tsx +++ b/app/components/dataPanel.tsx @@ -1,17 +1,19 @@ -import { Dispatch, SetStateAction, useRef, useEffect, useState } from "react"; -import { Node } from "./model"; +import { Dispatch, SetStateAction } from "react"; +import { JSONTree } from 'react-json-tree'; +import { Link, Node } from "./model"; import { Copy, SquareArrowOutUpRight, X } from "lucide-react"; import SyntaxHighlighter from 'react-syntax-highlighter'; import { dark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; interface Props { - obj: Node | undefined; - setObj: Dispatch>; + obj: Node | Link | undefined; + setObj: Dispatch>; url: string; } const excludedProperties = [ "category", + "label", "color", "expand", "collapsed", @@ -19,7 +21,10 @@ const excludedProperties = [ "isPathStartEnd", "visible", "index", + "curve", "__indexColor", + "isPathSelected", + "__controlPoints", "x", "y", "vx", @@ -30,20 +35,16 @@ const excludedProperties = [ export default function DataPanel({ obj, setObj, url }: Props) { - const containerRef = useRef(null); - const [containerHeight, setContainerHeight] = useState(0); - - useEffect(() => { - if (containerRef.current) { - setContainerHeight(containerRef.current.clientHeight); - } - }, [containerRef.current]); + debugger if (!obj) return null; - const label = `${obj.category}: ${obj.name}` + const type = "category" in obj + const label = type ? `${obj.category}: ${obj.name}` : obj.label const object = Object.entries(obj).filter(([k]) => !excludedProperties.includes(k)) + console.log(obj) + return (
@@ -52,7 +53,7 @@ export default function DataPanel({ obj, setObj, url }: Props) {
-
+
{ object.map(([key, value]) => (
@@ -73,40 +74,86 @@ export default function DataPanel({ obj, setObj, url }: Props) { > {value} - :

{value}

+ : typeof value === "object" ? + !excludedProperties.includes(k)))} + theme={{ + base00: '#343434', // background + base01: '#000000', + base02: '#CE9178', + base03: '#CE9178', // open values + base04: '#CE9178', + base05: '#CE9178', + base06: '#CE9178', + base07: '#CE9178', + base08: '#CE9178', + base09: '#b5cea8', // numbers + base0A: '#CE9178', + base0B: '#CE9178', // close values + base0C: '#CE9178', + base0D: '#99E4E5', // * keys + base0E: '#ae81ff', + base0F: '#cc6633' + }} + valueRenderer={(valueAsString, value, keyPath) => { + if (keyPath === "src") { + return + {value as string} + + } + return {value as string} + }} + /> + : {value} }
)) }
) diff --git a/app/components/elementMenu.tsx b/app/components/elementMenu.tsx index e1451f8a..82775c35 100644 --- a/app/components/elementMenu.tsx +++ b/app/components/elementMenu.tsx @@ -1,14 +1,14 @@ "use client" import { Dispatch, RefObject, SetStateAction, useEffect, useState } from "react"; -import { Node } from "./model"; +import { Link, Node } from "./model"; import { ChevronsLeftRight, Copy, EyeOff, Globe, Maximize2, Minimize2, Waypoints } from "lucide-react"; import DataPanel from "./dataPanel"; import { Path } from "../page"; import { Position } from "./graphView"; interface Props { - obj: Node | undefined; + obj: Node | Link | undefined; objects: Node[]; setPath: Dispatch>; handleRemove: (nodes: number[]) => void; @@ -20,7 +20,7 @@ interface Props { export default function ElementMenu({ obj, objects, setPath, handleRemove, position, url, handleExpand, parentRef }: Props) { - const [currentObj, setCurrentObj] = useState(); + const [currentObj, setCurrentObj] = useState(); const [containerWidth, setContainerWidth] = useState(0); useEffect(() => { @@ -80,13 +80,18 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit : <> - + { + "category" in obj && + <> + + + } - { - window.open(objURL, '_blank'); - }} - > - - + { + "category" in obj && + <> + { + window.open(objURL, '_blank'); + }} + > + + + + } - - + { + "category" in obj && + <> + + + + } } diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 33d6f63d..7a93b1f0 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -15,8 +15,8 @@ interface Props { setData: Dispatch> graph: Graph chartRef: RefObject - selectedObj: Node | undefined - setSelectedObj: Dispatch> + selectedObj: Node | Link | undefined + setSelectedObj: Dispatch> selectedObjects: Node[] setSelectedObjects: Dispatch> setPosition: Dispatch> @@ -65,6 +65,16 @@ export default function GraphView({ const [parentWidth, setParentWidth] = useState(0) const [parentHeight, setParentHeight] = useState(0) + useEffect(() => { + const timeout = setTimeout(() => { + chartRef.current?.zoomToFit(1000, 40) + }, 1000) + + return () => { + clearTimeout(timeout) + } + }, [data.nodes.length, data.links.length, chartRef]) + useEffect(() => { if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return chartRef.current.d3Force('link').id((link: any) => link.id).distance(50) @@ -94,13 +104,13 @@ export default function GraphView({ setSelectedObjects([]) } - const handleNodeRightClick = (node: Node, evt: MouseEvent) => { - if (evt.ctrlKey) { + const handleRightClick = (node: Node | Link, evt: MouseEvent) => { + if (evt.ctrlKey && "category" in node) { if (selectedObjects.some(obj => obj.id === node.id)) { setSelectedObjects(selectedObjects.filter(obj => obj.id !== node.id)) return } else { - setSelectedObjects([...selectedObjects, node]) + setSelectedObjects([...selectedObjects, node as Node]) } } else { setSelectedObjects([]) @@ -283,7 +293,8 @@ export default function GraphView({ onNodeDragEnd={(n, translate) => setPosition(prev => { return prev && { x: prev.x + translate.x * chartRef.current.zoom(), y: prev.y + translate.y * chartRef.current.zoom() } })} - onNodeRightClick={handleNodeRightClick} + onNodeRightClick={handleRightClick} + onLinkRightClick={handleRightClick} onLinkClick={handleLinkClick} onBackgroundRightClick={unsetSelectedObjects} onBackgroundClick={unsetSelectedObjects} diff --git a/app/components/model.ts b/app/components/model.ts index 080f7984..d38bc3a2 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -30,8 +30,6 @@ export type Link = LinkObject=0.10.0" } }, + "node_modules/react-base16-styling": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.10.0.tgz", + "integrity": "sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==", + "dependencies": { + "@types/lodash": "^4.17.0", + "color": "^4.2.3", + "csstype": "^3.1.3", + "lodash-es": "^4.17.21" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -6803,6 +6814,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-json-tree": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.19.0.tgz", + "integrity": "sha512-PqT1WRVcWP+RROsZPQfNEKIC1iM/ZMfY4g5jN6oDnXp5593PPRAYgoHcgYCDjflAHQMtxl8XGdlTwIBdEGUXvw==", + "dependencies": { + "@types/lodash": "^4.17.0", + "react-base16-styling": "^0.10.0" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-kapsule": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.6.tgz", @@ -7328,7 +7352,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } diff --git a/package.json b/package.json index b9cf75f9..638d687b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react-dom": "^18", "react-force-graph-2d": "^1.25.8", "react-gtm-module": "^2.0.11", + "react-json-tree": "^0.19.0", "react-resizable-panels": "^2.0.20", "react-syntax-highlighter": "^15.6.1", "react-type-animation": "^3.2.0", From bed88d3f1f7a0a8ca22135cfc0122309f235d671 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 12:02:20 +0200 Subject: [PATCH 11/39] commit --- app/components/graphView.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 33d6f63d..d76c912e 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -65,13 +65,6 @@ export default function GraphView({ const [parentWidth, setParentWidth] = useState(0) const [parentHeight, setParentHeight] = useState(0) - useEffect(() => { - if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return - chartRef.current.d3Force('link').id((link: any) => link.id).distance(50) - chartRef.current.d3Force('charge').strength(-300) - chartRef.current.d3Force('center').strength(0.05) - }, [chartRef, data.links.length, data.nodes.length]) - useEffect(() => { if (!parentRef.current) return setParentWidth(parentRef.current.clientWidth) From 9c8d9cda782cca3ebd419e357e0b767ffda49b9d Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 12:03:58 +0200 Subject: [PATCH 12/39] commit --- app/components/graphView.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 7a93b1f0..0cf432a9 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -75,13 +75,6 @@ export default function GraphView({ } }, [data.nodes.length, data.links.length, chartRef]) - useEffect(() => { - if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return - chartRef.current.d3Force('link').id((link: any) => link.id).distance(50) - chartRef.current.d3Force('charge').strength(-300) - chartRef.current.d3Force('center').strength(0.05) - }, [chartRef, data.links.length, data.nodes.length]) - useEffect(() => { if (!parentRef.current) return setParentWidth(parentRef.current.clientWidth) From d8b94787321d373332fcc36f71e912a977df394f Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 12:04:25 +0200 Subject: [PATCH 13/39] commit --- app/components/graphView.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 33d6f63d..5badbd39 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -66,17 +66,14 @@ export default function GraphView({ const [parentHeight, setParentHeight] = useState(0) useEffect(() => { - if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return - chartRef.current.d3Force('link').id((link: any) => link.id).distance(50) - chartRef.current.d3Force('charge').strength(-300) - chartRef.current.d3Force('center').strength(0.05) - }, [chartRef, data.links.length, data.nodes.length]) + const timeout = setTimeout(() => { + chartRef.current?.zoomToFit(1000, 40) + }, 1000) - useEffect(() => { - if (!parentRef.current) return - setParentWidth(parentRef.current.clientWidth) - setParentHeight(parentRef.current.clientHeight) - }, [parentRef.current?.clientWidth, parentRef.current?.clientHeight]) + return () => { + clearTimeout(timeout) + } + }, [data.nodes.length, data.links.length, chartRef]) useEffect(() => { setCooldownTime(4000) From 9b46538e13e13bba90202b6f3540670716e89d62 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 12:05:07 +0200 Subject: [PATCH 14/39] commit --- app/components/graphView.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 0cf432a9..7150ae17 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -65,16 +65,6 @@ export default function GraphView({ const [parentWidth, setParentWidth] = useState(0) const [parentHeight, setParentHeight] = useState(0) - useEffect(() => { - const timeout = setTimeout(() => { - chartRef.current?.zoomToFit(1000, 40) - }, 1000) - - return () => { - clearTimeout(timeout) - } - }, [data.nodes.length, data.links.length, chartRef]) - useEffect(() => { if (!parentRef.current) return setParentWidth(parentRef.current.clientWidth) From 95518d407d2e0fe65c15a77399c9697b4a779103 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 12:11:16 +0200 Subject: [PATCH 15/39] commit --- app/components/graphView.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index d76c912e..f9b8714a 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -66,10 +66,25 @@ export default function GraphView({ const [parentHeight, setParentHeight] = useState(0) useEffect(() => { - if (!parentRef.current) return - setParentWidth(parentRef.current.clientWidth) - setParentHeight(parentRef.current.clientHeight) - }, [parentRef.current?.clientWidth, parentRef.current?.clientHeight]) + const handleResize = () => { + if (!parentRef.current) return + setParentWidth(parentRef.current.clientWidth) + setParentHeight(parentRef.current.clientHeight) + } + + window.addEventListener('resize', handleResize) + + const observer = new ResizeObserver(handleResize) + + if (parentRef.current) { + observer.observe(parentRef.current) + } + + return () => { + window.removeEventListener('resize', handleResize) + observer.disconnect() + } + }, [parentRef]) useEffect(() => { setCooldownTime(4000) From ca2e1f01acacda8e03ddaab7beb5a2ceeeb9ec96 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 20 Jan 2025 14:17:48 +0200 Subject: [PATCH 16/39] commit --- app/components/code-graph.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7fca147d..7dddd7af 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -15,7 +15,6 @@ import dynamic from 'next/dynamic'; import { Position } from "./graphView"; import { prepareArg } from '../utils'; import { NodeObject } from "react-force-graph-2d"; -import html2canvas from 'html2canvas'; const GraphView = dynamic(() => import('./graphView')); From 8e5dc72a20fecb7f393ddca2fc299238b361e53a Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:29:32 +0200 Subject: [PATCH 17/39] fix test errors --- e2e/logic/POM/codeGraph.ts | 10 ++++++++-- e2e/tests/canvas.spec.ts | 3 +-- e2e/tests/chat.spec.ts | 5 +++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 9910ded3..5c3afa2c 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -152,6 +152,10 @@ export default class CodeGraph extends BasePage { return this.page.locator("//main[@data-name='main-chat']/*[last()-1]/p"); } + private get responseLoadingImg(): Locator { + return this.page.locator("//img[@alt='Waiting for response']"); + } + /* Canvas Locators*/ private get canvasElement(): Locator { @@ -277,7 +281,8 @@ export default class CodeGraph extends BasePage { } async getTextInLastChatElement(): Promise{ - await delay(2500); + await this.page.waitForSelector('img[alt="Waiting for response"]', { state: 'hidden' }); + await delay(2000); return (await this.lastElementInChat.textContent())!; } @@ -349,6 +354,7 @@ export default class CodeGraph extends BasePage { await this.selectGraphInComboBoxByName(graph).waitFor({ state : 'visible'}) await this.selectGraphInComboBoxByName(graph).click(); } + await delay(2000); // graph animation delay } async createProject(url : string): Promise { @@ -417,7 +423,7 @@ export default class CodeGraph extends BasePage { async nodeClick(x: number, y: number): Promise { await this.canvasElement.hover({ position: { x, y } }); - await this.canvasElement.click({ position: { x, y } }); + await this.canvasElement.click({ position: { x, y }, button: 'right' }); } async selectCodeGraphCheckbox(checkbox: string): Promise { diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index 3701ac86..022500e3 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -43,15 +43,14 @@ test.describe("Canvas tests", () => { test(`Verify center graph button centers nodes in canvas`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.clickCenter(); const initialGraph = await codeGraph.getCanvasScaling(); - await codeGraph.clickZoomOut(); await codeGraph.clickZoomOut(); await codeGraph.clickCenter(); const updatedGraph = await codeGraph.getCanvasScaling(); expect(Math.abs(initialGraph.scaleX - updatedGraph.scaleX)).toBeLessThanOrEqual(0.1); expect(Math.abs(initialGraph.scaleY - updatedGraph.scaleY)).toBeLessThanOrEqual(0.1); - }) nodes.slice(0,2).forEach((node) => { diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 1f245f11..0d1f71cc 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -110,12 +110,15 @@ test.describe("Chat tests", () => { const result = await chat.getTextInLastChatElement(); const number = result.match(/\d+/g)?.[0]!; responses.push(number); + } const identicalResponses = responses.every((value) => value === responses[0]); expect(identicalResponses).toBe(true); }); test(`Validate UI response matches API response for a given question in chat`, async () => { + const api = new ApiCalls(); + const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); @@ -123,8 +126,6 @@ test.describe("Chat tests", () => { const uiResponse = await chat.getTextInLastChatElement(); const number = uiResponse.match(/\d+/g)?.[0]!; - const api = new ApiCalls(); - const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); expect(number).toEqual(apiResponse.result.response.match(/\d+/g)?.[0]); }); }); From 17d8fd85ca4ef3b600e91b388b68b2d036bfd9d1 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Wed, 22 Jan 2025 10:38:36 +0200 Subject: [PATCH 18/39] Remove 'spin' animation from config --- tailwind.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index 060cc1d9..14d51792 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -79,7 +79,6 @@ module.exports = { animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", - 'spin': 'spin 1s linear', }, }, }, From 1a2a087d8a7fd10ccf68c1f0eddff3b5cf3ca235 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 22 Jan 2025 16:16:14 +0200 Subject: [PATCH 19/39] Refactor CodeGraph and GraphView components to improve node expansion handling and neighbor deletion logic. Updated deleteNeighbors function to handle expanded nodes correctly and replaced direct calls with handleExpand in GraphView for better clarity and maintainability. --- app/components/code-graph.tsx | 40 ++++++++++++++++++++--------------- app/components/graphView.tsx | 28 +++--------------------- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7a99062d..6297b205 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -166,29 +166,38 @@ export function CodeGraph({ } const deleteNeighbors = (nodes: Node[]) => { + if (nodes.length === 0) return; - + + const expandedNodes: Node[] = [] + graph.Elements = { - nodes: graph.Elements.nodes.map(node => { + nodes: graph.Elements.nodes.filter(node => { + if (!node.collapsed) return true + const isTarget = graph.Elements.links.some(link => link.target.id === node.id && nodes.some(n => n.id === link.source.id)); + + debugger + + if (!isTarget) return true - if (!isTarget || !node.collapsed) return node + const deleted = graph.NodesMap.delete(Number(node.id)) - if (node.expand) { - node.expand = false - deleteNeighbors([node]) + if (deleted && node.expand) { + expandedNodes.push(node) } - graph.NodesMap.delete(Number(node.id)) - }).filter(node => node !== undefined), + return false + }), links: graph.Elements.links } + + deleteNeighbors(expandedNodes) graph.removeLinks() } const handleExpand = async (nodes: Node[], expand: boolean) => { - if (expand) { const elements = await onFetchNode(nodes.map(n => n.id)) @@ -216,12 +225,11 @@ export function CodeGraph({ const handleSearchSubmit = (node: any) => { const chart = chartRef.current - + if (chart) { - const n = { name: node.properties.name, id: node.id } let chartNode = graph.Elements.nodes.find(n => n.id == node.id) - + if (!chartNode?.visible) { if (!chartNode) { chartNode = graph.extend({ nodes: [node], edges: [] }).nodes[0] @@ -233,8 +241,7 @@ export function CodeGraph({ graph.visibleLinks(true, [chartNode!.id]) setData({ ...graph.Elements }) } - - setSearchNode(n) + setTimeout(() => { chart.zoomToFit(1000, 150, (n: NodeObject) => n.id === chartNode!.id); }, 0) @@ -271,8 +278,7 @@ export function CodeGraph({
setSearchNode({ name })} + onValueChange={(node) => setSearchNode(node)} icon={} handleSubmit={handleSearchSubmit} node={searchNode} @@ -371,7 +377,7 @@ export function CodeGraph({ setSelectedObjects={setSelectedObjects} setPosition={setPosition} onFetchNode={onFetchNode} - deleteNeighbors={deleteNeighbors} + handleExpand={handleExpand} isShowPath={isShowPath} setPath={setPath} isPathResponse={isPathResponse} diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index f9b8714a..d40bfcf6 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -2,7 +2,6 @@ import ForceGraph2D from 'react-force-graph-2d'; import { Graph, GraphData, Link, Node } from './model'; import { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'; -import { toast } from '@/components/ui/use-toast'; import { Path } from '../page'; export interface Position { @@ -21,7 +20,7 @@ interface Props { setSelectedObjects: Dispatch> setPosition: Dispatch> onFetchNode: (nodeIds: number[]) => Promise - deleteNeighbors: (nodes: Node[]) => void + handleExpand: (nodes: Node[], expand: boolean) => void isShowPath: boolean setPath: Dispatch> isPathResponse: boolean | undefined @@ -48,7 +47,7 @@ export default function GraphView({ setSelectedObjects, setPosition, onFetchNode, - deleteNeighbors, + handleExpand, isShowPath, setPath, isPathResponse, @@ -132,28 +131,7 @@ export default function GraphView({ lastClick.current = { date: now, name: node.name } if (isDoubleClick) { - const expand = !node.expand - - if (expand) { - const elements = await onFetchNode([node.id]) - - if (elements.nodes.length === 0) { - toast({ - title: `No neighbors found`, - description: `No neighbors found`, - }) - - return - } - } else { - deleteNeighbors([node]); - } - - node.expand = expand - - setSelectedObj(undefined) - setData({ ...graph.Elements }) - + handleExpand([node], !node.expand) } else if (isShowPath) { setPath(prev => { if (!prev?.start?.name || (prev.end?.name && prev.end?.name !== "")) { From 43c94c74e8fa78eddc7735c55180fc048cbeb5a5 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 22 Jan 2025 16:24:00 +0200 Subject: [PATCH 20/39] fix input --- app/components/Input.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 83ed01be..9ab6da79 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -6,7 +6,6 @@ import { cn } from "@/lib/utils" import { prepareArg } from "../utils" interface Props extends React.InputHTMLAttributes { - value?: string graph: Graph onValueChange: (node: PathNode) => void handleSubmit?: (node: any) => void @@ -16,7 +15,7 @@ interface Props extends React.InputHTMLAttributes { scrollToBottom?: () => void } -export default function Input({ value, onValueChange, handleSubmit, graph, icon, node, className, parentClassName, scrollToBottom, ...props }: Props) { +export default function Input({ onValueChange, handleSubmit, graph, icon, node, className, parentClassName, scrollToBottom, ...props }: Props) { const [open, setOpen] = useState(false) const [options, setOptions] = useState([]) @@ -38,15 +37,15 @@ export default function Input({ value, onValueChange, handleSubmit, graph, icon, let isLastRequest = true const timeout = setTimeout(async () => { - if (!value || node?.id) { - if (!value) { + if (!node?.name) { + if (!node?.name) { setOptions([]) } setOpen(false) return } - const result = await fetch(`/api/repo/${prepareArg(graph.Id)}/?prefix=${prepareArg(value)}`, { + const result = await fetch(`/api/repo/${prepareArg(graph.Id)}/?prefix=${prepareArg(node?.name)}`, { method: 'POST' }) @@ -66,7 +65,7 @@ export default function Input({ value, onValueChange, handleSubmit, graph, icon, setOptions(completions || []) - if (completions?.length > 0) { + if (completions?.length > 0 && !node?.id) { setOpen(true) } else { setOpen(false) @@ -77,7 +76,7 @@ export default function Input({ value, onValueChange, handleSubmit, graph, icon, clearTimeout(timeout) isLastRequest = false } - }, [value, graph.Id]) + }, [node?.name, graph.Id]) const handleKeyDown = (e: React.KeyboardEvent) => { const container = containerRef.current @@ -87,6 +86,7 @@ export default function Input({ value, onValueChange, handleSubmit, graph, icon, const option = options.find((o, i) => i === selectedOption) if (!option) return if (handleSubmit) { + onValueChange({ name: option.properties.name, id: option.id }) handleSubmit(option) } else { if (!open) return @@ -144,7 +144,7 @@ export default function Input({ value, onValueChange, handleSubmit, graph, icon, }} onKeyDown={handleKeyDown} className={cn("w-full border p-2 rounded-md pointer-events-auto", className)} - value={value || ""} + value={node?.name || ""} onChange={(e) => { const newVal = e.target.value const invalidChars = /[%*()\-\[\]{};:"|~]/; From ec73ee4a994fde206df8882fe5d74f98f57915fb Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Fri, 24 Jan 2025 08:32:36 +0200 Subject: [PATCH 21/39] add docker-compose --- docker-compose.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2e4ac22b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + falkordb: + image: falkordb/falkordb:latest + ports: + - "6379:6379" + - "3001:3000" + volumes: + - ./:/data/ + stdin_open: true # Keep the container's STDIN open + tty: true # Allocate a pseudo-TTY + + code-graph-frontend: + image: falkordb/code-graph-frontend:latest + ports: + - "3000:3000" + depends_on: + - code-graph-backend + environment: + - BACKEND_URL=http://code-graph-backend:5000 # Backend service URL + - SECRET_TOKEN=Vespa + + code-graph-backend: + image: falkordb/code-graph-backend:latest + ports: + - "4000:5000" + depends_on: + - falkordb + environment: + - FALKORDB_HOST=falkordb + - FALKORDB_PORT=6379 + - OPENAI_API_KEY=YOUR_OPENAI_API_KEY + - SECRET_TOKEN=Vespa + - FLASK_RUN_HOST=0.0.0.0 + - FLASK_RUN_PORT=5000 From 8f05cf813e1f751f7b6de4ceefd0b03d215561b2 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:44:49 +0200 Subject: [PATCH 22/39] fix tests --- app/components/code-graph.tsx | 2 +- e2e/logic/POM/codeGraph.ts | 3 +- e2e/tests/chat.spec.ts | 62 +++++++++++++++++------------------ 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 7a99062d..a47cb798 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -271,7 +271,7 @@ export function CodeGraph({
setSearchNode({ name })} icon={} handleSubmit={handleSearchSubmit} diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 5c3afa2c..bde0a3c8 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -271,6 +271,7 @@ export default class CodeGraph extends BasePage { } async sendMessage(message: string) { + await delay(3000); //delay before sending a request to the AI model await waitToBeEnabled(this.askquestionInput); await this.askquestionInput.fill(message); await this.askquestionBtn.click(); @@ -282,7 +283,7 @@ export default class CodeGraph extends BasePage { async getTextInLastChatElement(): Promise{ await this.page.waitForSelector('img[alt="Waiting for response"]', { state: 'hidden' }); - await delay(2000); + await delay(2000); //delay for chat response animation return (await this.lastElementInChat.textContent())!; } diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 0d1f71cc..1ec2edab 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -31,7 +31,7 @@ test.describe("Chat tests", () => { await chat.selectGraph(GRAPH_ID); const isLoadingArray: boolean[] = []; - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 3; i++) { await chat.sendMessage(Node_Question); const isLoading: boolean = await chat.getpreviousQuestionLoadingImage(); isLoadingArray.push(isLoading); @@ -45,18 +45,46 @@ test.describe("Chat tests", () => { test("Verify auto-scroll and manual scroll in chat", async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 3; i++) { await chat.sendMessage(Node_Question); } await delay(500); await chat.scrollToTop(); const { scrollTop } = await chat.getScrollMetrics(); expect(scrollTop).toBeLessThanOrEqual(1); - await chat.sendMessage("Latest Message"); + await chat.sendMessage(Node_Question); await delay(500); expect(await chat.isAtBottom()).toBe(true); }); + test(`Validate consistent UI responses for repeated questions in chat`, async () => { + const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); + await chat.selectGraph(GRAPH_ID); + const responses: string[] = []; + for (let i = 0; i < 3; i++) { + await chat.sendMessage(Node_Question); + const result = await chat.getTextInLastChatElement(); + const number = result.match(/\d+/g)?.[0]!; + responses.push(number); + + } + const identicalResponses = responses.every((value) => value === responses[0]); + expect(identicalResponses).toBe(true); + }); + + test(`Validate UI response matches API response for a given question in chat`, async () => { + const api = new ApiCalls(); + const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); + const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); + await chat.selectGraph(GRAPH_ID); + + await chat.sendMessage(Node_Question); + const uiResponse = await chat.getTextInLastChatElement(); + const number = uiResponse.match(/\d+/g)?.[0]!; + + expect(number).toEqual(apiResponse.result.response.match(/\d+/g)?.[0]); + }); + nodesPath.forEach((path) => { test(`Verify successful node path connection between two nodes in chat for ${path.firstNode} and ${path.secondNode}`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); @@ -100,32 +128,4 @@ test.describe("Chat tests", () => { expect(selectedQuestion).toEqual(await chat.getLastQuestionInChat()) }); } - - test(`Validate consistent UI responses for repeated questions in chat`, async () => { - const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); - const responses: string[] = []; - for (let i = 0; i < 3; i++) { - await chat.sendMessage(Node_Question); - const result = await chat.getTextInLastChatElement(); - const number = result.match(/\d+/g)?.[0]!; - responses.push(number); - - } - const identicalResponses = responses.every((value) => value === responses[0]); - expect(identicalResponses).toBe(true); - }); - - test(`Validate UI response matches API response for a given question in chat`, async () => { - const api = new ApiCalls(); - const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); - const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); - - await chat.sendMessage(Node_Question); - const uiResponse = await chat.getTextInLastChatElement(); - const number = uiResponse.match(/\d+/g)?.[0]!; - - expect(number).toEqual(apiResponse.result.response.match(/\d+/g)?.[0]); - }); }); From a7c282d5e196ff3d152140ec4b95912d5ec794a5 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:14:04 +0200 Subject: [PATCH 23/39] fix chat error & increase timing --- app/components/chat.tsx | 4 ++-- e2e/logic/POM/codeGraph.ts | 6 ++++-- e2e/tests/chat.spec.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6e2c127d..6af9ac79 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -387,7 +387,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons parentClassName="w-full" graph={graph} onValueChange={({ name, id }) => setPath(prev => ({ start: { name, id }, end: prev?.end }))} - value={path?.start?.name} + value={path?.start?.name || ""} placeholder="Start typing starting point" type="text" icon={} @@ -397,7 +397,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons setPath(prev => ({ end: { name, id }, start: prev?.start }))} placeholder="Start typing end point" type="text" diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index bde0a3c8..49a40d68 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -271,7 +271,6 @@ export default class CodeGraph extends BasePage { } async sendMessage(message: string) { - await delay(3000); //delay before sending a request to the AI model await waitToBeEnabled(this.askquestionInput); await this.askquestionInput.fill(message); await this.askquestionBtn.click(); @@ -280,9 +279,12 @@ export default class CodeGraph extends BasePage { async clickOnLightBulbBtn(): Promise { await this.lightbulbBtn.click(); } + private get waitingForResponseIndicator(): Locator { + return this.page.locator('img[alt="Waiting for response"]'); + } async getTextInLastChatElement(): Promise{ - await this.page.waitForSelector('img[alt="Waiting for response"]', { state: 'hidden' }); + await this.waitingForResponseIndicator.waitFor({ state: 'hidden' }); await delay(2000); //delay for chat response animation return (await this.lastElementInChat.textContent())!; } diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 1ec2edab..5d908bc8 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -39,6 +39,7 @@ test.describe("Chat tests", () => { const prevIsLoading = isLoadingArray[i - 1]; expect(prevIsLoading).toBe(false); } + await delay(3000); } }); @@ -47,13 +48,14 @@ test.describe("Chat tests", () => { await chat.selectGraph(GRAPH_ID); for (let i = 0; i < 3; i++) { await chat.sendMessage(Node_Question); + await delay(3000); } - await delay(500); + await delay(500); // delay for scroll await chat.scrollToTop(); const { scrollTop } = await chat.getScrollMetrics(); expect(scrollTop).toBeLessThanOrEqual(1); await chat.sendMessage(Node_Question); - await delay(500); + await delay(500); // delay for scroll expect(await chat.isAtBottom()).toBe(true); }); @@ -66,7 +68,7 @@ test.describe("Chat tests", () => { const result = await chat.getTextInLastChatElement(); const number = result.match(/\d+/g)?.[0]!; responses.push(number); - + await delay(3000); //delay before asking next question } const identicalResponses = responses.every((value) => value === responses[0]); expect(identicalResponses).toBe(true); @@ -77,7 +79,7 @@ test.describe("Chat tests", () => { const apiResponse = await api.askQuestion(PROJECT_NAME, Node_Question); const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - + await delay(3000); await chat.sendMessage(Node_Question); const uiResponse = await chat.getTextInLastChatElement(); const number = uiResponse.match(/\d+/g)?.[0]!; From 7b9acf627775fe8e28daa6ed3de8023e1b892113 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:16:36 +0200 Subject: [PATCH 24/39] improve tests timing and locators --- e2e/logic/POM/codeGraph.ts | 49 +++++++++++++++++++++++--------------- e2e/logic/utils.ts | 34 ++++++++++++++++++++------ e2e/tests/chat.spec.ts | 2 ++ 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 49a40d68..4dfc6943 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -1,6 +1,6 @@ import { Locator, Page } from "playwright"; import BasePage from "../../infra/ui/basePage"; -import { delay, waitToBeEnabled } from "../utils"; +import { waitForStableText, waitToBeEnabled } from "../utils"; declare global { interface Window { @@ -253,7 +253,7 @@ export default class CodeGraph extends BasePage { } async isTipMenuVisible(): Promise { - await delay(500); + await this.page.waitForTimeout(500); return await this.genericMenu.isVisible(); } @@ -267,11 +267,12 @@ export default class CodeGraph extends BasePage { } async clickAskquestionBtn(): Promise { + await waitToBeEnabled(this.askquestionBtn); await this.askquestionBtn.click(); } async sendMessage(message: string) { - await waitToBeEnabled(this.askquestionInput); + await waitToBeEnabled(this.askquestionBtn); await this.askquestionInput.fill(message); await this.askquestionBtn.click(); } @@ -285,8 +286,7 @@ export default class CodeGraph extends BasePage { async getTextInLastChatElement(): Promise{ await this.waitingForResponseIndicator.waitFor({ state: 'hidden' }); - await delay(2000); //delay for chat response animation - return (await this.lastElementInChat.textContent())!; + return await waitForStableText(this.lastElementInChat); } async getLastChatElementButtonCount(): Promise{ @@ -326,7 +326,7 @@ export default class CodeGraph extends BasePage { } async isNotificationError(): Promise { - await delay(500); + await this.page.waitForTimeout(500); return await this.notificationError.isVisible(); } @@ -357,7 +357,7 @@ export default class CodeGraph extends BasePage { await this.selectGraphInComboBoxByName(graph).waitFor({ state : 'visible'}) await this.selectGraphInComboBoxByName(graph).click(); } - await delay(2000); // graph animation delay + await this.page.waitForTimeout(2000); // graph animation delay } async createProject(url : string): Promise { @@ -386,8 +386,9 @@ export default class CodeGraph extends BasePage { } async selectSearchBarOptionBtn(buttonNum: string): Promise { - await delay(1000); - await this.searchBarOptionBtn(buttonNum).click(); + const button = this.searchBarOptionBtn(buttonNum); + await button.waitFor({ state : "visible"}) + await button.click(); } async getSearchBarInputValue(): Promise { @@ -417,11 +418,13 @@ export default class CodeGraph extends BasePage { async clickCenter(): Promise { await this.centerBtn.click(); - await delay(2000); //animation delay + await this.page.waitForTimeout(2000); //animation delay } async clickOnRemoveNodeViaElementMenu(): Promise { - await this.elementMenuButton("Remove").click(); + const button = this.elementMenuButton("Remove"); + await button.waitFor({ state: "visible"}) + await button.click(); } async nodeClick(x: number, y: number): Promise { @@ -456,15 +459,21 @@ export default class CodeGraph extends BasePage { } async isNodeDetailsPanel(): Promise { + await this.page.waitForTimeout(500); return this.nodeDetailsPanel.isVisible(); } async clickOnViewNode(): Promise { - await this.elementMenuButton("View Node").click(); + const button = this.elementMenuButton("View Node"); + await button.waitFor({ state: "visible"}) + await button.click(); } async getNodeDetailsHeader(): Promise { - await this.elementMenuButton("View Node").click(); + const button = this.elementMenuButton("View Node"); + await button.waitFor({ state: "visible"}) + await button.click(); + await this.nodedetailsPanelHeader.waitFor({ state: "visible" }) const text = await this.nodedetailsPanelHeader.innerHTML(); return text; } @@ -480,14 +489,15 @@ export default class CodeGraph extends BasePage { } async clickOnCopyToClipboardNodePanelDetails(): Promise { + await this.copyToClipboardNodePanelDetails.waitFor({ state: "visible" }); await this.copyToClipboardNodePanelDetails.click(); - await delay(1000) return await this.page.evaluate(() => navigator.clipboard.readText()); } async clickOnCopyToClipboard(): Promise { - await this.elementMenuButton("Copy src to clipboard").click(); - await delay(1000) + const button = this.elementMenuButton("Copy src to clipboard"); + await button.waitFor({ state: "visible" }); + await button.click(); return await this.page.evaluate(() => navigator.clipboard.readText()); } @@ -496,15 +506,16 @@ export default class CodeGraph extends BasePage { } async getNodeDetailsPanelElements(): Promise { - await this.elementMenuButton("View Node").click(); - await delay(500) + const button = this.elementMenuButton("View Node"); + await button.waitFor({ state: "visible"}) + await button.click(); const elements = await this.nodedetailsPanelElements.all(); return Promise.all(elements.map(element => element.innerHTML())); } async getGraphDetails(): Promise { await this.canvasElementBeforeGraphSelection.waitFor({ state: 'detached' }); - await delay(2000) + await this.page.waitForTimeout(2000); //canvas animation await this.page.waitForFunction(() => !!window.graph); const graphData = await this.page.evaluate(() => { diff --git a/e2e/logic/utils.ts b/e2e/logic/utils.ts index 6ba3475d..efa4adb8 100644 --- a/e2e/logic/utils.ts +++ b/e2e/logic/utils.ts @@ -2,19 +2,39 @@ import { Locator } from "@playwright/test"; export const delay = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); -export const waitToBeEnabled = async (locator: Locator, timeout: number = 5000): Promise => { - const startTime = Date.now(); +export const waitToBeEnabled = async (locator: Locator, timeout: number = 5000): Promise => { + const elementHandle = await locator.elementHandle(); + if (!elementHandle) throw new Error("Element not found"); - while (Date.now() - startTime < timeout) { - if (await locator.isEnabled()) { - return true; + await locator.page().waitForFunction( + (el) => el && !(el as HTMLElement).hasAttribute("disabled"), + elementHandle, + { timeout } + ); +}; + +export const waitForStableText = async (locator: Locator, timeout: number = 5000): Promise => { + const elementHandle = await locator.elementHandle(); + if (!elementHandle) throw new Error("Element not found"); + + let previousText = ""; + let stableText = ""; + const pollingInterval = 300; + const maxChecks = timeout / pollingInterval; + + for (let i = 0; i < maxChecks; i++) { + stableText = await locator.textContent() ?? ""; + if (stableText === previousText && stableText.trim().length > 0) { + return stableText; } - await new Promise(resolve => setTimeout(resolve, 100)); + previousText = stableText; + await locator.page().waitForTimeout(pollingInterval); } - return false; + return stableText; }; + export function findNodeByName(nodes: { name: string }[], nodeName: string): any { return nodes.find((node) => node.name === nodeName); } \ No newline at end of file diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 5d908bc8..4bce6f0a 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -128,6 +128,8 @@ test.describe("Chat tests", () => { await chat.clickOnQuestionOptionsMenu(); const selectedQuestion = await chat.selectAndGetQuestionInOptionsMenu(questionNumber.toString()); expect(selectedQuestion).toEqual(await chat.getLastQuestionInChat()) + const result = await chat.getTextInLastChatElement(); + expect(result).toBeDefined(); }); } }); From e6972d494e34283767780c060c1d1c6ebe62c622 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:58:49 +0200 Subject: [PATCH 25/39] update locators --- app/components/elementMenu.tsx | 1 + e2e/logic/POM/codeGraph.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/components/elementMenu.tsx b/app/components/elementMenu.tsx index e1451f8a..b205ec0d 100644 --- a/app/components/elementMenu.tsx +++ b/app/components/elementMenu.tsx @@ -45,6 +45,7 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit left: Math.max(-34, Math.min(position.x - 33 - containerWidth / 2, (parentRef?.current?.clientWidth || 0) + 32 - containerWidth)), top: Math.min(position.y - 153, (parentRef?.current?.clientHeight || 0) - 9), }} + id="elementMenu" > { objects.some(o => o.id === obj.id) && objects.length > 1 ? diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 4dfc6943..426d29ab 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -193,6 +193,10 @@ export default class CodeGraph extends BasePage { private get nodeDetailsPanel(): Locator { return this.page.locator("//div[@data-name='node-details-panel']"); } + + private get elementMenu(): Locator { + return this.page.locator("//div[@id='elementMenu']"); + } private get nodedetailsPanelHeader(): Locator { return this.page.locator("//div[@data-name='node-details-panel']/header/p"); @@ -422,8 +426,8 @@ export default class CodeGraph extends BasePage { } async clickOnRemoveNodeViaElementMenu(): Promise { + await this.elementMenu.waitFor({ state: "visible", timeout: 10000}) const button = this.elementMenuButton("Remove"); - await button.waitFor({ state: "visible"}) await button.click(); } @@ -470,10 +474,10 @@ export default class CodeGraph extends BasePage { } async getNodeDetailsHeader(): Promise { + await this.elementMenu.waitFor({ state: "visible", timeout: 10000}) const button = this.elementMenuButton("View Node"); - await button.waitFor({ state: "visible"}) await button.click(); - await this.nodedetailsPanelHeader.waitFor({ state: "visible" }) + await this.nodedetailsPanelHeader.waitFor({ state: "visible", timeout: 10000 }) const text = await this.nodedetailsPanelHeader.innerHTML(); return text; } From fb07797d9fb4f9bc2f67135092d19385e6db8795 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 30 Jan 2025 13:32:49 +0200 Subject: [PATCH 26/39] replace test size to 2px --- app/components/graphView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index f9b8714a..8651eafd 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -230,7 +230,7 @@ export default function GraphView({ ctx.fillStyle = 'black'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.font = '4px Arial'; + ctx.font = '2px Arial'; const textWidth = ctx.measureText(node.name).width; const ellipsis = '...'; const ellipsisWidth = ctx.measureText(ellipsis).width; From e2f506e97952cb0448f15bbc5d3635c68b689ff5 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:35:23 +0200 Subject: [PATCH 27/39] add logging --- e2e/logic/POM/codeGraph.ts | 5 +++++ e2e/tests/nodeDetailsPanel.spec.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 426d29ab..02ee94a0 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -432,8 +432,13 @@ export default class CodeGraph extends BasePage { } async nodeClick(x: number, y: number): Promise { + await this.page.waitForTimeout(500); + console.log(`Clicking node at: X=${x}, Y=${y}`); await this.canvasElement.hover({ position: { x, y } }); + await this.page.waitForTimeout(300); // Allow hover to take effect + console.log("Hover successful"); await this.canvasElement.click({ position: { x, y }, button: 'right' }); + console.log("Right-click performed"); } async selectCodeGraphCheckbox(checkbox: string): Promise { diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index aa59a6fe..5f69afd4 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -25,6 +25,7 @@ test.describe("Node details panel tests", () => { const graphData = await codeGraph.getGraphDetails(); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const targetNode = findNodeByName(convertCoordinates, node.nodeName); + expect(targetNode).toBeDefined(); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); expect(await codeGraph.isNodeDetailsPanel()).toBe(true) From aaedf000c066e755cc7e391c39772e57b4c8a7fc Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:40:43 +0200 Subject: [PATCH 28/39] tests improvements --- e2e/logic/POM/codeGraph.ts | 127 ++++++++++++++++++++++++------------- e2e/logic/utils.ts | 11 ++++ e2e/tests/canvas.spec.ts | 6 +- e2e/tests/chat.spec.ts | 6 +- 4 files changed, 100 insertions(+), 50 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 02ee94a0..4ee1a0b4 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -1,6 +1,6 @@ import { Locator, Page } from "playwright"; import BasePage from "../../infra/ui/basePage"; -import { waitForStableText, waitToBeEnabled } from "../utils"; +import { waitForElementToBeVisible, waitForStableText, waitToBeEnabled } from "../utils"; declare global { interface Window { @@ -156,6 +156,10 @@ export default class CodeGraph extends BasePage { return this.page.locator("//img[@alt='Waiting for response']"); } + private get waitingForResponseIndicator(): Locator { + return this.page.locator('img[alt="Waiting for response"]'); + } + /* Canvas Locators*/ private get canvasElement(): Locator { @@ -245,69 +249,83 @@ export default class CodeGraph extends BasePage { } async clickCreateNewProjectBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.createNewProjectBtn); + if (!isVisible) throw new Error("'Create New Project' button is not visible!"); await this.createNewProjectBtn.click(); } - + async isCreateNewProjectDialog(): Promise { - return await this.createNewProjectDialog.isVisible(); + return await waitForElementToBeVisible(this.createNewProjectDialog); } - - async clickonTipBtn(): Promise { + + async clickOnTipBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.tipBtn); + if (!isVisible) throw new Error("'Tip' button is not visible!"); await this.tipBtn.click(); } async isTipMenuVisible(): Promise { - await this.page.waitForTimeout(500); - return await this.genericMenu.isVisible(); + // await this.page.waitForTimeout(500); + return await waitForElementToBeVisible(this.genericMenu); } - - async clickonTipMenuCloseBtn(): Promise { + + async clickOnTipMenuCloseBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.tipMenuCloseBtn); + if (!isVisible) throw new Error("'Tip Menu Close' button is not visible!"); await this.tipMenuCloseBtn.click(); } + /* Chat functionality */ - async clickOnshowPathBtn(): Promise { + async clickOnShowPathBtn(): Promise { await this.showPathBtn.click(); } - async clickAskquestionBtn(): Promise { - await waitToBeEnabled(this.askquestionBtn); + async clickAskQuestionBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.askquestionBtn); + if (!isVisible) throw new Error("'Ask Question' button is not visible!"); await this.askquestionBtn.click(); } - + async sendMessage(message: string) { await waitToBeEnabled(this.askquestionBtn); await this.askquestionInput.fill(message); await this.askquestionBtn.click(); } - + async clickOnLightBulbBtn(): Promise { await this.lightbulbBtn.click(); } - private get waitingForResponseIndicator(): Locator { - return this.page.locator('img[alt="Waiting for response"]'); - } async getTextInLastChatElement(): Promise{ await this.waitingForResponseIndicator.waitFor({ state: 'hidden' }); return await waitForStableText(this.lastElementInChat); } - async getLastChatElementButtonCount(): Promise{ + async getLastChatElementButtonCount(): Promise { + const isVisible = await waitForElementToBeVisible(this.lastChatElementButtonCount); + if (!isVisible) return null; return await this.lastChatElementButtonCount.count(); } - async scrollToTop() { + async scrollToTop(): Promise { + const isVisible = await waitForElementToBeVisible(this.chatContainer); + if (!isVisible) throw new Error("Chat container is not visible!"); + await this.chatContainer.evaluate((chat) => { - chat.scrollTop = 0; + chat.scrollTop = 0; }); } async getScrollMetrics() { - const scrollTop = await this.chatContainer.evaluate((el) => el.scrollTop); - const scrollHeight = await this.chatContainer.evaluate((el) => el.scrollHeight); - const clientHeight = await this.chatContainer.evaluate((el) => el.clientHeight); - return { scrollTop, scrollHeight, clientHeight }; + const isVisible = await waitForElementToBeVisible(this.chatContainer); + if (!isVisible) throw new Error("Chat container is not visible!"); + + return await this.chatContainer.evaluate((el) => ({ + scrollTop: el.scrollTop, + scrollHeight: el.scrollHeight, + clientHeight: el.clientHeight + })); } async isAtBottom(): Promise { @@ -323,10 +341,10 @@ export default class CodeGraph extends BasePage { await this.selectInputForShowPath(inputNum).fill(node); await this.selectFirstPathOption(inputNum).click(); } - + async isNodeVisibleInLastChatPath(node: string): Promise { - await this.locateNodeInLastChatPath(node).waitFor({ state: 'visible' }); - return await this.locateNodeInLastChatPath(node).isVisible(); + const nodeLocator = this.locateNodeInLastChatPath(node); + return await waitForElementToBeVisible(nodeLocator); } async isNotificationError(): Promise { @@ -335,21 +353,31 @@ export default class CodeGraph extends BasePage { } async clickOnNotificationErrorCloseBtn(): Promise { + const isVisible = await waitForElementToBeVisible(this.notificationErrorCloseBtn); + if (!isVisible) throw new Error("Notification error close button is not visible!"); await this.notificationErrorCloseBtn.click(); } - + async clickOnQuestionOptionsMenu(): Promise { + const isVisible = await waitForElementToBeVisible(this.questionOptionsMenu); + if (!isVisible) throw new Error("Question options menu is not visible!"); await this.questionOptionsMenu.click(); } - + async selectAndGetQuestionInOptionsMenu(questionNumber: string): Promise { - await this.selectQuestionInMenu(questionNumber).click(); - return await this.selectQuestionInMenu(questionNumber).innerHTML(); + const question = this.selectQuestionInMenu(questionNumber); + const isVisible = await waitForElementToBeVisible(question); + if (!isVisible) throw new Error(`Question ${questionNumber} in menu is not visible!`); + + await question.click(); + return await question.innerHTML(); } - + async getLastQuestionInChat(): Promise { + const isVisible = await waitForElementToBeVisible(this.lastQuestionInChat); + if (!isVisible) throw new Error("Last question in chat is not visible!"); return await this.lastQuestionInChat.innerText(); - } + } /* CodeGraph functionality */ async selectGraph(graph: string | number): Promise { @@ -435,7 +463,7 @@ export default class CodeGraph extends BasePage { await this.page.waitForTimeout(500); console.log(`Clicking node at: X=${x}, Y=${y}`); await this.canvasElement.hover({ position: { x, y } }); - await this.page.waitForTimeout(300); // Allow hover to take effect + await this.page.waitForTimeout(500); // Allow hover to take effect console.log("Hover successful"); await this.canvasElement.click({ position: { x, y }, button: 'right' }); console.log("Right-click performed"); @@ -474,17 +502,21 @@ export default class CodeGraph extends BasePage { async clickOnViewNode(): Promise { const button = this.elementMenuButton("View Node"); - await button.waitFor({ state: "visible"}) + const isButtonVisible = await waitForElementToBeVisible(button); + if (!isButtonVisible) throw new Error("'View Node' button is not visible!"); await button.click(); } async getNodeDetailsHeader(): Promise { - await this.elementMenu.waitFor({ state: "visible", timeout: 10000}) - const button = this.elementMenuButton("View Node"); - await button.click(); - await this.nodedetailsPanelHeader.waitFor({ state: "visible", timeout: 10000 }) - const text = await this.nodedetailsPanelHeader.innerHTML(); - return text; + const isMenuVisible = await waitForElementToBeVisible(this.elementMenu); + if (!isMenuVisible) throw new Error("Element menu did not appear!"); + + await this.clickOnViewNode(); + + const isHeaderVisible = await waitForElementToBeVisible(this.nodedetailsPanelHeader); + if (!isHeaderVisible) throw new Error("Node details panel header did not appear!"); + + return this.nodedetailsPanelHeader.innerHTML(); } async clickOnNodeDetailsCloseBtn(): Promise{ @@ -498,14 +530,16 @@ export default class CodeGraph extends BasePage { } async clickOnCopyToClipboardNodePanelDetails(): Promise { - await this.copyToClipboardNodePanelDetails.waitFor({ state: "visible" }); + const isButtonVisible = await waitForElementToBeVisible(this.copyToClipboardNodePanelDetails); + if (!isButtonVisible) throw new Error("'copy to clipboard button is not visible!"); await this.copyToClipboardNodePanelDetails.click(); return await this.page.evaluate(() => navigator.clipboard.readText()); } async clickOnCopyToClipboard(): Promise { const button = this.elementMenuButton("Copy src to clipboard"); - await button.waitFor({ state: "visible" }); + const isVisible = await waitForElementToBeVisible(button); + if (!isVisible) throw new Error("View Node button is not visible!"); await button.click(); return await this.page.evaluate(() => navigator.clipboard.readText()); } @@ -516,8 +550,13 @@ export default class CodeGraph extends BasePage { async getNodeDetailsPanelElements(): Promise { const button = this.elementMenuButton("View Node"); - await button.waitFor({ state: "visible"}) + const isVisible = await waitForElementToBeVisible(button); + if (!isVisible) throw new Error("View Node button is not visible!"); await button.click(); + + const isPanelVisible = await waitForElementToBeVisible(this.nodedetailsPanelElements.first()); + if (!isPanelVisible) throw new Error("Node details panel did not appear!"); + const elements = await this.nodedetailsPanelElements.all(); return Promise.all(elements.map(element => element.innerHTML())); } diff --git a/e2e/logic/utils.ts b/e2e/logic/utils.ts index efa4adb8..17b575d8 100644 --- a/e2e/logic/utils.ts +++ b/e2e/logic/utils.ts @@ -34,6 +34,17 @@ export const waitForStableText = async (locator: Locator, timeout: number = 5000 return stableText; }; +export const waitForElementToBeVisible = async (locator:Locator,time=400,retry=5):Promise => { + + while(retry > 0){ + if(await locator.isVisible()){ + return true + } + retry = retry-1 + await delay(time) + } + return false +} export function findNodeByName(nodes: { name: string }[], nodeName: string): any { return nodes.find((node) => node.name === nodeName); diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index 022500e3..e54858d8 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -100,7 +100,7 @@ test.describe("Canvas tests", () => { test(`Verify "Clear graph" button resets canvas view for path ${path.firstNode} and ${path.secondNode}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - await codeGraph.clickOnshowPathBtn(); + await codeGraph.clickOnShowPathBtn(); await codeGraph.insertInputForShowPath("1", path.firstNode); await codeGraph.insertInputForShowPath("2", path.secondNode); const initialGraph = await codeGraph.getGraphDetails(); @@ -185,7 +185,7 @@ test.describe("Canvas tests", () => { test(`Verify successful node path connection in canvas between ${firstNode} and ${secondNode} via UI`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - await codeGraph.clickOnshowPathBtn(); + await codeGraph.clickOnShowPathBtn(); await codeGraph.insertInputForShowPath("1", firstNode); await codeGraph.insertInputForShowPath("2", secondNode); const result = await codeGraph.getGraphDetails(); @@ -200,7 +200,7 @@ test.describe("Canvas tests", () => { test(`Validate node path connection in canvas ui and confirm via api for path ${path.firstNode} and ${path.secondNode}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - await codeGraph.clickOnshowPathBtn(); + await codeGraph.clickOnShowPathBtn(); await codeGraph.insertInputForShowPath("1", path.firstNode); await codeGraph.insertInputForShowPath("2", path.secondNode); const result = await codeGraph.getGraphDetails(); diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 4bce6f0a..d81ac33d 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -91,7 +91,7 @@ test.describe("Chat tests", () => { test(`Verify successful node path connection between two nodes in chat for ${path.firstNode} and ${path.secondNode}`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - await chat.clickOnshowPathBtn(); + await chat.clickOnShowPathBtn(); await chat.insertInputForShowPath("1", path.firstNode); await chat.insertInputForShowPath("2", path.secondNode); expect(await chat.isNodeVisibleInLastChatPath(path.firstNode)).toBe(true); @@ -103,7 +103,7 @@ test.describe("Chat tests", () => { test(`Verify unsuccessful node path connection between two nodes in chat for ${path.firstNode} and ${path.secondNode}`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - await chat.clickOnshowPathBtn(); + await chat.clickOnShowPathBtn(); await chat.insertInputForShowPath("1", path.secondNode); await chat.insertInputForShowPath("2", path.firstNode); await delay(500); @@ -114,7 +114,7 @@ test.describe("Chat tests", () => { test("Validate error notification and its closure when sending an empty question in chat", async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); await chat.selectGraph(GRAPH_ID); - await chat.clickAskquestionBtn(); + await chat.clickAskQuestionBtn(); expect(await chat.isNotificationError()).toBe(true); await chat.clickOnNotificationErrorCloseBtn(); expect(await chat.isNotificationError()).toBe(false); From 451b02d2562eabab75263945b52c5dbf970bfadb Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Fri, 31 Jan 2025 19:01:50 +0200 Subject: [PATCH 29/39] fix failing tests --- e2e/logic/POM/codeGraph.ts | 13 ++++--------- e2e/tests/navBar.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 4ee1a0b4..38e9c101 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -259,13 +259,11 @@ export default class CodeGraph extends BasePage { } async clickOnTipBtn(): Promise { - const isVisible = await waitForElementToBeVisible(this.tipBtn); - if (!isVisible) throw new Error("'Tip' button is not visible!"); await this.tipBtn.click(); } async isTipMenuVisible(): Promise { - // await this.page.waitForTimeout(500); + await this.page.waitForTimeout(500); return await waitForElementToBeVisible(this.genericMenu); } @@ -454,19 +452,16 @@ export default class CodeGraph extends BasePage { } async clickOnRemoveNodeViaElementMenu(): Promise { - await this.elementMenu.waitFor({ state: "visible", timeout: 10000}) - const button = this.elementMenuButton("Remove"); + const button = this.elementMenuButton("Remove"); + const isVisible = await waitForElementToBeVisible(button); + if (!isVisible) throw new Error("'View Node' button is not visible!"); await button.click(); } async nodeClick(x: number, y: number): Promise { - await this.page.waitForTimeout(500); - console.log(`Clicking node at: X=${x}, Y=${y}`); await this.canvasElement.hover({ position: { x, y } }); await this.page.waitForTimeout(500); // Allow hover to take effect - console.log("Hover successful"); await this.canvasElement.click({ position: { x, y }, button: 'right' }); - console.log("Right-click performed"); } async selectCodeGraphCheckbox(checkbox: string): Promise { diff --git a/e2e/tests/navBar.spec.ts b/e2e/tests/navBar.spec.ts index 9f294a75..f282ca0a 100644 --- a/e2e/tests/navBar.spec.ts +++ b/e2e/tests/navBar.spec.ts @@ -41,9 +41,9 @@ test.describe(' Navbar tests', () => { test("Validate Tip popup visibility and closure functionality", async () => { const navBar = await browser.createNewPage(CodeGraph, urls.baseUrl); - await navBar.clickonTipBtn(); + await navBar.clickOnTipBtn(); expect(await navBar.isTipMenuVisible()).toBe(true); - await navBar.clickonTipMenuCloseBtn(); + await navBar.clickOnTipMenuCloseBtn(); expect(await navBar.isTipMenuVisible()).toBe(false); }); }); From 8f4343c7d70237970c17e396b76422cced123634 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 2 Feb 2025 12:40:26 +0200 Subject: [PATCH 30/39] Add CI screenshots on test failure --- .github/workflows/playwright.yml | 13 ++++++++++++- playwright.config.ts | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3893ea77..83f75e3a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -33,9 +33,20 @@ jobs: npm install npm run build NEXTAUTH_SECRET=SECRET npm start & npx playwright test --reporter=dot,list + - name: Ensure required directories exist + run: | + mkdir -p playwright-report + mkdir -p playwright-report/artifacts - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} + if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 + - name: Upload failed test screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: failed-test-screenshots + path: playwright-report/artifacts/ + retention-days: 30 diff --git a/playwright.config.ts b/playwright.config.ts index 7a5292d5..2ef25586 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -22,7 +22,8 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 2 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [['html', { outputFolder: 'playwright-report' }]], + outputDir: 'playwright-report/artifacts', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -30,6 +31,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + screenshot: 'only-on-failure', }, /* Configure projects for major browsers */ From bbe20700c189a5eae6309dda5bc254d14e033bc3 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 2 Feb 2025 13:05:10 +0200 Subject: [PATCH 31/39] Update playwright.yml --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 83f75e3a..06b8b283 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -33,7 +33,7 @@ jobs: npm install npm run build NEXTAUTH_SECRET=SECRET npm start & npx playwright test --reporter=dot,list - - name: Ensure required directories exist + - name: Ensure required directories exist run: | mkdir -p playwright-report mkdir -p playwright-report/artifacts From 6a3781327e3f38aeeb72da687ce462888ae3a254 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 2 Feb 2025 13:55:27 +0200 Subject: [PATCH 32/39] add logging --- e2e/logic/POM/codeGraph.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 38e9c101..22ec6f87 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -454,14 +454,20 @@ export default class CodeGraph extends BasePage { async clickOnRemoveNodeViaElementMenu(): Promise { const button = this.elementMenuButton("Remove"); const isVisible = await waitForElementToBeVisible(button); - if (!isVisible) throw new Error("'View Node' button is not visible!"); + if (!isVisible) throw new Error("'Remove' button is not visible!"); await button.click(); } async nodeClick(x: number, y: number): Promise { + await this.page.waitForTimeout(1000); + console.log("x: ", x, " y: ", y); + await this.canvasElement.hover({ position: { x, y } }); await this.page.waitForTimeout(500); // Allow hover to take effect await this.canvasElement.click({ position: { x, y }, button: 'right' }); + + const isMenuVisible = await waitForElementToBeVisible(this.elementMenu); + if (!isMenuVisible) throw new Error("Element menu did not appear!"); } async selectCodeGraphCheckbox(checkbox: string): Promise { From 44282ad89484eec841053061d1fabc6b68703803 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:10:55 +0200 Subject: [PATCH 33/39] add polling with retries --- e2e/logic/POM/codeGraph.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 22ec6f87..7c4423ae 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -228,6 +228,10 @@ export default class CodeGraph extends BasePage { private get copyToClipboardNodePanelDetails(): Locator { return this.page.locator(`//div[@data-name='node-details-panel']//button[@title='Copy src to clipboard']`); } + + private get nodeToolTip(): Locator { + return this.page.locator("//div[contains(@class, 'graph-tooltip')]"); + } /* NavBar functionality */ async clickOnFalkorDbLogo(): Promise { @@ -459,15 +463,23 @@ export default class CodeGraph extends BasePage { } async nodeClick(x: number, y: number): Promise { - await this.page.waitForTimeout(1000); - console.log("x: ", x, " y: ", y); - - await this.canvasElement.hover({ position: { x, y } }); - await this.page.waitForTimeout(500); // Allow hover to take effect - await this.canvasElement.click({ position: { x, y }, button: 'right' }); - - const isMenuVisible = await waitForElementToBeVisible(this.elementMenu); - if (!isMenuVisible) throw new Error("Element menu did not appear!"); + console.log(`Clicking node at (${x}, ${y})`); + + for (let attempt = 1; attempt <= 5; attempt++) { + await this.canvasElement.hover({ position: { x, y } }); + await this.page.waitForTimeout(500); + + if (await waitForElementToBeVisible(this.nodeToolTip)) { + console.log("Tooltip visible, right-clicking..."); + await this.canvasElement.click({ position: { x, y }, button: 'right' }); + return; + } + + console.warn(`Attempt ${attempt}: Tooltip not visible, retrying...`); + await this.page.waitForTimeout(1000); + } + + throw new Error("Tooltip not visible after multiple attempts!"); } async selectCodeGraphCheckbox(checkbox: string): Promise { From 364ace5b5d982eab56afe6862c803d19e880b57a Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:46:19 +0200 Subject: [PATCH 34/39] add logging and timing --- e2e/logic/POM/codeGraph.ts | 13 +++++++++++-- e2e/tests/nodeDetailsPanel.spec.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 7c4423ae..5d92023e 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -481,6 +481,14 @@ export default class CodeGraph extends BasePage { throw new Error("Tooltip not visible after multiple attempts!"); } + + async nodeClicktest(x: number, y: number): Promise { + console.log(`Clicking node at (${x}, ${y})`); + await this.page.waitForTimeout(500); + await this.canvasElement.hover({ position: { x, y } }); + await this.canvasElement.click({ position: { x, y }, button: 'right' }); + await this.page.waitForTimeout(5000); + } async selectCodeGraphCheckbox(checkbox: string): Promise { await this.codeGraphCheckbox(checkbox).click(); @@ -576,8 +584,8 @@ export default class CodeGraph extends BasePage { async getGraphDetails(): Promise { await this.canvasElementBeforeGraphSelection.waitFor({ state: 'detached' }); - await this.page.waitForTimeout(2000); //canvas animation - await this.page.waitForFunction(() => !!window.graph); + await this.page.waitForTimeout(3000); //canvas animation + await this.page.waitForFunction(() => window.graph && window.graph.elements.nodes.length > 0); const graphData = await this.page.evaluate(() => { return window.graph; @@ -587,6 +595,7 @@ export default class CodeGraph extends BasePage { } async transformNodeCoordinates(graphData: any): Promise { + await this.page.waitForTimeout(3000); const { canvasLeft, canvasTop, canvasWidth, canvasHeight, transform } = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { const rect = canvas.getBoundingClientRect(); const ctx = canvas.getContext('2d'); diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index 5f69afd4..b37f2483 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -37,8 +37,11 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); + const nod = findNodeByName(graphData.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", node1.screenX, " ", node1.screenY); await codeGraph.nodeClick(node1.screenX, node1.screenY); await codeGraph.clickOnViewNode(); await codeGraph.clickOnNodeDetailsCloseBtn(); @@ -51,8 +54,11 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); + const nod = findNodeByName(graphData.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", node1.screenX, " ", node1.screenY); await codeGraph.nodeClick(node1.screenX, node1.screenY); expect(await codeGraph.getNodeDetailsHeader()).toContain(node.nodeName.toUpperCase()) }) @@ -63,8 +69,11 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); + const nod = findNodeByName(graphData.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const nodeData = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", nodeData.screenX, " ", nodeData.screenY); await codeGraph.nodeClick(nodeData.screenX, nodeData.screenY); await codeGraph.clickOnViewNode(); const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); From 0fefd041332c9acf931cb9b14553564c6c9f68a6 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sun, 2 Feb 2025 20:51:49 +0200 Subject: [PATCH 35/39] add more logging improve transformnNode --- e2e/logic/POM/codeGraph.ts | 65 ++++++++++++++++++++---------- e2e/tests/canvas.spec.ts | 9 +++++ e2e/tests/nodeDetailsPanel.spec.ts | 7 ++++ 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 5d92023e..a0ef2e73 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -595,31 +595,54 @@ export default class CodeGraph extends BasePage { } async transformNodeCoordinates(graphData: any): Promise { - await this.page.waitForTimeout(3000); - const { canvasLeft, canvasTop, canvasWidth, canvasHeight, transform } = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { - const rect = canvas.getBoundingClientRect(); - const ctx = canvas.getContext('2d'); - const transform = ctx?.getTransform()!; - return { - canvasLeft: rect.left, - canvasTop: rect.top, - canvasWidth: rect.width, - canvasHeight: rect.height, - transform, - }; - }); - - const screenCoordinates = graphData.elements.nodes.map((node: any) => { - const adjustedX = node.x * transform.a + transform.e; + let maxRetries = 5; + let transform = null; + let canvasRect = null; + + // Ensure the graph is fully loaded before proceeding + await this.page.waitForFunction(() => window.graph && window.graph.elements?.nodes?.length > 0); + await this.page.waitForTimeout(3000); // Wait for canvas animations to settle + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + await this.page.waitForTimeout(1000); // Allow further stabilization before fetching data + + // Fetch canvas properties + const result = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { + const rect = canvas.getBoundingClientRect(); + const ctx = canvas.getContext('2d'); + return { + canvasLeft: rect.left, + canvasTop: rect.top, + canvasWidth: rect.width, + canvasHeight: rect.height, + transform: ctx?.getTransform() || null, // Ensure transform is not null + }; + }); + + if (!result.transform) { + console.warn(`Attempt ${attempt}: Transform not available yet, retrying...`); + continue; // Retry if transform is not available + } + + // If we successfully get a transform, store and break out of retry loop + transform = result.transform; + canvasRect = result; + break; + } + + if (!transform) throw new Error("Canvas transform data not available after multiple attempts!"); + + // Convert graph coordinates to screen coordinates + return graphData.elements.nodes.map((node: any) => { + const adjustedX = node.x * transform.a + transform.e; const adjustedY = node.y * transform.d + transform.f; - const screenX = canvasLeft + adjustedX - 35; - const screenY = canvasTop + adjustedY - 190; + const screenX = canvasRect!.canvasLeft + adjustedX - 35; + const screenY = canvasRect!.canvasTop + adjustedY - 190; - return {...node, screenX, screenY,}; + return { ...node, screenX, screenY }; }); - - return screenCoordinates; } + async getCanvasScaling(): Promise<{ scaleX: number; scaleY: number }> { const { scaleX, scaleY } = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index e54858d8..d5048c6b 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -58,8 +58,11 @@ test.describe("Canvas tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const initialGraph = await codeGraph.getGraphDetails(); + const nod = findNodeByName(initialGraph.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); const targetNode = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", targetNode.screenX, " ", targetNode.screenY); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnRemoveNodeViaElementMenu(); const updatedGraph = await codeGraph.getGraphDetails(); @@ -73,8 +76,11 @@ test.describe("Canvas tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const initialGraph = await codeGraph.getGraphDetails(); + const nod = findNodeByName(initialGraph.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); const targetNode = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", targetNode.screenX, " ", targetNode.screenY); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnRemoveNodeViaElementMenu(); await codeGraph.clickOnUnhideNodesBtn(); @@ -170,8 +176,11 @@ test.describe("Canvas tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); + const nod = findNodeByName(graphData.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const targetNode = findNodeByName(convertCoordinates, node.nodeName); + console.log("after x:", targetNode.x, " after y: ",targetNode.y); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); const result = await codeGraph.clickOnCopyToClipboard(); const api = new ApiCalls(); diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index b37f2483..f9fb104e 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -23,8 +23,11 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); + const nod = findNodeByName(graphData.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const targetNode = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", targetNode.screenX, " ", targetNode.screenY); expect(targetNode).toBeDefined(); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); @@ -89,8 +92,12 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); + const nod = findNodeByName(graphData.elements.nodes, node.nodeName); + console.log("before x:", nod.x, " before y: ",nod.y); + const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); + console.log("after test: ", node1.screenX, " ", node1.screenY); const api = new ApiCalls(); const response = await api.getProject(PROJECT_NAME); const data: any = response.result.entities.nodes; From c214063a727648e3bee8a3d3eecb29dc92b652cd Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:54:59 +0200 Subject: [PATCH 36/39] Update testData.ts --- e2e/config/testData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/config/testData.ts b/e2e/config/testData.ts index dab5ebf7..3cb008e2 100644 --- a/e2e/config/testData.ts +++ b/e2e/config/testData.ts @@ -20,7 +20,7 @@ export const nodesPath: { firstNode: string; secondNode: string }[] = [ ]; export const nodes: { nodeName: string; }[] = [ - { nodeName: "import_data"}, + { nodeName: "ask"}, { nodeName: "add_edge" }, { nodeName: "test_kg_delete"}, { nodeName: "list_graphs"} From 681e65537db2cad94a308ad7af3c54b4e0ca0b85 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:12:45 +0200 Subject: [PATCH 37/39] remove logging, increase workers --- e2e/logic/POM/codeGraph.ts | 29 +++++++++-------------------- e2e/tests/canvas.spec.ts | 9 --------- e2e/tests/nodeDetailsPanel.spec.ts | 18 +----------------- playwright.config.ts | 4 ++-- 4 files changed, 12 insertions(+), 48 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index a0ef2e73..7cc9ccd7 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -462,23 +462,18 @@ export default class CodeGraph extends BasePage { await button.click(); } - async nodeClick(x: number, y: number): Promise { - console.log(`Clicking node at (${x}, ${y})`); - - for (let attempt = 1; attempt <= 5; attempt++) { + async nodeClick(x: number, y: number): Promise { + for (let attempt = 1; attempt <= 2; attempt++) { await this.canvasElement.hover({ position: { x, y } }); await this.page.waitForTimeout(500); if (await waitForElementToBeVisible(this.nodeToolTip)) { - console.log("Tooltip visible, right-clicking..."); await this.canvasElement.click({ position: { x, y }, button: 'right' }); return; } - - console.warn(`Attempt ${attempt}: Tooltip not visible, retrying...`); await this.page.waitForTimeout(1000); } - + throw new Error("Tooltip not visible after multiple attempts!"); } @@ -584,8 +579,8 @@ export default class CodeGraph extends BasePage { async getGraphDetails(): Promise { await this.canvasElementBeforeGraphSelection.waitFor({ state: 'detached' }); - await this.page.waitForTimeout(3000); //canvas animation await this.page.waitForFunction(() => window.graph && window.graph.elements.nodes.length > 0); + await this.page.waitForTimeout(2000); //canvas animation const graphData = await this.page.evaluate(() => { return window.graph; @@ -595,18 +590,14 @@ export default class CodeGraph extends BasePage { } async transformNodeCoordinates(graphData: any): Promise { - let maxRetries = 5; + let maxRetries = 3; let transform = null; let canvasRect = null; - - // Ensure the graph is fully loaded before proceeding await this.page.waitForFunction(() => window.graph && window.graph.elements?.nodes?.length > 0); - await this.page.waitForTimeout(3000); // Wait for canvas animations to settle for (let attempt = 1; attempt <= maxRetries; attempt++) { - await this.page.waitForTimeout(1000); // Allow further stabilization before fetching data + await this.page.waitForTimeout(1000); - // Fetch canvas properties const result = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { const rect = canvas.getBoundingClientRect(); const ctx = canvas.getContext('2d'); @@ -615,24 +606,22 @@ export default class CodeGraph extends BasePage { canvasTop: rect.top, canvasWidth: rect.width, canvasHeight: rect.height, - transform: ctx?.getTransform() || null, // Ensure transform is not null + transform: ctx?.getTransform() || null, }; }); if (!result.transform) { console.warn(`Attempt ${attempt}: Transform not available yet, retrying...`); - continue; // Retry if transform is not available + continue; } - // If we successfully get a transform, store and break out of retry loop transform = result.transform; canvasRect = result; break; } if (!transform) throw new Error("Canvas transform data not available after multiple attempts!"); - - // Convert graph coordinates to screen coordinates + return graphData.elements.nodes.map((node: any) => { const adjustedX = node.x * transform.a + transform.e; const adjustedY = node.y * transform.d + transform.f; diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index d5048c6b..e54858d8 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -58,11 +58,8 @@ test.describe("Canvas tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const initialGraph = await codeGraph.getGraphDetails(); - const nod = findNodeByName(initialGraph.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); const targetNode = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", targetNode.screenX, " ", targetNode.screenY); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnRemoveNodeViaElementMenu(); const updatedGraph = await codeGraph.getGraphDetails(); @@ -76,11 +73,8 @@ test.describe("Canvas tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const initialGraph = await codeGraph.getGraphDetails(); - const nod = findNodeByName(initialGraph.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); const targetNode = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", targetNode.screenX, " ", targetNode.screenY); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnRemoveNodeViaElementMenu(); await codeGraph.clickOnUnhideNodesBtn(); @@ -176,11 +170,8 @@ test.describe("Canvas tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); - const nod = findNodeByName(graphData.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const targetNode = findNodeByName(convertCoordinates, node.nodeName); - console.log("after x:", targetNode.x, " after y: ",targetNode.y); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); const result = await codeGraph.clickOnCopyToClipboard(); const api = new ApiCalls(); diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index f9fb104e..3aea588c 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -23,11 +23,8 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); - const nod = findNodeByName(graphData.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const targetNode = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", targetNode.screenX, " ", targetNode.screenY); expect(targetNode).toBeDefined(); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); @@ -40,11 +37,8 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); - const nod = findNodeByName(graphData.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", node1.screenX, " ", node1.screenY); await codeGraph.nodeClick(node1.screenX, node1.screenY); await codeGraph.clickOnViewNode(); await codeGraph.clickOnNodeDetailsCloseBtn(); @@ -57,11 +51,8 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); - const nod = findNodeByName(graphData.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", node1.screenX, " ", node1.screenY); await codeGraph.nodeClick(node1.screenX, node1.screenY); expect(await codeGraph.getNodeDetailsHeader()).toContain(node.nodeName.toUpperCase()) }) @@ -72,11 +63,8 @@ test.describe("Node details panel tests", () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); const graphData = await codeGraph.getGraphDetails(); - const nod = findNodeByName(graphData.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const nodeData = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", nodeData.screenX, " ", nodeData.screenY); await codeGraph.nodeClick(nodeData.screenX, nodeData.screenY); await codeGraph.clickOnViewNode(); const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); @@ -91,13 +79,9 @@ test.describe("Node details panel tests", () => { test(`Validate view node panel keys via api for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); - const nod = findNodeByName(graphData.elements.nodes, node.nodeName); - console.log("before x:", nod.x, " before y: ",nod.y); - + const graphData = await codeGraph.getGraphDetails(); const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); const node1 = findNodeByName(convertCoordinates, node.nodeName); - console.log("after test: ", node1.screenX, " ", node1.screenY); const api = new ApiCalls(); const response = await api.getProject(PROJECT_NAME); const data: any = response.result.entities.nodes; diff --git a/playwright.config.ts b/playwright.config.ts index 2ef25586..8b5b1550 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,11 +16,11 @@ export default defineConfig({ /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, + forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 2 : undefined, + workers: process.env.CI ? 3 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [['html', { outputFolder: 'playwright-report' }]], outputDir: 'playwright-report/artifacts', From 97997146a7bc1fff2afc08416cb7fd5d4ac12cb4 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 6 Feb 2025 12:08:18 +0200 Subject: [PATCH 38/39] fix build --- app/components/code-graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 1dad952d..8e0341f8 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -243,7 +243,7 @@ export function CodeGraph({ setData({ ...graph.Elements }) } - setSearchNode(n) + setSearchNode(chartNode) setTimeout(() => { chart.zoomToFit(1000, 150, (n: NodeObject) => n.id === chartNode!.id); }, 0) From b35fd2b27cd257a0f98df0a6fd545f4809f052ec Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 11 Feb 2025 11:32:57 +0200 Subject: [PATCH 39/39] Implement dynamic zoom to fit functionality with improved padding calculation --- app/components/chat.tsx | 63 +++++------------------------------ app/components/code-graph.tsx | 61 ++++++++++++++++++--------------- app/components/graphView.tsx | 32 +++++++++++++++--- app/components/toolbar.tsx | 9 ++++- app/page.tsx | 8 +++-- 5 files changed, 84 insertions(+), 89 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6af9ac79..66ad6a95 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import { AlignLeft, ArrowDown, ArrowRight, ChevronDown, Lightbulb, Undo2 } from "lucide-react"; import { Path } from "../page"; import Input from "./Input"; -import { Graph, GraphData } from "./model"; +import { Graph, GraphData, Node } from "./model"; import { cn } from "@/lib/utils"; import { TypeAnimation } from "react-type-animation"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; @@ -25,50 +25,6 @@ enum MessageTypes { Text, } -const EDGE_STYLE = { - "line-color": "gray", - "target-arrow-color": "gray", - "opacity": 0.5, -} - - -const PATH_EDGE_STYLE = { - width: 0.5, - "line-style": "dashed", - "line-color": "#FF66B3", - "arrow-scale": 0.3, - "target-arrow-color": "#FF66B3", - "opacity": 1 -} - -const SELECTED_PATH_EDGE_STYLE = { - width: 1, - "line-style": "solid", - "line-color": "#FF66B3", - "arrow-scale": 0.6, - "target-arrow-color": "#FF66B3", -}; - -const NODE_STYLE = { - "border-width": 0.5, - "color": "gray", - "border-color": "black", - "background-color": "gray", - "opacity": 0.5 -} - -const PATH_NODE_STYLE = { - "border-width": 0.5, - "border-color": "#FF66B3", - "border-opacity": 1, -} - -const SELECTED_PATH_NODE_STYLE = { - "border-width": 1, - "border-color": "#FF66B3", - "border-opacity": 1, -}; - interface Message { type: MessageTypes; text?: string; @@ -85,6 +41,7 @@ interface Props { isPathResponse: boolean | undefined setIsPathResponse: (isPathResponse: boolean | undefined) => void setData: Dispatch> + setNodesToZoom: Dispatch> chartRef: any } @@ -107,7 +64,7 @@ const RemoveLastPath = (messages: Message[]) => { return messages } -export function Chat({ repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData, chartRef }: Props) { +export function Chat({ repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData, setNodesToZoom, chartRef }: Props) { // Holds the messages in the chat const [messages, setMessages] = useState([]); @@ -156,7 +113,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons const handleSetSelectedPath = (p: PathData) => { const chart = chartRef.current - + if (!chart) return setSelectedPath(prev => { if (prev) { @@ -210,9 +167,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons }); } setData({ ...graph.Elements }) - setTimeout(() => { - chart.zoomToFit(1000, 150, (n: NodeObject) => p.nodes.some(node => node.id === n.id)); - }, 0) + setNodesToZoom([...p.nodes]) } // A function that handles the change event of the url input box @@ -308,9 +263,7 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons setPath(undefined) setIsPathResponse(true) setData({ ...graph.Elements }) - setTimeout(() => { - chart.zoomToFit(1000, 150, (n: NodeObject) => formattedPaths.some(p => p.nodes.some(node => node.id === n.id))); - }, 0) + setNodesToZoom([...formattedPaths.flatMap(path => path.nodes)]) } const getTip = (disabled = false) => @@ -434,10 +387,10 @@ export function Chat({ repo, path, setPath, graph, selectedPathId, isPathRespons } if (selectedPath?.nodes.every(node => p?.nodes.some((n) => n.id === node.id)) && selectedPath.nodes.length === p.nodes.length) return - + if (!isPathResponse) { setIsPathResponse(undefined) - + } handleSetSelectedPath(p) }} diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 8e0341f8..7c62b37b 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -14,7 +14,6 @@ import { Checkbox } from '@/components/ui/checkbox'; import dynamic from 'next/dynamic'; import { Position } from "./graphView"; import { prepareArg } from '../utils'; -import { NodeObject } from "react-force-graph-2d"; const GraphView = dynamic(() => import('./graphView')); @@ -27,6 +26,8 @@ interface Props { setOptions: Dispatch> isShowPath: boolean setPath: Dispatch> + nodesToZoom: Node[] | undefined + setNodesToZoom: Dispatch> chartRef: RefObject selectedValue: string selectedPathId: number | undefined @@ -44,6 +45,8 @@ export function CodeGraph({ setOptions, isShowPath, setPath, + nodesToZoom, + setNodesToZoom, chartRef, selectedValue, setSelectedPathId, @@ -167,18 +170,16 @@ export function CodeGraph({ } const deleteNeighbors = (nodes: Node[]) => { - + if (nodes.length === 0) return; - + const expandedNodes: Node[] = [] - + graph.Elements = { nodes: graph.Elements.nodes.filter(node => { if (!node.collapsed) return true - + const isTarget = graph.Elements.links.some(link => link.target.id === node.id && nodes.some(n => n.id === link.source.id)); - - debugger if (!isTarget) return true @@ -192,43 +193,53 @@ export function CodeGraph({ }), links: graph.Elements.links } - + deleteNeighbors(expandedNodes) graph.removeLinks() } const handleExpand = async (nodes: Node[], expand: boolean) => { + const chart = chartRef.current; + if (!chart) return; + if (expand) { - const elements = await onFetchNode(nodes.map(n => n.id)) + const elements = await onFetchNode(nodes.map(n => n.id)); if (elements.nodes.length === 0) { toast({ title: `No neighbors found`, description: `No neighbors found`, - }) - return + }); + return; } + + nodes.forEach((node) => { + node.expand = expand; + }); + + setNodesToZoom([...elements.nodes, ...nodes]); + setSelectedObj(undefined); + setData({ ...graph.Elements }); } else { - const deleteNodes = nodes.filter(n => n.expand) + const deleteNodes = nodes.filter(n => n.expand); if (deleteNodes.length > 0) { deleteNeighbors(deleteNodes); } - } - nodes.forEach((node) => { - node.expand = expand - }) + nodes.forEach((node) => { + node.expand = expand; + }); - setSelectedObj(undefined) - setData({ ...graph.Elements }) - } + setSelectedObj(undefined); + setData({ ...graph.Elements }); + } + }; const handleSearchSubmit = (node: any) => { const chart = chartRef.current if (chart) { - let chartNode = graph.Elements.nodes.find(n => n.id == node.id) if (!chartNode?.visible) { @@ -242,11 +253,9 @@ export function CodeGraph({ graph.visibleLinks(true, [chartNode!.id]) setData({ ...graph.Elements }) } - + setSearchNode(chartNode) - setTimeout(() => { - chart.zoomToFit(1000, 150, (n: NodeObject) => n.id === chartNode!.id); - }, 0) + setNodesToZoom([chartNode!]); } } @@ -403,7 +412,6 @@ export function CodeGraph({ /> > graph: Graph chartRef: RefObject selectedObj: Node | Link | undefined @@ -19,10 +19,11 @@ interface Props { selectedObjects: Node[] setSelectedObjects: Dispatch> setPosition: Dispatch> - onFetchNode: (nodeIds: number[]) => Promise handleExpand: (nodes: Node[], expand: boolean) => void isShowPath: boolean setPath: Dispatch> + nodesToZoom: Node[] | undefined + setNodesToZoom: Dispatch> isPathResponse: boolean | undefined selectedPathId: number | undefined setSelectedPathId: (selectedPathId: number) => void @@ -38,7 +39,6 @@ const PADDING = 2; export default function GraphView({ data, - setData, graph, chartRef, selectedObj, @@ -46,10 +46,11 @@ export default function GraphView({ selectedObjects, setSelectedObjects, setPosition, - onFetchNode, handleExpand, isShowPath, setPath, + nodesToZoom, + setNodesToZoom, isPathResponse, selectedPathId, setSelectedPathId, @@ -278,6 +279,27 @@ export default function GraphView({ onEngineStop={() => { setCooldownTicks(0) setCooldownTime(0) + + const chart = chartRef.current; + if (!chart || !nodesToZoom || nodesToZoom.length === 0) return; + + // Get canvas dimensions + const canvas = document.querySelector('.force-graph-container canvas') as HTMLCanvasElement; + if (!canvas) return; + + // Calculate padding as 10% of the smallest canvas dimension, with minimum of 40px + const minDimension = Math.min(canvas.width, canvas.height); + const padding = Math.max(minDimension * 0.1, 40); + + console.log('Canvas dimensions:', canvas.width, canvas.height); + console.log('nodesToZoom', nodesToZoom.map(n => n.id)); + + chart.zoomToFit(1000, padding, (n: NodeObject) => { + const isMatch = nodesToZoom.some(node => node.id === n.id); + console.log('Checking node:', n.id, 'isMatch:', isMatch); + return isMatch; + }); + setNodesToZoom(undefined); }} cooldownTicks={cooldownTicks} cooldownTime={cooldownTime} diff --git a/app/components/toolbar.tsx b/app/components/toolbar.tsx index f718d741..97355a50 100644 --- a/app/components/toolbar.tsx +++ b/app/components/toolbar.tsx @@ -19,7 +19,14 @@ export function Toolbar({ chartRef, className }: Props) { const handleCenterClick = () => { const chart = chartRef.current if (chart) { - chart.zoomToFit(1000, 40) + // Get canvas dimensions + const canvas = document.querySelector('.force-graph-container canvas') as HTMLCanvasElement; + if (!canvas) return; + + // Calculate padding as 10% of the smallest canvas dimension, with minimum of 40px + const minDimension = Math.min(canvas.width, canvas.height); + const padding = minDimension * 0.1 + chart.zoomToFit(1000, padding) } } diff --git a/app/page.tsx b/app/page.tsx index ef7fa374..4551f0e2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { Chat } from './components/chat'; -import { Graph, GraphData } from './components/model'; +import { Graph, GraphData, Node } from './components/model'; import { BookOpen, Github, HomeIcon, X } from 'lucide-react'; import Link from 'next/link'; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; @@ -62,6 +62,7 @@ export default function Home() { const [options, setOptions] = useState([]); const [path, setPath] = useState(); const [isSubmit, setIsSubmit] = useState(false); + const [nodesToZoom, setNodesToZoom] = useState(); const chartRef = useRef() useEffect(() => { @@ -269,6 +270,8 @@ export default function Home() { onFetchGraph={onFetchGraph} onFetchNode={onFetchNode} setPath={setPath} + nodesToZoom={nodesToZoom} + setNodesToZoom={setNodesToZoom} isShowPath={!!path} selectedValue={selectedValue} selectedPathId={selectedPathId} @@ -290,7 +293,8 @@ export default function Home() { isPathResponse={isPathResponse} setIsPathResponse={setIsPathResponse} setData={setData} - /> + setNodesToZoom={setNodesToZoom} + />