From 78816dec4a2f60924407bcdce603d7d8f1575d51 Mon Sep 17 00:00:00 2001 From: Avi Avni Date: Tue, 8 Jul 2025 02:12:17 -0700 Subject: [PATCH 01/65] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index edd18fcb..2a712b33 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ npm run dev curl -X POST http://127.0.0.1:5000/analyze_folder -H "Content-Type: application/json" -d '{"path": "", "ignore": ["./.github", "./sbin", "./.git","./deps", "./bin", "./build"]}' -H "Authorization: " ``` -Note: At the moment code-graph can analyze both the C & Python source files. -Support for additional languages e.g. JavaScript, Go, Java is planned to be added +Note: At the moment code-graph can analyze both the Java & Python source files. +Support for additional languages e.g. C, JavaScript, Go is planned to be added in the future. Browse to [http://localhost:3000](http://localhost:3000) From 3cd8513577c8d676c8cc4ede760d618a9e66aaa3 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:22:07 +0300 Subject: [PATCH 02/65] fix tests --- app/components/elementMenu.tsx | 1 + e2e/config/constants.ts | 4 +- e2e/config/testData.ts | 2 +- e2e/logic/POM/codeGraph.ts | 147 ++++++++++++++++++++++------- e2e/logic/utils.ts | 6 +- e2e/tests/canvas.spec.ts | 98 ++++++++----------- e2e/tests/chat.spec.ts | 37 ++++---- e2e/tests/nodeDetailsPanel.spec.ts | 78 +++++++-------- e2e/tests/searchBar.spec.ts | 14 +-- 9 files changed, 230 insertions(+), 157 deletions(-) diff --git a/app/components/elementMenu.tsx b/app/components/elementMenu.tsx index d6fd8d99..4d8919da 100644 --- a/app/components/elementMenu.tsx +++ b/app/components/elementMenu.tsx @@ -41,6 +41,7 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit setContainerWidth(ref.clientWidth) }} className="absolute z-10 bg-black rounded-lg shadow-lg flex divide-x divide-[#434343]" + id="elementMenu" style={{ 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), diff --git a/e2e/config/constants.ts b/e2e/config/constants.ts index 63bcec56..98d3cf31 100644 --- a/e2e/config/constants.ts +++ b/e2e/config/constants.ts @@ -1,5 +1,5 @@ -export const GRAPH_ID = "GraphRAG-SDK"; -export const PROJECT_NAME = "GraphRAG-SDK"; +export const GRAPHRAG_SDK = "GraphRAG-SDK"; +export const FLASK_GRAPH = "flask"; export const CHAT_OPTTIONS_COUNT = 1; export const Node_Question = "how many nodes do we have?"; export const Edge_Question = "how many edges do we have?"; \ No newline at end of file diff --git a/e2e/config/testData.ts b/e2e/config/testData.ts index dab5ebf7..db9551ff 100644 --- a/e2e/config/testData.ts +++ b/e2e/config/testData.ts @@ -1,7 +1,7 @@ export const searchData: { searchInput: string; completedSearchInput?: string; }[] = [ { searchInput: "test"}, { searchInput: "set"}, - { searchInput: "low", completedSearchInput: "lower" }, + { searchInput: "low", completedSearchInput: "lower_items" }, { searchInput: "as", completedSearchInput: "ask"}, ]; diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 9910ded3..d2120e49 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -120,6 +120,10 @@ export default class CodeGraph extends BasePage { return this.page.locator("//main[@data-name='main-chat']/*[last()-2]//img[@alt='Waiting for response']") } + private get waitingForResponseImage(): Locator { + return this.page.locator("//img[@alt='Waiting for response']") + } + private get selectInputForShowPath(): (inputNum: string) => Locator { return (inputNum: string) => this.page.locator(`(//main[@data-name='main-chat']//input)[${inputNum}]`); } @@ -189,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"); @@ -277,7 +285,12 @@ export default class CodeGraph extends BasePage { } async getTextInLastChatElement(): Promise{ - await delay(2500); + // Wait for loading indicator to disappear + await this.waitingForResponseImage.waitFor({ state: 'hidden', timeout: 15000 }); + + // Short delay to ensure text is fully rendered + await delay(1000); + return (await this.lastElementInChat.textContent())!; } @@ -416,8 +429,18 @@ 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.waitForCanvasAnimationToEnd(); + for (let attempt = 1; attempt <= 3; attempt++) { + await this.canvasElement.hover({ position: { x, y } }); + await this.page.waitForTimeout(500); + await this.canvasElement.click({ position: { x, y }, button: 'left' }); + if (await this.elementMenu.isVisible()) { + return; + } + await this.page.waitForTimeout(1000); + } + + throw new Error(`Failed to click, elementMenu not visible after multiple attempts.`); } async selectCodeGraphCheckbox(checkbox: string): Promise { @@ -493,42 +516,39 @@ export default class CodeGraph extends BasePage { return Promise.all(elements.map(element => element.innerHTML())); } - async getGraphDetails(): Promise { - await this.canvasElementBeforeGraphSelection.waitFor({ state: 'detached' }); - await delay(2000) - await this.page.waitForFunction(() => !!window.graph); + async getGraphNodes(): Promise { + await this.waitForCanvasAnimationToEnd(); const graphData = await this.page.evaluate(() => { - return window.graph; - }); - - return graphData; - } - - async transformNodeCoordinates(graphData: any): Promise { - 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, - }; + return (window as any).graph; }); - - const screenCoordinates = 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; - return {...node, screenX, screenY,}; - }); + let transformData: any = null; + for (let attempt = 0; attempt < 3; attempt++) { + await this.page.waitForTimeout(1000); + + transformData = await this.canvasElement.evaluate((canvas: HTMLCanvasElement) => { + const rect = canvas.getBoundingClientRect(); + const ctx = canvas.getContext('2d'); + return { + left: rect.left, + top: rect.top, + transform: ctx?.getTransform() || null, + }; + }); - return screenCoordinates; + if (transformData.transform) break; + console.warn(`Attempt ${attempt + 1}: Transform data not available, retrying...`); + } + + if (!transformData?.transform) throw new Error("Canvas transform data not available!"); + + const { a, e, d, f } = transformData.transform; + return graphData.elements.nodes.map((node: any) => ({ + ...node, + screenX: transformData.left + node.x * a + e - 35, + screenY: transformData.top + node.y * d + f - 190, + })); } async getCanvasScaling(): Promise<{ scaleX: number; scaleY: number }> { @@ -543,4 +563,63 @@ export default class CodeGraph extends BasePage { return { scaleX, scaleY }; } + async getGraphDetails(): Promise { + await this.canvasElementBeforeGraphSelection.waitFor({ state: 'detached' }); + await this.waitForCanvasAnimationToEnd(); + await this.page.waitForFunction(() => !!window.graph); + + const graphData = await this.page.evaluate(() => { + return window.graph; + }); + + return graphData; + } + + async waitForCanvasAnimationToEnd(timeout = 15000, checkInterval = 500): Promise { + const canvasHandle = await this.canvasElement.elementHandle(); + + if (!canvasHandle) { + throw new Error("Canvas element not found!"); + } + + await this.page.waitForFunction( + async ({ canvas, checkInterval, timeout }) => { + const ctx = canvas.getContext('2d'); + if (!ctx) return false; + + const width = canvas.width; + const height = canvas.height; + + let previousData = ctx.getImageData(0, 0, width, height).data; + const startTime = Date.now(); + + return new Promise((resolve) => { + const checkCanvas = () => { + if (Date.now() - startTime > timeout) { + resolve(true); + return; + } + + setTimeout(() => { + const currentData = ctx.getImageData(0, 0, width, height).data; + if (JSON.stringify(previousData) === JSON.stringify(currentData)) { + resolve(true); + } else { + previousData = currentData; + checkCanvas(); + } + }, checkInterval); + }; + checkCanvas(); + }); + }, + { + canvas: await canvasHandle.evaluateHandle((el) => el as HTMLCanvasElement), + checkInterval, + timeout + }, + { timeout } + ); + } + } diff --git a/e2e/logic/utils.ts b/e2e/logic/utils.ts index 6ba3475d..47833e70 100644 --- a/e2e/logic/utils.ts +++ b/e2e/logic/utils.ts @@ -17,4 +17,8 @@ export const waitToBeEnabled = async (locator: Locator, timeout: number = 5000): export function findNodeByName(nodes: { name: string }[], nodeName: string): any { return nodes.find((node) => node.name === nodeName); - } \ No newline at end of file +} + +export function findFirstNodeWithSrc(nodes: { src?: string }[]): any { + return nodes.find((node) => node.src !== undefined); +} \ No newline at end of file diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index 3701ac86..8869ec2c 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from "@playwright/test"; import BrowserWrapper from "../infra/ui/browserWrapper"; import CodeGraph from "../logic/POM/codeGraph"; import urls from "../config/urls.json"; -import { GRAPH_ID, PROJECT_NAME } from "../config/constants"; +import { GRAPHRAG_SDK } from "../config/constants"; import { findNodeByName } from "../logic/utils"; import { nodesPath, categories, nodes } from "../config/testData"; import { ApiCalls } from "../logic/api/apiCalls"; @@ -20,7 +20,7 @@ test.describe("Canvas tests", () => { test(`Verify zoom in functionality on canvas`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); const initialGraph = await codeGraph.getCanvasScaling(); await codeGraph.clickZoomIn(); await codeGraph.clickZoomIn(); @@ -31,7 +31,7 @@ test.describe("Canvas tests", () => { test(`Verify zoom out functionality on canvas`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); const initialGraph = await codeGraph.getCanvasScaling(); await codeGraph.clickZoomOut(); await codeGraph.clickZoomOut(); @@ -42,29 +42,29 @@ 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.selectGraph(GRAPHRAG_SDK); + 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); + expect(Math.abs(initialGraph.scaleX - updatedGraph.scaleX)).toBeLessThanOrEqual(0.2); + expect(Math.abs(initialGraph.scaleY - updatedGraph.scaleY)).toBeLessThanOrEqual(0.2); }) nodes.slice(0,2).forEach((node) => { test(`Validate node hide functionality via element menu in canvas for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const initialGraph = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); - const targetNode = findNodeByName(convertCoordinates, node.nodeName); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const initialGraph = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(initialGraph, node.nodeName); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnRemoveNodeViaElementMenu(); - const updatedGraph = await codeGraph.getGraphDetails(); - const targetNodeForUpdateGraph = findNodeByName(updatedGraph.elements.nodes, node.nodeName); + const updatedGraph = await codeGraph.getGraphNodes(); + const targetNodeForUpdateGraph = findNodeByName(updatedGraph, node.nodeName); expect(targetNodeForUpdateGraph.visible).toBe(false); }); }) @@ -72,15 +72,14 @@ test.describe("Canvas tests", () => { nodes.slice(0,2).forEach((node) => { test(`Validate unhide node functionality after hiding a node in canvas for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const initialGraph = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); - const targetNode = findNodeByName(convertCoordinates, node.nodeName); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const initialGraph = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(initialGraph, node.nodeName); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnRemoveNodeViaElementMenu(); await codeGraph.clickOnUnhideNodesBtn(); - const updatedGraph = await codeGraph.getGraphDetails(); - const targetNodeForUpdateGraph = findNodeByName(updatedGraph.elements.nodes, node.nodeName); + const updatedGraph = await codeGraph.getGraphNodes(); + const targetNodeForUpdateGraph = findNodeByName(updatedGraph, node.nodeName); expect(targetNodeForUpdateGraph.visible).toBe(true); }); }) @@ -89,30 +88,30 @@ test.describe("Canvas tests", () => { const checkboxIndex = index + 1; test(`Verify that unchecking the ${category} checkbox hides ${category} nodes on the canvas`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); await codeGraph.selectCodeGraphCheckbox(checkboxIndex.toString()); - const result = await codeGraph.getGraphDetails(); - const findItem = result.categories.find((item: { name: string; }) => item.name === category) - expect(findItem.show).toBe(false) + const result = await codeGraph.getGraphNodes(); + const findItem = result.find((item: { category: string; }) => item.category === category); + expect(findItem?.visible).toBe(false); }); }) nodesPath.forEach((path) => { 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.selectGraph(GRAPHRAG_SDK); await codeGraph.clickOnshowPathBtn(); await codeGraph.insertInputForShowPath("1", path.firstNode); await codeGraph.insertInputForShowPath("2", path.secondNode); - const initialGraph = await codeGraph.getGraphDetails(); - const firstNode = findNodeByName(initialGraph.elements.nodes, path.firstNode); - const secondNode = findNodeByName(initialGraph.elements.nodes, path.secondNode); + const initialGraph = await codeGraph.getGraphNodes(); + const firstNode = findNodeByName(initialGraph, path.firstNode); + const secondNode = findNodeByName(initialGraph, path.secondNode); expect(firstNode.isPath).toBe(true); expect(secondNode.isPath).toBe(true); await codeGraph.clickOnClearGraphBtn(); - const updateGraph = await codeGraph.getGraphDetails(); - const firstNode1 = findNodeByName(updateGraph.elements.nodes, path.firstNode); - const secondNode1 = findNodeByName(updateGraph.elements.nodes, path.secondNode); + const updateGraph = await codeGraph.getGraphNodes(); + const firstNode1 = findNodeByName(updateGraph, path.firstNode); + const secondNode1 = findNodeByName(updateGraph, path.secondNode); expect(firstNode1.isPath).toBe(false); expect(secondNode1.isPath).toBe(false); }); @@ -133,22 +132,21 @@ test.describe("Canvas tests", () => { const nodeIndex: number = index + 1; test(`Validate canvas node dragging for node: ${index}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const initialGraph = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(initialGraph); - await codeGraph.changeNodePosition(convertCoordinates[nodeIndex].screenX, convertCoordinates[nodeIndex].screenY); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const initialGraph = await codeGraph.getGraphNodes(); + await codeGraph.changeNodePosition(initialGraph[nodeIndex].screenX, initialGraph[nodeIndex].screenY); const updateGraph = await codeGraph.getGraphDetails(); - expect(updateGraph.elements.nodes[nodeIndex].x).not.toBe(initialGraph.elements.nodes[nodeIndex].x); - expect(updateGraph.elements.nodes[nodeIndex].y).not.toBe(initialGraph.elements.nodes[nodeIndex].y); + expect(updateGraph.elements.nodes[nodeIndex].x).not.toBe(initialGraph[nodeIndex].x); + expect(updateGraph.elements.nodes[nodeIndex].y).not.toBe(initialGraph[nodeIndex].y); }); } test(`Validate node and edge counts in canvas match API data`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); const { nodes, edges } = await codeGraph.getMetricsPanelInfo(); const api = new ApiCalls(); - const response = await api.projectInfo(PROJECT_NAME); + const response = await api.projectInfo(GRAPHRAG_SDK); expect(response.result.info.node_count).toEqual(parseInt(nodes)); expect(response.result.info.edge_count).toEqual(parseInt(edges)); }); @@ -156,36 +154,20 @@ test.describe("Canvas tests", () => { test(`Validate displayed nodes match API response after selecting a graph via UI`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); const graphData = await codeGraph.getGraphDetails(); const api = new ApiCalls(); - const response = await api.getProject(PROJECT_NAME); + const response = await api.getProject(GRAPHRAG_SDK); const isMatching = graphData.elements.nodes.slice(0, 2).every( (node: any, index: number) => node.name === response.result.entities.nodes[index].properties.name ); expect(isMatching).toBe(true) }); - nodes.slice(0,2).forEach((node) => { - test(`Validate copy to clipboard functionality for node and verify with api for ${node.nodeName}`, async () => { - const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); - const targetNode = findNodeByName(convertCoordinates, node.nodeName); - await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); - const result = await codeGraph.clickOnCopyToClipboard(); - const api = new ApiCalls(); - const response = await api.getProject(PROJECT_NAME); - const matchingNode = response.result.entities.nodes.find(nod => nod.properties?.name === node.nodeName); - expect(matchingNode?.properties.src).toBe(result); - }); - }) - nodesPath.forEach(({firstNode, secondNode}) => { 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.selectGraph(GRAPHRAG_SDK); await codeGraph.clickOnshowPathBtn(); await codeGraph.insertInputForShowPath("1", firstNode); await codeGraph.insertInputForShowPath("2", secondNode); @@ -200,7 +182,7 @@ test.describe("Canvas tests", () => { nodesPath.forEach((path) => { 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.selectGraph(GRAPHRAG_SDK); await codeGraph.clickOnshowPathBtn(); await codeGraph.insertInputForShowPath("1", path.firstNode); await codeGraph.insertInputForShowPath("2", path.secondNode); @@ -209,7 +191,7 @@ test.describe("Canvas tests", () => { const secondNodeRes = findNodeByName(result.elements.nodes, path.secondNode); const api = new ApiCalls(); - const response = await api.showPath(PROJECT_NAME ,firstNodeRes.id, secondNodeRes.id); + const response = await api.showPath(GRAPHRAG_SDK ,firstNodeRes.id, secondNodeRes.id); const callsRelationObject = response.result.paths[0].find(item => item.relation === "CALLS") expect(callsRelationObject?.src_node).toBe(firstNodeRes.id); expect(callsRelationObject?.dest_node).toBe(secondNodeRes.id); diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 1f245f11..6244def4 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -3,7 +3,7 @@ import BrowserWrapper from "../infra/ui/browserWrapper"; import urls from "../config/urls.json"; import { ApiCalls } from "../logic/api/apiCalls"; import CodeGraph from "../logic/POM/codeGraph"; -import { CHAT_OPTTIONS_COUNT, GRAPH_ID, Node_Question, PROJECT_NAME } from "../config/constants"; +import { CHAT_OPTTIONS_COUNT, GRAPHRAG_SDK, Node_Question } from "../config/constants"; import { delay } from "../logic/utils"; import { nodesPath } from "../config/testData"; @@ -20,7 +20,7 @@ test.describe("Chat tests", () => { test(`Validate clicking the lightbulb button displays the correct options at the end of the chat`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); + await chat.selectGraph(GRAPHRAG_SDK); await chat.clickOnLightBulbBtn(); const count = await chat.getLastChatElementButtonCount(); expect(count).toBe(CHAT_OPTTIONS_COUNT); @@ -28,10 +28,10 @@ test.describe("Chat tests", () => { test(`Validate that multiple consecutive questions receive individual answers`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); + await chat.selectGraph(GRAPHRAG_SDK); 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); @@ -39,20 +39,22 @@ test.describe("Chat tests", () => { const prevIsLoading = isLoadingArray[i - 1]; expect(prevIsLoading).toBe(false); } + await delay(3000); } }); 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++) { + await chat.selectGraph(GRAPHRAG_SDK); + 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("Latest Message"); + await chat.sendMessage(Node_Question); await delay(500); expect(await chat.isAtBottom()).toBe(true); }); @@ -60,7 +62,7 @@ test.describe("Chat tests", () => { 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); - await chat.selectGraph(GRAPH_ID); + await chat.selectGraph(GRAPHRAG_SDK); await chat.clickOnshowPathBtn(); await chat.insertInputForShowPath("1", path.firstNode); await chat.insertInputForShowPath("2", path.secondNode); @@ -72,7 +74,7 @@ test.describe("Chat tests", () => { nodesPath.forEach((path) => { 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.selectGraph(GRAPHRAG_SDK); await chat.clickOnshowPathBtn(); await chat.insertInputForShowPath("1", path.secondNode); await chat.insertInputForShowPath("2", path.firstNode); @@ -83,7 +85,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.selectGraph(GRAPHRAG_SDK); await chat.clickAskquestionBtn(); expect(await chat.isNotificationError()).toBe(true); await chat.clickOnNotificationErrorCloseBtn(); @@ -94,7 +96,7 @@ test.describe("Chat tests", () => { const questionNumber = index + 1; test(`Validate displaying question ${index} in chat after selection from options menu`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); + await chat.selectGraph(GRAPHRAG_SDK); await chat.clickOnQuestionOptionsMenu(); const selectedQuestion = await chat.selectAndGetQuestionInOptionsMenu(questionNumber.toString()); expect(selectedQuestion).toEqual(await chat.getLastQuestionInChat()) @@ -103,28 +105,31 @@ test.describe("Chat tests", () => { test(`Validate consistent UI responses for repeated questions in chat`, async () => { const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); + await chat.selectGraph(GRAPHRAG_SDK); 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); + await delay(3000); //delay before asking next question } 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(GRAPHRAG_SDK, Node_Question); + const chat = await browser.createNewPage(CodeGraph, urls.baseUrl); - await chat.selectGraph(GRAPH_ID); + await chat.selectGraph(GRAPHRAG_SDK); await chat.sendMessage(Node_Question); 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]); }); }); diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index aa59a6fe..358363ff 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -2,10 +2,10 @@ import { test, expect } from "@playwright/test"; import BrowserWrapper from "../infra/ui/browserWrapper"; import CodeGraph from "../logic/POM/codeGraph"; import urls from "../config/urls.json"; -import { GRAPH_ID, PROJECT_NAME } from "../config/constants"; +import { FLASK_GRAPH, GRAPHRAG_SDK, } from "../config/constants"; import { nodes } from "../config/testData"; import { ApiCalls } from "../logic/api/apiCalls"; -import { findNodeByName } from "../logic/utils"; +import { findFirstNodeWithSrc, findNodeByName } from "../logic/utils"; test.describe("Node details panel tests", () => { let browser: BrowserWrapper; @@ -21,10 +21,10 @@ test.describe("Node details panel tests", () => { nodes.slice(0,2).forEach((node) => { test(`Validate node details panel displayed on node click for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); - const targetNode = findNodeByName(convertCoordinates, node.nodeName); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(graphData, node.nodeName); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); expect(await codeGraph.isNodeDetailsPanel()).toBe(true) @@ -34,11 +34,11 @@ test.describe("Node details panel tests", () => { nodes.slice(0,2).forEach((node) => { test(`Validate node details panel is not displayed after close interaction for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); - const node1 = findNodeByName(convertCoordinates, node.nodeName); - await codeGraph.nodeClick(node1.screenX, node1.screenY); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(graphData, node.nodeName); + await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); await codeGraph.clickOnNodeDetailsCloseBtn(); expect(await codeGraph.isNodeDetailsPanel()).toBe(false) @@ -48,11 +48,11 @@ test.describe("Node details panel tests", () => { nodes.forEach((node) => { test(`Validate node details panel header displays correct node name: ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); - const node1 = findNodeByName(convertCoordinates, node.nodeName); - await codeGraph.nodeClick(node1.screenX, node1.screenY); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findNodeByName(graphData, node.nodeName); + await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); expect(await codeGraph.getNodeDetailsHeader()).toContain(node.nodeName.toUpperCase()) }) }) @@ -60,42 +60,44 @@ test.describe("Node details panel tests", () => { nodes.slice(0,2).forEach((node) => { test(`Validate copy functionality for node inside node details panel and verify with api for ${node.nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); - const graphData = await codeGraph.getGraphDetails(); - const convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); - const nodeData = findNodeByName(convertCoordinates, node.nodeName); - await codeGraph.nodeClick(nodeData.screenX, nodeData.screenY); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(FLASK_GRAPH); + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findFirstNodeWithSrc(graphData); + await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); - const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); + const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); const api = new ApiCalls(); - const response = await api.getProject(PROJECT_NAME); - const foundNode = response.result.entities.nodes.find(nod => nod.properties?.name === node.nodeName); + const response = await api.getProject(FLASK_GRAPH); + const foundNode = response.result.entities.nodes.find((nod) => nod.properties?.name === targetNode.name); expect(foundNode?.properties.src).toBe(result); }); }) - nodes.slice(0,2).forEach((node) => { + nodes.slice(0, 2).forEach((node) => { 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 convertCoordinates = await codeGraph.transformNodeCoordinates(graphData); - const node1 = findNodeByName(convertCoordinates, node.nodeName); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(GRAPHRAG_SDK); + const graphData = await codeGraph.getGraphNodes(); + const node1 = findNodeByName(graphData, node.nodeName); const api = new ApiCalls(); - const response = await api.getProject(PROJECT_NAME); + const response = await api.getProject(GRAPHRAG_SDK); const data: any = response.result.entities.nodes; const findNode = data.find((nod: any) => nod.properties.name === node.nodeName); - await codeGraph.nodeClick(node1.screenX, node1.screenY); let elements = await codeGraph.getNodeDetailsPanelElements(); - elements.splice(2,1) - const apiFields = [...Object.keys(findNode), ...Object.keys(findNode.properties || {})]; - + elements.splice(2, 1); + const apiFields = [ + ...Object.keys(findNode), + ...Object.keys(findNode.properties || {}), + ]; + const isValid = elements.every((field) => { - const cleanedField = field.replace(':', '').trim(); - return apiFields.includes(cleanedField); + const cleanedField = field.replace(":", "").trim(); + return apiFields.includes(cleanedField); }); - expect(isValid).toBe(true) + expect(isValid).toBe(true); }); - }) + }); }); \ No newline at end of file diff --git a/e2e/tests/searchBar.spec.ts b/e2e/tests/searchBar.spec.ts index a7427d3d..cb1074a7 100644 --- a/e2e/tests/searchBar.spec.ts +++ b/e2e/tests/searchBar.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from "@playwright/test"; import BrowserWrapper from "../infra/ui/browserWrapper"; import CodeGraph from "../logic/POM/codeGraph"; import urls from "../config/urls.json"; -import { GRAPH_ID, PROJECT_NAME } from "../config/constants"; +import { GRAPHRAG_SDK } from "../config/constants"; import { delay } from "../logic/utils"; import { searchData, specialCharacters } from "../config/testData"; import { ApiCalls } from "../logic/api/apiCalls"; @@ -21,7 +21,7 @@ test.describe("search bar tests", () => { searchData.slice(0, 2).forEach(({ searchInput }) => { test(`Verify search bar auto-complete behavior for input: ${searchInput} via UI`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); await codeGraph.fillSearchBar(searchInput); await delay(1000); const textList = await codeGraph.getSearchBarElementsText(); @@ -34,7 +34,7 @@ test.describe("search bar tests", () => { searchData.slice(2, 4).forEach(({ searchInput, completedSearchInput }) => { test(`Validate search bar updates with selected element: ${searchInput}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); await codeGraph.fillSearchBar(searchInput); await codeGraph.selectSearchBarOptionBtn("1"); expect(await codeGraph.getSearchBarInputValue()).toBe( @@ -46,7 +46,7 @@ test.describe("search bar tests", () => { searchData.slice(0, 2).forEach(({ searchInput }) => { test(`Verify auto-scroll scroll in search bar list for: ${searchInput}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); await codeGraph.fillSearchBar(searchInput); await codeGraph.scrollToBottomInSearchBarList(); expect(await codeGraph.isScrolledToBottomInSearchBarList()).toBe(true); @@ -56,7 +56,7 @@ test.describe("search bar tests", () => { specialCharacters.forEach(({ character, expectedRes }) => { test(`Verify entering special characters behavior in search bar for: ${character}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); await codeGraph.fillSearchBar(character); await delay(1000); expect((await codeGraph.getSearchBarInputValue()).includes(character)).toBe(expectedRes); @@ -66,11 +66,11 @@ test.describe("search bar tests", () => { searchData.slice(0, 2).forEach(({ searchInput}) => { test(`search bar auto complete via ui and validating via api for: ${searchInput}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await codeGraph.selectGraph(GRAPH_ID); + await codeGraph.selectGraph(GRAPHRAG_SDK); await codeGraph.fillSearchBar(searchInput); const count = await codeGraph.getSearchAutoCompleteCount(); const api = new ApiCalls(); - const response = await api.searchAutoComplete(PROJECT_NAME, searchInput); + const response = await api.searchAutoComplete(GRAPHRAG_SDK, searchInput); expect(count).toBe(response.result.completions.length); }); }) From 9377bd7c583aefc647e33f90fab123bb70364c36 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:23:56 +0300 Subject: [PATCH 03/65] implement sharding in CI --- .github/workflows/playwright.yml | 59 ++++++++++++++++++++++++++++++-- playwright.config.ts | 7 +++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3893ea77..a27108c7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -8,6 +8,10 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2] services: falkordb: image: falkordb/falkordb:latest @@ -32,10 +36,61 @@ jobs: run: | npm install npm run build - NEXTAUTH_SECRET=SECRET npm start & npx playwright test --reporter=dot,list + NEXTAUTH_SECRET=SECRET npm start & + npx playwright test --shard=${{ matrix.shard }}/2 --reporter=dot,list,json - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: - name: playwright-report + name: playwright-report-shard-${{ matrix.shard }} path: playwright-report/ retention-days: 30 + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-results-shard-${{ matrix.shard }} + path: test-results/ + retention-days: 30 + + merge-reports: + if: ${{ !cancelled() }} + needs: [test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Download all test results + uses: actions/download-artifact@v4 + with: + path: all-test-results + pattern: test-results-shard-* + - name: Download all reports + uses: actions/download-artifact@v4 + with: + path: all-reports + pattern: playwright-report-shard-* + - name: Merge test results + run: | + # Create combined results directory + mkdir -p combined-results + + # Merge JSON results + npx playwright merge-reports --reporter=html,json all-reports/playwright-report-shard-*/ + + # Move merged report to combined directory + mv playwright-report combined-results/ + + # Create a summary of all test results + echo "## Test Results Summary" > combined-results/summary.md + echo "Total shards: 2" >> combined-results/summary.md + echo "Workers per shard: 2" >> combined-results/summary.md + echo "Generated on: $(date)" >> combined-results/summary.md + - name: Upload combined report + uses: actions/upload-artifact@v4 + with: + name: combined-playwright-report + path: combined-results/ + retention-days: 30 diff --git a/playwright.config.ts b/playwright.config.ts index 7a5292d5..64751558 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -22,7 +22,12 @@ 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: process.env.CI ? [ + ['dot'], + ['list'], + ['json', { outputFile: 'test-results/results.json' }], + ['html', { open: 'never', outputFolder: 'playwright-report' }] + ] : 'html', /* 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('/')`. */ From 0e4d9a2a43e5a53cc3b2fc5751330b7d34fbd59a Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:42:57 +0300 Subject: [PATCH 04/65] fix nodeDetailsPanel tests --- e2e/tests/nodeDetailsPanel.spec.ts | 31 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index 358363ff..6443da66 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -57,22 +57,21 @@ test.describe("Node details panel tests", () => { }) }) - nodes.slice(0,2).forEach((node) => { - test(`Validate copy functionality for node inside node details panel and verify with api for ${node.nodeName}`, async () => { - const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); - await browser.setPageToFullScreen(); - await codeGraph.selectGraph(FLASK_GRAPH); - const graphData = await codeGraph.getGraphNodes(); - const targetNode = findFirstNodeWithSrc(graphData); - await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); - await codeGraph.clickOnViewNode(); - const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); - const api = new ApiCalls(); - const response = await api.getProject(FLASK_GRAPH); - const foundNode = response.result.entities.nodes.find((nod) => nod.properties?.name === targetNode.name); - expect(foundNode?.properties.src).toBe(result); - }); - }) + + test(`Validate copy functionality for node inside node details panel and verify with api`, async () => { + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); + await browser.setPageToFullScreen(); + await codeGraph.selectGraph(FLASK_GRAPH); + const graphData = await codeGraph.getGraphNodes(); + const targetNode = findFirstNodeWithSrc(graphData); + await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); + await codeGraph.clickOnViewNode(); + const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); + const api = new ApiCalls(); + const response = await api.getProject(FLASK_GRAPH); + const foundNode = response.result.entities.nodes.find((nod) => nod.properties?.name === targetNode.name); + expect(foundNode?.properties.src).toBe(result); + }); nodes.slice(0, 2).forEach((node) => { test(`Validate view node panel keys via api for ${node.nodeName}`, async () => { From a9ef1986d7ba8a30bf4935dbeedb984af0a0d3dd Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:29:17 +0300 Subject: [PATCH 05/65] fix tests --- e2e/logic/utils.ts | 4 ++++ e2e/tests/nodeDetailsPanel.spec.ts | 16 ++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/e2e/logic/utils.ts b/e2e/logic/utils.ts index 47833e70..744851bf 100644 --- a/e2e/logic/utils.ts +++ b/e2e/logic/utils.ts @@ -21,4 +21,8 @@ export function findNodeByName(nodes: { name: string }[], nodeName: string): any export function findFirstNodeWithSrc(nodes: { src?: string }[]): any { return nodes.find((node) => node.src !== undefined); +} + +export function findNodeWithSpecificSrc(nodes: { src?: string }[], srcContent: string): any { + return nodes.find((node) => node.src && node.src.includes(srcContent)); } \ No newline at end of file diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index 6443da66..5b6d26d0 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -2,10 +2,10 @@ import { test, expect } from "@playwright/test"; import BrowserWrapper from "../infra/ui/browserWrapper"; import CodeGraph from "../logic/POM/codeGraph"; import urls from "../config/urls.json"; -import { FLASK_GRAPH, GRAPHRAG_SDK, } from "../config/constants"; +import { FLASK_GRAPH, GRAPHRAG_SDK } from "../config/constants"; +import { findNodeByName, findFirstNodeWithSrc, findNodeWithSpecificSrc } from "../logic/utils"; import { nodes } from "../config/testData"; import { ApiCalls } from "../logic/api/apiCalls"; -import { findFirstNodeWithSrc, findNodeByName } from "../logic/utils"; test.describe("Node details panel tests", () => { let browser: BrowserWrapper; @@ -58,17 +58,21 @@ test.describe("Node details panel tests", () => { }) - test(`Validate copy functionality for node inside node details panel and verify with api`, async () => { + test.only(`Validate copy functionality for node inside node details panel and verify with api`, async () => { + const api = new ApiCalls(); + const response = await api.getProject(FLASK_GRAPH); + const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await browser.setPageToFullScreen(); await codeGraph.selectGraph(FLASK_GRAPH); const graphData = await codeGraph.getGraphNodes(); - const targetNode = findFirstNodeWithSrc(graphData); + const targetNode = findNodeWithSpecificSrc(graphData, "test_options_work"); + + await new Promise(resolve => setTimeout(resolve, 2000)); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); - const api = new ApiCalls(); - const response = await api.getProject(FLASK_GRAPH); + const foundNode = response.result.entities.nodes.find((nod) => nod.properties?.name === targetNode.name); expect(foundNode?.properties.src).toBe(result); }); From c5011ff8ead45c425287bb5ac88f82c1dbf63673 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:33:06 +0300 Subject: [PATCH 06/65] Update nodeDetailsPanel.spec.ts --- e2e/tests/nodeDetailsPanel.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index 5b6d26d0..ad7afd80 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -58,7 +58,7 @@ test.describe("Node details panel tests", () => { }) - test.only(`Validate copy functionality for node inside node details panel and verify with api`, async () => { + test(`Validate copy functionality for node inside node details panel and verify with api`, async () => { const api = new ApiCalls(); const response = await api.getProject(FLASK_GRAPH); From abde36540156e6a86a0cd6b0f207b0c1a008c280 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:18:08 +0300 Subject: [PATCH 07/65] Update nodeDetailsPanel.spec.ts --- e2e/tests/nodeDetailsPanel.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index ad7afd80..7ac30cdd 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -58,7 +58,7 @@ test.describe("Node details panel tests", () => { }) - test(`Validate copy functionality for node inside node details panel and verify with api`, async () => { + test.only(`Validate copy functionality for node inside node details panel and verify with api`, async () => { const api = new ApiCalls(); const response = await api.getProject(FLASK_GRAPH); @@ -66,14 +66,18 @@ test.describe("Node details panel tests", () => { await browser.setPageToFullScreen(); await codeGraph.selectGraph(FLASK_GRAPH); const graphData = await codeGraph.getGraphNodes(); - const targetNode = findNodeWithSpecificSrc(graphData, "test_options_work"); - + + const targetNode = findFirstNodeWithSrc(graphData); + await new Promise(resolve => setTimeout(resolve, 2000)); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); - + await new Promise(resolve => setTimeout(resolve, 2000)); const foundNode = response.result.entities.nodes.find((nod) => nod.properties?.name === targetNode.name); + console.log("API Node:", foundNode); + console.log("Copied Result:", result); + expect(foundNode?.properties.src).toBe(result); }); From 11b8e910696733796164daa07baa141e1b1f0241 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:20:38 +0300 Subject: [PATCH 08/65] Update nodeDetailsPanel.spec.ts --- e2e/tests/nodeDetailsPanel.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index 7ac30cdd..c902d839 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -58,7 +58,7 @@ test.describe("Node details panel tests", () => { }) - test.only(`Validate copy functionality for node inside node details panel and verify with api`, async () => { + test(`Validate copy functionality for node inside node details panel and verify with api`, async () => { const api = new ApiCalls(); const response = await api.getProject(FLASK_GRAPH); From 5656d2b051e0a7167cfeb2e557725c01e709a951 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:14:18 +0300 Subject: [PATCH 09/65] Update nodeDetailsPanel.spec.ts --- e2e/tests/nodeDetailsPanel.spec.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/e2e/tests/nodeDetailsPanel.spec.ts b/e2e/tests/nodeDetailsPanel.spec.ts index c902d839..75901439 100644 --- a/e2e/tests/nodeDetailsPanel.spec.ts +++ b/e2e/tests/nodeDetailsPanel.spec.ts @@ -59,27 +59,26 @@ test.describe("Node details panel tests", () => { test(`Validate copy functionality for node inside node details panel and verify with api`, async () => { - const api = new ApiCalls(); - const response = await api.getProject(FLASK_GRAPH); - const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await browser.setPageToFullScreen(); await codeGraph.selectGraph(FLASK_GRAPH); const graphData = await codeGraph.getGraphNodes(); + const targetNode = graphData.find(node => + node.name === "root" && node.src !== undefined + ) || findFirstNodeWithSrc(graphData); - const targetNode = findFirstNodeWithSrc(graphData); await new Promise(resolve => setTimeout(resolve, 2000)); await codeGraph.nodeClick(targetNode.screenX, targetNode.screenY); await codeGraph.clickOnViewNode(); const result = await codeGraph.clickOnCopyToClipboardNodePanelDetails(); - await new Promise(resolve => setTimeout(resolve, 2000)); + + const api = new ApiCalls(); + const response = await api.getProject(FLASK_GRAPH); const foundNode = response.result.entities.nodes.find((nod) => nod.properties?.name === targetNode.name); - console.log("API Node:", foundNode); - console.log("Copied Result:", result); expect(foundNode?.properties.src).toBe(result); - }); +}); nodes.slice(0, 2).forEach((node) => { test(`Validate view node panel keys via api for ${node.nodeName}`, async () => { From f7a2de22789803086250be21c8ef975dfc0cf5a6 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:24:49 +0300 Subject: [PATCH 10/65] update CI --- .github/workflows/playwright.yml | 46 +------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a27108c7..e72babaa 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -37,7 +37,7 @@ jobs: npm install npm run build NEXTAUTH_SECRET=SECRET npm start & - npx playwright test --shard=${{ matrix.shard }}/2 --reporter=dot,list,json + npx playwright test --shard=${{ matrix.shard }}/2 --reporter=dot,list - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: @@ -50,47 +50,3 @@ jobs: name: test-results-shard-${{ matrix.shard }} path: test-results/ retention-days: 30 - - merge-reports: - if: ${{ !cancelled() }} - needs: [test] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm ci - - name: Download all test results - uses: actions/download-artifact@v4 - with: - path: all-test-results - pattern: test-results-shard-* - - name: Download all reports - uses: actions/download-artifact@v4 - with: - path: all-reports - pattern: playwright-report-shard-* - - name: Merge test results - run: | - # Create combined results directory - mkdir -p combined-results - - # Merge JSON results - npx playwright merge-reports --reporter=html,json all-reports/playwright-report-shard-*/ - - # Move merged report to combined directory - mv playwright-report combined-results/ - - # Create a summary of all test results - echo "## Test Results Summary" > combined-results/summary.md - echo "Total shards: 2" >> combined-results/summary.md - echo "Workers per shard: 2" >> combined-results/summary.md - echo "Generated on: $(date)" >> combined-results/summary.md - - name: Upload combined report - uses: actions/upload-artifact@v4 - with: - name: combined-playwright-report - path: combined-results/ - retention-days: 30 From 4eee80f11b861ec1b95b3404569ec657ddbdcad8 Mon Sep 17 00:00:00 2001 From: Dandan7 <182233217+danshalev7@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:04:08 +0300 Subject: [PATCH 11/65] Update page.tsx Replaced the multiple H1 tags with H2 tags, keeping 'CODE GRAPH' as sole H1 tag --- app/page.tsx | 58 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index a58f1a56..7938501e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -180,35 +180,35 @@ export default function Home() {

