spanner-graph-notebook 1.1.2__tar.gz → 1.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spanner_graph_notebook-1.1.2/spanner_graph_notebook.egg-info → spanner_graph_notebook-1.1.4}/PKG-INFO +3 -2
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/app.js +5 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/graph-server.js +27 -20
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/node.js +14 -1
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/spanner-config.js +3 -2
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/spanner-store.js +7 -2
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-forcegraph.js +3 -2
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-sidebar.js +3 -1
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/graph-server.test.ts +17 -13
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/node.test.ts +28 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/spanner-config.test.ts +69 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/spanner-store.test.ts +82 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/setup.py +1 -1
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4/spanner_graph_notebook.egg-info}/PKG-INFO +3 -2
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/conversion.py +13 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_entities.py +15 -2
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_server.py +117 -108
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/magics.py +28 -17
- spanner_graph_notebook-1.1.4/tests/conversion_test.py +231 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/graph_entities_test.py +41 -22
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/graph_server_test.py +53 -36
- spanner_graph_notebook-1.1.4/tests/node_expansion_test.py +165 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/server_test.py +35 -3
- spanner_graph_notebook-1.1.2/tests/conversion_test.py +0 -80
- spanner_graph_notebook-1.1.2/tests/node_expansion_test.py +0 -106
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/CONTRIBUTING.md +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/LICENSE +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/MANIFEST.in +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/README.md +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/authentication.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/colab_usage.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/full_viz.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/hero.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/hero_with_properties.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/jupyter-spin-up.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/jupyter.gif +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/load_ext.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/mock_data_result.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/new_notebook.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/notebook_package_load.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/query_graph.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/sample_jupyter.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/babel.config.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/jest-puppeteer.config.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/jest.config.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/package.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/edge.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/graph-object.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/schema.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-menu.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-table.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/dev.html +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/graph-bg.svg +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/index.html +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/test.html +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/jest.setup.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/mock-data.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/serve-content.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/edge.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/graph-object.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/schema.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/visualization/spanner-menu.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/visualization/spanner-sidebar.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/visualization/spanner-table.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/app.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/jest-visual.setup.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-forcegraph.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-menu.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-sidebar.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-table.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tsconfig.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/sample.ipynb +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/setup.cfg +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/SOURCES.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/dependency_links.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/requires.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/top_level.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/__init__.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/database.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/dev_util/__init__.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/dev_util/serve_dev.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_mock_data.csv +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_mock_schema.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_visualization.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/schema_manager.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/__init__.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/database_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/magics_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/sample_notebook_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/schema_manager_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/test_notebook.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: spanner-graph-notebook
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Visually query Spanner Graph data in notebooks.
|
|
5
5
|
Description-Content-Type: text/markdown
|
|
6
6
|
License-File: LICENSE
|
|
@@ -15,6 +15,7 @@ Requires-Dist: portpicker
|
|
|
15
15
|
Requires-Dist: pydata-google-auth
|
|
16
16
|
Dynamic: description
|
|
17
17
|
Dynamic: description-content-type
|
|
18
|
+
Dynamic: license-file
|
|
18
19
|
Dynamic: requires-dist
|
|
19
20
|
Dynamic: summary
|
|
20
21
|
|
|
@@ -164,6 +164,11 @@ class SpannerApp {
|
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
if (data.error) {
|
|
168
|
+
this.graph.showErrorStateForNode(node, error);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
167
172
|
const newData = this.store.appendGraphData(data.response.nodes, data.response.edges)
|
|
168
173
|
if (newData) {
|
|
169
174
|
// Show success message before appending data
|
|
@@ -13,8 +13,17 @@
|
|
|
13
13
|
* limitations under the License.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
function isColab() {
|
|
17
|
+
return typeof google !== 'undefined'
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
class GraphServer {
|
|
17
21
|
isFetching = false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Port number to connect to graph server. In colab environments, this is always undefined
|
|
25
|
+
* and not used (we use colab callbacks instead).
|
|
26
|
+
*/
|
|
18
27
|
port = 8195;
|
|
19
28
|
|
|
20
29
|
endpoints = {
|
|
@@ -58,17 +67,21 @@ class GraphServer {
|
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
constructor(port, params) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
if (isColab()) {
|
|
71
|
+
this.port = undefined;
|
|
72
|
+
} else {
|
|
73
|
+
let numericalPort = port;
|
|
74
|
+
if (typeof numericalPort !== 'number') {
|
|
75
|
+
numericalPort = Number.parseInt(numericalPort);
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
if (isNaN(numericalPort)) {
|
|
78
|
+
console.error('Graph Server was not given a numerical port', {port});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
68
81
|
}
|
|
69
|
-
}
|
|
70
82
|
|
|
71
|
-
|
|
83
|
+
this.port = numericalPort;
|
|
84
|
+
}
|
|
72
85
|
this.params = params
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -109,10 +122,7 @@ class GraphServer {
|
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
const {project, instance, database, graph} = JSON.parse(this.params);
|
|
113
|
-
|
|
114
125
|
const request = {
|
|
115
|
-
project, instance, database, graph,
|
|
116
126
|
uid: node.uid,
|
|
117
127
|
node_labels: node.labels,
|
|
118
128
|
node_properties: validProperties,
|
|
@@ -125,21 +135,18 @@ class GraphServer {
|
|
|
125
135
|
|
|
126
136
|
this.isFetching = true;
|
|
127
137
|
|
|
128
|
-
if (
|
|
138
|
+
if (isColab()) {
|
|
129
139
|
return google.colab.kernel.invokeFunction('graph_visualization.NodeExpansion', [request, this.params])
|
|
130
140
|
.then(result => result.data['application/json'])
|
|
131
141
|
.finally(() => this.isFetching = false);
|
|
132
142
|
}
|
|
133
143
|
|
|
134
|
-
// For non-Colab environment, combine params and request
|
|
135
|
-
const fullRequest = {
|
|
136
|
-
...JSON.parse(this.params),
|
|
137
|
-
...request
|
|
138
|
-
};
|
|
139
|
-
|
|
140
144
|
return fetch(this.buildRoute(this.endpoints.postNodeExpansion), {
|
|
141
145
|
method: 'POST',
|
|
142
|
-
body: JSON.stringify(
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
params: this.params,
|
|
148
|
+
request
|
|
149
|
+
})
|
|
143
150
|
})
|
|
144
151
|
.then(response => {
|
|
145
152
|
if (!response.ok) {
|
|
@@ -161,7 +168,7 @@ class GraphServer {
|
|
|
161
168
|
|
|
162
169
|
this.isFetching = true;
|
|
163
170
|
|
|
164
|
-
if (
|
|
171
|
+
if (isColab()) {
|
|
165
172
|
return google.colab.kernel.invokeFunction('graph_visualization.Query', [], request)
|
|
166
173
|
.then(result => result.data['application/json'])
|
|
167
174
|
.finally(() => this.isFetching = false);
|
|
@@ -39,6 +39,13 @@ class Node extends GraphObject {
|
|
|
39
39
|
*/
|
|
40
40
|
identifiers = [];
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Denotes if the node was created in the absence of a node from the query response.
|
|
44
|
+
* This may happen when a query only returns edges. The edge has UIDs for a source/destination node,
|
|
45
|
+
* but no nodes were returned from the response.
|
|
46
|
+
* @type {boolean}
|
|
47
|
+
*/
|
|
48
|
+
intermediate = false;
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* The following properties will be set by ForceGraph.
|
|
@@ -58,6 +65,7 @@ class Node extends GraphObject {
|
|
|
58
65
|
* @property {Object} key_property_names
|
|
59
66
|
* @property {string} color
|
|
60
67
|
* @property {string} identifier
|
|
68
|
+
* @property {bool} intermediate
|
|
61
69
|
*/
|
|
62
70
|
|
|
63
71
|
/**
|
|
@@ -65,11 +73,12 @@ class Node extends GraphObject {
|
|
|
65
73
|
* @param {NodeData} params
|
|
66
74
|
*/
|
|
67
75
|
constructor(params) {
|
|
68
|
-
const { labels, title, properties, value, key_property_names, identifier } = params;
|
|
76
|
+
const { labels, title, properties, value, key_property_names, identifier, intermediate } = params;
|
|
69
77
|
super({ labels, title, properties, key_property_names, identifier });
|
|
70
78
|
|
|
71
79
|
this.value = value;
|
|
72
80
|
this.instantiated = true;
|
|
81
|
+
this.intermediate = intermediate || false;
|
|
73
82
|
|
|
74
83
|
// Parse the human-readable unique identifiers that
|
|
75
84
|
// distinguishes a node from its peers
|
|
@@ -81,6 +90,10 @@ class Node extends GraphObject {
|
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
}
|
|
93
|
+
|
|
94
|
+
isIntermediateNode() {
|
|
95
|
+
return this.intermediate;
|
|
96
|
+
}
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
if (typeof module !== 'undefined' && module.exports) {
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/spanner-config.js
RENAMED
|
@@ -207,7 +207,7 @@ class GraphConfig {
|
|
|
207
207
|
|
|
208
208
|
for (const uid of Object.keys(this.nodes)) {
|
|
209
209
|
const node = this.nodes[uid];
|
|
210
|
-
if (!node || !(node instanceof Node)) {
|
|
210
|
+
if (!node || !(node instanceof Node) || node.isIntermediateNode()) {
|
|
211
211
|
continue;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -411,7 +411,8 @@ class GraphConfig {
|
|
|
411
411
|
const newEdges = this.parseEdges(edgesData);
|
|
412
412
|
|
|
413
413
|
for (const uid of Object.keys(newNodes)) {
|
|
414
|
-
|
|
414
|
+
const existingNode = this.nodes[uid];
|
|
415
|
+
if (!(existingNode instanceof Node) || existingNode.isIntermediateNode()) {
|
|
415
416
|
this.nodes[uid] = newNodes[uid];
|
|
416
417
|
}
|
|
417
418
|
}
|
|
@@ -450,11 +450,15 @@ class GraphStore {
|
|
|
450
450
|
* @returns {string} The color for the node based on its label.
|
|
451
451
|
*/
|
|
452
452
|
getColorForNodeByLabel(node) {
|
|
453
|
-
const defaultColor = 'rgb(100, 100, 100)'
|
|
453
|
+
const defaultColor = 'rgb(100, 100, 100)'
|
|
454
454
|
if (!node || !(node instanceof Node)) {
|
|
455
455
|
return defaultColor;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
if (node.isIntermediateNode()) {
|
|
459
|
+
return 'rgb(128, 134, 139)';
|
|
460
|
+
}
|
|
461
|
+
|
|
458
462
|
const nodeColor = this.config.nodeColors[node.getLabels()];
|
|
459
463
|
if (nodeColor) {
|
|
460
464
|
return nodeColor;
|
|
@@ -489,7 +493,8 @@ class GraphStore {
|
|
|
489
493
|
const newNodes = [];
|
|
490
494
|
if (Array.isArray(nodesData)) {
|
|
491
495
|
for (const nodeData of nodesData) {
|
|
492
|
-
|
|
496
|
+
const existingNode = this.config.nodes[nodeData.identifier];
|
|
497
|
+
if (existingNode && !existingNode.isIntermediateNode()) {
|
|
493
498
|
continue;
|
|
494
499
|
}
|
|
495
500
|
|
|
@@ -1086,6 +1086,7 @@ class GraphVisualization {
|
|
|
1086
1086
|
}
|
|
1087
1087
|
|
|
1088
1088
|
const showLabel =
|
|
1089
|
+
!node.isIntermediateNode() && (
|
|
1089
1090
|
// The user has chosen to view labels
|
|
1090
1091
|
this.store.config.showLabels ||
|
|
1091
1092
|
// Always show labels in Schema view
|
|
@@ -1096,7 +1097,7 @@ class GraphVisualization {
|
|
|
1096
1097
|
this.selectedNodeNeighbors.includes(node) ||
|
|
1097
1098
|
this.focusedNodeNeighbors.includes(node) ||
|
|
1098
1099
|
this.focusedEdgeNeighbors.includes(node) ||
|
|
1099
|
-
this.selectedEdgeNeighbors.includes(node);
|
|
1100
|
+
this.selectedEdgeNeighbors.includes(node));
|
|
1100
1101
|
|
|
1101
1102
|
// Init label
|
|
1102
1103
|
ctx.save();
|
|
@@ -1438,7 +1439,7 @@ class GraphVisualization {
|
|
|
1438
1439
|
*/
|
|
1439
1440
|
_showMouseContextMenu(node, event) {
|
|
1440
1441
|
// Don't show context menu in schema mode
|
|
1441
|
-
if (this.store.config.viewMode === GraphConfig.ViewModes.SCHEMA) {
|
|
1442
|
+
if (this.store.config.viewMode === GraphConfig.ViewModes.SCHEMA || node.isIntermediateNode()) {
|
|
1442
1443
|
return;
|
|
1443
1444
|
}
|
|
1444
1445
|
|
|
@@ -277,7 +277,9 @@ class SidebarConstructor {
|
|
|
277
277
|
if (this.store.config.selectedGraphObject) {
|
|
278
278
|
if (this.store.config.selectedGraphObject instanceof Node) {
|
|
279
279
|
this.properties();
|
|
280
|
-
this.
|
|
280
|
+
if (!this.store.config.selectedGraphObject.isIntermediateNode()) {
|
|
281
|
+
this.expandNode();
|
|
282
|
+
}
|
|
281
283
|
this.neighbors();
|
|
282
284
|
} else {
|
|
283
285
|
this.neighbors();
|
|
@@ -148,19 +148,23 @@ describe('GraphServer', () => {
|
|
|
148
148
|
{
|
|
149
149
|
method: 'POST',
|
|
150
150
|
body: JSON.stringify({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
151
|
+
params: JSON.stringify({
|
|
152
|
+
project: 'test-project',
|
|
153
|
+
instance: 'test-instance',
|
|
154
|
+
database: 'test-database',
|
|
155
|
+
graph: 'test-graph',
|
|
156
|
+
mock: false
|
|
157
|
+
}),
|
|
158
|
+
request: {
|
|
159
|
+
uid: 'node1',
|
|
160
|
+
node_labels: ['TestLabel1'],
|
|
161
|
+
node_properties: [{
|
|
162
|
+
key: 'name',
|
|
163
|
+
value: 'my-node-name',
|
|
164
|
+
type: 'STRING'
|
|
165
|
+
}],
|
|
166
|
+
direction: 'OUTGOING'
|
|
167
|
+
}
|
|
164
168
|
})
|
|
165
169
|
}
|
|
166
170
|
);
|
|
@@ -57,4 +57,32 @@ describe('Node', () => {
|
|
|
57
57
|
expect(graphNode.identifiers).toEqual(['foo', 'boolean', 'zero']);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('Intermediate Nodes', () => {
|
|
62
|
+
it('should create a non-intermediate node by default', () => {
|
|
63
|
+
const graphNode = new GraphNode(validNodeData);
|
|
64
|
+
expect(graphNode.intermediate).toBe(false);
|
|
65
|
+
expect(graphNode.isIntermediateNode()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should create an intermediate node when specified', () => {
|
|
69
|
+
const intermediateNodeData = {
|
|
70
|
+
...validNodeData,
|
|
71
|
+
intermediate: true
|
|
72
|
+
};
|
|
73
|
+
const graphNode = new GraphNode(intermediateNodeData);
|
|
74
|
+
expect(graphNode.intermediate).toBe(true);
|
|
75
|
+
expect(graphNode.isIntermediateNode()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle undefined intermediate flag', () => {
|
|
79
|
+
const nodeData = {
|
|
80
|
+
...validNodeData,
|
|
81
|
+
intermediate: undefined
|
|
82
|
+
};
|
|
83
|
+
const graphNode = new GraphNode(nodeData);
|
|
84
|
+
expect(graphNode.intermediate).toBe(false);
|
|
85
|
+
expect(graphNode.isIntermediateNode()).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
60
88
|
});
|
|
@@ -306,4 +306,73 @@ describe('GraphConfig', () => {
|
|
|
306
306
|
expect(mockConfig.nodeCount).toBe(2);
|
|
307
307
|
});
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
describe('Intermediate Node Handling', () => {
|
|
311
|
+
test('should exclude intermediate nodes from color assignment', () => {
|
|
312
|
+
const mockConfig = new GraphConfig({
|
|
313
|
+
nodesData: [
|
|
314
|
+
{ identifier: "1", labels: ['Person'], properties: {} },
|
|
315
|
+
{ identifier: "2", labels: ['Movie'], properties: {}, intermediate: true }
|
|
316
|
+
],
|
|
317
|
+
edgesData: [],
|
|
318
|
+
colorPalette: ['#FF0000'],
|
|
319
|
+
colorScheme: GraphConfig.ColorScheme.LABEL,
|
|
320
|
+
rowsData: [],
|
|
321
|
+
schemaData: {},
|
|
322
|
+
queryResult: {}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Should only assign color to non-intermediate node
|
|
326
|
+
expect(Object.keys(mockConfig.nodeColors).length).toBe(1);
|
|
327
|
+
expect(mockConfig.nodeColors['Person']).toBeDefined();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('should allow replacing intermediate nodes with real nodes', () => {
|
|
331
|
+
const mockConfig = new GraphConfig({
|
|
332
|
+
nodesData: [
|
|
333
|
+
{ identifier: "1", labels: ['Person'], properties: {}, intermediate: true }
|
|
334
|
+
],
|
|
335
|
+
edgesData: [],
|
|
336
|
+
colorPalette: ['#FF0000'],
|
|
337
|
+
colorScheme: GraphConfig.ColorScheme.LABEL,
|
|
338
|
+
rowsData: [],
|
|
339
|
+
schemaData: {},
|
|
340
|
+
queryResult: {}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Append a real node with the same identifier
|
|
344
|
+
mockConfig.appendGraphData([
|
|
345
|
+
{ identifier: "1", labels: ['Person'], properties: { name: 'John' } }
|
|
346
|
+
], []);
|
|
347
|
+
|
|
348
|
+
const node = mockConfig.nodes["1"];
|
|
349
|
+
expect(node).toBeDefined();
|
|
350
|
+
expect(node.isIntermediateNode()).toBe(false);
|
|
351
|
+
expect(node.properties.name).toBe('John');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('should not replace real nodes with intermediate nodes', () => {
|
|
355
|
+
const mockConfig = new GraphConfig({
|
|
356
|
+
nodesData: [
|
|
357
|
+
{ identifier: "1", labels: ['Person'], properties: { name: 'John' } }
|
|
358
|
+
],
|
|
359
|
+
edgesData: [],
|
|
360
|
+
colorPalette: ['#FF0000'],
|
|
361
|
+
colorScheme: GraphConfig.ColorScheme.LABEL,
|
|
362
|
+
rowsData: [],
|
|
363
|
+
schemaData: {},
|
|
364
|
+
queryResult: {}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Try to append an intermediate node with the same identifier
|
|
368
|
+
mockConfig.appendGraphData([
|
|
369
|
+
{ identifier: "1", labels: ['Person'], properties: {}, intermediate: true }
|
|
370
|
+
], []);
|
|
371
|
+
|
|
372
|
+
const node = mockConfig.nodes["1"];
|
|
373
|
+
expect(node).toBeDefined();
|
|
374
|
+
expect(node.isIntermediateNode()).toBe(false);
|
|
375
|
+
expect(node.properties.name).toBe('John');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
309
378
|
});
|
|
@@ -925,4 +925,86 @@ describe('GraphStore', () => {
|
|
|
925
925
|
expect(store.getEdgeTypesOfNodeSorted(undefined)).toEqual([]);
|
|
926
926
|
expect(store.getEdgeTypesOfNodeSorted({} as any)).toEqual([]);
|
|
927
927
|
});
|
|
928
|
+
|
|
929
|
+
describe('Intermediate Node Handling', () => {
|
|
930
|
+
test('should return correct color for intermediate nodes', () => {
|
|
931
|
+
const intermediateNode = new GraphNode({
|
|
932
|
+
identifier: "1",
|
|
933
|
+
labels: ['Person'],
|
|
934
|
+
properties: {},
|
|
935
|
+
intermediate: true
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
const color = store.getColorForNodeByLabel(intermediateNode);
|
|
939
|
+
expect(color).toBe('rgb(128, 134, 139)');
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('should return correct color for non-intermediate nodes', () => {
|
|
943
|
+
const regularNode = new GraphNode({
|
|
944
|
+
identifier: "1",
|
|
945
|
+
labels: ['Person'],
|
|
946
|
+
properties: {}
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// Set up the node color explicitly for the test
|
|
950
|
+
store.config.nodeColors['Person'] = '#FF0000';
|
|
951
|
+
|
|
952
|
+
const color = store.getColorForNodeByLabel(regularNode);
|
|
953
|
+
expect(color).toBe('#FF0000'); // Now we expect the color we explicitly set
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test('should handle node expansion requests for intermediate nodes', () => {
|
|
957
|
+
const intermediateNode = new GraphNode({
|
|
958
|
+
identifier: "1",
|
|
959
|
+
labels: ['Person'],
|
|
960
|
+
properties: {},
|
|
961
|
+
intermediate: true
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Mock the event listener
|
|
965
|
+
const mockCallback = jest.fn();
|
|
966
|
+
store.addEventListener(GraphStore.EventTypes.NODE_EXPANSION_REQUEST, mockCallback);
|
|
967
|
+
|
|
968
|
+
// Request expansion
|
|
969
|
+
store.requestNodeExpansion(intermediateNode, 'OUTGOING', 'CONNECTS_TO');
|
|
970
|
+
|
|
971
|
+
// Verify the callback was called with correct parameters
|
|
972
|
+
expect(mockCallback).toHaveBeenCalledWith(
|
|
973
|
+
intermediateNode,
|
|
974
|
+
'OUTGOING',
|
|
975
|
+
'CONNECTS_TO',
|
|
976
|
+
expect.any(Array),
|
|
977
|
+
expect.any(Object)
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test('should handle node expansion requests for non-intermediate nodes', () => {
|
|
982
|
+
const regularNode = new GraphNode({
|
|
983
|
+
identifier: "1",
|
|
984
|
+
labels: ['Person'],
|
|
985
|
+
properties: { name: 'John' }
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// Mock the event listener
|
|
989
|
+
const mockCallback = jest.fn();
|
|
990
|
+
store.addEventListener(GraphStore.EventTypes.NODE_EXPANSION_REQUEST, mockCallback);
|
|
991
|
+
|
|
992
|
+
// Request expansion
|
|
993
|
+
store.requestNodeExpansion(regularNode, 'OUTGOING', 'CONNECTS_TO');
|
|
994
|
+
|
|
995
|
+
// Verify the callback was called with correct parameters
|
|
996
|
+
expect(mockCallback).toHaveBeenCalledWith(
|
|
997
|
+
regularNode,
|
|
998
|
+
'OUTGOING',
|
|
999
|
+
'CONNECTS_TO',
|
|
1000
|
+
expect.arrayContaining([
|
|
1001
|
+
expect.objectContaining({
|
|
1002
|
+
key: 'name',
|
|
1003
|
+
value: 'John'
|
|
1004
|
+
})
|
|
1005
|
+
]),
|
|
1006
|
+
expect.any(Object)
|
|
1007
|
+
);
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
928
1010
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: spanner-graph-notebook
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Visually query Spanner Graph data in notebooks.
|
|
5
5
|
Description-Content-Type: text/markdown
|
|
6
6
|
License-File: LICENSE
|
|
@@ -15,6 +15,7 @@ Requires-Dist: portpicker
|
|
|
15
15
|
Requires-Dist: pydata-google-auth
|
|
16
16
|
Dynamic: description
|
|
17
17
|
Dynamic: description-content-type
|
|
18
|
+
Dynamic: license-file
|
|
18
19
|
Dynamic: requires-dist
|
|
19
20
|
Dynamic: summary
|
|
20
21
|
|
|
@@ -76,4 +76,17 @@ def get_nodes_edges(data: Dict[str, List[Any]], fields: List[StructType.Field],
|
|
|
76
76
|
edge = Edge.from_json(item)
|
|
77
77
|
edges.append(edge)
|
|
78
78
|
|
|
79
|
+
# Create placeholder nodes for nodes that were not returned
|
|
80
|
+
# from the query but are identified in the edges
|
|
81
|
+
missing_node_identifiers = set()
|
|
82
|
+
for edge in edges:
|
|
83
|
+
if edge.source not in node_identifiers:
|
|
84
|
+
missing_node_identifiers.add(edge.source)
|
|
85
|
+
if edge.destination not in node_identifiers:
|
|
86
|
+
missing_node_identifiers.add(edge.destination)
|
|
87
|
+
|
|
88
|
+
for identifier in missing_node_identifiers:
|
|
89
|
+
nodes.append(Node.make_intermediate(identifier))
|
|
90
|
+
node_identifiers.add(identifier)
|
|
91
|
+
|
|
79
92
|
return nodes, edges
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_entities.py
RENAMED
|
@@ -146,14 +146,16 @@ class Node:
|
|
|
146
146
|
identifier (str): The unique identifier for the node.
|
|
147
147
|
labels (List[str]): The labels associated with the node.
|
|
148
148
|
properties (Dict[str, Any]): The properties of the node.
|
|
149
|
+
intermediate (bool): Determines if this node was generated outside of the query response
|
|
149
150
|
"""
|
|
150
151
|
|
|
151
152
|
def __init__(self, identifier: str, labels: List[str],
|
|
152
|
-
properties: Dict[str, Any]):
|
|
153
|
+
properties: Dict[str, Any], intermediate = False):
|
|
153
154
|
self.identifier = identifier
|
|
154
155
|
self.labels = labels
|
|
155
156
|
self.key_property_names = []
|
|
156
157
|
self.properties = properties
|
|
158
|
+
self.intermediate = intermediate
|
|
157
159
|
|
|
158
160
|
def __repr__(self):
|
|
159
161
|
return (f"Node(identifier={self.identifier}, "
|
|
@@ -165,9 +167,20 @@ class Node:
|
|
|
165
167
|
"identifier": self.identifier,
|
|
166
168
|
"labels": self.labels,
|
|
167
169
|
"properties": self.properties,
|
|
168
|
-
"key_property_names": self.key_property_names
|
|
170
|
+
"key_property_names": self.key_property_names,
|
|
171
|
+
"intermediate": self.intermediate
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
@staticmethod
|
|
175
|
+
def make_intermediate(identifier: str):
|
|
176
|
+
"""Create a Node instance in the absence of a node from the query response"""
|
|
177
|
+
return Node(
|
|
178
|
+
identifier=identifier,
|
|
179
|
+
labels=["Intermediate"],
|
|
180
|
+
properties={"note": "This node represents a referenced entity that wasn't returned in the query results."},
|
|
181
|
+
intermediate=True
|
|
182
|
+
)
|
|
183
|
+
|
|
171
184
|
@classmethod
|
|
172
185
|
def from_json(cls, data: Dict[str, Any]) -> Node:
|
|
173
186
|
"""Create a Node instance from a JSON object."""
|