Skip to content

Commit 86302f3

Browse files
authored
Merge branch 'staging' into upgrade-canvas-proformence
2 parents 082e0e6 + 3bc61bf commit 86302f3

File tree

12 files changed

+1561
-2355
lines changed

12 files changed

+1561
-2355
lines changed

app/components/code-graph.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import dynamic from 'next/dynamic';
1515
import { Position } from "./graphView";
1616
import { prepareArg } from '../utils';
1717
import { NodeObject, ForceGraphMethods } from "react-force-graph-2d";
18+
import { handleZoomToFit } from "@/lib/utils";
1819

1920
const GraphView = dynamic(() => import('./graphView'));
2021

@@ -68,6 +69,7 @@ export function CodeGraph({
6869
const [cooldownTicks, setCooldownTicks] = useState<number | undefined>(0)
6970
const [cooldownTime, setCooldownTime] = useState<number>(0)
7071
const containerRef = useRef<HTMLDivElement>(null);
72+
const [zoomedNodes, setZoomedNodes] = useState<Node[]>([]);
7173

7274
useEffect(() => {
7375
setData({ ...graph.Elements })
@@ -167,17 +169,17 @@ export function CodeGraph({
167169
}
168170

169171
const deleteNeighbors = (nodes: Node[]) => {
170-
172+
171173
if (nodes.length === 0) return;
172-
174+
173175
const expandedNodes: Node[] = []
174-
176+
175177
graph.Elements = {
176178
nodes: graph.Elements.nodes.filter(node => {
177179
if (!node.collapsed) return true
178-
180+
179181
const isTarget = graph.Elements.links.some(link => link.target.id === node.id && nodes.some(n => n.id === link.source.id));
180-
182+
181183
if (!isTarget) return true
182184

183185
const deleted = graph.NodesMap.delete(Number(node.id))
@@ -190,7 +192,7 @@ export function CodeGraph({
190192
}),
191193
links: graph.Elements.links
192194
}
193-
195+
194196
deleteNeighbors(expandedNodes)
195197

196198
graph.removeLinks()
@@ -239,12 +241,12 @@ export function CodeGraph({
239241
}
240242
graph.visibleLinks(true, [chartNode!.id])
241243
setData({ ...graph.Elements })
244+
setZoomedNodes([node])
245+
return
242246
}
243-
247+
248+
handleZoomToFit(chartRef, 4, (n: NodeObject<Node>) => n.id === node.id)
244249
setSearchNode(chartNode)
245-
setTimeout(() => {
246-
chart.zoomToFit(1000, 150, (n: NodeObject<Node>) => n.id === chartNode!.id);
247-
}, 0)
248250
}
249251
}
250252

@@ -307,7 +309,9 @@ export function CodeGraph({
307309
graph={graph}
308310
onValueChange={(node) => setSearchNode(node)}
309311
icon={<Search />}
310-
handleSubmit={handleSearchSubmit}
312+
handleSubmit={(node) => {
313+
handleSearchSubmit(node)
314+
}}
311315
node={searchNode}
312316
/>
313317
<Labels categories={graph.Categories} onClick={onCategoryClick} />
@@ -380,6 +384,7 @@ export function CodeGraph({
380384
/>
381385
<button
382386
className="pointer-events-auto bg-white p-2 rounded-md"
387+
title='downloadImage'
383388
onClick={handleDownloadImage}
384389
>
385390
<Download />
@@ -420,6 +425,8 @@ export function CodeGraph({
420425
setCooldownTicks={setCooldownTicks}
421426
cooldownTime={cooldownTime}
422427
setCooldownTime={setCooldownTime}
428+
setZoomedNodes={setZoomedNodes}
429+
zoomedNodes={zoomedNodes}
423430
/>
424431
</div>
425432
: <div className="flex flex-col items-center justify-center h-full text-gray-400">

app/components/graphView.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import ForceGraph2D, { ForceGraphMethods } from 'react-force-graph-2d';
1+
import ForceGraph2D, { ForceGraphMethods, NodeObject } from 'react-force-graph-2d';
22
import { Graph, GraphData, Link, Node } from './model';
33
import { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react';
44
import { Path } from '../page';
5+
import { handleZoomToFit } from '@/lib/utils';
56

67
export interface Position {
78
x: number,
@@ -29,6 +30,8 @@ interface Props {
2930
setCooldownTicks: Dispatch<SetStateAction<number | undefined>>
3031
cooldownTime: number | undefined
3132
setCooldownTime: Dispatch<SetStateAction<number>>
33+
setZoomedNodes: Dispatch<SetStateAction<Node[]>>
34+
zoomedNodes: Node[]
3235
}
3336

3437
const PATH_COLOR = "#ffde21"
@@ -55,7 +58,9 @@ export default function GraphView({
5558
cooldownTicks,
5659
cooldownTime,
5760
setCooldownTicks,
58-
setCooldownTime
61+
setCooldownTime,
62+
zoomedNodes,
63+
setZoomedNodes
5964
}: Props) {
6065

6166
const parentRef = useRef<HTMLDivElement>(null)
@@ -85,14 +90,9 @@ export default function GraphView({
8590
}, [parentRef])
8691

8792
useEffect(() => {
88-
setCooldownTime(4000)
93+
setCooldownTime(2000)
8994
setCooldownTicks(undefined)
90-
}, [graph.Id])
91-
92-
useEffect(() => {
93-
setCooldownTime(1000)
94-
setCooldownTicks(undefined)
95-
}, [graph.getElements().length])
95+
}, [graph.Id, graph.getElements().length])
9696

9797
const unsetSelectedObjects = (evt?: MouseEvent) => {
9898
if (evt?.ctrlKey || (!selectedObj && selectedObjects.length === 0)) return
@@ -143,13 +143,35 @@ export default function GraphView({
143143
}
144144
}
145145

146+
const avoidOverlap = (nodes: Position[]) => {
147+
const spacing = NODE_SIZE * 2.5;
148+
nodes.forEach((nodeA, i) => {
149+
nodes.forEach((nodeB, j) => {
150+
if (i !== j) {
151+
const dx = nodeA.x - nodeB.x;
152+
const dy = nodeA.y - nodeB.y;
153+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
154+
155+
if (distance < spacing) {
156+
const pushStrength = (spacing - distance) / distance * 0.5;
157+
nodeA.x += dx * pushStrength;
158+
nodeA.y += dy * pushStrength;
159+
nodeB.x -= dx * pushStrength;
160+
nodeB.y -= dy * pushStrength;
161+
}
162+
}
163+
});
164+
});
165+
};
166+
146167
return (
147168
<div ref={parentRef} className="relative w-fill h-full">
148169
<ForceGraph2D
149170
ref={chartRef}
150171
height={parentHeight}
151172
width={parentWidth}
152173
graphData={data}
174+
onEngineTick={() => avoidOverlap(data.nodes as Position[])}
153175
nodeVisibility="visible"
154176
linkVisibility="visible"
155177
linkCurvature="curve"
@@ -316,6 +338,8 @@ export default function GraphView({
316338
onEngineStop={() => {
317339
setCooldownTicks(0)
318340
setCooldownTime(0)
341+
handleZoomToFit(chartRef, zoomedNodes.length === 1 ? 4 : 1, (n: NodeObject<Node>) => zoomedNodes.some(node => node.id === n.id))
342+
setZoomedNodes([])
319343
}}
320344
cooldownTicks={cooldownTicks}
321345
cooldownTime={cooldownTime}

e2e/config/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const GRAPH_ID = "GraphRAG-SDK";
22
export const PROJECT_NAME = "GraphRAG-SDK";
3+
export const PROJECT_CLICK = "flask";
34
export const CHAT_OPTTIONS_COUNT = 1;
45
export const Node_Question = "how many nodes do we have?";
56
export const Edge_Question = "how many edges do we have?";

e2e/config/testData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const searchData: { searchInput: string; completedSearchInput?: string; }[] = [
22
{ searchInput: "test"},
33
{ searchInput: "set"},
4-
{ searchInput: "low", completedSearchInput: "lower" },
4+
{ searchInput: "low", completedSearchInput: "lower_items" },
55
{ searchInput: "as", completedSearchInput: "ask"},
66
];
77

e2e/logic/POM/codeGraph.ts

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Locator, Page } from "playwright";
1+
import { Download, Locator, Page } from "playwright";
22
import BasePage from "../../infra/ui/basePage";
33
import { waitForElementToBeVisible, waitForStableText, waitToBeEnabled } from "../utils";
44

@@ -228,9 +228,13 @@ export default class CodeGraph extends BasePage {
228228
private get copyToClipboardNodePanelDetails(): Locator {
229229
return this.page.locator(`//div[@data-name='node-details-panel']//button[@title='Copy src to clipboard']`);
230230
}
231-
232-
private get nodeToolTip(): Locator {
233-
return this.page.locator("//div[contains(@class, 'graph-tooltip')]");
231+
232+
private get nodeToolTip(): (node: string) => Locator {
233+
return (node: string) => this.page.locator(`//div[contains(@class, 'force-graph-container')]/div[contains(text(), '${node}')]`);
234+
}
235+
236+
private get downloadImageBtn(): Locator {
237+
return this.page.locator("//button[@title='downloadImage']");
234238
}
235239

236240
/* NavBar functionality */
@@ -423,6 +427,7 @@ export default class CodeGraph extends BasePage {
423427
const button = this.searchBarOptionBtn(buttonNum);
424428
await button.waitFor({ state : "visible"})
425429
await button.click();
430+
await this.page.waitForTimeout(4000);
426431
}
427432

428433
async getSearchBarInputValue(): Promise<string> {
@@ -463,20 +468,20 @@ export default class CodeGraph extends BasePage {
463468
}
464469

465470
async nodeClick(x: number, y: number): Promise<void> {
466-
for (let attempt = 1; attempt <= 2; attempt++) {
471+
for (let attempt = 1; attempt <= 3; attempt++) {
467472
await this.canvasElement.hover({ position: { x, y } });
468473
await this.page.waitForTimeout(500);
469-
470-
if (await waitForElementToBeVisible(this.nodeToolTip)) {
471-
await this.canvasElement.click({ position: { x, y }, button: 'right' });
474+
await this.canvasElement.click({ position: { x, y }, button: 'right' });
475+
if (await this.elementMenu.isVisible()) {
472476
return;
473477
}
474478
await this.page.waitForTimeout(1000);
475479
}
476-
477-
throw new Error("Tooltip not visible after multiple attempts!");
480+
481+
throw new Error(`Failed to click, elementMenu not visible after multiple attempts.`);
478482
}
479483

484+
480485
async selectCodeGraphCheckbox(checkbox: string): Promise<void> {
481486
await this.codeGraphCheckbox(checkbox).click();
482487
}
@@ -637,4 +642,60 @@ export default class CodeGraph extends BasePage {
637642
return { scaleX, scaleY };
638643
}
639644

645+
async downloadImage(): Promise<Download> {
646+
await this.page.waitForLoadState('networkidle');
647+
const [download] = await Promise.all([
648+
this.page.waitForEvent('download'),
649+
this.downloadImageBtn.click(),
650+
]);
651+
652+
return download;
653+
}
654+
655+
async rightClickAtCanvasCenter(): Promise<void> {
656+
const boundingBox = await this.canvasElement.boundingBox();
657+
if (!boundingBox) throw new Error('Canvas bounding box not found');
658+
const centerX = boundingBox.x + boundingBox.width / 2;
659+
const centerY = boundingBox.y + boundingBox.height / 2;
660+
await this.page.mouse.click(centerX, centerY, { button: 'right' });
661+
}
662+
663+
async hoverAtCanvasCenter(): Promise<void> {
664+
const boundingBox = await this.canvasElement.boundingBox();
665+
if (!boundingBox) throw new Error('Canvas bounding box not found');
666+
const centerX = boundingBox.x + boundingBox.width / 2;
667+
const centerY = boundingBox.y + boundingBox.height / 2;
668+
await this.page.mouse.move(centerX, centerY);
669+
}
670+
671+
async isNodeToolTipVisible(node: string): Promise<boolean> {
672+
return await this.nodeToolTip(node).isVisible();
673+
}
674+
675+
async waitForCanvasAnimationToEnd(timeout = 5000): Promise<void> {
676+
const canvasHandle = await this.canvasElement.elementHandle();
677+
678+
if (!canvasHandle) {
679+
throw new Error("Canvas element not found!");
680+
}
681+
682+
await this.page.waitForFunction(
683+
(canvas) => {
684+
const ctx = (canvas as HTMLCanvasElement).getContext('2d');
685+
if (!ctx) return false;
686+
687+
const imageData1 = ctx.getImageData(0, 0, (canvas as HTMLCanvasElement).width, (canvas as HTMLCanvasElement).height).data;
688+
689+
return new Promise<boolean>((resolve) => {
690+
setTimeout(() => {
691+
const imageData2 = ctx.getImageData(0, 0, (canvas as HTMLCanvasElement).width, (canvas as HTMLCanvasElement).height).data;
692+
resolve(JSON.stringify(imageData1) === JSON.stringify(imageData2));
693+
}, 500);
694+
});
695+
},
696+
canvasHandle as any,
697+
{ timeout }
698+
);
699+
}
700+
640701
}

e2e/logic/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const waitForStableText = async (locator: Locator, timeout: number = 5000
3434
return stableText;
3535
};
3636

37-
export const waitForElementToBeVisible = async (locator:Locator,time=400,retry=5):Promise<boolean> => {
37+
export const waitForElementToBeVisible = async (locator:Locator,time=500,retry=10):Promise<boolean> => {
3838

3939
while(retry > 0){
4040
if(await locator.isVisible()){
@@ -48,4 +48,9 @@ export const waitForElementToBeVisible = async (locator:Locator,time=400,retry=5
4848

4949
export function findNodeByName(nodes: { name: string }[], nodeName: string): any {
5050
return nodes.find((node) => node.name === nodeName);
51-
}
51+
}
52+
53+
export function findFirstNodeWithSrc(nodes: { src?: string }[]): any {
54+
return nodes.find((node) => node.src !== undefined);
55+
}
56+

0 commit comments

Comments
 (0)