Skip to content

Commit 9c90edb

Browse files
committed
[fix] Add support for link on url fragments and added tests
1 parent c96fae7 commit 9c90edb

File tree

6 files changed

+240
-92
lines changed

6 files changed

+240
-92
lines changed

src/js/netjsongraph.render.js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,7 @@ class NetJSONGraphRender {
128128
generateGraphOption(JSONData, self) {
129129
const categories = [];
130130
const configs = self.config;
131-
// The parseUrlFragments function extracts node IDs from the URL.
132-
// We then use these IDs to proactively cache the corresponding nodes
133-
// in `this.indexedNode`. This avoids a second data traversal
134-
// for subsequent lookups and is done here while we are already
135-
// iterating over the nodes to set their ECharts properties.
136-
const fragments = self.utils.parseUrlFragments();
137-
const nodes = JSONData.nodes.map((node) => {
131+
const nodes = JSONData.nodes.map((node, index) => {
138132
const nodeResult = JSON.parse(JSON.stringify(node));
139133
const {nodeStyleConfig, nodeSizeConfig, nodeEmphasisConfig} =
140134
self.utils.getNodeStyle(node, configs, "graph");
@@ -157,12 +151,20 @@ class NetJSONGraphRender {
157151
// Preserve original NetJSON node for sidebar use
158152
/* eslint-disable no-underscore-dangle */
159153
nodeResult._source = JSON.parse(JSON.stringify(node));
160-
// Store the clicked node in this.indexedNode for easy access later without need for traverse
161-
self.utils.setIndexedNodeFromUrlFragments(self, fragments, node);
154+
// Store each node's index in `this.indexedNode` for quick lookup,
155+
// allowing direct access to the node via `this.data.nodes[index]` without traversing the array.
156+
self.nodeIndex = self.nodeIndex || {};
157+
self.nodeIndex[node.id] = index;
162158
return nodeResult;
163159
});
164-
const links = JSONData.links.map((link) => {
160+
const links = JSONData.links.map((link, index) => {
165161
const linkResult = JSON.parse(JSON.stringify(link));
162+
// Similarly, create a lookup for links using "source-target" as the key,
163+
// storing only the index for direct access via `this.data.links[index]`.
164+
self.nodeIndex = self.nodeIndex || {};
165+
const {source, target} = linkResult;
166+
self.nodeIndex[`${source}-${target}`] = index;
167+
166168
const {linkStyleConfig, linkEmphasisConfig} = self.utils.getLinkStyle(
167169
link,
168170
configs,
@@ -249,9 +251,8 @@ class NetJSONGraphRender {
249251
const flatNodes = JSONData.flatNodes || {};
250252
const linesData = [];
251253
let nodesData = [];
252-
const fragments = self.utils.parseUrlFragments();
253254

254-
nodes.forEach((node) => {
255+
nodes.forEach((node, index) => {
255256
if (node.properties) {
256257
// Maintain flatNodes lookup regardless of whether the node is rendered as a marker
257258
if (!JSONData.flatNodes) {
@@ -299,10 +300,12 @@ class NetJSONGraphRender {
299300
});
300301
}
301302
}
302-
// Store the clicked node in this.indexedNode for easy access later without need for traverse
303-
self.utils.setIndexedNodeFromUrlFragments(self, fragments, node);
303+
// Store each node's index in `this.indexedNode` for quick lookup,
304+
// allowing direct access to the node via `this.data.nodes[index]` without traversing the array.
305+
self.nodeIndex = self.nodeIndex || {};
306+
self.nodeIndex[node.id] = index;
304307
});
305-
links.forEach((link) => {
308+
links.forEach((link, index) => {
306309
if (!flatNodes[link.source]) {
307310
console.warn(`Node ${link.source} does not exist!`);
308311
} else if (!flatNodes[link.target]) {
@@ -329,6 +332,11 @@ class NetJSONGraphRender {
329332
link,
330333
});
331334
}
335+
// Similarly, create a lookup for links using "source-target" as the key,
336+
// storing only the index for direct access via `this.data.links[index]`.
337+
self.nodeIndex = self.nodeIndex || {};
338+
const {source, target} = link;
339+
self.nodeIndex[`${source}-${target}`] = index;
332340
});
333341

334342
nodesData = nodesData.concat(clusters);

src/js/netjsongraph.util.js

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,32 +1248,41 @@ class NetJSONGraphUtil {
12481248
}
12491249

12501250
addActionToUrl(self, params) {
1251-
console.log(params);
12521251
if (!self.config.bookmarkableActions.enabled || params.data.cluster) {
12531252
return;
12541253
}
12551254
const fragments = this.parseUrlFragments();
12561255
const {id} = self.config.bookmarkableActions;
1257-
let nodeId;
1258-
self.indexedNode = self.indexedNode || {};
1256+
let nodeId, index, nodeData;
12591257
if (self.config.render === self.utils.graphRender) {
1260-
nodeId = params.data.id;
1261-
self.indexedNode[nodeId] = params.data;
1262-
}
1263-
if (self.config.render === self.utils.mapRender) {
1264-
nodeId = params.data.node.id;
1265-
self.indexedNode[nodeId] = params.data.node;
1258+
if (params.dataType === "node") {
1259+
nodeId = params.data.id;
1260+
index = self.nodeIndex[nodeId];
1261+
nodeData = self.data.nodes[index];
1262+
} else if (params.dataType === "edge") {
1263+
const {source, target} = params.data;
1264+
nodeId = `${source}-${target}`;
1265+
index = self.nodeIndex[source];
1266+
nodeData = self.data.links[index];
1267+
}
1268+
} else if (self.config.render === self.utils.mapRender) {
1269+
if (params.seriesType === "scatter") {
1270+
nodeId = params.data.node.id;
1271+
index = self.nodeIndex[nodeId];
1272+
nodeData = self.data.nodes[index];
1273+
} else if (params.seriesType === "lines") {
1274+
const {source, target} = params.data.link;
1275+
nodeId = `${source}-${target}`;
1276+
index = self.nodeIndex[source];
1277+
nodeData = self.data.links[index];
1278+
}
12661279
}
12671280
if (!fragments[id]) {
12681281
fragments[id] = new URLSearchParams();
12691282
fragments[id].set("id", id);
12701283
}
1271-
// Sync nodeId to URL only if defined, prevents pushing empty states in case of links
1272-
if (nodeId) {
1273-
fragments[id].set("nodeId", nodeId);
1274-
const state = self.indexedNode[nodeId];
1275-
this.updateUrlFragments(fragments, state);
1276-
}
1284+
fragments[id].set("nodeId", nodeId);
1285+
this.updateUrlFragments(fragments, nodeData);
12771286
}
12781287

12791288
removeUrlFragment(id) {
@@ -1285,20 +1294,6 @@ class NetJSONGraphUtil {
12851294
this.updateUrlFragments(fragments, state);
12861295
}
12871296

1288-
setIndexedNodeFromUrlFragments(self, fragments, node) {
1289-
if (!self.config.bookmarkableActions.enabled || !Object.keys(fragments).length) {
1290-
return;
1291-
}
1292-
const {id} = self.config.bookmarkableActions;
1293-
const fragmentParams = fragments[id] && fragments[id].get ? fragments[id] : null;
1294-
const nodeId =
1295-
fragmentParams && fragmentParams.get ? fragmentParams.get("nodeId") : undefined;
1296-
if (nodeId === node.id) {
1297-
self.indexedNode = self.indexedNode || {};
1298-
self.indexedNode[nodeId] = node;
1299-
}
1300-
}
1301-
13021297
applyUrlFragmentState(self) {
13031298
if (!self.config.bookmarkableActions.enabled) {
13041299
return;
@@ -1308,29 +1303,33 @@ class NetJSONGraphUtil {
13081303
const fragmentParams = fragments[id] && fragments[id].get ? fragments[id] : null;
13091304
const nodeId =
13101305
fragmentParams && fragmentParams.get ? fragmentParams.get("nodeId") : undefined;
1311-
if (!self.indexedNode || !self.indexedNode[nodeId]) {
1306+
if (!nodeId || !self.nodeIndex || !self.nodeIndex[nodeId] == null) {
13121307
return;
13131308
}
1314-
const node = self.indexedNode[nodeId];
1309+
const [source, target] = nodeId.split("-");
1310+
const index = self.nodeIndex[nodeId];
1311+
// If source && target both exists then the node is a link
1312+
const node = source && target ? self.data.links[index] : self.data.nodes[index];
13151313
const nodeType =
13161314
self.config.graphConfig.series.type || self.config.mapOptions.nodeConfig.type;
1317-
const {location, cluster} = node;
1315+
const {location, cluster} = node || {};
1316+
// Only adjust the map view if this is a scatter-type node (leaflet map)
13181317
if (["scatter", "effectScatter"].includes(nodeType)) {
1318+
// For links, fall back to the default map center from config
1319+
const center = location
1320+
? [location.lat, location.lng]
1321+
: self.config.mapOptions.center;
13191322
const zoom =
13201323
cluster != null
13211324
? self.config.disableClusteringAtLevel
13221325
: self.leaflet?.getZoom();
1323-
self.leaflet?.setView([location.lat, location.lng], zoom);
1326+
self.leaflet?.setView(center, zoom);
13241327
}
1325-
self.config.onClickElement.call(self, "node", node);
1328+
self.config.onClickElement.call(self, source && target ? "link" : "node", node);
13261329
}
13271330

13281331
setupHashChangeHandler(self) {
13291332
window.addEventListener("popstate", () => {
1330-
const currentNode = history.state;
1331-
if (currentNode != null && !self.indexedNode[currentNode.id]) {
1332-
self.indexedNode[currentNode.id] = currentNode;
1333-
}
13341333
this.applyUrlFragmentState(self);
13351334
});
13361335
}

test/browser.test.utils.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ export const getElementByCss = async (driver, css, waitTime = 1000) => {
4747
}
4848
};
4949

50+
export const getElementByXpath = async (driver, xpath, waitTime = 1000) => {
51+
try {
52+
return await driver.wait(until.elementLocated(By.xpath(xpath)), waitTime);
53+
} catch (err) {
54+
console.error("Error finding element by XPath:", xpath, err);
55+
return null;
56+
}
57+
};
58+
5059
export const getElementsByCss = async (driver, css, waitTime = 1000) => {
5160
try {
5261
return await driver.wait(until.elementsLocated(By.css(css)), waitTime);

test/netjsongraph.browser.test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
getElementByCss,
3+
getElementByXpath,
34
tearDown,
45
captureConsoleErrors,
56
getDriver,
@@ -139,4 +140,90 @@ describe("Chart Rendering Test", () => {
139140
);
140141
expect(hasDots).toBe(true);
141142
});
143+
144+
test("render Basic usage example with url fragments for a node", async () => {
145+
driver.get(`${urls.basicUsage}#id=basicUsage&nodeId=10.149.3.3`);
146+
const canvas = await getElementByCss(driver, "canvas", 2000);
147+
const consoleErrors = await captureConsoleErrors(driver);
148+
const sideBar = await getElementByCss(driver, ".njg-sideBar");
149+
const node = await getElementByXpath(
150+
driver,
151+
"//span[@class='njg-valueLabel' and text()='10.149.3.3']",
152+
);
153+
const nodeId = await node.getText();
154+
155+
printConsoleErrors(consoleErrors);
156+
expect(consoleErrors.length).toBe(0);
157+
expect(canvas).not.toBeNull();
158+
expect(sideBar).not.toBeNull();
159+
expect(nodeId).toBe("10.149.3.3");
160+
});
161+
162+
test("render Basic usage example with url fragments for a link", async () => {
163+
driver.get(`${urls.basicUsage}#id=basicUsage&nodeId=172.16.155.5-172.16.155.4`);
164+
const canvas = await getElementByCss(driver, "canvas", 2000);
165+
const consoleErrors = await captureConsoleErrors(driver);
166+
const sideBar = await getElementByCss(driver, ".njg-sideBar");
167+
const source = await getElementByXpath(
168+
driver,
169+
"//span[@class='njg-valueLabel' and text()='172.16.155.5']",
170+
);
171+
const target = await getElementByXpath(
172+
driver,
173+
"//span[@class='njg-valueLabel' and text()='172.16.155.4']",
174+
);
175+
const sourceId = await source.getText();
176+
const targetId = await target.getText();
177+
178+
printConsoleErrors(consoleErrors);
179+
expect(consoleErrors.length).toBe(0);
180+
expect(canvas).not.toBeNull();
181+
expect(sideBar).not.toBeNull();
182+
expect(sourceId).toBe("172.16.155.5");
183+
expect(targetId).toBe("172.16.155.4");
184+
});
185+
186+
test("render Geographic map example with url fragments for a node", async () => {
187+
driver.get(`${urls.geographicMap}#id=geographicMap&nodeId=172.16.169.1`);
188+
const canvas = await getElementByCss(driver, "canvas", 2000);
189+
const consoleErrors = await captureConsoleErrors(driver);
190+
const sideBar = await getElementByCss(driver, ".njg-sideBar");
191+
const node = await getElementByXpath(
192+
driver,
193+
"//span[@class='njg-valueLabel' and text()='172.16.169.1']",
194+
);
195+
const nodeId = await node.getText();
196+
197+
printConsoleErrors(consoleErrors);
198+
expect(consoleErrors.length).toBe(0);
199+
expect(canvas).not.toBeNull();
200+
expect(sideBar).not.toBeNull();
201+
expect(nodeId).toBe("172.16.169.1");
202+
});
203+
204+
test("render Geographic map example with url fragments for a link", async () => {
205+
driver.get(
206+
`${urls.geographicMap}#id=geographicMap&nodeId=172.16.185.12-172.16.185.13`,
207+
);
208+
const canvas = await getElementByCss(driver, "canvas", 2000);
209+
const consoleErrors = await captureConsoleErrors(driver);
210+
const sideBar = await getElementByCss(driver, ".njg-sideBar");
211+
const source = await getElementByXpath(
212+
driver,
213+
"//span[@class='njg-valueLabel' and text()='172.16.185.12']",
214+
);
215+
const target = await getElementByXpath(
216+
driver,
217+
"//span[@class='njg-valueLabel' and text()='172.16.185.13']",
218+
);
219+
const sourceId = await source.getText();
220+
const targetId = await target.getText();
221+
222+
printConsoleErrors(consoleErrors);
223+
expect(consoleErrors.length).toBe(0);
224+
expect(canvas).not.toBeNull();
225+
expect(sideBar).not.toBeNull();
226+
expect(sourceId).toBe("172.16.185.12");
227+
expect(targetId).toBe("172.16.185.13");
228+
});
142229
});

test/netjsongraph.render.test.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,6 @@ describe("generateMapOption - node processing and dynamic styling", () => {
511511
})),
512512
parseUrlFragments: jest.fn(),
513513
setupHashChangeHandler: jest.fn(),
514-
setIndexedNodeFromUrlFragments: jest.fn(),
515514
},
516515
};
517516
});
@@ -985,7 +984,6 @@ describe("Test disableClusteringAtLevel: 0", () => {
985984
})),
986985
setupHashChangeHandler: jest.fn(),
987986
parseUrlFragments: jest.fn(),
988-
setIndexedNodeFromUrlFragments: jest.fn(),
989987
},
990988
event: {
991989
emit: jest.fn(),
@@ -1083,7 +1081,6 @@ describe("Test leaflet zoomend handler and zoom control state", () => {
10831081
generateMapOption: jest.fn(() => ({series: []})),
10841082
echartsSetOption: jest.fn(),
10851083
parseUrlFragments: jest.fn(),
1086-
setIndexedNodeFromUrlFragments: jest.fn(),
10871084
setupHashChangeHandler: jest.fn(),
10881085
},
10891086
event: {
@@ -1227,7 +1224,6 @@ describe("mapRender – polygon overlay & moveend bbox logic", () => {
12271224
deepMergeObj: jest.fn((a, b) => ({...a, ...b})),
12281225
getBBoxData: jest.fn(() => Promise.resolve({nodes: [{id: "n1"}], links: []})),
12291226
parseUrlFragments: jest.fn(),
1230-
setIndexedNodeFromUrlFragments: jest.fn(),
12311227
setupHashChangeHandler: jest.fn(),
12321228
},
12331229
event: {emit: jest.fn()},
@@ -1288,7 +1284,6 @@ describe("graph label visibility and fallbacks", () => {
12881284
nodeEmphasisConfig: {nodeStyle: {}, nodeSize: 12},
12891285
})),
12901286
parseUrlFragments: jest.fn(),
1291-
setIndexedNodeFromUrlFragments: jest.fn(),
12921287
setupHashChangeHandler: jest.fn(),
12931288
},
12941289
echarts: {
@@ -1326,7 +1321,6 @@ describe("graph label visibility and fallbacks", () => {
13261321
nodeEmphasisConfig: {nodeStyle: {}, nodeSize: 12},
13271322
})),
13281323
parseUrlFragments: jest.fn(),
1329-
setIndexedNodeFromUrlFragments: jest.fn(),
13301324
setupHashChangeHandler: jest.fn(),
13311325
},
13321326
echarts: {
@@ -1358,7 +1352,6 @@ describe("graph label visibility and fallbacks", () => {
13581352
generateGraphOption: jest.fn(() => ({series: []})),
13591353
echartsSetOption: jest.fn(),
13601354
parseUrlFragments: jest.fn(),
1361-
setIndexedNodeFromUrlFragments: jest.fn(),
13621355
setupHashChangeHandler: jest.fn(),
13631356
},
13641357
echarts: {
@@ -1404,7 +1397,6 @@ describe("map series ids and name fallbacks", () => {
14041397
linkEmphasisConfig: {linkStyle: {}},
14051398
})),
14061399
parseUrlFragments: jest.fn(),
1407-
setIndexedNodeFromUrlFragments: jest.fn(),
14081400
setupHashChangeHandler: jest.fn(),
14091401
},
14101402
};
@@ -1469,7 +1461,6 @@ describe("map series ids and name fallbacks", () => {
14691461
generateMapOption: jest.fn(() => ({series: []})),
14701462
echartsSetOption: jest.fn(),
14711463
parseUrlFragments: jest.fn(),
1472-
setIndexedNodeFromUrlFragments: jest.fn(),
14731464
setupHashChangeHandler: jest.fn(),
14741465
},
14751466
event: {emit: jest.fn()},

0 commit comments

Comments
 (0)