Github

- - - - -
- HOW TO USE THE PRODUCT - -
- { - TIPS.map((tip, index) => ( -
-
-

{tip.title}

-

{tip.keyboardCommand}

-
-

{tip.description}

-
- )) - } -
-
+ + + + +
+ HOW TO USE THE PRODUCT + +
+ { + TIPS.map((tip, index) => ( +
+
+

{tip.title}

+

{tip.keyboardCommand}

+
+

{tip.description}

+
+ )) + } +
+ {/* - avoidOverlap(data.nodes as Position[])} - nodeVisibility="visible" - linkVisibility="visible" - linkCurvature="curve" - linkDirectionalArrowRelPos={1} - linkDirectionalArrowColor={(link) => (link.isPath || link.isPathSelected) ? PATH_COLOR : link.color} - linkDirectionalArrowLength={(link) => link.source.id === link.target.id ? 0 : (link.id === selectedObj?.id || link.isPathSelected) ? 3 : 2} - nodeRelSize={NODE_SIZE} - linkLineDash={(link) => (link.isPath && !link.isPathSelected) ? [5, 5] : []} - linkColor={(link) => (link.isPath || link.isPathSelected) ? PATH_COLOR : link.color} - linkWidth={(link) => (link.id === selectedObj?.id || link.isPathSelected) ? 2 : 1} - nodeCanvasObjectMode={() => 'after'} - linkCanvasObjectMode={() => 'after'} - nodeCanvasObject={(node, ctx) => { - if (!node.x || !node.y) return - - if (isPathResponse) { - if (node.isPathSelected) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 1 - } else if (node.isPath) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 0.5 - } else { - ctx.fillStyle = '#E5E5E5'; - ctx.strokeStyle = 'gray'; - ctx.lineWidth = 0.5 - } - } else if (isPathResponse === undefined) { - if (node.isPathSelected) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 1 - } else if (node.isPath) { - ctx.fillStyle = node.color; - ctx.strokeStyle = PATH_COLOR; - ctx.lineWidth = 0.5 - } else { - ctx.fillStyle = node.color; - ctx.strokeStyle = 'black'; - ctx.lineWidth = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id ? 1 : 0.5 - } - } else { - ctx.fillStyle = node.color; - ctx.strokeStyle = 'black'; - ctx.lineWidth = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id ? 1 : 0.5 - } - - ctx.beginPath(); - ctx.arc(node.x, node.y, NODE_SIZE, 0, 2 * Math.PI, false); - ctx.stroke(); - ctx.fill(); - - ctx.fillStyle = 'black'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = '2px Arial'; - const textWidth = ctx.measureText(node.name).width; - const ellipsis = '...'; - const ellipsisWidth = ctx.measureText(ellipsis).width; - const nodeSize = NODE_SIZE * 2 - PADDING; - let { name } = { ...node } - - // truncate text if it's too long - if (textWidth > nodeSize) { - while (name.length > 0 && ctx.measureText(name).width + ellipsisWidth > nodeSize) { - name = name.slice(0, -1); - } - name += ellipsis; - } - - // add label - ctx.fillText(name, node.x, node.y); - }} - linkCanvasObject={(link, ctx) => { - const start = link.source; - const end = link.target; - - if (!start.x || !start.y || !end.x || !end.y) return - - let textX, textY, angle; - - if (start.id === end.id) { - const radius = NODE_SIZE * link.curve * 6.2; - const angleOffset = -Math.PI / 4; // 45 degrees offset for text alignment - textX = start.x + radius * Math.cos(angleOffset); - textY = start.y + radius * Math.sin(angleOffset); - angle = -angleOffset; - } else { - const midX = (start.x + end.x) / 2; - const midY = (start.y + end.y) / 2; - const offset = link.curve / 2; - - angle = Math.atan2(end.y - start.y, end.x - start.x); - - // maintain label vertical orientation for legibility - if (angle > Math.PI / 2) angle = -(Math.PI - angle); - if (angle < -Math.PI / 2) angle = -(-Math.PI - angle); - - // Calculate perpendicular offset - const perpX = -Math.sin(angle) * offset; - const perpY = Math.cos(angle) * offset; - - // Adjust position to compensate for rotation around origin - const cos = Math.cos(angle); - const sin = Math.sin(angle); - textX = midX + perpX; - textY = midY + perpY; - const rotatedX = textX * cos + textY * sin; - const rotatedY = -textX * sin + textY * cos; - textX = rotatedX; - textY = rotatedY; - } - - // Setup text properties to measure background size - ctx.font = '2px Arial'; - const padding = 0.5; - // Get text width and height - const label = graph.LabelsMap.get(link.label)! - let { textWidth, textHeight } = label - - if (!textWidth || !textHeight) { - const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = ctx.measureText(link.label) - textWidth = width - textHeight = actualBoundingBoxAscent + actualBoundingBoxDescent - graph.LabelsMap.set(link.label, { ...label, textWidth, textHeight }) - } - - // Save the current context state - ctx.save(); - - // add label with background and rotation - ctx.rotate(angle); - - // Draw background - ctx.fillStyle = 'white'; - ctx.fillRect( - textX - textWidth / 2 - padding, - textY - textHeight / 2 - padding, - textWidth + padding * 2, - textHeight + padding * 2 - ); - - // Draw text - ctx.globalAlpha = 1; - ctx.fillStyle = 'black'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(link.label, textX, textY); - - ctx.restore(); // reset rotation - }} - onNodeClick={screenSize > Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) || isShowPath ? handleNodeClick : handleRightClick} + Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) || isShowPath ? (node: Node, _evt: MouseEvent) => handleNodeClick(node) : (node: Node, evt: MouseEvent) => handleRightClick(node, evt)} onNodeRightClick={handleRightClick} - onNodeDragEnd={(n, translate) => setPosition(prev => { - return prev && { x: prev.x + translate.x * (chartRef.current?.zoom() ?? 1), y: prev.y + translate.y * (chartRef.current?.zoom() ?? 1) } - })} onLinkClick={screenSize > Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) && isPathResponse ? handleLinkClick : handleRightClick} onLinkRightClick={handleRightClick} - onBackgroundRightClick={unsetSelectedObjects} onBackgroundClick={unsetSelectedObjects} + onBackgroundRightClick={unsetSelectedObjects} onZoom={() => unsetSelectedObjects()} - onEngineStop={() => { - setCooldownTicks(0) - debugger - handleZoomToFit(chartRef, zoomedNodes.length === 1 ? 4 : 1, (n: NodeObject) => zoomedNodes.some(node => node.id === n.id)) - setZoomedNodes([]) - }} + onEngineStop={handleEngineStop} cooldownTicks={cooldownTicks} - cooldownTime={6000} /> ) diff --git a/app/components/model.ts b/app/components/model.ts index 130e60a7..415877bd 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -1,4 +1,3 @@ -import { LinkObject, NodeObject } from 'react-force-graph-2d' import { Path } from '@/lib/utils' export interface GraphData { @@ -13,34 +12,37 @@ export interface Category { export interface Label { name: string, - textWidth: number, - textHeight: number, } -export type Node = NodeObject<{ +export interface Node { id: number, name: string, category: string, color: string, + visible: boolean, collapsed: boolean, expand: boolean, - visible: boolean, isPathSelected: boolean, isPath: boolean, + x?: number, + y?: number, + vx?: number, + vy?: number, [key: string]: any, -}> +} -export type Link = LinkObject +} const COLORS_ORDER_NAME = [ "blue", @@ -226,18 +228,19 @@ export class Graph { let label = this.labelsMap.get(edgeData.relation) if (!label) { - label = { name: edgeData.relation, textWidth: 0, textHeight: 0 } + label = { name: edgeData.relation } this.labelsMap.set(edgeData.relation, label) this.labels.push(label) } link = { id: edgeData.id, - source, - target, + source: edgeData.src_node, + target: edgeData.dest_node, label: edgeData.relation, visible: true, expand: false, + color: "#999999", collapsed, isPathSelected: false, isPath: !!path, @@ -251,12 +254,12 @@ export class Graph { 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 sameNodesLinks = this.elements.links.filter(l => (l.source === start && l.target === end) || (l.target === start && l.source === end)) const index = sameNodesLinks.findIndex(l => l.id === link.id) ?? 0 const even = index % 2 === 0 let curve - if (start.id === end.id) { + if (start === end) { if (even) { curve = Math.floor(-(index / 2)) - 3 } else { @@ -282,7 +285,7 @@ export class Graph { this.elements = { nodes: this.elements.nodes, links: this.elements.links.map(link => { - if (this.elements.nodes.map(n => n.id).includes(link.source.id) && this.elements.nodes.map(n => n.id).includes(link.target.id)) { + if (this.elements.nodes.map(n => n.id).includes(link.source) && this.elements.nodes.map(n => n.id).includes(link.target)) { return link } this.linksMap.delete(link.id) @@ -291,15 +294,15 @@ export class Graph { } public visibleLinks(visible: boolean, ids?: number[]) { - const elements = ids ? this.elements.links.filter(link => ids.includes(link.source.id) || ids.includes(link.target.id)) : this.elements.links + const elements = ids ? this.elements.links.filter(link => ids.includes(link.source) || ids.includes(link.target)) : this.elements.links elements.forEach(link => { - if (visible && this.elements.nodes.map(n => n.id).includes(link.source.id) && link.source.visible && this.elements.nodes.map(n => n.id).includes(link.target.id) && link.target.visible) { + if (visible && this.elements.nodes.find(n => n.id === link.source)?.visible && this.elements.nodes.find(n => n.id === link.target)?.visible) { // eslint-disable-next-line no-param-reassign link.visible = true } - if (!visible && ((this.elements.nodes.map(n => n.id).includes(link.source.id) && !link.source.visible) || (this.elements.nodes.map(n => n.id).includes(link.target.id) && !link.target.visible))) { + if (!visible && (this.elements.nodes.find(n => n.id === link.source)?.visible === false || this.elements.nodes.find(n => n.id === link.target)?.visible === false)) { // eslint-disable-next-line no-param-reassign link.visible = false } diff --git a/app/page.tsx b/app/page.tsx index c78b4901..9d4144a9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,8 @@ 'use client' -import { MutableRefObject, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Chat } from './components/chat'; -import { Graph, GraphData, Link as LinkType, Node } from './components/model'; +import { Graph, GraphData, Node } from './components/model'; import { AlignRight, BookOpen, BoomBox, Download, Github, HomeIcon, Search, X } from 'lucide-react'; import Link from 'next/link'; import { ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; @@ -17,10 +17,9 @@ import { Progress } from '@/components/ui/progress'; import { Carousel, CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel'; import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'; import Input from './components/Input'; -import { ForceGraphMethods, NodeObject } from 'react-force-graph-2d'; import { Labels } from './components/labels'; import { Toolbar } from './components/toolbar'; -import { cn, handleZoomToFit, Message, Path, PathData, PathNode } from '@/lib/utils'; +import { cn, GraphRef, handleZoomToFit, Message, Path, PathData, PathNode } from '@/lib/utils'; import { GraphContext } from './components/provider'; type Tip = { @@ -67,8 +66,8 @@ export default function Home() { const [options, setOptions] = useState([]); const [path, setPath] = useState(); const [isSubmit, setIsSubmit] = useState(false); - const desktopChartRef = useRef>() - const mobileChartRef = useRef>() + const desktopChartRef = useRef() + const mobileChartRef = useRef() const [menuOpen, setMenuOpen] = useState(false) const [chatOpen, setChatOpen] = useState(false) const [searchNode, setSearchNode] = useState({}); @@ -190,7 +189,7 @@ export default function Home() { return graph.extend(json.result.neighbors, true) } - const handleSearchSubmit = (node: any, chartRef: MutableRefObject | undefined>) => { + const handleSearchSubmit = (node: any, chartRef: GraphRef) => { const chart = chartRef.current if (chart) { @@ -211,7 +210,7 @@ export default function Home() { } setTimeout(() => { - handleZoomToFit(chartRef, 4, (n: NodeObject) => n.id === chartNode!.id) + handleZoomToFit(chartRef, 4, (n: GraphNode) => n.id === chartNode!.id) }, 0) setSearchNode(chartNode) setOptionsOpen(false) diff --git a/lib/utils.ts b/lib/utils.ts index 2946411b..2dcb8c2d 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -2,7 +2,6 @@ import { Link, Node } from "@/app/components/model" import { type ClassValue, clsx } from "clsx" import { MutableRefObject } from "react" import { twMerge } from "tailwind-merge" -import { ForceGraphMethods, NodeObject } from "react-force-graph-2d" export type PathData = { nodes: any[] @@ -35,13 +34,13 @@ export interface Message { graphName?: string; } -export type GraphRef = MutableRefObject | undefined> +export type GraphRef = MutableRefObject export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -export function handleZoomToFit(chartRef: GraphRef, paddingMultiplier = 1, filter?: (node: NodeObject) => boolean) { +export function handleZoomToFit(chartRef: GraphRef, paddingMultiplier = 1, filter?: (node: NodeGraph) => boolean) { const chart = chartRef.current if (chart) { // Find the currently visible canvas by checking display property diff --git a/package-lock.json b/package-lock.json index 1eeadbdb..bb1f5672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { + "@falkordb/canvas": "^0.0.6", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -30,7 +31,6 @@ "playwright": "^1.49.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-force-graph-2d": "^1.27.0", "react-gtm-module": "^2.0.11", "react-json-tree": "^0.19.0", "react-resizable-panels": "^2.1.7", @@ -216,6 +216,26 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@falkordb/canvas": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.6.tgz", + "integrity": "sha512-5r2dEt3O05dz5drfzHUxEShjqAi1kkDMUadvgBV3rq2YOlZmU9vAOYneU0mpAU1UJOprc+yc78xjTEGMict3Vw==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "force-graph": "^1.44.4", + "react": "^19.2.3" + } + }, + "node_modules/@falkordb/canvas/node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -874,9 +894,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1895,7 +1915,8 @@ "node_modules/@tweenjs/tween.js": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", - "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==" + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.6", @@ -2181,9 +2202,10 @@ "license": "ISC" }, "node_modules/accessor-fn": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.1.tgz", - "integrity": "sha512-zZpFYBqIL1Aqg+f2qmYHJ8+yIZF7/tP6PUGx2/QM0uGPSO5UegpinmkNwDohxWtOj586BpMPVRUjce2HI6xB3A==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", "engines": { "node": ">=12" } @@ -2562,6 +2584,7 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", "funding": { "type": "individual", "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" @@ -2722,9 +2745,10 @@ "license": "CC-BY-4.0" }, "node_modules/canvas-color-tracker": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.1.tgz", - "integrity": "sha512-eNycxGS7oQ3IS/9QQY41f/aQjiO9Y/MtedhCgSdsbLSxC9EyUD8L3ehl/Q3Kfmvt8um79S45PBV+5Rxm5ztdSw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", "dependencies": { "tinycolor2": "^1.6.0" }, @@ -2940,9 +2964,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3": { @@ -3007,7 +3031,8 @@ "node_modules/d3-binarytree": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", - "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==" + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" }, "node_modules/d3-brush": { "version": "3.0.0", @@ -3150,9 +3175,10 @@ } }, "node_modules/d3-force-3d": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz", - "integrity": "sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", "dependencies": { "d3-binarytree": "1", "d3-dispatch": "1 - 3", @@ -3205,7 +3231,8 @@ "node_modules/d3-octree": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==" + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" }, "node_modules/d3-path": { "version": "3.1.0", @@ -4340,9 +4367,10 @@ "license": "ISC" }, "node_modules/float-tooltip": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.4.tgz", - "integrity": "sha512-UUcH+5MHMnHf7a3qF2ZJ7J5PTtTKHRqdaoC3VAHZuX8ooEegNWxpmmHk192lABXw0+O+FzGB4anpEiqe6iv+WA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", "dependencies": { "d3-selection": "2 - 3", "kapsule": "^1.16", @@ -4363,9 +4391,10 @@ } }, "node_modules/force-graph": { - "version": "1.49.3", - "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.49.3.tgz", - "integrity": "sha512-blBqeFq3vdIzqGgvWrML9xA2R0nS5nvjHsEt9lcWVZ29IcdWQ6wa4G0CG/Uv8bP9olwpsJPZSJe3W8vNhiMCnQ==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.0.tgz", + "integrity": "sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==", + "license": "MIT", "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", @@ -4378,7 +4407,7 @@ "d3-scale-chromatic": "1 - 3", "d3-selection": "2 - 3", "d3-zoom": "2 - 3", - "float-tooltip": "^1.6", + "float-tooltip": "^1.7", "index-array-by": "1", "kapsule": "^1.16", "lodash-es": "4" @@ -4843,6 +4872,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", "engines": { "node": ">=12" } @@ -5343,14 +5373,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jerrypick": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.1.tgz", - "integrity": "sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==", - "engines": { - "node": ">=12" - } - }, "node_modules/jiti": { "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", @@ -5430,9 +5452,10 @@ } }, "node_modules/kapsule": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.0.tgz", - "integrity": "sha512-4f/z/Luu0cEXmagCwaFyzvfZai2HKgB4CQLwmsMUA+jlUbW94HfFSX+TWZxzWoMSO6b6aR+FD2Xd5z88VYZJTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", "dependencies": { "lodash-es": "4" }, @@ -5673,12 +5696,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -6228,9 +6251,10 @@ "license": "MIT" }, "node_modules/preact": { - "version": "10.26.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz", - "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.0.tgz", + "integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -6343,22 +6367,6 @@ "react": "^18.3.1" } }, - "node_modules/react-force-graph-2d": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.27.0.tgz", - "integrity": "sha512-NJAc7lWvY8PxBMn2uFXDDaeLcgJp37IYF6r0geOJlmMs5Lsf0IDmr35T7KNszHV3OUmTrZuPlyxrrzwGwyKhRg==", - "dependencies": { - "force-graph": "^1.49", - "prop-types": "15", - "react-kapsule": "^2.5" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-gtm-module": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/react-gtm-module/-/react-gtm-module-2.0.11.tgz", @@ -6383,20 +6391,6 @@ "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", - "integrity": "sha512-aE4Nq7dDG8R/LdNmvOL6Azjr97I2E7ycFDJRkoHJSp9OQgTJDT3MHTJtJDrOTwzCl6sllYSqrtcndaCzizyAjQ==", - "dependencies": { - "jerrypick": "^1.1.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, "node_modules/react-remove-scroll": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", @@ -7332,7 +7326,8 @@ "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", diff --git a/package.json b/package.json index a24819ed..b30ce343 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@falkordb/canvas": "^0.0.6", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -31,7 +32,6 @@ "playwright": "^1.49.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-force-graph-2d": "^1.27.0", "react-gtm-module": "^2.0.11", "react-json-tree": "^0.19.0", "react-resizable-panels": "^2.1.7", From 15859f744a206b3d072260577bb5cb2aa52ea012 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 18 Dec 2025 16:56:38 +0200 Subject: [PATCH 22/65] Update Next.js dependency to version 15.5.8 in package.json and package-lock.json --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1eeadbdb..87cb3095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "embla-carousel-react": "^8.5.2", "html2canvas": "^1.4.1", "lucide-react": "^0.486.0", - "next": "^15.5.5", + "next": "^15.5.8", "playwright": "^1.49.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -874,9 +874,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -5673,12 +5673,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", diff --git a/package.json b/package.json index a24819ed..bb052ea7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "embla-carousel-react": "^8.5.2", "html2canvas": "^1.4.1", "lucide-react": "^0.486.0", - "next": "^15.5.5", + "next": "^15.5.8", "playwright": "^1.49.1", "react": "^18.3.1", "react-dom": "^18.3.1", From da9522f72378e5b711748322652b04f217270b43 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 18 Dec 2025 17:45:26 +0200 Subject: [PATCH 23/65] Refactor components to use canvasRef instead of chartRef for improved consistency and functionality --- app/components/ForceGraph.tsx | 9 +++++---- app/components/chat.tsx | 20 +++++++++---------- app/components/code-graph.tsx | 9 ++++----- app/components/graphView.tsx | 13 ++++++------- app/components/toolbar.tsx | 18 ++++++++++-------- app/page.tsx | 26 ++++++++++++------------- lib/utils.ts | 36 ++--------------------------------- 7 files changed, 50 insertions(+), 81 deletions(-) diff --git a/app/components/ForceGraph.tsx b/app/components/ForceGraph.tsx index 1312b28e..53109bbb 100644 --- a/app/components/ForceGraph.tsx +++ b/app/components/ForceGraph.tsx @@ -8,7 +8,7 @@ import { GraphData, Link, Node } from "./model" interface Props { data: GraphData canvasRef: GraphRef - onNodeClick?: (node: Node) => void + onNodeClick?: (node: Node, event: MouseEvent) => void onNodeRightClick?: (node: Node, event: MouseEvent) => void onLinkClick?: (link: Link, event: MouseEvent) => void onLinkRightClick?: (link: Link, event: MouseEvent) => void @@ -113,7 +113,7 @@ export default function ForceGraph({ const handleNodeClick = useCallback((node: any, event: MouseEvent) => { if (onNodeClick) { const originalNode = data.nodes.find(n => n.id === node.id) - if (originalNode) onNodeClick(originalNode) + if (originalNode) onNodeClick(originalNode, event) } }, [onNodeClick, data.nodes]) @@ -144,15 +144,16 @@ export default function ForceGraph({ // Update event handlers useEffect(() => { if (!canvasRef.current || !canvasLoaded) return + canvasRef.current.setConfig({ onNodeClick: handleNodeClick, onNodeRightClick: handleNodeRightClick, onLinkClick: handleLinkClick, onLinkRightClick: handleLinkRightClick, onBackgroundClick, - onBackgroundRightClick, + // onBackgroundRightClick, onEngineStop, - onZoom + // onZoom }) }, [ handleNodeClick, diff --git a/app/components/chat.tsx b/app/components/chat.tsx index f344244f..ef5e822d 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -2,9 +2,9 @@ import { toast } from "@/components/ui/use-toast"; import { Dispatch, FormEvent, MutableRefObject, SetStateAction, useEffect, useRef, useState } from "react"; import Image from "next/image"; import { AlignLeft, ArrowRight, ChevronDown, Lightbulb, Undo2 } from "lucide-react"; -import { handleZoomToFit, Message, MessageTypes, Path, PathData } from "@/lib/utils"; +import { Message, MessageTypes, Path, PathData } from "@/lib/utils"; import Input from "./Input"; -import { Graph, GraphData, Link, Node } from "./model"; +import { Graph, GraphData } from "./model"; import { cn, GraphRef } from "@/lib/utils"; import { TypeAnimation } from "react-type-animation"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; @@ -20,7 +20,7 @@ interface Props { isPathResponse: boolean | undefined setIsPathResponse: (isPathResponse: boolean | undefined) => void setData: Dispatch> - chartRef: GraphRef + canvasRef: GraphRef messages: Message[] setMessages: Dispatch> query: string @@ -51,7 +51,7 @@ const RemoveLastPath = (messages: Message[]) => { return messages } -export function Chat({ messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData, chartRef, paths, setPaths }: Props) { +export function Chat({ messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData, canvasRef, paths, setPaths }: Props) { const [sugOpen, setSugOpen] = useState(false); @@ -91,9 +91,9 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set }, [isPathResponse]) const handleSetSelectedPath = (p: PathData) => { - const chart = chartRef.current + const canvas = canvasRef.current - if (!chart) return + if (!canvas) 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)))) { @@ -147,7 +147,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set } setData({ ...graph.Elements }) setTimeout(() => { - handleZoomToFit(chartRef, 2, (n: GraphNode) => p.nodes.some(node => node.id === n.id)); + canvas.zoomToFit(2, (n: GraphNode) => p.nodes.some(node => node.id === n.id)); }, 0) setChatOpen && setChatOpen(false) } @@ -206,9 +206,9 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set } const handleSubmit = async () => { - const chart = chartRef.current + const canvas = canvasRef.current - if (!chart) return + if (!canvas) return setSelectedPath(undefined) @@ -247,7 +247,7 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set setIsPathResponse(true) setData({ ...graph.Elements }) setTimeout(() => { - handleZoomToFit(chartRef, 2, (n: GraphNode) => formattedPaths.some(p => p.nodes.some(node => node.id === n.id))); + canvas.zoomToFit(2, (n: GraphNode) => formattedPaths.some(p => p.nodes.some(node => node.id === n.id))); }, 0) } diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index fdf31e5f..6d0c7503 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -27,7 +27,7 @@ interface Props { setOptions: Dispatch> isShowPath: boolean setPath: Dispatch> - chartRef: GraphRef + canvasRef: GraphRef selectedValue: string selectedPathId: number | undefined setSelectedPathId: (selectedPathId: number) => void @@ -54,7 +54,7 @@ export function CodeGraph({ setOptions, isShowPath, setPath, - chartRef, + canvasRef: chartRef, selectedValue, setSelectedPathId, isPathResponse, @@ -177,7 +177,7 @@ export function CodeGraph({ 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)); + const isTarget = graph.Elements.links.some(link => link.target === node.id && nodes.some(n => n.id === link.source)); if (!isTarget) return true @@ -332,7 +332,6 @@ export function CodeGraph({ selectedPathId={selectedPathId} setSelectedPathId={setSelectedPathId} cooldownTicks={cooldownTicks} - setCooldownTicks={setCooldownTicks} setZoomedNodes={setZoomedNodes} zoomedNodes={zoomedNodes} /> @@ -364,7 +363,7 @@ export function CodeGraph({ } diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index ae13e38a..fe15c9ff 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -4,8 +4,9 @@ import { Graph, GraphData, Link, Node } from './model'; import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { Path } from '@/lib/utils'; import { Fullscreen } from 'lucide-react'; -import { GraphRef, handleZoomToFit } from '@/lib/utils'; +import { GraphRef } from '@/lib/utils'; import ForceGraph from './ForceGraph'; +import { GraphNode } from '@falkordb/canvas'; export interface Position { x: number, @@ -29,7 +30,6 @@ interface Props { selectedPathId: number | undefined setSelectedPathId: (selectedPathId: number) => void cooldownTicks: number | undefined - setCooldownTicks: Dispatch> setZoomedNodes: Dispatch> zoomedNodes: Node[] } @@ -37,7 +37,7 @@ interface Props { export default function GraphView({ data, graph, - chartRef, + chartRef: canvasRef, selectedObj, setSelectedObj, selectedObjects, @@ -50,7 +50,6 @@ export default function GraphView({ selectedPathId, setSelectedPathId, cooldownTicks, - setCooldownTicks, zoomedNodes, setZoomedNodes }: Props) { @@ -122,20 +121,20 @@ export default function GraphView({ } const handleEngineStop = () => { - handleZoomToFit(chartRef, zoomedNodes.length === 1 ? 4 : 1, (n: any) => zoomedNodes.some(node => node.id === n.id)) + canvasRef.current?.zoomToFit(zoomedNodes.length === 1 ? 4 : 1, (n: GraphNode) => zoomedNodes.some(node => node.id === n.id)) setZoomedNodes([]) } return (
-
Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) || isShowPath ? (node: Node, _evt: MouseEvent) => handleNodeClick(node) : (node: Node, evt: MouseEvent) => handleRightClick(node, evt)} onNodeRightClick={handleRightClick} onLinkClick={screenSize > Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) && isPathResponse ? handleLinkClick : handleRightClick} diff --git a/app/components/toolbar.tsx b/app/components/toolbar.tsx index 9769f25e..10b3404b 100644 --- a/app/components/toolbar.tsx +++ b/app/components/toolbar.tsx @@ -3,24 +3,26 @@ import { cn } from "@/lib/utils" import { GraphRef } from "@/lib/utils"; interface Props { - chartRef: GraphRef + canvasRef: GraphRef className?: string handleDownloadImage?: () => void } -export function Toolbar({ chartRef, className, handleDownloadImage }: Props) { +export function Toolbar({ canvasRef, className, handleDownloadImage }: Props) { const handleZoomClick = (changefactor: number) => { - const chart = chartRef.current - if (chart) { - chart.zoom(chart.zoom() * changefactor) + const canvas = canvasRef.current + + if (canvas) { + canvas.zoom(canvas.getZoom() * changefactor) } } const handleCenterClick = () => { - const chart = chartRef.current - if (chart) { - chart.zoomToFit(1000, 40) + const canvas = canvasRef.current + + if (canvas) { + canvas.zoomToFit() } } diff --git a/app/page.tsx b/app/page.tsx index 9d4144a9..cfbef292 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -19,8 +19,8 @@ import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } import Input from './components/Input'; import { Labels } from './components/labels'; import { Toolbar } from './components/toolbar'; -import { cn, GraphRef, handleZoomToFit, Message, Path, PathData, PathNode } from '@/lib/utils'; -import { GraphContext } from './components/provider'; +import { cn, GraphRef, Message, Path, PathData, PathNode } from '@/lib/utils'; +import { GraphNode } from '@falkordb/canvas'; type Tip = { title: string @@ -66,8 +66,8 @@ export default function Home() { const [options, setOptions] = useState([]); const [path, setPath] = useState(); const [isSubmit, setIsSubmit] = useState(false); - const desktopChartRef = useRef() - const mobileChartRef = useRef() + const desktopChartRef = useRef(null) + const mobileChartRef = useRef(null) const [menuOpen, setMenuOpen] = useState(false) const [chatOpen, setChatOpen] = useState(false) const [searchNode, setSearchNode] = useState({}); @@ -189,10 +189,10 @@ export default function Home() { return graph.extend(json.result.neighbors, true) } - const handleSearchSubmit = (node: any, chartRef: GraphRef) => { - const chart = chartRef.current + const handleSearchSubmit = (node: any, canvasRef: GraphRef) => { + const canvas = canvasRef.current - if (chart) { + if (canvas) { let chartNode = graph.Elements.nodes.find(n => n.id == node.id) if (!chartNode?.visible) { @@ -210,7 +210,7 @@ export default function Home() { } setTimeout(() => { - handleZoomToFit(chartRef, 4, (n: GraphNode) => n.id === chartNode!.id) + canvas.zoomToFit(4, (n: GraphNode) => n.id === chartNode!.id) }, 0) setSearchNode(chartNode) setOptionsOpen(false) @@ -389,7 +389,7 @@ export default function Home() { graph={graph} data={data} setData={setData} - chartRef={desktopChartRef} + canvasRef={desktopChartRef} options={options} setOptions={setOptions} onFetchGraph={onFetchGraph} @@ -428,7 +428,7 @@ export default function Home() { setQuery={setQuery} selectedPath={selectedPath} setSelectedPath={setSelectedPath} - chartRef={desktopChartRef} + canvasRef={desktopChartRef} setPath={setPath} path={path} repo={graph.Id} @@ -507,7 +507,7 @@ export default function Home() { graph={graph} data={data} setData={setData} - chartRef={mobileChartRef} + canvasRef={mobileChartRef} options={options} setOptions={setOptions} onFetchGraph={onFetchGraph} @@ -549,7 +549,7 @@ export default function Home() { setQuery={setQuery} selectedPath={selectedPath} setSelectedPath={setSelectedPath} - chartRef={mobileChartRef} + canvasRef={mobileChartRef} setPath={setPath} path={path} repo={graph.Id} @@ -577,7 +577,7 @@ export default function Home() { +export type GraphRef = MutableRefObject export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } - -export function handleZoomToFit(chartRef: GraphRef, paddingMultiplier = 1, filter?: (node: NodeGraph) => boolean) { - const chart = chartRef.current - if (chart) { - // Find the currently visible canvas by checking display property - const canvases = document.querySelectorAll('.force-graph-container canvas') as NodeListOf; - const container = Array.from(canvases).find(canvas => { - const container = canvas.parentElement; - - if (!container) return false; - - // Check if element is actually in viewport - const rect = container.getBoundingClientRect(); - const isInViewport = rect.width > 0 && - rect.height > 0 && - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth; - - return isInViewport; - })?.parentElement; - - if (!container) return; - - // Calculate padding as 10% of the smallest canvas dimension - const minDimension = Math.min(container.clientWidth, container.clientHeight); - - const padding = minDimension * 0.1 * paddingMultiplier; - - chart.zoomToFit(1000, padding, filter); - } -} \ No newline at end of file From 39e074fbfac04ec9892eaf94207a166b94090ee3 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 18 Dec 2025 17:47:46 +0200 Subject: [PATCH 24/65] Update FalkorDB canvas dependency to version 0.0.7 and enable background right-click and zoom functionalities in ForceGraph component --- app/components/ForceGraph.tsx | 4 ++-- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/components/ForceGraph.tsx b/app/components/ForceGraph.tsx index 53109bbb..297c4eab 100644 --- a/app/components/ForceGraph.tsx +++ b/app/components/ForceGraph.tsx @@ -151,9 +151,9 @@ export default function ForceGraph({ onLinkClick: handleLinkClick, onLinkRightClick: handleLinkRightClick, onBackgroundClick, - // onBackgroundRightClick, + onBackgroundRightClick, onEngineStop, - // onZoom + onZoom }) }, [ handleNodeClick, diff --git a/package-lock.json b/package-lock.json index bb1f5672..6d13c2c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { - "@falkordb/canvas": "^0.0.6", + "@falkordb/canvas": "^0.0.7", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -217,9 +217,9 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.6.tgz", - "integrity": "sha512-5r2dEt3O05dz5drfzHUxEShjqAi1kkDMUadvgBV3rq2YOlZmU9vAOYneU0mpAU1UJOprc+yc78xjTEGMict3Vw==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.7.tgz", + "integrity": "sha512-EGUz7xUO17+LL89dbO4Uy3ySeHmNxBN8x78b5tbHVNrQJ5kpRyTFGtKH3KqgYSkk5N4PxCxLs2AMK1GBujFjpQ==", "license": "MIT", "dependencies": { "d3": "^7.9.0", diff --git a/package.json b/package.json index b30ce343..9f7c91d7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@falkordb/canvas": "^0.0.6", + "@falkordb/canvas": "^0.0.7", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", From 77d26952e295914d708dd42ccb26115e42da8e45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 07:52:12 +0000 Subject: [PATCH 25/65] Initial plan From 95ee131e8002eef1bbe28c9a862a39d092f187ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 07:57:08 +0000 Subject: [PATCH 26/65] Upgrade Next.js to 15.5.9 and eslint-config-next to 15.5.9 Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> --- package-lock.json | 36 ++++++++++++++++++++---------------- package.json | 4 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18543659..7dbc4ca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "lucide-react": "^0.441.0", - "next": "^15.5.7", + "next": "^15.5.9", "playwright": "^1.49.1", "react": "^18", "react-dom": "^18", @@ -41,7 +41,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9", - "eslint-config-next": "^15.1.2", + "eslint-config-next": "^15.5.9", "tailwindcss": "^3.4.3", "typescript": "^5" } @@ -850,16 +850,17 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.2.tgz", - "integrity": "sha512-sgfw3+WdaYOGPKCvM1L+UucBmRfh8V2Ygefp7ELON0+0vY7uohQwXXnVWg3rY7mXDKharQR3o7uedpfvnU2hlQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.9.tgz", + "integrity": "sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==", "dev": true, + "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } @@ -869,6 +870,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -885,6 +887,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4404,12 +4407,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.2.tgz", - "integrity": "sha512-PrMm1/4zWSJ689wd/ypWIR5ZF1uvmp3EkgpgBV1Yu6PhEobBjXMGgT8bVNelwl17LXojO8D5ePFRiI4qXjsPRA==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.9.tgz", + "integrity": "sha512-852JYI3NkFNzW8CqsMhI0K2CDRxTObdZ2jQJj5CtpEaOkYHn13107tHpNuD/h0WRpU4FAbCdUaxQsrfBtNK9Kw==", "dev": true, + "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.1.2", + "@next/eslint-plugin-next": "15.5.9", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -6207,12 +6211,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", diff --git a/package.json b/package.json index 6ed6c16a..8608f2b9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "lucide-react": "^0.441.0", - "next": "^15.5.7", + "next": "^15.5.9", "playwright": "^1.49.1", "react": "^18", "react-dom": "^18", @@ -42,7 +42,7 @@ "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9", - "eslint-config-next": "^15.1.2", + "eslint-config-next": "^15.5.9", "tailwindcss": "^3.4.3", "typescript": "^5" } From b0fb8f5475536b64ee44d6bc2816fcdb97895cbd Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 24 Dec 2025 14:12:31 +0200 Subject: [PATCH 27/65] Update FalkorDB canvas dependency to version 0.0.8 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d13c2c4..de1d59b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { - "@falkordb/canvas": "^0.0.7", + "@falkordb/canvas": "^0.0.8", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -217,9 +217,9 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.7.tgz", - "integrity": "sha512-EGUz7xUO17+LL89dbO4Uy3ySeHmNxBN8x78b5tbHVNrQJ5kpRyTFGtKH3KqgYSkk5N4PxCxLs2AMK1GBujFjpQ==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.8.tgz", + "integrity": "sha512-vPHEpwr7D06qUrxSbSovnH8lTdbRahlQ+wi0Hv0e7++xqvNGapMZLICrjblh/oyO2H+Ce823nTh3qZtarjdkeQ==", "license": "MIT", "dependencies": { "d3": "^7.9.0", diff --git a/package.json b/package.json index 9f7c91d7..8b6b8233 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@falkordb/canvas": "^0.0.7", + "@falkordb/canvas": "^0.0.8", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", From 3f68425f8076a2353f5e836d253e39b7b62953d0 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 30 Dec 2025 12:48:57 +0200 Subject: [PATCH 28/65] Update FalkorDB canvas dependency to version 0.0.11 and improve error handling in API routes --- app/api/repo/route.ts | 4 +-- app/components/ForceGraph.tsx | 50 +++++++---------------------------- package-lock.json | 16 +++++------ package.json | 4 +-- 4 files changed, 21 insertions(+), 53 deletions(-) diff --git a/app/api/repo/route.ts b/app/api/repo/route.ts index 421be274..f4f667f1 100644 --- a/app/api/repo/route.ts +++ b/app/api/repo/route.ts @@ -22,7 +22,7 @@ export async function GET() { return NextResponse.json({ result: repositories }, { status: 200 }) } catch (err) { console.error(err) - return NextResponse.json((err as Error).message, { status: 400 }) + return NextResponse.json(err instanceof Error ? err.message : err, { status: 400 }) } } @@ -57,6 +57,6 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "success" }, { status: 200 }); } catch (err) { console.error(err) - return NextResponse.json((err as Error).message, { status: 400 }); + return NextResponse.json(err instanceof Error ? err.message : err, { status: 400 }); } } \ No newline at end of file diff --git a/app/components/ForceGraph.tsx b/app/components/ForceGraph.tsx index 297c4eab..50b39d2b 100644 --- a/app/components/ForceGraph.tsx +++ b/app/components/ForceGraph.tsx @@ -57,10 +57,7 @@ export default function ForceGraph({ backgroundColor = "#FFFFFF", foregroundColor = "#000000" }: Props) { - const parentRef = useRef(null) const [canvasLoaded, setCanvasLoaded] = useState(false) - const [parentWidth, setParentWidth] = useState(0) - const [parentHeight, setParentHeight] = useState(0) // Load falkordb-canvas dynamically (client-only) useEffect(() => { @@ -69,33 +66,6 @@ export default function ForceGraph({ }) }, []) - // Handle parent resize - useEffect(() => { - const handleResize = () => { - if (!parentRef.current) return - setParentWidth(parentRef.current.clientWidth) - setParentHeight(parentRef.current.clientHeight) - } - - handleResize() - - const observer = new ResizeObserver(handleResize) - if (parentRef.current) { - observer.observe(parentRef.current) - } - - return () => { - observer.disconnect() - } - }, []) - - // Update canvas dimensions - useEffect(() => { - if (!canvasRef.current || !canvasLoaded) return - canvasRef.current.setWidth(parentWidth) - canvasRef.current.setHeight(parentHeight) - }, [canvasRef, parentWidth, parentHeight, canvasLoaded]) - // Update canvas colors useEffect(() => { if (!canvasRef.current || !canvasLoaded) return @@ -144,7 +114,7 @@ export default function ForceGraph({ // Update event handlers useEffect(() => { if (!canvasRef.current || !canvasLoaded) return - + canvasRef.current.setConfig({ onNodeClick: handleNodeClick, onNodeRightClick: handleNodeRightClick, @@ -156,15 +126,15 @@ export default function ForceGraph({ onZoom }) }, [ - handleNodeClick, - handleNodeRightClick, - handleLinkClick, - handleLinkRightClick, - onBackgroundClick, - onBackgroundRightClick, + handleNodeClick, + handleNodeRightClick, + handleLinkClick, + handleLinkRightClick, + onBackgroundClick, + onBackgroundRightClick, onEngineStop, onZoom, - canvasRef, + canvasRef, canvasLoaded ]) @@ -178,8 +148,6 @@ export default function ForceGraph({ }, [canvasRef, data, canvasLoaded]) return ( -
- -
+ ) } diff --git a/package-lock.json b/package-lock.json index de1d59b9..0065a0c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { - "@falkordb/canvas": "^0.0.8", + "@falkordb/canvas": "^0.0.11", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -42,7 +42,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@types/node": "^22.15.26", + "@types/node": "^20.19.4", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "^15.5.13", @@ -217,9 +217,9 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.8.tgz", - "integrity": "sha512-vPHEpwr7D06qUrxSbSovnH8lTdbRahlQ+wi0Hv0e7++xqvNGapMZLICrjblh/oyO2H+Ce823nTh3qZtarjdkeQ==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.11.tgz", + "integrity": "sha512-Wc+p2+u24+OKBZ3i8BRBJdArEpuay4n9EDoeLnrf1tcWV4oIW7amRatc5a+eIuPF3vrsGJdzWbpWC2/X7MLVyQ==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -1953,9 +1953,9 @@ "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==" }, "node_modules/@types/node": { - "version": "22.19.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", - "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8b6b8233..6414a751 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@falkordb/canvas": "^0.0.8", + "@falkordb/canvas": "^0.0.11", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -43,7 +43,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@types/node": "^22.15.26", + "@types/node": "^20.19.4", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-syntax-highlighter": "^15.5.13", From f9d16ad277effeaa0da0a54128e96c25f5e81618 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 30 Dec 2025 13:52:26 +0200 Subject: [PATCH 29/65] Refactor ForceGraph and CodeGraph components to integrate cooldown ticks state management and update FalkorDB canvas dependency to version 0.0.12 --- app/components/ForceGraph.tsx | 1 - app/components/code-graph.tsx | 3 + app/components/graphView.tsx | 6 +- app/components/toolbar.tsx | 18 +- app/page.tsx | 2 + components/ui/switch.tsx | 29 +++ package-lock.json | 390 ++++++++++++++++++++-------------- package.json | 3 +- 8 files changed, 284 insertions(+), 168 deletions(-) create mode 100644 components/ui/switch.tsx diff --git a/app/components/ForceGraph.tsx b/app/components/ForceGraph.tsx index 50b39d2b..522e47f6 100644 --- a/app/components/ForceGraph.tsx +++ b/app/components/ForceGraph.tsx @@ -52,7 +52,6 @@ export default function ForceGraph({ onBackgroundRightClick, onZoom, onEngineStop, - onNodeDragEnd, cooldownTicks, backgroundColor = "#FFFFFF", foregroundColor = "#000000" diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 6d0c7503..f7f1f9b7 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -332,6 +332,7 @@ export function CodeGraph({ selectedPathId={selectedPathId} setSelectedPathId={setSelectedPathId} cooldownTicks={cooldownTicks} + setCooldownTicks={setCooldownTicks} setZoomedNodes={setZoomedNodes} zoomedNodes={zoomedNodes} /> @@ -365,6 +366,8 @@ export function CodeGraph({ className="gap-4" canvasRef={chartRef} handleDownloadImage={handleDownloadImage} + setCooldownTicks={setCooldownTicks} + cooldownTicks={cooldownTicks} />
diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index fe15c9ff..4ca440e2 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -30,13 +30,13 @@ interface Props { selectedPathId: number | undefined setSelectedPathId: (selectedPathId: number) => void cooldownTicks: number | undefined + setCooldownTicks: Dispatch> setZoomedNodes: Dispatch> zoomedNodes: Node[] } export default function GraphView({ data, - graph, chartRef: canvasRef, selectedObj, setSelectedObj, @@ -50,6 +50,7 @@ export default function GraphView({ selectedPathId, setSelectedPathId, cooldownTicks, + setCooldownTicks, zoomedNodes, setZoomedNodes }: Props) { @@ -121,8 +122,11 @@ export default function GraphView({ } const handleEngineStop = () => { + if (cooldownTicks === 0) return + canvasRef.current?.zoomToFit(zoomedNodes.length === 1 ? 4 : 1, (n: GraphNode) => zoomedNodes.some(node => node.id === n.id)) setZoomedNodes([]) + setCooldownTicks(0) } return ( diff --git a/app/components/toolbar.tsx b/app/components/toolbar.tsx index 10b3404b..1327febd 100644 --- a/app/components/toolbar.tsx +++ b/app/components/toolbar.tsx @@ -1,18 +1,21 @@ import { Download, Fullscreen, ZoomIn, ZoomOut } from "lucide-react"; import { cn } from "@/lib/utils" import { GraphRef } from "@/lib/utils"; +import { Switch } from "@/components/ui/switch"; interface Props { canvasRef: GraphRef className?: string handleDownloadImage?: () => void + setCooldownTicks: (ticks?: 0) => void + cooldownTicks: number | undefined } -export function Toolbar({ canvasRef, className, handleDownloadImage }: Props) { +export function Toolbar({ canvasRef, className, handleDownloadImage, setCooldownTicks, cooldownTicks }: Props) { const handleZoomClick = (changefactor: number) => { const canvas = canvasRef.current - + if (canvas) { canvas.zoom(canvas.getZoom() * changefactor) } @@ -20,14 +23,21 @@ export function Toolbar({ canvasRef, className, handleDownloadImage }: Props) { const handleCenterClick = () => { const canvas = canvasRef.current - + if (canvas) { canvas.zoomToFit() } } return ( -
+
+ { + setCooldownTicks(cooldownTicks === undefined ? 0 : undefined) + }} + /> @@ -89,11 +90,11 @@ export default function ElementMenu({ obj, objects, setPath, handleRemove, posit title="Copy src to clipboard" onClick={async () => { try { - await navigator.clipboard.writeText(obj.src || ""); + await navigator.clipboard.writeText(obj.data.src || ""); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); - textArea.value = obj.src || ""; + textArea.value = obj.data.src || ""; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 4ca440e2..fba164ad 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -1,12 +1,12 @@ 'use client' import { Graph, GraphData, Link, Node } from './model'; -import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; -import { Path } from '@/lib/utils'; +import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; +import { Path, PATH_COLOR } from '@/lib/utils'; import { Fullscreen } from 'lucide-react'; import { GraphRef } from '@/lib/utils'; import ForceGraph from './ForceGraph'; -import { GraphNode } from '@falkordb/canvas'; +import { GraphLink, GraphNode } from '@falkordb/canvas'; export interface Position { x: number, @@ -23,7 +23,7 @@ interface Props { selectedObjects: Node[] setSelectedObjects: Dispatch> setPosition: Dispatch> - handleExpand: (nodes: Node[], expand: boolean) => void + handleExpand: (nodes: Node[], expand: boolean) => void isShowPath: boolean setPath: Dispatch> isPathResponse: boolean | undefined @@ -35,8 +35,12 @@ interface Props { zoomedNodes: Node[] } +const NODE_SIZE = 6; +const PADDING = 2; + export default function GraphView({ data, + graph, chartRef: canvasRef, selectedObj, setSelectedObj, @@ -72,13 +76,13 @@ export default function GraphView({ } }, []) - const unsetSelectedObjects = (evt?: MouseEvent) => { + const unsetSelectedObjects = useCallback((evt?: MouseEvent) => { if (evt?.ctrlKey || (!selectedObj && selectedObjects.length === 0)) return setSelectedObj(undefined) setSelectedObjects([]) - } + }, [selectedObj, selectedObjects, setSelectedObj, setSelectedObjects]) - const handleRightClick = (element: Node | Link, evt: MouseEvent) => { + const handleRightClick = useCallback((element: Node | Link, evt: MouseEvent) => { if (evt.ctrlKey && "category" in element) { if (selectedObjects.some(obj => obj.id === element.id)) { setSelectedObjects(selectedObjects.filter(obj => obj.id !== element.id)) @@ -92,7 +96,7 @@ export default function GraphView({ setSelectedObj(element) setPosition({ x: evt.clientX, y: evt.clientY }) - } + }, [selectedObjects, setSelectedObjects, setSelectedObj, setPosition]) const handleLinkClick = (link: Link, evt: MouseEvent) => { unsetSelectedObjects(evt) @@ -100,34 +104,220 @@ export default function GraphView({ setSelectedPathId(link.id) } - const handleNodeClick = async (node: Node) => { + const handleNodeClick = useCallback(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() - date.getTime() < 1000 && name === node.data.name + lastClick.current = { date: now, name: node.data.name } if (isDoubleClick) { handleExpand([node], !node.expand) } else if (isShowPath) { setPath(prev => { if (!prev?.start?.name || (prev.end?.name && prev.end?.name !== "")) { - return ({ start: { id: Number(node.id), name: node.name } }) + return ({ start: { id: Number(node.id), name: node.data.name } }) } else { - return ({ end: { id: Number(node.id), name: node.name }, start: prev.start }) + return ({ end: { id: Number(node.id), name: node.data.name }, start: prev.start }) } }) return } - } + }, [handleExpand, isShowPath, setPath]) - const handleEngineStop = () => { - if (cooldownTicks === 0) return + const handleEngineStop = useCallback(() => { + if (zoomedNodes.length > 0) { + canvasRef.current?.zoomToFit(zoomedNodes.length === 1 ? 4 : 1, (n: GraphNode) => zoomedNodes.some(node => node.id === n.id)) + setZoomedNodes([]) + } + + if (cooldownTicks !== -1) return - canvasRef.current?.zoomToFit(zoomedNodes.length === 1 ? 4 : 1, (n: GraphNode) => zoomedNodes.some(node => node.id === n.id)) - setZoomedNodes([]) setCooldownTicks(0) - } + }, [zoomedNodes, cooldownTicks, canvasRef]) + + const nodeCanvasObject = useCallback((node: GraphNode, ctx: CanvasRenderingContext2D) => { + if (!node.x || !node.y) return + + if (isPathResponse) { + if (node.data.isPathSelected) { + ctx.fillStyle = node.color; + ctx.strokeStyle = PATH_COLOR; + ctx.lineWidth = 1.5 + } else if (node.data.isPath) { + ctx.fillStyle = node.color; + ctx.strokeStyle = PATH_COLOR; + ctx.lineWidth = 1 + } else { + ctx.fillStyle = '#E5E5E5'; + ctx.strokeStyle = 'gray'; + ctx.lineWidth = 1 + } + } else if (isPathResponse === undefined) { + if (node.data.isPathSelected) { + ctx.fillStyle = node.color; + ctx.strokeStyle = PATH_COLOR; + ctx.lineWidth = 1.5 + } else if (node.data.isPath) { + ctx.fillStyle = node.color; + ctx.strokeStyle = PATH_COLOR; + ctx.lineWidth = 1 + } else { + ctx.fillStyle = node.color; + ctx.strokeStyle = 'black'; + ctx.lineWidth = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id ? 1.5 : 1 + } + } else { + ctx.fillStyle = node.color; + ctx.strokeStyle = 'black'; + ctx.lineWidth = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id ? 1.5 : 1 + } + + ctx.beginPath(); + ctx.arc(node.x, node.y, NODE_SIZE + ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); + ctx.fill(); + + ctx.fillStyle = 'black'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = '4px Arial'; + let name = node.data.name || ""; + const textWidth = ctx.measureText(name).width; + const ellipsis = '...'; + const ellipsisWidth = ctx.measureText(ellipsis).width; + const nodeSize = (NODE_SIZE + ctx.lineWidth / 2) * 2 - PADDING; + + // truncate text if it's too long + if (textWidth > nodeSize) { + while (name.length > 0 && ctx.measureText(name).width + ellipsisWidth > nodeSize) { + name = name.slice(0, -1); + } + name += ellipsis; + } + + // add label + ctx.fillText(name, node.x, node.y); + }, [selectedObj, selectedObjects, isPathResponse]) + + const nodePointerAreaPaint = useCallback((node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => { + if (!node.x || !node.y) return + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(node.x, node.y, NODE_SIZE + 4 + ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.fill(); + }, []) + + const linkCanvasObject = useCallback((link: GraphLink, ctx: CanvasRenderingContext2D) => { + const start = link.source; + const end = link.target; + + if (!start.x || !start.y || !end.x || !end.y) return + + const sameNodesLinks = graph.Elements.links.filter(l => (l.source === start.id && l.target === end.id) || (l.target === start.id && l.source === 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); + const textY = start.y + radius * Math.sin(angleOffset); + + ctx.save(); + 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); + + let textAngle = Math.atan2(end.y - start.y, end.x - start.x) + + // maintain label vertical orientation for legibility + if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); + if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); + + ctx.save(); + ctx.translate(midX, midY); + ctx.rotate(textAngle); + } + + // add label + ctx.globalAlpha = 1; + ctx.fillStyle = 'black'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = '2px Arial'; + ctx.fillText(link.relationship, 0, 0); + ctx.restore() + }, [graph.Elements.links]) + + const linkPointerAreaPaint = useCallback((link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => { + const start = link.source; + const end = link.target; + + if (!start.x || !start.y || !end.x || !end.y) return + + ctx.fillStyle = color; + + // Calculate curve the same way as linkCanvasObject + const sameNodesLinks = graph.Elements.links.filter(l => (l.source === start.id && l.target === end.id) || (l.target === start.id && l.source === end.id)) + const index = sameNodesLinks.findIndex(l => l.id === link.id) || 0 + const even = index % 2 === 0 + let curve + + if (start.id === end.id) { + // Self-loop: draw clickable area at the loop position + if (even) { + curve = Math.floor(-(index / 2)) - 3 + } else { + curve = Math.floor((index + 1) / 2) + 2 + } + + const curvature = curve * 0.1 + const radius = NODE_SIZE * curvature * 6.2; + const angleOffset = -Math.PI / 4; + const textX = start.x + radius * Math.cos(angleOffset); + const textY = start.y + radius * Math.sin(angleOffset); + + ctx.beginPath(); + ctx.arc(textX, textY, 5, 0, 2 * Math.PI, false); + ctx.fill(); + } else { + // Regular link: draw clickable area at the curve midpoint + if (even) { + curve = Math.floor(-(index / 2)) + } else { + curve = Math.floor((index + 1) / 2) + } + + const curvature = curve * 0.1 + const midX = (start.x + end.x) / 2 + (end.y - start.y) * (curvature / 2); + const midY = (start.y + end.y) / 2 + (start.x - end.x) * (curvature / 2); + + ctx.beginPath(); + ctx.arc(midX, midY, 5, 0, 2 * Math.PI, false); + ctx.fill(); + } + }, [graph.Elements.links]) return (
@@ -147,6 +337,10 @@ export default function GraphView({ onBackgroundRightClick={unsetSelectedObjects} onZoom={() => unsetSelectedObjects()} onEngineStop={handleEngineStop} + nodeCanvasObject={nodeCanvasObject} + nodePointerAreaPaint={nodePointerAreaPaint} + linkCanvasObject={linkCanvasObject} + linkPointerAreaPaint={linkPointerAreaPaint} cooldownTicks={cooldownTicks} />
diff --git a/app/components/model.ts b/app/components/model.ts index 7a9274bd..6875f941 100644 --- a/app/components/model.ts +++ b/app/components/model.ts @@ -16,7 +16,6 @@ export interface Label { export interface Node { id: number, - name: string, category: string, color: string, visible: boolean, @@ -24,9 +23,11 @@ export interface Node { expand: boolean, isPathSelected: boolean, isPath: boolean, - [key: string]: any, + data: { + name: string, + [key: string]: any, + } } - export interface Link { id: number, source: number, @@ -35,9 +36,10 @@ export interface Link { visible: boolean, isPathSelected: boolean, isPath: boolean, - curve: number, color: string, - [key: string]: any, + data: { + [key: string]: any, + }, } const COLORS_ORDER_NAME = [ @@ -161,18 +163,18 @@ export class Graph { node = { id: nodeData.id, - name: nodeData.name, color: getCategoryColorValue(category.index), category: category.name, expand: false, visible: true, collapsed, isPath: !!path, - isPathSelected: path?.start?.id === nodeData.id || path?.end?.id === nodeData.id + isPathSelected: path?.start?.id === nodeData.id || path?.end?.id === nodeData.id, + data: { + ...nodeData.properties, + } } - Object.entries(nodeData.properties).forEach(([key, value]) => { - node[key] = value - }) + this.nodesMap.set(nodeData.id, node) this.elements.nodes.push(node) newElements.nodes.push(node) @@ -195,14 +197,17 @@ export class Graph { 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 + isPathSelected: path?.start?.id === edgeData.src_node || path?.end?.id === edgeData.src_node, + data: { + name: edgeData.src_node + } + } this.nodesMap.set(edgeData.src_node, source) } @@ -210,14 +215,16 @@ export class Graph { 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 + isPathSelected: path?.start?.id === edgeData.dest_node || path?.end?.id === edgeData.dest_node, + data: { + name: edgeData.dest_node + } } this.nodesMap.set(edgeData.dest_node, target) } @@ -235,45 +242,17 @@ export class Graph { target: edgeData.dest_node, label: edgeData.relation, visible: true, - expand: false, color: "#999999", - collapsed, isPathSelected: false, isPath: !!path, - curve: 0 + data: { ...edgeData.properties } } + this.linksMap.set(edgeData.id, link) this.elements.links.push(link) 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 === start && l.target === end) || (l.target === start && l.source === end)) - const index = sameNodesLinks.findIndex(l => l.id === link.id) ?? 0 - const even = index % 2 === 0 - let curve - - if (start === end) { - if (even) { - curve = Math.floor(-(index / 2)) - 3 - } else { - curve = Math.floor((index + 1) / 2) + 2 - } - } else { - if (even) { - curve = Math.floor(-(index / 2)) - } else { - curve = Math.floor((index + 1) / 2) - } - - } - - link.curve = curve * 0.1 - }) - - return newElements } diff --git a/app/components/toolbar.tsx b/app/components/toolbar.tsx index 1327febd..b4d11258 100644 --- a/app/components/toolbar.tsx +++ b/app/components/toolbar.tsx @@ -33,9 +33,9 @@ export function Toolbar({ canvasRef, className, handleDownloadImage, setCooldown
{ - setCooldownTicks(cooldownTicks === undefined ? 0 : undefined) + setCooldownTicks(cooldownTicks !== 0 ? 0 : undefined) }} /> } { - (graph.getElements().some(e => !e.visible)) && + hasHiddenElements &&
Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) || isShowPath ? (node: Node, _evt: MouseEvent) => handleNodeClick(node) : (node: Node, evt: MouseEvent) => handleRightClick(node, evt)} diff --git a/app/page.tsx b/app/page.tsx index 49b4fb05..edc5b978 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -142,32 +142,37 @@ export default function Home() { } async function onFetchGraph(graphName: string) { + try { + const result = await fetch(`/api/repo/${prepareArg(graphName)}`, { + method: 'GET' + }) + + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return + } - setGraph(Graph.empty()) + const json = await result.json() + const g = Graph.create(json.result.entities, graphName) + setGraph(g) - const result = await fetch(`/api/repo/${prepareArg(graphName)}`, { - method: 'GET' - }) + if (cooldownTicks === 0) setCooldownTicks(-1) - if (!result.ok) { + setIsPathResponse(false) + chatPanel.current?.expand() + // @ts-ignore + window.graph = g + } catch (error) { toast({ variant: "destructive", title: "Uh oh! Something went wrong.", - description: await result.text(), + description: "Failed to load repository graph. Please try again.", }) - return } - - const json = await result.json() - const g = Graph.create(json.result.entities, graphName) - setGraph(g) - - if (cooldownTicks === 0) setCooldownTicks(-1) - - setIsPathResponse(false) - chatPanel.current?.expand() - // @ts-ignore - window.graph = g } // Send the user query to the server to expand a node @@ -416,8 +421,9 @@ export default function Home() {
- + Date: Tue, 24 Feb 2026 11:49:22 +0200 Subject: [PATCH 56/65] Remove test.only from node path connection test to ensure all tests run --- e2e/tests/canvas.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index ef53a439..75e8c514 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -176,8 +176,7 @@ test.describe("Canvas tests", () => { }); nodesPath.forEach(({firstNode, secondNode}) => { - // BUG: canvas.getGraphData() returns stale data - isPath property not syncing after path selection - test.only(`Verify successful node path connection in canvas between ${firstNode} and ${secondNode} via UI`, async () => { + 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(GRAPHRAG_SDK); await codeGraph.clickOnShowPathBtn("Show the path"); From 0d98edaf9a8877956fc255cf8e5a68e64bd51661 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 11:51:23 +0200 Subject: [PATCH 57/65] Update graph data retrieval to use graphDesktop function for improved compatibility --- e2e/logic/POM/codeGraph.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 612b2d68..40de1472 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -590,7 +590,7 @@ export default class CodeGraph extends BasePage { await this.waitForCanvasAnimationToEnd(); // Wait for the graph data to be available on window object (set by handleEngineStop) await this.page.waitForFunction(() => { - const data = (window as any).graph?.(); + const data = (window as any).graphDesktop(); // Check both possible structures: { nodes } or { elements: { nodes } } return data && ((Array.isArray(data.nodes) && data.nodes.length > 0) || (data.elements && Array.isArray(data.elements.nodes) && data.elements.nodes.length > 0)); @@ -598,7 +598,7 @@ export default class CodeGraph extends BasePage { { timeout: 5000 }); const graphData = await this.page.evaluate(() => { - return (window as any).graph?.(); + return (window as any).graphDesktop(); }); let transformData: any = null; From 8462d3b56003875dd63ea4834afcc99ff4e481f8 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 12:01:01 +0200 Subject: [PATCH 58/65] Fix canvas selection logic in handleDownloadImage for improved reliability --- app/page.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index edc5b978..d755ea8b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -269,8 +269,9 @@ export default function Home() { const handleDownloadImage = async () => { try { - const canvases = document.querySelectorAll('.force-graph-container canvas') as NodeListOf; - if (!canvases) { + const canvases = Array.from(document.querySelectorAll('falkordb-canvas').values()).map(canvas => canvas.shadowRoot?.querySelector('canvas')).filter((c): c is HTMLCanvasElement => !!c); + + if (canvases.length === 0) { toast({ title: "Error", description: "Canvas not found", From d764425b6c43590bd216c0a081dcad112dd37988 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 12:23:27 +0200 Subject: [PATCH 59/65] Remove debug console logs from canvas tests for cleaner output --- e2e/tests/canvas.spec.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index 75e8c514..7ca9cb18 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -105,17 +105,13 @@ test.describe("Canvas tests", () => { await codeGraph.insertInputForShowPath("2", path.secondNode); const initialGraph = await codeGraph.getGraphNodes(); const firstNode = findNodeByName(initialGraph, path.firstNode); - console.log("firstNode: ", firstNode); const secondNode = findNodeByName(initialGraph, path.secondNode); - console.log("secondNode: ", secondNode); expect(firstNode.isPath).toBe(true); expect(secondNode.isPath).toBe(true); await codeGraph.clickOnClearGraphBtn(); const updateGraph = await codeGraph.getGraphNodes(); const firstNode1 = findNodeByName(updateGraph, path.firstNode); const secondNode1 = findNodeByName(updateGraph, path.secondNode); - console.log("firstNode1: ", firstNode1); - console.log("secondNode1: ", secondNode1); expect(firstNode1.isPath).toBe(false); expect(secondNode1.isPath).toBe(false); }); @@ -184,12 +180,8 @@ test.describe("Canvas tests", () => { await codeGraph.insertInputForShowPath("2", secondNode); const result = await codeGraph.getGraphDetails(); const firstNodeRes = findNodeByName(result?.nodes, firstNode); - console.log("firstNodeRes: ", firstNodeRes); const secondnodeRes = findNodeByName(result?.nodes, secondNode); - console.log("secondNodeRes: ", secondnodeRes); - console.log("firstNodeRes: ", firstNodeRes.isPath); - console.log("secondnodeRes: ", secondnodeRes.isPath); expect(firstNodeRes).toBeDefined(); expect(secondnodeRes).toBeDefined(); From a2049969c1a0aec076f74ea21ed4811a614d6ddb Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 12:24:33 +0200 Subject: [PATCH 60/65] Remove stale data bug comments from canvas tests for clarity --- e2e/tests/canvas.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/e2e/tests/canvas.spec.ts b/e2e/tests/canvas.spec.ts index 7ca9cb18..eac29290 100644 --- a/e2e/tests/canvas.spec.ts +++ b/e2e/tests/canvas.spec.ts @@ -55,7 +55,6 @@ test.describe("Canvas tests", () => { }) - // BUG: canvas.getGraphData() returns stale data - visibility state not syncing after hide test(`Validate node hide functionality via element menu in canvas for ${nodes[0].nodeName}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPHRAG_SDK); @@ -83,7 +82,6 @@ test.describe("Canvas tests", () => { categories.forEach((category, index) => { const checkboxIndex = index + 1; - // BUG: canvas.getGraphData() returns stale data - visibility state not syncing after checkbox uncheck test(`Verify that unchecking the ${category} checkbox hides ${category} nodes on the canvas`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPHRAG_SDK); @@ -95,7 +93,6 @@ test.describe("Canvas tests", () => { }) nodesPath.forEach((path) => { - // BUG: canvas.getGraphData() returns stale data - isPath property not syncing after path selection 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 browser.setPageToFullScreen(); @@ -131,7 +128,6 @@ test.describe("Canvas tests", () => { for (let index = 0; index < 3; index++) { const nodeIndex: number = index + 1; - // BUG: canvas.getGraphData() returns stale data - node x/y positions not updating after drag test(`Validate canvas node dragging for node: ${index}`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPHRAG_SDK); @@ -213,7 +209,6 @@ test.describe("Canvas tests", () => { }); }) - // BUG: download button click does not trigger a file download test(`Verify file download is triggered and saved after clicking download`, async () => { const codeGraph = await browser.createNewPage(CodeGraph, urls.baseUrl); await codeGraph.selectGraph(GRAPHRAG_SDK); From 80d7c4fe42dbce8b1d38adb4350a24a9c0a373c7 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 12:54:44 +0200 Subject: [PATCH 61/65] Update Playwright imports to use @playwright/test and remove outdated dependencies --- e2e/infra/ui/basePage.ts | 2 +- e2e/infra/ui/browserWrapper.ts | 2 +- e2e/logic/POM/codeGraph.ts | 10 ++-- package-lock.json | 91 +++++++++++++++++----------------- package.json | 6 +-- 5 files changed, 56 insertions(+), 55 deletions(-) diff --git a/e2e/infra/ui/basePage.ts b/e2e/infra/ui/basePage.ts index 69baf604..b3aaa253 100644 --- a/e2e/infra/ui/basePage.ts +++ b/e2e/infra/ui/basePage.ts @@ -1,4 +1,4 @@ -import { Page } from 'playwright'; +import { Page } from '@playwright/test'; export default class BasePage { protected page: Page; diff --git a/e2e/infra/ui/browserWrapper.ts b/e2e/infra/ui/browserWrapper.ts index 985bf25c..77fa5d07 100644 --- a/e2e/infra/ui/browserWrapper.ts +++ b/e2e/infra/ui/browserWrapper.ts @@ -1,4 +1,4 @@ -import { chromium, Browser, BrowserContext, Page } from 'playwright'; +import { chromium, Browser, BrowserContext, Page } from '@playwright/test'; import BasePage from './basePage'; export default class BrowserWrapper { diff --git a/e2e/logic/POM/codeGraph.ts b/e2e/logic/POM/codeGraph.ts index 40de1472..99b02355 100644 --- a/e2e/logic/POM/codeGraph.ts +++ b/e2e/logic/POM/codeGraph.ts @@ -1,4 +1,4 @@ -import { Download, Locator, Page } from "playwright"; +import { Download, Locator, Page } from "@playwright/test"; import BasePage from "../../infra/ui/basePage"; import { interactWhenVisible, waitForElementToBeVisible, waitForStableText, waitToBeEnabled } from "../utils"; @@ -499,10 +499,10 @@ export default class CodeGraph extends BasePage { await this.page.mouse.click(10, 10); await this.clearGraphBtn.click(); } - + async clickOnUnhideNodesBtn(): Promise { await interactWhenVisible( - this.unhideNodesBtn, (el) => el.click(),`Unhide Nodes Button` + this.unhideNodesBtn, (el) => el.click(), `Unhide Nodes Button` ); } @@ -593,7 +593,7 @@ export default class CodeGraph extends BasePage { const data = (window as any).graphDesktop(); // Check both possible structures: { nodes } or { elements: { nodes } } return data && ((Array.isArray(data.nodes) && data.nodes.length > 0) || - (data.elements && Array.isArray(data.elements.nodes) && data.elements.nodes.length > 0)); + (data.elements && Array.isArray(data.elements.nodes) && data.elements.nodes.length > 0)); }, { timeout: 5000 }); @@ -693,7 +693,7 @@ export default class CodeGraph extends BasePage { const data = (window as any).graph?.(); // Check both possible structures: { nodes } or { elements: { nodes } } return data && ((Array.isArray(data.nodes) && data.nodes.length > 0) || - (data.elements && Array.isArray(data.elements.nodes) && data.elements.nodes.length > 0)); + (data.elements && Array.isArray(data.elements.nodes) && data.elements.nodes.length > 0)); }, { timeout: 5000 }); const graphData = await this.page.evaluate(() => { diff --git a/package-lock.json b/package-lock.json index 691e885a..e6346077 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "html2canvas": "^1.4.1", "lucide-react": "^0.486.0", "next": "^15.5.8", - "playwright": "^1.49.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-gtm-module": "^2.0.11", @@ -1080,6 +1079,52 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -6651,50 +6696,6 @@ "node": ">= 6" } }, - "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 40da1a8b..d75081f9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "HOST=0.0.0.0 PORT=3000 next dev", "build": "next build", "start": "HOST=0.0.0.0 PORT=3000 next start", - "lint": "next lint" + "lint": "next lint", + "typecheck": "tsc --noEmit" }, "dependencies": { "@falkordb/canvas": "^0.0.37", @@ -30,7 +31,6 @@ "html2canvas": "^1.4.1", "lucide-react": "^0.486.0", "next": "^15.5.8", - "playwright": "^1.49.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-gtm-module": "^2.0.11", @@ -53,4 +53,4 @@ "tailwindcss": "^3.4.17", "typescript": "^5.7.3" } -} +} \ No newline at end of file From 40eba8f1f4ceaea37e27793918517f200c14bd12 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 15:57:08 +0200 Subject: [PATCH 62/65] Enhance graph components with new link handling and update dependencies --- app/components/ForceGraph.tsx | 3 ++ app/components/chat.tsx | 87 +++++++++++++++++++++++++++++------ app/components/code-graph.tsx | 9 ++-- app/components/graphView.tsx | 81 +++++++++++++++++++++++--------- app/page.tsx | 8 ++-- package-lock.json | 8 ++-- package.json | 2 +- 7 files changed, 150 insertions(+), 48 deletions(-) diff --git a/app/components/ForceGraph.tsx b/app/components/ForceGraph.tsx index 6ba3196c..80c7f2fa 100644 --- a/app/components/ForceGraph.tsx +++ b/app/components/ForceGraph.tsx @@ -19,6 +19,7 @@ interface Props { nodePointerAreaPaint: (node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => void linkCanvasObject: (link: any, ctx: CanvasRenderingContext2D) => void linkPointerAreaPaint: (link: any, color: string, ctx: CanvasRenderingContext2D) => void + linkLineDash: (link: any) => number[] | null onZoom: () => void onEngineStop: () => void cooldownTicks: number | undefined @@ -61,6 +62,7 @@ export default function ForceGraph({ nodePointerAreaPaint, linkCanvasObject, linkPointerAreaPaint, + linkLineDash, cooldownTicks, backgroundColor = "#FFFFFF", foregroundColor = "#000000" @@ -140,6 +142,7 @@ export default function ForceGraph({ onEngineStop: handleEngineStop, node: { nodeCanvasObject, nodePointerAreaPaint }, link: { linkCanvasObject, linkPointerAreaPaint }, + linkLineDash, onZoom }) }, [ diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 28107c75..ff99e169 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -9,7 +9,7 @@ import { cn, GraphRef } from "@/lib/utils"; import { TypeAnimation } from "react-type-animation"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { prepareArg } from "../utils"; -import { GraphNode } from "@falkordb/canvas"; +import { dataToGraphData, GraphLink, GraphNode } from "@falkordb/canvas"; interface Props { repo: string @@ -19,7 +19,6 @@ interface Props { selectedPathId: number | undefined isPathResponse: boolean | undefined setIsPathResponse: (isPathResponse: boolean | undefined) => void - setData: Dispatch> canvasRef: GraphRef messages: Message[] setMessages: Dispatch> @@ -27,6 +26,7 @@ interface Props { setQuery: Dispatch> selectedPath: PathData | undefined setSelectedPath: Dispatch> + setCooldownTicks: Dispatch> setChatOpen?: Dispatch> paths: PathData[] setPaths: Dispatch> @@ -52,7 +52,7 @@ const RemoveLastPath = (messages: Message[]) => { return messages } -export function Chat({ messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setData, canvasRef, paths, setPaths }: Props) { +export function Chat({ messages, setMessages, query, setQuery, selectedPath, setSelectedPath, setChatOpen, repo, path, setPath, graph, selectedPathId, isPathResponse, setIsPathResponse, setCooldownTicks, canvasRef, paths, setPaths }: Props) { const [sugOpen, setSugOpen] = useState(false); @@ -273,27 +273,86 @@ export function Chat({ messages, setMessages, query, setQuery, selectedPath, set return } - const formattedPaths: PathData[] = json.result.paths.map((p: any) => ({ nodes: p.filter((n: any, i: number) => i % 2 === 0), links: p.filter((l: any, i: number) => i % 2 !== 0) })) - formattedPaths.forEach((p: any) => graph.extend(p, false, path)) - + const formattedPaths: PathData[] = json.result.paths.map((p: any) => ({ nodes: p.filter((_n: any, i: number) => i % 2 === 0), links: p.filter((l: any, i: number) => i % 2 !== 0) })) + const elements = formattedPaths.reduce( + (acc, p) => { + const el = graph.extend(p, false, path) + acc.nodes.push(...el.nodes) + acc.links.push(...el.links) + return acc + }, + { nodes: [], links: [] } + ) setPaths(formattedPaths) setMessages((prev) => [...prev.slice(0, -1), { type: MessageTypes.PathResponse, paths: formattedPaths, graphName: graph.Id }]); setIsPathResponse(true) const currentData = canvas.getGraphData(); - const nodesSet = new Set(formattedPaths.flatMap(p => p.nodes.map((n: Node) => n.id))); - const linksSet = new Set(formattedPaths.flatMap(p => p.links.map((l: any) => l.id))); + const nodesMap = new Map(currentData.nodes.map(n => [n.id, n])) + const linksMap = new Map(currentData.links.map(l => [l.id, l])) - currentData.nodes.forEach(n => { - n.data.isPath = nodesSet.has(n.id); + formattedPaths.flatMap(p => p.nodes).forEach(n => { + const node = nodesMap.get(n.id); + if (node) { + node.data.isPath = true; + } }); - currentData.links.forEach(l => { - l.data.isPath = linksSet.has(l.id); - l.color = linksSet.has(l.id) ? PATH_COLOR : l.color; + formattedPaths.flatMap(p => p.links).forEach(l => { + const link = linksMap.get(l.id); + + if (link) { + link.data.isPath = true; + link.color = PATH_COLOR; + } }); - canvas.setGraphData(currentData) + // Filter for only new elements + const newDataElements = { + nodes: elements.nodes.filter(n => !nodesMap.has(n.id)) + .map(({ category, color, data, id, isPath, isPathSelected, visible }) => ({ + color, + id, + labels: [category], + data: { + ...data, + isPath, + isPathSelected + }, + visible, + })), + links: elements.links.filter(l => !linksMap.has(l.id)) + .map(({ color, id, source, target, data, isPath, isPathSelected, visible, label }) => ({ + color: isPath ? PATH_COLOR : color, + id, + source, + target, + data: { + ...data, + isPath, + isPathSelected + }, + visible, + relationship: label, + })) + } + + // Convert only new data to GraphData format + const newGraphData = dataToGraphData( + newDataElements, + undefined, + new Map(currentData.nodes.map(n => [n.id, n])) + ) + + // Merge with existing data + canvasRef.current?.setGraphData({ + nodes: [...currentData.nodes, ...newGraphData.nodes], + links: [...currentData.links, ...newGraphData.links] + }) + + if (elements.nodes.length !== 0 || elements.links.length !== 0) { + setCooldownTicks(-1) + } setTimeout(() => { const nodesMap = new Map(formattedPaths.flatMap(p => p.nodes.map((n: Node) => [n.id, n]))) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 0f591502..b75f9ef1 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -182,7 +182,7 @@ export function CodeGraph({ if (nodes.length === 0) return; const expandedNodes: Node[] = [] - const deleteIdsMap = new Map() + const deleteIdsMap = new Set() graph.Elements = { nodes: graph.Elements.nodes.filter(node => { @@ -192,7 +192,7 @@ export function CodeGraph({ if (!isTarget) return true - deleteIdsMap.set(node.id, true) + deleteIdsMap.add(node.id) const deleted = graph.NodesMap.delete(Number(node.id)) if (deleted && node.expand) { @@ -204,7 +204,7 @@ export function CodeGraph({ links: graph.Elements.links } - deleteNeighbors(expandedNodes) + deleteNeighbors(expandedNodes)?.forEach(id => deleteIdsMap.add(id)) graph.removeLinks() @@ -273,6 +273,8 @@ export function CodeGraph({ nodes: [...currentData.nodes, ...newGraphData.nodes], links: [...currentData.links, ...newGraphData.links] }) + + setCooldownTicks(-1) } else { const deleteNodes = nodes.filter(n => n.expand) if (deleteNodes.length > 0) { @@ -287,6 +289,7 @@ export function CodeGraph({ currentData.links = currentData.links.filter(link => !deleteIdsMap.has(Number(link.source.id)) && !deleteIdsMap.has(Number(link.target.id))) canvasRef.current?.setGraphData(currentData) + setCooldownTicks(-1) } } } diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 4c20b3b4..2554e96b 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -24,7 +24,7 @@ interface Props { selectedObjects: Node[] setSelectedObjects: Dispatch> setPosition: Dispatch> - handleExpand: (nodes: Node[], expand: boolean) => void + handleExpand: (nodes: Node[], expand: boolean) => void isShowPath: boolean setPath: Dispatch> isPathResponse: boolean | undefined @@ -207,7 +207,7 @@ export default function GraphView({ ctx.fillStyle = color; ctx.beginPath(); - ctx.arc(node.x, node.y, NODE_SIZE + 4 + ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.arc(node.x, node.y, NODE_SIZE + 2 + ctx.lineWidth / 2, 0, 2 * Math.PI, false); ctx.fill(); }, []) @@ -222,6 +222,9 @@ export default function GraphView({ const even = index % 2 === 0 let curve + ctx.strokeStyle = '#999999'; + ctx.lineWidth = 0.1; + if (start.id === end.id) { if (even) { curve = Math.floor(-(index / 2)) - 3 @@ -231,14 +234,25 @@ export default function GraphView({ 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); - const textY = start.y + radius * Math.sin(angleOffset); + const d = link.curve * 70; + + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + ctx.bezierCurveTo(start.x, start.y - d, start.x + d, start.y, start.x, start.y); + ctx.stroke(); + + // Midpoint of cubic bezier: P0=(sx,sy), P1=(sx,sy-d), P2=(sx+d,sy), P3=(sx,sy) + const textX = start.x + 0.375 * d; + const textY = start.y - 0.375 * d; + + // Tangent at midpoint is (0.75d, 0.75d), angle always resolves to PI/4 + let textAngle = Math.atan2(0.75 * d, 0.75 * d); + if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); + if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); ctx.save(); ctx.translate(textX, textY); - ctx.rotate(-angleOffset); + ctx.rotate(textAngle); } else { if (even) { curve = Math.floor(-(index / 2)) @@ -248,8 +262,23 @@ export default function GraphView({ 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); + const dx = end.x - start.x; + const dy = end.y - start.y; + const dist = Math.sqrt(dx * dx + dy * dy); + const curvature = link.curve; + const cpX = (start.x + end.x) / 2 + (dy / dist) * curvature * dist; + const cpY = (start.y + end.y) / 2 + (-dx / dist) * curvature * dist; + + // Draw the quadratic bezier curve + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + ctx.quadraticCurveTo(cpX, cpY, end.x, end.y); + ctx.stroke(); + + // Midpoint of quadratic bezier at t=0.5 + const t = 0.5; + const midX = (1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * cpX + t * t * end.x; + const midY = (1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * cpY + t * t * end.y; let textAngle = Math.atan2(end.y - start.y, end.x - start.x) @@ -272,22 +301,26 @@ export default function GraphView({ ctx.restore() }, [graph.Elements.links]) + const linkLineDash = useCallback((link: GraphLink) => { + if (link.data.isPath && !link.data.isPathSelected) return [5, 5] + return null + }, []) + const linkPointerAreaPaint = useCallback((link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => { const start = link.source; const end = link.target; if (!start.x || !start.y || !end.x || !end.y) return - ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.lineWidth = 1; - // Calculate curve the same way as linkCanvasObject const sameNodesLinks = graph.Elements.links.filter(l => (l.source === start.id && l.target === end.id) || (l.target === start.id && l.source === end.id)) const index = sameNodesLinks.findIndex(l => l.id === link.id) || 0 const even = index % 2 === 0 let curve if (start.id === end.id) { - // Self-loop: draw clickable area at the loop position if (even) { curve = Math.floor(-(index / 2)) - 3 } else { @@ -295,16 +328,13 @@ export default function GraphView({ } const curvature = curve * 0.1 - const radius = NODE_SIZE * curvature * 6.2; - const angleOffset = -Math.PI / 4; - const textX = start.x + radius * Math.cos(angleOffset); - const textY = start.y + radius * Math.sin(angleOffset); + const d = curvature * 70; ctx.beginPath(); - ctx.arc(textX, textY, 5, 0, 2 * Math.PI, false); - ctx.fill(); + ctx.moveTo(start.x, start.y); + ctx.bezierCurveTo(start.x, start.y - d, start.x + d, start.y, start.x, start.y); + ctx.stroke(); } else { - // Regular link: draw clickable area at the curve midpoint if (even) { curve = Math.floor(-(index / 2)) } else { @@ -312,12 +342,16 @@ export default function GraphView({ } const curvature = curve * 0.1 - const midX = (start.x + end.x) / 2 + (end.y - start.y) * (curvature / 2); - const midY = (start.y + end.y) / 2 + (start.x - end.x) * (curvature / 2); + const dx = end.x - start.x; + const dy = end.y - start.y; + const dist = Math.sqrt(dx * dx + dy * dy); + const cpX = (start.x + end.x) / 2 + (dy / dist) * curvature * dist; + const cpY = (start.y + end.y) / 2 + (-dx / dist) * curvature * dist; ctx.beginPath(); - ctx.arc(midX, midY, 5, 0, 2 * Math.PI, false); - ctx.fill(); + ctx.moveTo(start.x, start.y); + ctx.quadraticCurveTo(cpX, cpY, end.x, end.y); + ctx.stroke(); } }, [graph.Elements.links]) @@ -344,6 +378,7 @@ export default function GraphView({ nodePointerAreaPaint={nodePointerAreaPaint} linkCanvasObject={linkCanvasObject} linkPointerAreaPaint={linkPointerAreaPaint} + linkLineDash={linkLineDash} cooldownTicks={cooldownTicks} />
diff --git a/app/page.tsx b/app/page.tsx index d755ea8b..7739e5a4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,6 @@ 'use client' import { useEffect, useRef, useState } from 'react'; -import { Chat } from './components/chat'; import { Graph, GraphData, Node } from './components/model'; import { AlignRight, BookOpen, BoomBox, Download, Github, HomeIcon, Search, X } from 'lucide-react'; import Link from 'next/link'; @@ -22,6 +21,7 @@ import { cn, GraphRef, Message, Path, PathData, PathNode } from '@/lib/utils'; import type { GraphNode } from '@falkordb/canvas'; import dynamic from 'next/dynamic'; +const Chat = dynamic(() => import('./components/chat').then(mod => mod.Chat), { ssr: false }); const CodeGraph = dynamic(() => import('./components/code-graph').then(mod => mod.CodeGraph), { ssr: false }); type Tip = { @@ -203,6 +203,8 @@ export default function Home() { const handleSearchSubmit = (node: any, canvasRef: GraphRef) => { const canvas = canvasRef.current + debugger + if (canvas) { let chartNode = graph.Elements.nodes.find(n => n.id == node.id) @@ -477,9 +479,9 @@ export default function Home() { selectedPathId={selectedPathId} isPathResponse={isPathResponse} setIsPathResponse={setIsPathResponse} - setData={setData} paths={paths} setPaths={setPaths} + setCooldownTicks={setCooldownTicks} /> @@ -601,10 +603,10 @@ export default function Home() { selectedPathId={selectedPathId} isPathResponse={isPathResponse} setIsPathResponse={setIsPathResponse} - setData={setData} setChatOpen={setChatOpen} paths={paths} setPaths={setPaths} + setCooldownTicks={setCooldownTicks} /> diff --git a/package-lock.json b/package-lock.json index e6346077..d3260c9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { - "@falkordb/canvas": "^0.0.37", + "@falkordb/canvas": "^0.0.38", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -251,9 +251,9 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.37.tgz", - "integrity": "sha512-/y4omfjh0GdXp2M5QqPS2OyU8dPHndSCfYq4MbgvjoKvRRAVX4TLcmNMsRELXZX4+18QT7yi4vUbKgG4SEMyNA==", + "version": "0.0.38", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.38.tgz", + "integrity": "sha512-Lxsn0S+zG4AO5hRpgaPfsWw4QgeO+ifEY47utZxcTnMjtiI4nUH6sBzpONz8xhNXU7RWywjYaT+jzjb1PIPyGQ==", "license": "MIT", "dependencies": { "d3": "^7.9.0", diff --git a/package.json b/package.json index d75081f9..a9aeeea8 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@falkordb/canvas": "^0.0.37", + "@falkordb/canvas": "^0.0.38", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", From 11e1630b9e1e27362badee2dfa4ddd3cb7c26c6d Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 24 Feb 2026 16:32:42 +0200 Subject: [PATCH 63/65] Refactor link curvature calculation in GraphView for improved scaling --- app/components/graphView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 2554e96b..63606835 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -234,7 +234,7 @@ export default function GraphView({ link.curve = curve * 0.1 - const d = link.curve * 70; + const d = link.curve * NODE_SIZE * 11.67; ctx.beginPath(); ctx.moveTo(start.x, start.y); @@ -328,7 +328,7 @@ export default function GraphView({ } const curvature = curve * 0.1 - const d = curvature * 70; + const d = curvature * NODE_SIZE * 11.67; ctx.beginPath(); ctx.moveTo(start.x, start.y); From 5d7ecba4f9cf8f1042ec5d78aff6a197d6dc0909 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 25 Feb 2026 16:01:32 +0200 Subject: [PATCH 64/65] Update @falkordb/canvas to version 0.0.40 and refactor graph component handlers for improved link and node interactions --- app/components/ForceGraph.tsx | 61 ++++++++++-- app/components/graphView.tsx | 181 +++++++-------------------------- app/page.tsx | 60 +++++++++-- package-lock.json | 183 ++++------------------------------ package.json | 2 +- 5 files changed, 162 insertions(+), 325 deletions(-) diff --git a/app/components/ForceGraph.tsx b/app/components/ForceGraph.tsx index 80c7f2fa..decb57f3 100644 --- a/app/components/ForceGraph.tsx +++ b/app/components/ForceGraph.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useState } from "react" -import type { Data, GraphNode } from "@falkordb/canvas" +import type { Data, GraphLink, GraphNode } from "@falkordb/canvas" import { GraphRef, PATH_COLOR } from "@/lib/utils" import { GraphData, Link, Node } from "./model" @@ -10,15 +10,17 @@ interface Props { data: GraphData canvasRef: GraphRef onNodeClick: (node: Node, event: MouseEvent) => void + onNodeHover: (node: Node | null) => void onNodeRightClick: (node: Node, event: MouseEvent) => void + isNodeSelected: (node: GraphNode) => boolean onLinkClick: (link: Link, event: MouseEvent) => void + onLinkHover: (link: Link | null) => void onLinkRightClick: (link: Link, event: MouseEvent) => void + isLinkSelected: (link: GraphLink) => boolean onBackgroundClick: (event: MouseEvent) => void onBackgroundRightClick: (event: MouseEvent) => void nodeCanvasObject: (node: GraphNode, ctx: CanvasRenderingContext2D) => void nodePointerAreaPaint: (node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => void - linkCanvasObject: (link: any, ctx: CanvasRenderingContext2D) => void - linkPointerAreaPaint: (link: any, color: string, ctx: CanvasRenderingContext2D) => void linkLineDash: (link: any) => number[] | null onZoom: () => void onEngineStop: () => void @@ -51,17 +53,19 @@ export default function ForceGraph({ data, canvasRef, onNodeClick, + onNodeHover, onNodeRightClick, + isNodeSelected, onLinkClick, + onLinkHover, onLinkRightClick, + isLinkSelected, onBackgroundClick, onBackgroundRightClick, onZoom, onEngineStop, nodeCanvasObject, nodePointerAreaPaint, - linkCanvasObject, - linkPointerAreaPaint, linkLineDash, cooldownTicks, backgroundColor = "#FFFFFF", @@ -99,25 +103,49 @@ export default function ForceGraph({ }, [canvasRef, cooldownTicks, canvasLoaded]) // Map node click handler - const handleNodeClick = useCallback((node: any, event: MouseEvent) => { + const handleNodeClick = useCallback((node: GraphNode, event: MouseEvent) => { const originalNode = data.nodes.find(n => n.id === node.id) if (originalNode) onNodeClick(originalNode, event) }, [onNodeClick, data.nodes]) + // Map node hover handler + const handleNodeHover = useCallback((node: GraphNode | null) => { + if (!node) { + onNodeHover(null) + return + } + + const originalNode = data.nodes.find(n => n.id === node.id) + + if (originalNode) onNodeHover(originalNode) + }, [onNodeHover, data.nodes]) + // Map node right click handler - const handleNodeRightClick = useCallback((node: any, event: MouseEvent) => { + const handleNodeRightClick = useCallback((node: GraphNode, event: MouseEvent) => { const originalNode = data.nodes.find(n => n.id === node.id) if (originalNode) onNodeRightClick(originalNode, event) }, [onNodeRightClick, data.nodes]) // Map link click handler - const handleLinkClick = useCallback((link: any, event: MouseEvent) => { + const handleLinkClick = useCallback((link: GraphLink, event: MouseEvent) => { const originalLink = data.links.find(l => l.id === link.id) if (originalLink) onLinkClick(originalLink, event) }, [onLinkClick, data.links]) + // Map link hover handler + const handleLinkHover = useCallback((link: GraphLink | null) => { + if (!link) { + onLinkHover(null) + return + } + + const originalLink = data.links.find(l => l.id === link.id) + + if (originalLink) onLinkHover(originalLink) + }, [onLinkHover, data.links]) + // Map link right click handler - const handleLinkRightClick = useCallback((link: any, event: MouseEvent) => { + const handleLinkRightClick = useCallback((link: GraphLink, event: MouseEvent) => { const originalLink = data.links.find(l => l.id === link.id) if (originalLink) onLinkRightClick(originalLink, event) }, [onLinkRightClick, data.links]) @@ -132,27 +160,38 @@ export default function ForceGraph({ if (!canvasRef.current || !canvasLoaded) return canvasRef.current.setConfig({ autoStopOnSettle: false, + // nodes will display node.data.captionsKeys in the canvas captionsKeys: ["name", "title"], onNodeClick: handleNodeClick, onNodeRightClick: handleNodeRightClick, + onNodeHover: handleNodeHover, + isNodeSelected: isNodeSelected, onLinkClick: handleLinkClick, onLinkRightClick: handleLinkRightClick, + onLinkHover: handleLinkHover, + isLinkSelected: isLinkSelected, onBackgroundClick, onBackgroundRightClick, onEngineStop: handleEngineStop, node: { nodeCanvasObject, nodePointerAreaPaint }, - link: { linkCanvasObject, linkPointerAreaPaint }, linkLineDash, onZoom }) }, [ handleNodeClick, handleNodeRightClick, + handleNodeHover, handleLinkClick, handleLinkRightClick, + handleLinkHover, + isNodeSelected, + isLinkSelected, onBackgroundClick, onBackgroundRightClick, handleEngineStop, + nodeCanvasObject, + nodePointerAreaPaint, + linkLineDash, onZoom, canvasRef, canvasLoaded @@ -168,6 +207,6 @@ export default function ForceGraph({ }, [canvasRef, data, canvasLoaded]) return ( - + ) } diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index 63606835..aefda794 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -63,6 +63,7 @@ export default function GraphView({ const lastClick = useRef<{ date: Date, name: string }>({ date: new Date(), name: "" }) const [screenSize, setScreenSize] = useState(0) + const [hoverElement, setHoverElement] = useState() useEffect(() => { const handleResize = () => { @@ -106,6 +107,30 @@ export default function GraphView({ setSelectedPathId(link.id) } + const handleNodeHover = useCallback((node: Node | null) => { + setHoverElement(node) + }, []) + + const handleLinkHover = useCallback((link: Link | null) => { + setHoverElement(link) + }, []) + + const isNodeSelected = useCallback((node: GraphNode) => { + if (isPathResponse) { + return node.data.isPathSelected + } else { + return selectedObjects.some(obj => "category" in obj && obj.id === node.id) || (selectedObj && "category" in selectedObj && selectedObj?.id === node.id) || (hoverElement && ('category' in hoverElement) && hoverElement.id === node.id) + } + }, [isPathResponse, selectedObjects, selectedObj, hoverElement]) + + const isLinkSelected = useCallback((link: GraphLink) => { + if (isPathResponse) { + return link.data.isPathSelected + } else { + return selectedObjects.some(obj => "source" in obj && obj.id === link.id) || (selectedObj && "source" in selectedObj && selectedObj?.id === link.id) || (hoverElement && 'source' in hoverElement && hoverElement.id === link.id) + } + }, [isPathResponse, selectedObjects, selectedObj, hoverElement]) + const handleNodeClick = useCallback(async (node: Node) => { const now = new Date() const { date, name } = lastClick.current @@ -141,6 +166,9 @@ export default function GraphView({ const nodeCanvasObject = useCallback((node: GraphNode, ctx: CanvasRenderingContext2D) => { if (!node.x || !node.y) return + const isHovered = !!hoverElement && !('source' in hoverElement) && hoverElement.id === node.id + const isSelected = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id + if (isPathResponse) { if (node.data.isPathSelected) { ctx.fillStyle = node.color; @@ -167,12 +195,12 @@ export default function GraphView({ } else { ctx.fillStyle = node.color; ctx.strokeStyle = 'black'; - ctx.lineWidth = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id ? 1.5 : 1 + ctx.lineWidth = isSelected || isHovered ? 1.5 : 1 } } else { ctx.fillStyle = node.color; ctx.strokeStyle = 'black'; - ctx.lineWidth = selectedObjects.some(obj => obj.id === node.id) || selectedObj?.id === node.id ? 1.5 : 1 + ctx.lineWidth = isSelected || isHovered ? 1.5 : 1 } ctx.beginPath(); @@ -183,7 +211,7 @@ export default function GraphView({ ctx.fillStyle = 'black'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.font = '4px Arial'; + ctx.font = '2px Arial'; let name = node.data.name || ""; const textWidth = ctx.measureText(name).width; const ellipsis = '...'; @@ -200,7 +228,7 @@ export default function GraphView({ // add label ctx.fillText(name, node.x, node.y); - }, [selectedObj, selectedObjects, isPathResponse]) + }, [selectedObj, selectedObjects, isPathResponse, hoverElement]) const nodePointerAreaPaint = useCallback((node: GraphNode, color: string, ctx: CanvasRenderingContext2D) => { if (!node.x || !node.y) return @@ -211,150 +239,11 @@ export default function GraphView({ ctx.fill(); }, []) - const linkCanvasObject = useCallback((link: GraphLink, ctx: CanvasRenderingContext2D) => { - const start = link.source; - const end = link.target; - - if (!start.x || !start.y || !end.x || !end.y) return - - const sameNodesLinks = graph.Elements.links.filter(l => (l.source === start.id && l.target === end.id) || (l.target === start.id && l.source === end.id)) - const index = sameNodesLinks.findIndex(l => l.id === link.id) || 0 - const even = index % 2 === 0 - let curve - - ctx.strokeStyle = '#999999'; - ctx.lineWidth = 0.1; - - 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 d = link.curve * NODE_SIZE * 11.67; - - ctx.beginPath(); - ctx.moveTo(start.x, start.y); - ctx.bezierCurveTo(start.x, start.y - d, start.x + d, start.y, start.x, start.y); - ctx.stroke(); - - // Midpoint of cubic bezier: P0=(sx,sy), P1=(sx,sy-d), P2=(sx+d,sy), P3=(sx,sy) - const textX = start.x + 0.375 * d; - const textY = start.y - 0.375 * d; - - // Tangent at midpoint is (0.75d, 0.75d), angle always resolves to PI/4 - let textAngle = Math.atan2(0.75 * d, 0.75 * d); - if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); - if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); - - ctx.save(); - ctx.translate(textX, textY); - ctx.rotate(textAngle); - } else { - if (even) { - curve = Math.floor(-(index / 2)) - } else { - curve = Math.floor((index + 1) / 2) - } - - link.curve = curve * 0.1 - - const dx = end.x - start.x; - const dy = end.y - start.y; - const dist = Math.sqrt(dx * dx + dy * dy); - const curvature = link.curve; - const cpX = (start.x + end.x) / 2 + (dy / dist) * curvature * dist; - const cpY = (start.y + end.y) / 2 + (-dx / dist) * curvature * dist; - - // Draw the quadratic bezier curve - ctx.beginPath(); - ctx.moveTo(start.x, start.y); - ctx.quadraticCurveTo(cpX, cpY, end.x, end.y); - ctx.stroke(); - - // Midpoint of quadratic bezier at t=0.5 - const t = 0.5; - const midX = (1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * cpX + t * t * end.x; - const midY = (1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * cpY + t * t * end.y; - - let textAngle = Math.atan2(end.y - start.y, end.x - start.x) - - // maintain label vertical orientation for legibility - if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); - if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); - - ctx.save(); - ctx.translate(midX, midY); - ctx.rotate(textAngle); - } - - // add label - ctx.globalAlpha = 1; - ctx.fillStyle = 'black'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = '2px Arial'; - ctx.fillText(link.relationship, 0, 0); - ctx.restore() - }, [graph.Elements.links]) - const linkLineDash = useCallback((link: GraphLink) => { if (link.data.isPath && !link.data.isPathSelected) return [5, 5] return null }, []) - const linkPointerAreaPaint = useCallback((link: GraphLink, color: string, ctx: CanvasRenderingContext2D) => { - const start = link.source; - const end = link.target; - - if (!start.x || !start.y || !end.x || !end.y) return - - ctx.strokeStyle = color; - ctx.lineWidth = 1; - - const sameNodesLinks = graph.Elements.links.filter(l => (l.source === start.id && l.target === end.id) || (l.target === start.id && l.source === 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 - } - - const curvature = curve * 0.1 - const d = curvature * NODE_SIZE * 11.67; - - ctx.beginPath(); - ctx.moveTo(start.x, start.y); - ctx.bezierCurveTo(start.x, start.y - d, start.x + d, start.y, start.x, start.y); - ctx.stroke(); - } else { - if (even) { - curve = Math.floor(-(index / 2)) - } else { - curve = Math.floor((index + 1) / 2) - } - - const curvature = curve * 0.1 - const dx = end.x - start.x; - const dy = end.y - start.y; - const dist = Math.sqrt(dx * dx + dy * dy); - const cpX = (start.x + end.x) / 2 + (dy / dist) * curvature * dist; - const cpY = (start.y + end.y) / 2 + (-dx / dist) * curvature * dist; - - ctx.beginPath(); - ctx.moveTo(start.x, start.y); - ctx.quadraticCurveTo(cpX, cpY, end.x, end.y); - ctx.stroke(); - } - }, [graph.Elements.links]) - return (
@@ -367,17 +256,19 @@ export default function GraphView({ data={data} canvasRef={canvasRef} onNodeClick={screenSize > Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) || isShowPath ? (node: Node, _evt: MouseEvent) => handleNodeClick(node) : (node: Node, evt: MouseEvent) => handleRightClick(node, evt)} + onNodeHover={handleNodeHover} onNodeRightClick={handleRightClick} + isNodeSelected={isNodeSelected} onLinkClick={screenSize > Number(process.env.NEXT_PUBLIC_MOBILE_BREAKPOINT) && isPathResponse ? handleLinkClick : handleRightClick} + onLinkHover={handleLinkHover} onLinkRightClick={handleRightClick} + isLinkSelected={isLinkSelected} onBackgroundClick={unsetSelectedObjects} onBackgroundRightClick={unsetSelectedObjects} onZoom={() => unsetSelectedObjects()} onEngineStop={handleEngineStop} nodeCanvasObject={nodeCanvasObject} nodePointerAreaPaint={nodePointerAreaPaint} - linkCanvasObject={linkCanvasObject} - linkPointerAreaPaint={linkPointerAreaPaint} linkLineDash={linkLineDash} cooldownTicks={cooldownTicks} /> diff --git a/app/page.tsx b/app/page.tsx index 7739e5a4..f8f465c5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -200,11 +200,9 @@ export default function Home() { return graph.extend(json.result.neighbors, true) } - const handleSearchSubmit = (node: any, canvasRef: GraphRef) => { + const handleSearchSubmit = async (node: any, canvasRef: GraphRef) => { const canvas = canvasRef.current - debugger - if (canvas) { let chartNode = graph.Elements.nodes.find(n => n.id == node.id) @@ -216,11 +214,58 @@ export default function Home() { setZoomedNodes([chartNode]) graph.visibleLinks(true, [chartNode!.id]) - setData({ ...graph.Elements }) + + const currentData = canvas.getGraphData() + + const { dataToGraphData } = await import('@falkordb/canvas') + const graphNode = dataToGraphData({ + nodes: [{ + color: chartNode.color, + id: chartNode.id, + labels: [chartNode.category], + visible: chartNode.visible, + data: { + ...chartNode.data, + isPath: chartNode.isPath, + isPathSelected: chartNode.isPathSelected, + } + }], links: [] + }).nodes[0] + + if (graphNode) { + currentData.nodes.push(graphNode) + } + + canvas.setGraphData(currentData) + + + setTimeout(() => { + canvas.zoomToFit(4, (n: GraphNode) => n.id === chartNode!.id) + }, 0) + setSearchNode(chartNode) + setOptionsOpen(false) + return } + chartNode.visible = true graph.visibleLinks(true, [chartNode!.id]) - setData({ ...graph.Elements }) + + const currentData = canvas.getGraphData() + + const graphNode = currentData.nodes.find(n => n.id === chartNode!.id) + if (graphNode) { + graphNode.visible = true + } + + currentData.links.forEach(canvasLink => { + const appLink = graph.LinksMap.get(canvasLink.id) + + if (appLink) { + canvasLink.visible = appLink.visible + } + }) + + canvas.setGraphData(currentData) } setTimeout(() => { @@ -240,7 +285,7 @@ export default function Home() { graph.Categories.find(c => c.name === name)!.show = show graph.Elements.nodes.forEach(node => { - if (!(node.category === name)) return + if (node.category !== name) return node.visible = show }) @@ -255,6 +300,7 @@ export default function Home() { canvasNode.visible = appNode.visible; } }); + currentData.links.forEach(canvasLink => { const appLink = graph.LinksMap.get(canvasLink.id); @@ -272,7 +318,7 @@ export default function Home() { const handleDownloadImage = async () => { try { const canvases = Array.from(document.querySelectorAll('falkordb-canvas').values()).map(canvas => canvas.shadowRoot?.querySelector('canvas')).filter((c): c is HTMLCanvasElement => !!c); - + if (canvases.length === 0) { toast({ title: "Error", diff --git a/package-lock.json b/package-lock.json index d3260c9f..52f8d36d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { - "@falkordb/canvas": "^0.0.38", + "@falkordb/canvas": "v0.0.40", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -52,6 +52,25 @@ "typescript": "^5.7.3" } }, + "../falkordb-canvas": { + "name": "@falkordb/canvas", + "version": "0.0.40", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "force-graph": "^1.44.4", + "react": "^19.2.3" + }, + "devDependencies": { + "@types/d3": "^7.4.3", + "@types/react": "^19.2.7", + "@typescript-eslint/eslint-plugin": "^8.18.2", + "@typescript-eslint/parser": "^8.18.2", + "eslint": "^9.17.0", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -251,24 +270,8 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.38", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.38.tgz", - "integrity": "sha512-Lxsn0S+zG4AO5hRpgaPfsWw4QgeO+ifEY47utZxcTnMjtiI4nUH6sBzpONz8xhNXU7RWywjYaT+jzjb1PIPyGQ==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "force-graph": "^1.44.4", - "react": "^19.2.3" - } - }, - "node_modules/@falkordb/canvas/node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "resolved": "../falkordb-canvas", + "link": true }, "node_modules/@floating-ui/core": { "version": "1.7.3", @@ -2132,12 +2135,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@tweenjs/tween.js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", - "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2768,15 +2765,6 @@ "win32" ] }, - "node_modules/accessor-fn": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", - "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3161,16 +3149,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bezier-js": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", - "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3328,18 +3306,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas-color-tracker": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", - "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", - "license": "MIT", - "dependencies": { - "tinycolor2": "^1.6.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/canvas2svg": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/canvas2svg/-/canvas2svg-1.0.16.tgz", @@ -3627,12 +3593,6 @@ "node": ">=12" } }, - "node_modules/d3-binarytree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", - "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", - "license": "MIT" - }, "node_modules/d3-brush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", @@ -3776,22 +3736,6 @@ "node": ">=12" } }, - "node_modules/d3-force-3d": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", - "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", - "license": "MIT", - "dependencies": { - "d3-binarytree": "1", - "d3-dispatch": "1 - 3", - "d3-octree": "1", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -3834,12 +3778,6 @@ "node": ">=12" } }, - "node_modules/d3-octree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", - "license": "MIT" - }, "node_modules/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", @@ -4982,20 +4920,6 @@ "dev": true, "license": "ISC" }, - "node_modules/float-tooltip": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", - "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", - "license": "MIT", - "dependencies": { - "d3-selection": "2 - 3", - "kapsule": "^1.16", - "preact": "10" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5012,32 +4936,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/force-graph": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.0.tgz", - "integrity": "sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==", - "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "18 - 25", - "accessor-fn": "1", - "bezier-js": "3 - 6", - "canvas-color-tracker": "^1.3", - "d3-array": "1 - 3", - "d3-drag": "2 - 3", - "d3-force-3d": "2 - 3", - "d3-scale": "1 - 4", - "d3-scale-chromatic": "1 - 3", - "d3-selection": "2 - 3", - "d3-zoom": "2 - 3", - "float-tooltip": "^1.7", - "index-array-by": "1", - "kapsule": "^1.16", - "lodash-es": "4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -5454,15 +5352,6 @@ "node": ">=0.8.19" } }, - "node_modules/index-array-by": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", - "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6057,18 +5946,6 @@ "node": ">=4.0" } }, - "node_modules/kapsule": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", - "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", - "license": "MIT", - "dependencies": { - "lodash-es": "4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6862,16 +6739,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/preact": { - "version": "10.28.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.1.tgz", - "integrity": "sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7943,12 +7810,6 @@ "node": ">=0.8" } }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index a9aeeea8..626b5523 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@falkordb/canvas": "^0.0.38", + "@falkordb/canvas": "v0.0.40", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", From a7d8ef2c2e84c9cf8adc1dc65ee987676943eb7e Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 25 Feb 2026 16:09:16 +0200 Subject: [PATCH 65/65] Update @falkordb/canvas dependency version to use caret notation for flexibility --- package-lock.json | 183 ++++++++++++++++++++++++++++++++++++++++------ package.json | 4 +- 2 files changed, 163 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52f8d36d..e206ca0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "code-graph", "version": "0.2.0", "dependencies": { - "@falkordb/canvas": "v0.0.40", + "@falkordb/canvas": "^0.0.40", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -52,25 +52,6 @@ "typescript": "^5.7.3" } }, - "../falkordb-canvas": { - "name": "@falkordb/canvas", - "version": "0.0.40", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "force-graph": "^1.44.4", - "react": "^19.2.3" - }, - "devDependencies": { - "@types/d3": "^7.4.3", - "@types/react": "^19.2.7", - "@typescript-eslint/eslint-plugin": "^8.18.2", - "@typescript-eslint/parser": "^8.18.2", - "eslint": "^9.17.0", - "tsup": "^8.5.1", - "typescript": "^5.9.3" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -270,8 +251,24 @@ } }, "node_modules/@falkordb/canvas": { - "resolved": "../falkordb-canvas", - "link": true + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.40.tgz", + "integrity": "sha512-sEIl10QHUXT5oGXkdLxY8saxStVQSxKfYd2THxJ4bjR77eq9L+OI8KYY5WRunX3UiZhfPiOy9PLaHHq0IuM8jg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "force-graph": "^1.44.4", + "react": "^19.2.3" + } + }, + "node_modules/@falkordb/canvas/node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/@floating-ui/core": { "version": "1.7.3", @@ -2135,6 +2132,12 @@ "tslib": "^2.8.0" } }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2765,6 +2768,15 @@ "win32" ] }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3149,6 +3161,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3306,6 +3328,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-color-tracker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/canvas2svg": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/canvas2svg/-/canvas2svg-1.0.16.tgz", @@ -3593,6 +3627,12 @@ "node": ">=12" } }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, "node_modules/d3-brush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", @@ -3736,6 +3776,22 @@ "node": ">=12" } }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -3778,6 +3834,12 @@ "node": ">=12" } }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, "node_modules/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", @@ -4920,6 +4982,20 @@ "dev": true, "license": "ISC" }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4936,6 +5012,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/force-graph": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.1.tgz", + "integrity": "sha512-uEEX8iRzgq1IKRISOw6RrB2RLMhcI25xznQYrCTVvxZHZZ+A2jH6qIolYuwavVxAMi64pFp2yZm4KFVdD993cg==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "^1.3", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "float-tooltip": "^1.7", + "index-array-by": "1", + "kapsule": "^1.16", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -5352,6 +5454,15 @@ "node": ">=0.8.19" } }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5946,6 +6057,18 @@ "node": ">=4.0" } }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6739,6 +6862,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/preact": { + "version": "10.28.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", + "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7810,6 +7943,12 @@ "node": ">=0.8" } }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 626b5523..646de52f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@falkordb/canvas": "v0.0.40", + "@falkordb/canvas": "^0.0.40", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -53,4 +53,4 @@ "tailwindcss": "^3.4.17", "typescript": "^5.7.3" } -} \ No newline at end of file +}