anywidget-graph 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- anywidget_graph/__init__.py +1 -1
- anywidget_graph/backends/__init__.py +71 -0
- anywidget_graph/backends/arango.py +201 -0
- anywidget_graph/backends/grafeo.py +70 -0
- anywidget_graph/backends/ladybug.py +94 -0
- anywidget_graph/backends/neo4j.py +143 -0
- anywidget_graph/converters/__init__.py +35 -0
- anywidget_graph/converters/base.py +77 -0
- anywidget_graph/converters/common.py +92 -0
- anywidget_graph/converters/cypher.py +107 -0
- anywidget_graph/converters/gql.py +15 -0
- anywidget_graph/converters/graphql.py +208 -0
- anywidget_graph/converters/gremlin.py +159 -0
- anywidget_graph/converters/sparql.py +167 -0
- anywidget_graph/ui/__init__.py +19 -0
- anywidget_graph/ui/icons.js +15 -0
- anywidget_graph/ui/index.js +199 -0
- anywidget_graph/ui/neo4j.js +193 -0
- anywidget_graph/ui/properties.js +137 -0
- anywidget_graph/ui/schema.js +178 -0
- anywidget_graph/ui/settings.js +299 -0
- anywidget_graph/ui/styles.css +584 -0
- anywidget_graph/ui/toolbar.js +106 -0
- anywidget_graph/widget.py +334 -1724
- {anywidget_graph-0.2.0.dist-info → anywidget_graph-0.2.1.dist-info}/METADATA +2 -2
- anywidget_graph-0.2.1.dist-info/RECORD +28 -0
- anywidget_graph-0.2.0.dist-info/RECORD +0 -6
- {anywidget_graph-0.2.0.dist-info → anywidget_graph-0.2.1.dist-info}/WHEEL +0 -0
anywidget_graph/widget.py
CHANGED
|
@@ -8,1624 +8,93 @@ import anywidget
|
|
|
8
8
|
import traitlets
|
|
9
9
|
from traitlets import observe
|
|
10
10
|
|
|
11
|
+
from anywidget_graph.backends import DatabaseBackend
|
|
12
|
+
from anywidget_graph.backends.grafeo import GrafeoBackend
|
|
13
|
+
from anywidget_graph.ui import get_css, get_esm
|
|
14
|
+
|
|
11
15
|
if TYPE_CHECKING:
|
|
12
16
|
from collections.abc import Callable
|
|
13
17
|
|
|
14
|
-
_ESM = r"""
|
|
15
|
-
import Graph from "https://esm.sh/graphology@0.25.4";
|
|
16
|
-
import Sigma from "https://esm.sh/sigma@3.0.0";
|
|
17
|
-
import neo4j from "https://cdn.jsdelivr.net/npm/neo4j-driver@5.28.0/lib/browser/neo4j-web.esm.min.js";
|
|
18
|
-
|
|
19
|
-
// === CONSTANTS ===
|
|
20
|
-
const QUERY_LANGUAGES = [
|
|
21
|
-
{ id: "cypher", name: "Cypher", enabled: true },
|
|
22
|
-
{ id: "gql", name: "GQL", enabled: false },
|
|
23
|
-
{ id: "sparql", name: "SPARQL", enabled: false },
|
|
24
|
-
{ id: "gremlin", name: "Gremlin", enabled: false },
|
|
25
|
-
{ id: "graphql", name: "GraphQL", enabled: false },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const BACKENDS = [
|
|
29
|
-
{ id: "neo4j", name: "Neo4j" },
|
|
30
|
-
{ id: "grafeo", name: "Grafeo" },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// === SVG ICONS ===
|
|
34
|
-
const ICONS = {
|
|
35
|
-
play: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
|
|
36
|
-
settings: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4m0 14v4m11-11h-4M5 12H1m17.36-5.64l-2.83 2.83M9.47 14.53l-2.83 2.83m0-10.72l2.83 2.83m5.06 5.06l2.83 2.83"/></svg>`,
|
|
37
|
-
close: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
|
|
38
|
-
chevron: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>`,
|
|
39
|
-
chevronRight: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
|
|
40
|
-
node: `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="8"/></svg>`,
|
|
41
|
-
edge: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14m-4-4l4 4-4 4"/></svg>`,
|
|
42
|
-
property: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>`,
|
|
43
|
-
sidebar: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>`,
|
|
44
|
-
refresh: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>`,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// === DATABASE CONNECTION ===
|
|
48
|
-
let driver = null;
|
|
49
|
-
|
|
50
|
-
async function connectNeo4j(uri, username, password, model) {
|
|
51
|
-
if (driver) {
|
|
52
|
-
await driver.close();
|
|
53
|
-
driver = null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
model.set("connection_status", "connecting");
|
|
57
|
-
model.save_changes();
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
driver = neo4j.driver(uri, neo4j.auth.basic(username, password));
|
|
61
|
-
await driver.verifyConnectivity();
|
|
62
|
-
model.set("connection_status", "connected");
|
|
63
|
-
model.set("query_error", "");
|
|
64
|
-
model.save_changes();
|
|
65
|
-
|
|
66
|
-
// Fetch schema after successful connection
|
|
67
|
-
await fetchSchema(model);
|
|
68
|
-
return true;
|
|
69
|
-
} catch (error) {
|
|
70
|
-
model.set("connection_status", "error");
|
|
71
|
-
model.set("query_error", "Connection failed: " + error.message);
|
|
72
|
-
model.save_changes();
|
|
73
|
-
driver = null;
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function fetchSchema(model) {
|
|
79
|
-
if (!driver) return;
|
|
80
|
-
|
|
81
|
-
const database = model.get("connection_database") || "neo4j";
|
|
82
|
-
const session = driver.session({ database });
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// Fetch node labels and their properties
|
|
86
|
-
const labelsResult = await session.run("CALL db.labels()");
|
|
87
|
-
const nodeTypes = [];
|
|
88
|
-
|
|
89
|
-
for (const record of labelsResult.records) {
|
|
90
|
-
const label = record.get(0);
|
|
91
|
-
// Get properties for this label
|
|
92
|
-
const propsResult = await session.run(
|
|
93
|
-
`MATCH (n:\`${label}\`) UNWIND keys(n) AS key RETURN DISTINCT key LIMIT 20`
|
|
94
|
-
);
|
|
95
|
-
const properties = propsResult.records.map(r => r.get(0));
|
|
96
|
-
nodeTypes.push({ label, properties, count: null });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Fetch relationship types and their properties
|
|
100
|
-
const relTypesResult = await session.run("CALL db.relationshipTypes()");
|
|
101
|
-
const edgeTypes = [];
|
|
102
|
-
|
|
103
|
-
for (const record of relTypesResult.records) {
|
|
104
|
-
const type = record.get(0);
|
|
105
|
-
// Get properties for this relationship type
|
|
106
|
-
const propsResult = await session.run(
|
|
107
|
-
`MATCH ()-[r:\`${type}\`]->() UNWIND keys(r) AS key RETURN DISTINCT key LIMIT 20`
|
|
108
|
-
);
|
|
109
|
-
const properties = propsResult.records.map(r => r.get(0));
|
|
110
|
-
edgeTypes.push({ type, properties, count: null });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
model.set("schema_node_types", nodeTypes);
|
|
114
|
-
model.set("schema_edge_types", edgeTypes);
|
|
115
|
-
model.save_changes();
|
|
116
|
-
} catch (error) {
|
|
117
|
-
console.error("Failed to fetch schema:", error);
|
|
118
|
-
} finally {
|
|
119
|
-
await session.close();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function disconnectNeo4j(model) {
|
|
124
|
-
if (driver) {
|
|
125
|
-
await driver.close();
|
|
126
|
-
driver = null;
|
|
127
|
-
}
|
|
128
|
-
model.set("connection_status", "disconnected");
|
|
129
|
-
model.save_changes();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function executeNeo4jQuery(query, database, model) {
|
|
133
|
-
if (!driver) {
|
|
134
|
-
model.set("query_error", "Not connected to database");
|
|
135
|
-
model.save_changes();
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
model.set("query_running", true);
|
|
140
|
-
model.set("query_error", "");
|
|
141
|
-
model.save_changes();
|
|
142
|
-
|
|
143
|
-
const session = driver.session({ database: database || "neo4j" });
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const result = await session.run(query);
|
|
147
|
-
model.set("query_running", false);
|
|
148
|
-
model.save_changes();
|
|
149
|
-
return processNeo4jRecords(result.records);
|
|
150
|
-
} catch (error) {
|
|
151
|
-
model.set("query_running", false);
|
|
152
|
-
model.set("query_error", "Query error: " + error.message);
|
|
153
|
-
model.save_changes();
|
|
154
|
-
return null;
|
|
155
|
-
} finally {
|
|
156
|
-
await session.close();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function processNeo4jRecords(records) {
|
|
161
|
-
const nodes = new Map();
|
|
162
|
-
const edges = [];
|
|
163
|
-
|
|
164
|
-
for (const record of records) {
|
|
165
|
-
for (const key of record.keys) {
|
|
166
|
-
const value = record.get(key);
|
|
167
|
-
processNeo4jValue(value, nodes, edges);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return { nodes: Array.from(nodes.values()), edges };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function processNeo4jValue(value, nodes, edges) {
|
|
175
|
-
if (!value) return;
|
|
176
|
-
|
|
177
|
-
if (neo4j.isNode(value)) {
|
|
178
|
-
const nodeId = value.elementId || value.identity.toString();
|
|
179
|
-
if (!nodes.has(nodeId)) {
|
|
180
|
-
const props = {};
|
|
181
|
-
for (const [k, v] of Object.entries(value.properties)) {
|
|
182
|
-
props[k] = neo4j.isInt(v) ? v.toNumber() : v;
|
|
183
|
-
}
|
|
184
|
-
nodes.set(nodeId, {
|
|
185
|
-
id: nodeId,
|
|
186
|
-
label: props.name || props.title || value.labels[0] || nodeId,
|
|
187
|
-
labels: value.labels,
|
|
188
|
-
...props,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
} else if (neo4j.isRelationship(value)) {
|
|
192
|
-
const props = {};
|
|
193
|
-
for (const [k, v] of Object.entries(value.properties)) {
|
|
194
|
-
props[k] = neo4j.isInt(v) ? v.toNumber() : v;
|
|
195
|
-
}
|
|
196
|
-
edges.push({
|
|
197
|
-
source: value.startNodeElementId || value.start.toString(),
|
|
198
|
-
target: value.endNodeElementId || value.end.toString(),
|
|
199
|
-
label: value.type,
|
|
200
|
-
...props,
|
|
201
|
-
});
|
|
202
|
-
} else if (neo4j.isPath(value)) {
|
|
203
|
-
for (const segment of value.segments) {
|
|
204
|
-
processNeo4jValue(segment.start, nodes, edges);
|
|
205
|
-
processNeo4jValue(segment.end, nodes, edges);
|
|
206
|
-
processNeo4jValue(segment.relationship, nodes, edges);
|
|
207
|
-
}
|
|
208
|
-
} else if (Array.isArray(value)) {
|
|
209
|
-
for (const item of value) {
|
|
210
|
-
processNeo4jValue(item, nodes, edges);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// === UI COMPONENTS ===
|
|
216
|
-
function createToolbar(model, wrapper) {
|
|
217
|
-
const toolbar = document.createElement("div");
|
|
218
|
-
toolbar.className = "awg-toolbar";
|
|
219
|
-
|
|
220
|
-
// Schema sidebar toggle (left)
|
|
221
|
-
const schemaBtn = document.createElement("button");
|
|
222
|
-
schemaBtn.className = "awg-btn";
|
|
223
|
-
schemaBtn.innerHTML = ICONS.sidebar;
|
|
224
|
-
schemaBtn.title = "Toggle schema browser";
|
|
225
|
-
schemaBtn.addEventListener("click", () => toggleSchemaPanel(wrapper));
|
|
226
|
-
toolbar.appendChild(schemaBtn);
|
|
227
|
-
|
|
228
|
-
// Query container (collapsible input)
|
|
229
|
-
if (model.get("show_query_input")) {
|
|
230
|
-
const queryContainer = document.createElement("div");
|
|
231
|
-
queryContainer.className = "awg-query-container";
|
|
232
|
-
|
|
233
|
-
const queryInput = document.createElement("input");
|
|
234
|
-
queryInput.type = "text";
|
|
235
|
-
queryInput.className = "awg-query-input";
|
|
236
|
-
queryInput.placeholder = "Enter query (e.g., MATCH (n) RETURN n LIMIT 25)";
|
|
237
|
-
queryInput.value = model.get("query") || "";
|
|
238
|
-
|
|
239
|
-
queryInput.addEventListener("input", (e) => {
|
|
240
|
-
model.set("query", e.target.value);
|
|
241
|
-
model.save_changes();
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
queryInput.addEventListener("keydown", (e) => {
|
|
245
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
executeQuery(model);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
model.on("change:query", () => {
|
|
252
|
-
if (queryInput.value !== model.get("query")) {
|
|
253
|
-
queryInput.value = model.get("query") || "";
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
queryContainer.appendChild(queryInput);
|
|
258
|
-
|
|
259
|
-
// Play button
|
|
260
|
-
const playBtn = document.createElement("button");
|
|
261
|
-
playBtn.className = "awg-btn awg-btn-primary";
|
|
262
|
-
playBtn.innerHTML = ICONS.play;
|
|
263
|
-
playBtn.title = "Run query";
|
|
264
|
-
playBtn.addEventListener("click", () => executeQuery(model));
|
|
265
|
-
|
|
266
|
-
model.on("change:query_running", () => {
|
|
267
|
-
playBtn.disabled = model.get("query_running");
|
|
268
|
-
playBtn.innerHTML = model.get("query_running")
|
|
269
|
-
? '<span class="awg-spinner"></span>'
|
|
270
|
-
: ICONS.play;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
queryContainer.appendChild(playBtn);
|
|
274
|
-
toolbar.appendChild(queryContainer);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Settings button
|
|
278
|
-
if (model.get("show_settings")) {
|
|
279
|
-
const settingsBtn = document.createElement("button");
|
|
280
|
-
settingsBtn.className = "awg-btn";
|
|
281
|
-
settingsBtn.innerHTML = ICONS.settings;
|
|
282
|
-
settingsBtn.title = "Connection settings";
|
|
283
|
-
|
|
284
|
-
// Status dot
|
|
285
|
-
const statusDot = document.createElement("span");
|
|
286
|
-
statusDot.className = "awg-status-dot";
|
|
287
|
-
updateStatusDot(statusDot, model.get("connection_status"));
|
|
288
|
-
settingsBtn.appendChild(statusDot);
|
|
289
|
-
|
|
290
|
-
model.on("change:connection_status", () => {
|
|
291
|
-
updateStatusDot(statusDot, model.get("connection_status"));
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
settingsBtn.addEventListener("click", () => {
|
|
295
|
-
toggleSettingsPanel(wrapper);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
toolbar.appendChild(settingsBtn);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Properties panel toggle (right)
|
|
302
|
-
const propsBtn = document.createElement("button");
|
|
303
|
-
propsBtn.className = "awg-btn";
|
|
304
|
-
propsBtn.innerHTML = ICONS.property;
|
|
305
|
-
propsBtn.title = "Toggle properties panel";
|
|
306
|
-
propsBtn.addEventListener("click", () => togglePropertiesPanel(wrapper));
|
|
307
|
-
toolbar.appendChild(propsBtn);
|
|
308
|
-
|
|
309
|
-
return toolbar;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function updateStatusDot(dot, status) {
|
|
313
|
-
dot.className = "awg-status-dot awg-status-" + status;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function toggleSettingsPanel(wrapper) {
|
|
317
|
-
const panel = wrapper.querySelector(".awg-settings-panel");
|
|
318
|
-
if (panel) {
|
|
319
|
-
panel.classList.toggle("awg-panel-open");
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function toggleSchemaPanel(wrapper) {
|
|
324
|
-
const panel = wrapper.querySelector(".awg-schema-panel");
|
|
325
|
-
if (panel) {
|
|
326
|
-
panel.classList.toggle("awg-panel-open");
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function createSchemaSidebar(model) {
|
|
331
|
-
const panel = document.createElement("div");
|
|
332
|
-
panel.className = "awg-schema-panel";
|
|
333
|
-
|
|
334
|
-
// Header
|
|
335
|
-
const header = document.createElement("div");
|
|
336
|
-
header.className = "awg-panel-header";
|
|
337
|
-
header.innerHTML = `<span>Schema</span>`;
|
|
338
|
-
|
|
339
|
-
const refreshBtn = document.createElement("button");
|
|
340
|
-
refreshBtn.className = "awg-btn awg-btn-icon awg-btn-sm";
|
|
341
|
-
refreshBtn.innerHTML = ICONS.refresh;
|
|
342
|
-
refreshBtn.title = "Refresh schema";
|
|
343
|
-
refreshBtn.addEventListener("click", () => fetchSchema(model));
|
|
344
|
-
header.appendChild(refreshBtn);
|
|
345
|
-
|
|
346
|
-
panel.appendChild(header);
|
|
347
|
-
|
|
348
|
-
// Content
|
|
349
|
-
const content = document.createElement("div");
|
|
350
|
-
content.className = "awg-schema-content";
|
|
351
|
-
|
|
352
|
-
function renderSchema() {
|
|
353
|
-
content.innerHTML = "";
|
|
354
|
-
|
|
355
|
-
const nodeTypes = model.get("schema_node_types") || [];
|
|
356
|
-
const edgeTypes = model.get("schema_edge_types") || [];
|
|
357
|
-
|
|
358
|
-
if (nodeTypes.length === 0 && edgeTypes.length === 0) {
|
|
359
|
-
const empty = document.createElement("div");
|
|
360
|
-
empty.className = "awg-schema-empty";
|
|
361
|
-
empty.textContent = "Connect to load schema";
|
|
362
|
-
content.appendChild(empty);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Node types section
|
|
367
|
-
if (nodeTypes.length > 0) {
|
|
368
|
-
const nodeSection = document.createElement("div");
|
|
369
|
-
nodeSection.className = "awg-schema-section";
|
|
370
|
-
|
|
371
|
-
const nodeHeader = document.createElement("div");
|
|
372
|
-
nodeHeader.className = "awg-schema-section-header";
|
|
373
|
-
nodeHeader.innerHTML = `${ICONS.node} <span>Node Labels</span>`;
|
|
374
|
-
nodeSection.appendChild(nodeHeader);
|
|
375
|
-
|
|
376
|
-
nodeTypes.forEach(({ label, properties }) => {
|
|
377
|
-
const item = createSchemaItem(label, properties, "node", model);
|
|
378
|
-
nodeSection.appendChild(item);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
content.appendChild(nodeSection);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Edge types section
|
|
385
|
-
if (edgeTypes.length > 0) {
|
|
386
|
-
const edgeSection = document.createElement("div");
|
|
387
|
-
edgeSection.className = "awg-schema-section";
|
|
388
|
-
|
|
389
|
-
const edgeHeader = document.createElement("div");
|
|
390
|
-
edgeHeader.className = "awg-schema-section-header";
|
|
391
|
-
edgeHeader.innerHTML = `${ICONS.edge} <span>Relationships</span>`;
|
|
392
|
-
edgeSection.appendChild(edgeHeader);
|
|
393
|
-
|
|
394
|
-
edgeTypes.forEach(({ type, properties }) => {
|
|
395
|
-
const item = createSchemaItem(type, properties, "edge", model);
|
|
396
|
-
edgeSection.appendChild(item);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
content.appendChild(edgeSection);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
model.on("change:schema_node_types", renderSchema);
|
|
404
|
-
model.on("change:schema_edge_types", renderSchema);
|
|
405
|
-
renderSchema();
|
|
406
|
-
|
|
407
|
-
panel.appendChild(content);
|
|
408
|
-
return panel;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function createSchemaItem(name, properties, type, model) {
|
|
412
|
-
const item = document.createElement("div");
|
|
413
|
-
item.className = "awg-schema-item";
|
|
414
|
-
|
|
415
|
-
const itemHeader = document.createElement("div");
|
|
416
|
-
itemHeader.className = "awg-schema-item-header";
|
|
417
|
-
|
|
418
|
-
const expandBtn = document.createElement("span");
|
|
419
|
-
expandBtn.className = "awg-schema-expand";
|
|
420
|
-
expandBtn.innerHTML = ICONS.chevronRight;
|
|
421
|
-
|
|
422
|
-
const nameSpan = document.createElement("span");
|
|
423
|
-
nameSpan.className = "awg-schema-name";
|
|
424
|
-
nameSpan.textContent = name;
|
|
425
|
-
|
|
426
|
-
itemHeader.appendChild(expandBtn);
|
|
427
|
-
itemHeader.appendChild(nameSpan);
|
|
428
|
-
|
|
429
|
-
// Click on name to query
|
|
430
|
-
nameSpan.addEventListener("click", (e) => {
|
|
431
|
-
e.stopPropagation();
|
|
432
|
-
let query;
|
|
433
|
-
if (type === "node") {
|
|
434
|
-
query = `MATCH (n:\`${name}\`) RETURN n LIMIT 25`;
|
|
435
|
-
} else {
|
|
436
|
-
query = `MATCH (a)-[r:\`${name}\`]->(b) RETURN a, r, b LIMIT 25`;
|
|
437
|
-
}
|
|
438
|
-
model.set("query", query);
|
|
439
|
-
model.save_changes();
|
|
440
|
-
executeQuery(model);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// Properties list (collapsed by default)
|
|
444
|
-
const propsList = document.createElement("div");
|
|
445
|
-
propsList.className = "awg-schema-props";
|
|
446
|
-
|
|
447
|
-
if (properties && properties.length > 0) {
|
|
448
|
-
properties.forEach(prop => {
|
|
449
|
-
const propItem = document.createElement("div");
|
|
450
|
-
propItem.className = "awg-schema-prop";
|
|
451
|
-
propItem.innerHTML = `${ICONS.property} <span>${prop}</span>`;
|
|
452
|
-
|
|
453
|
-
// Click on property to query with that property
|
|
454
|
-
propItem.addEventListener("click", (e) => {
|
|
455
|
-
e.stopPropagation();
|
|
456
|
-
let query;
|
|
457
|
-
if (type === "node") {
|
|
458
|
-
query = `MATCH (n:\`${name}\`) RETURN n.\`${prop}\` AS ${prop}, n LIMIT 25`;
|
|
459
|
-
} else {
|
|
460
|
-
query = `MATCH (a)-[r:\`${name}\`]->(b) RETURN r.\`${prop}\` AS ${prop}, a, r, b LIMIT 25`;
|
|
461
|
-
}
|
|
462
|
-
model.set("query", query);
|
|
463
|
-
model.save_changes();
|
|
464
|
-
executeQuery(model);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
propsList.appendChild(propItem);
|
|
468
|
-
});
|
|
469
|
-
} else {
|
|
470
|
-
const noProp = document.createElement("div");
|
|
471
|
-
noProp.className = "awg-schema-prop awg-schema-prop-empty";
|
|
472
|
-
noProp.textContent = "No properties";
|
|
473
|
-
propsList.appendChild(noProp);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
item.appendChild(itemHeader);
|
|
477
|
-
item.appendChild(propsList);
|
|
478
|
-
|
|
479
|
-
// Toggle expand/collapse
|
|
480
|
-
itemHeader.addEventListener("click", () => {
|
|
481
|
-
item.classList.toggle("awg-schema-item-expanded");
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
return item;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function createSettingsPanel(model) {
|
|
488
|
-
const panel = document.createElement("div");
|
|
489
|
-
panel.className = "awg-settings-panel";
|
|
490
|
-
|
|
491
|
-
// Header
|
|
492
|
-
const header = document.createElement("div");
|
|
493
|
-
header.className = "awg-panel-header";
|
|
494
|
-
header.innerHTML = "<span>Settings</span>";
|
|
495
|
-
panel.appendChild(header);
|
|
496
|
-
|
|
497
|
-
// Form
|
|
498
|
-
const form = document.createElement("div");
|
|
499
|
-
form.className = "awg-panel-form";
|
|
500
|
-
|
|
501
|
-
// Dark mode toggle
|
|
502
|
-
form.appendChild(createFormGroup("Theme", () => {
|
|
503
|
-
const toggle = document.createElement("label");
|
|
504
|
-
toggle.className = "awg-toggle";
|
|
505
|
-
const checkbox = document.createElement("input");
|
|
506
|
-
checkbox.type = "checkbox";
|
|
507
|
-
checkbox.checked = model.get("dark_mode");
|
|
508
|
-
checkbox.addEventListener("change", (e) => {
|
|
509
|
-
model.set("dark_mode", e.target.checked);
|
|
510
|
-
model.save_changes();
|
|
511
|
-
});
|
|
512
|
-
model.on("change:dark_mode", () => {
|
|
513
|
-
checkbox.checked = model.get("dark_mode");
|
|
514
|
-
});
|
|
515
|
-
const slider = document.createElement("span");
|
|
516
|
-
slider.className = "awg-toggle-slider";
|
|
517
|
-
const label = document.createElement("span");
|
|
518
|
-
label.className = "awg-toggle-label";
|
|
519
|
-
label.textContent = "Dark mode";
|
|
520
|
-
toggle.appendChild(checkbox);
|
|
521
|
-
toggle.appendChild(slider);
|
|
522
|
-
toggle.appendChild(label);
|
|
523
|
-
return toggle;
|
|
524
|
-
}));
|
|
525
|
-
|
|
526
|
-
// Backend selector
|
|
527
|
-
form.appendChild(createFormGroup("Backend", () => {
|
|
528
|
-
const select = document.createElement("select");
|
|
529
|
-
select.className = "awg-select";
|
|
530
|
-
BACKENDS.forEach(b => {
|
|
531
|
-
const opt = document.createElement("option");
|
|
532
|
-
opt.value = b.id;
|
|
533
|
-
opt.textContent = b.name;
|
|
534
|
-
opt.selected = model.get("database_backend") === b.id;
|
|
535
|
-
select.appendChild(opt);
|
|
536
|
-
});
|
|
537
|
-
select.addEventListener("change", (e) => {
|
|
538
|
-
model.set("database_backend", e.target.value);
|
|
539
|
-
model.save_changes();
|
|
540
|
-
updateFormVisibility();
|
|
541
|
-
});
|
|
542
|
-
return select;
|
|
543
|
-
}));
|
|
544
|
-
|
|
545
|
-
// Neo4j fields container
|
|
546
|
-
const neo4jFields = document.createElement("div");
|
|
547
|
-
neo4jFields.className = "awg-neo4j-fields";
|
|
548
|
-
|
|
549
|
-
// URI
|
|
550
|
-
neo4jFields.appendChild(createFormGroup("URI", () => {
|
|
551
|
-
const input = document.createElement("input");
|
|
552
|
-
input.type = "text";
|
|
553
|
-
input.className = "awg-input";
|
|
554
|
-
input.placeholder = "neo4j+s://localhost:7687";
|
|
555
|
-
input.value = model.get("connection_uri") || "";
|
|
556
|
-
input.addEventListener("input", (e) => {
|
|
557
|
-
model.set("connection_uri", e.target.value);
|
|
558
|
-
model.save_changes();
|
|
559
|
-
});
|
|
560
|
-
return input;
|
|
561
|
-
}));
|
|
562
|
-
|
|
563
|
-
// Username
|
|
564
|
-
neo4jFields.appendChild(createFormGroup("Username", () => {
|
|
565
|
-
const input = document.createElement("input");
|
|
566
|
-
input.type = "text";
|
|
567
|
-
input.className = "awg-input";
|
|
568
|
-
input.placeholder = "neo4j";
|
|
569
|
-
input.value = model.get("connection_username") || "";
|
|
570
|
-
input.addEventListener("input", (e) => {
|
|
571
|
-
model.set("connection_username", e.target.value);
|
|
572
|
-
model.save_changes();
|
|
573
|
-
});
|
|
574
|
-
return input;
|
|
575
|
-
}));
|
|
576
|
-
|
|
577
|
-
// Password
|
|
578
|
-
neo4jFields.appendChild(createFormGroup("Password", () => {
|
|
579
|
-
const input = document.createElement("input");
|
|
580
|
-
input.type = "password";
|
|
581
|
-
input.className = "awg-input";
|
|
582
|
-
input.value = model.get("connection_password") || "";
|
|
583
|
-
input.addEventListener("input", (e) => {
|
|
584
|
-
model.set("connection_password", e.target.value);
|
|
585
|
-
model.save_changes();
|
|
586
|
-
});
|
|
587
|
-
return input;
|
|
588
|
-
}));
|
|
589
|
-
|
|
590
|
-
form.appendChild(neo4jFields);
|
|
591
|
-
|
|
592
|
-
// Database name
|
|
593
|
-
form.appendChild(createFormGroup("Database", () => {
|
|
594
|
-
const input = document.createElement("input");
|
|
595
|
-
input.type = "text";
|
|
596
|
-
input.className = "awg-input";
|
|
597
|
-
input.placeholder = "neo4j";
|
|
598
|
-
input.value = model.get("connection_database") || "neo4j";
|
|
599
|
-
input.addEventListener("input", (e) => {
|
|
600
|
-
model.set("connection_database", e.target.value);
|
|
601
|
-
model.save_changes();
|
|
602
|
-
});
|
|
603
|
-
return input;
|
|
604
|
-
}));
|
|
605
|
-
|
|
606
|
-
// Query language
|
|
607
|
-
form.appendChild(createFormGroup("Language", () => {
|
|
608
|
-
const select = document.createElement("select");
|
|
609
|
-
select.className = "awg-select";
|
|
610
|
-
QUERY_LANGUAGES.forEach(lang => {
|
|
611
|
-
const opt = document.createElement("option");
|
|
612
|
-
opt.value = lang.id;
|
|
613
|
-
opt.textContent = lang.name;
|
|
614
|
-
opt.disabled = !lang.enabled;
|
|
615
|
-
opt.selected = model.get("query_language") === lang.id;
|
|
616
|
-
select.appendChild(opt);
|
|
617
|
-
});
|
|
618
|
-
select.addEventListener("change", (e) => {
|
|
619
|
-
model.set("query_language", e.target.value);
|
|
620
|
-
model.save_changes();
|
|
621
|
-
});
|
|
622
|
-
return select;
|
|
623
|
-
}));
|
|
624
|
-
|
|
625
|
-
panel.appendChild(form);
|
|
626
|
-
|
|
627
|
-
// Connect button
|
|
628
|
-
const actions = document.createElement("div");
|
|
629
|
-
actions.className = "awg-panel-actions";
|
|
630
|
-
|
|
631
|
-
const connectBtn = document.createElement("button");
|
|
632
|
-
connectBtn.className = "awg-btn awg-btn-primary awg-btn-full";
|
|
633
|
-
|
|
634
|
-
const statusIndicator = document.createElement("div");
|
|
635
|
-
statusIndicator.className = "awg-connection-status";
|
|
636
|
-
|
|
637
|
-
function updateConnectButton() {
|
|
638
|
-
const status = model.get("connection_status");
|
|
639
|
-
const backend = model.get("database_backend");
|
|
640
|
-
|
|
641
|
-
if (backend === "grafeo") {
|
|
642
|
-
connectBtn.textContent = "Python Backend";
|
|
643
|
-
connectBtn.disabled = true;
|
|
644
|
-
statusIndicator.innerHTML = '<span class="awg-status-dot-inline awg-status-connected"></span> Grafeo';
|
|
645
|
-
} else if (status === "connected") {
|
|
646
|
-
connectBtn.textContent = "Disconnect";
|
|
647
|
-
connectBtn.disabled = false;
|
|
648
|
-
statusIndicator.innerHTML = '<span class="awg-status-dot-inline awg-status-connected"></span> Connected';
|
|
649
|
-
} else if (status === "connecting") {
|
|
650
|
-
connectBtn.textContent = "Connecting...";
|
|
651
|
-
connectBtn.disabled = true;
|
|
652
|
-
statusIndicator.innerHTML = '<span class="awg-status-dot-inline awg-status-connecting"></span> Connecting';
|
|
653
|
-
} else if (status === "error") {
|
|
654
|
-
connectBtn.textContent = "Retry";
|
|
655
|
-
connectBtn.disabled = false;
|
|
656
|
-
statusIndicator.innerHTML = '<span class="awg-status-dot-inline awg-status-error"></span> Error';
|
|
657
|
-
} else {
|
|
658
|
-
connectBtn.textContent = "Connect";
|
|
659
|
-
connectBtn.disabled = false;
|
|
660
|
-
statusIndicator.innerHTML = '<span class="awg-status-dot-inline awg-status-disconnected"></span> Disconnected';
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function updateFormVisibility() {
|
|
665
|
-
const backend = model.get("database_backend");
|
|
666
|
-
neo4jFields.style.display = backend === "neo4j" ? "block" : "none";
|
|
667
|
-
updateConnectButton();
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
connectBtn.addEventListener("click", async () => {
|
|
671
|
-
const backend = model.get("database_backend");
|
|
672
|
-
if (backend !== "neo4j") return;
|
|
673
|
-
|
|
674
|
-
const status = model.get("connection_status");
|
|
675
|
-
if (status === "connected") {
|
|
676
|
-
await disconnectNeo4j(model);
|
|
677
|
-
} else {
|
|
678
|
-
await connectNeo4j(
|
|
679
|
-
model.get("connection_uri"),
|
|
680
|
-
model.get("connection_username"),
|
|
681
|
-
model.get("connection_password"),
|
|
682
|
-
model
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
model.on("change:connection_status", updateConnectButton);
|
|
688
|
-
model.on("change:database_backend", updateFormVisibility);
|
|
689
|
-
|
|
690
|
-
actions.appendChild(statusIndicator);
|
|
691
|
-
actions.appendChild(connectBtn);
|
|
692
|
-
panel.appendChild(actions);
|
|
693
|
-
|
|
694
|
-
// Error display
|
|
695
|
-
const errorDiv = document.createElement("div");
|
|
696
|
-
errorDiv.className = "awg-panel-error";
|
|
697
|
-
function updateError() {
|
|
698
|
-
const err = model.get("query_error");
|
|
699
|
-
errorDiv.textContent = err || "";
|
|
700
|
-
errorDiv.style.display = err ? "block" : "none";
|
|
701
|
-
}
|
|
702
|
-
model.on("change:query_error", updateError);
|
|
703
|
-
updateError();
|
|
704
|
-
panel.appendChild(errorDiv);
|
|
705
|
-
|
|
706
|
-
// Initialize
|
|
707
|
-
updateFormVisibility();
|
|
708
|
-
|
|
709
|
-
return panel;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function createFormGroup(label, inputFn) {
|
|
713
|
-
const group = document.createElement("div");
|
|
714
|
-
group.className = "awg-form-group";
|
|
715
|
-
|
|
716
|
-
const labelEl = document.createElement("label");
|
|
717
|
-
labelEl.className = "awg-label";
|
|
718
|
-
labelEl.textContent = label;
|
|
719
|
-
group.appendChild(labelEl);
|
|
720
|
-
|
|
721
|
-
group.appendChild(inputFn());
|
|
722
|
-
return group;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function createPropertiesPanel(model) {
|
|
726
|
-
const panel = document.createElement("div");
|
|
727
|
-
panel.className = "awg-properties-panel";
|
|
728
|
-
|
|
729
|
-
// Header
|
|
730
|
-
const header = document.createElement("div");
|
|
731
|
-
header.className = "awg-panel-header";
|
|
732
|
-
header.innerHTML = "<span>Properties</span>";
|
|
733
|
-
panel.appendChild(header);
|
|
734
|
-
|
|
735
|
-
// Content
|
|
736
|
-
const content = document.createElement("div");
|
|
737
|
-
content.className = "awg-properties-content";
|
|
738
|
-
|
|
739
|
-
function renderProperties() {
|
|
740
|
-
content.innerHTML = "";
|
|
741
|
-
|
|
742
|
-
const selectedNode = model.get("selected_node");
|
|
743
|
-
const selectedEdge = model.get("selected_edge");
|
|
744
|
-
|
|
745
|
-
if (!selectedNode && !selectedEdge) {
|
|
746
|
-
const empty = document.createElement("div");
|
|
747
|
-
empty.className = "awg-properties-empty";
|
|
748
|
-
empty.textContent = "Click a node or edge to view properties";
|
|
749
|
-
content.appendChild(empty);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (selectedNode) {
|
|
754
|
-
// Node header
|
|
755
|
-
const nodeHeader = document.createElement("div");
|
|
756
|
-
nodeHeader.className = "awg-properties-header";
|
|
757
|
-
nodeHeader.innerHTML = `${ICONS.node} <span class="awg-properties-type">Node</span>`;
|
|
758
|
-
content.appendChild(nodeHeader);
|
|
759
|
-
|
|
760
|
-
// Node ID
|
|
761
|
-
const idItem = document.createElement("div");
|
|
762
|
-
idItem.className = "awg-property-item";
|
|
763
|
-
idItem.innerHTML = `<span class="awg-property-key">id</span><span class="awg-property-value">${selectedNode.id || "N/A"}</span>`;
|
|
764
|
-
content.appendChild(idItem);
|
|
765
|
-
|
|
766
|
-
// Labels
|
|
767
|
-
if (selectedNode.labels && selectedNode.labels.length > 0) {
|
|
768
|
-
const labelsItem = document.createElement("div");
|
|
769
|
-
labelsItem.className = "awg-property-item";
|
|
770
|
-
labelsItem.innerHTML = `<span class="awg-property-key">labels</span><span class="awg-property-value awg-property-labels">${selectedNode.labels.map(l => `<span class="awg-label-tag">${l}</span>`).join("")}</span>`;
|
|
771
|
-
content.appendChild(labelsItem);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Other properties
|
|
775
|
-
Object.entries(selectedNode).forEach(([key, value]) => {
|
|
776
|
-
if (key === "id" || key === "labels" || key === "label" || key === "x" || key === "y" || key === "size" || key === "color") return;
|
|
777
|
-
const item = document.createElement("div");
|
|
778
|
-
item.className = "awg-property-item";
|
|
779
|
-
const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
780
|
-
item.innerHTML = `<span class="awg-property-key">${key}</span><span class="awg-property-value">${displayValue}</span>`;
|
|
781
|
-
content.appendChild(item);
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (selectedEdge) {
|
|
786
|
-
// Edge header
|
|
787
|
-
const edgeHeader = document.createElement("div");
|
|
788
|
-
edgeHeader.className = "awg-properties-header";
|
|
789
|
-
edgeHeader.innerHTML = `${ICONS.edge} <span class="awg-properties-type">Relationship</span>`;
|
|
790
|
-
content.appendChild(edgeHeader);
|
|
791
|
-
|
|
792
|
-
// Edge type/label
|
|
793
|
-
if (selectedEdge.label) {
|
|
794
|
-
const typeItem = document.createElement("div");
|
|
795
|
-
typeItem.className = "awg-property-item";
|
|
796
|
-
typeItem.innerHTML = `<span class="awg-property-key">type</span><span class="awg-property-value"><span class="awg-edge-tag">${selectedEdge.label}</span></span>`;
|
|
797
|
-
content.appendChild(typeItem);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Source/Target
|
|
801
|
-
const sourceItem = document.createElement("div");
|
|
802
|
-
sourceItem.className = "awg-property-item";
|
|
803
|
-
sourceItem.innerHTML = `<span class="awg-property-key">source</span><span class="awg-property-value awg-property-truncate">${selectedEdge.source}</span>`;
|
|
804
|
-
content.appendChild(sourceItem);
|
|
805
|
-
|
|
806
|
-
const targetItem = document.createElement("div");
|
|
807
|
-
targetItem.className = "awg-property-item";
|
|
808
|
-
targetItem.innerHTML = `<span class="awg-property-key">target</span><span class="awg-property-value awg-property-truncate">${selectedEdge.target}</span>`;
|
|
809
|
-
content.appendChild(targetItem);
|
|
810
|
-
|
|
811
|
-
// Other properties
|
|
812
|
-
Object.entries(selectedEdge).forEach(([key, value]) => {
|
|
813
|
-
if (key === "source" || key === "target" || key === "label" || key === "size" || key === "color") return;
|
|
814
|
-
const item = document.createElement("div");
|
|
815
|
-
item.className = "awg-property-item";
|
|
816
|
-
const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
817
|
-
item.innerHTML = `<span class="awg-property-key">${key}</span><span class="awg-property-value">${displayValue}</span>`;
|
|
818
|
-
content.appendChild(item);
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
model.on("change:selected_node", renderProperties);
|
|
824
|
-
model.on("change:selected_edge", renderProperties);
|
|
825
|
-
renderProperties();
|
|
826
|
-
|
|
827
|
-
panel.appendChild(content);
|
|
828
|
-
return panel;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function togglePropertiesPanel(wrapper) {
|
|
832
|
-
const panel = wrapper.querySelector(".awg-properties-panel");
|
|
833
|
-
if (panel) {
|
|
834
|
-
panel.classList.toggle("awg-panel-open");
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
async function executeQuery(model) {
|
|
839
|
-
const backend = model.get("database_backend");
|
|
840
|
-
const query = model.get("query");
|
|
841
|
-
|
|
842
|
-
if (!query.trim()) {
|
|
843
|
-
model.set("query_error", "Please enter a query");
|
|
844
|
-
model.save_changes();
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (backend === "neo4j") {
|
|
849
|
-
const result = await executeNeo4jQuery(
|
|
850
|
-
query,
|
|
851
|
-
model.get("connection_database"),
|
|
852
|
-
model
|
|
853
|
-
);
|
|
854
|
-
|
|
855
|
-
if (result) {
|
|
856
|
-
model.set("nodes", result.nodes);
|
|
857
|
-
model.set("edges", result.edges);
|
|
858
|
-
model.save_changes();
|
|
859
|
-
}
|
|
860
|
-
} else if (backend === "grafeo") {
|
|
861
|
-
// Trigger Python-side execution
|
|
862
|
-
model.set("_execute_query", model.get("_execute_query") + 1);
|
|
863
|
-
model.save_changes();
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// === MAIN RENDER FUNCTION ===
|
|
868
|
-
function render({ model, el }) {
|
|
869
|
-
const wrapper = document.createElement("div");
|
|
870
|
-
wrapper.className = "awg-wrapper";
|
|
871
|
-
|
|
872
|
-
// Apply dark mode
|
|
873
|
-
function updateTheme() {
|
|
874
|
-
wrapper.classList.toggle("awg-dark", model.get("dark_mode"));
|
|
875
|
-
}
|
|
876
|
-
updateTheme();
|
|
877
|
-
model.on("change:dark_mode", updateTheme);
|
|
878
|
-
|
|
879
|
-
// Create toolbar if enabled
|
|
880
|
-
if (model.get("show_toolbar")) {
|
|
881
|
-
const toolbar = createToolbar(model, wrapper);
|
|
882
|
-
wrapper.appendChild(toolbar);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// Main content area (schema + graph + properties)
|
|
886
|
-
const content = document.createElement("div");
|
|
887
|
-
content.className = "awg-content";
|
|
888
|
-
|
|
889
|
-
// Create schema sidebar (left) - collapsed by default
|
|
890
|
-
const schemaSidebar = createSchemaSidebar(model);
|
|
891
|
-
content.appendChild(schemaSidebar);
|
|
892
|
-
|
|
893
|
-
// Create graph container
|
|
894
|
-
const container = document.createElement("div");
|
|
895
|
-
container.className = "awg-graph-container";
|
|
896
|
-
container.style.width = model.get("width") + "px";
|
|
897
|
-
container.style.height = model.get("height") + "px";
|
|
898
|
-
content.appendChild(container);
|
|
899
|
-
|
|
900
|
-
// Create properties panel (right) - collapsed by default
|
|
901
|
-
const propertiesPanel = createPropertiesPanel(model);
|
|
902
|
-
content.appendChild(propertiesPanel);
|
|
903
|
-
|
|
904
|
-
// Create settings panel if enabled (inside properties panel area)
|
|
905
|
-
if (model.get("show_settings")) {
|
|
906
|
-
const settingsPanel = createSettingsPanel(model);
|
|
907
|
-
content.appendChild(settingsPanel);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
wrapper.appendChild(content);
|
|
911
|
-
el.appendChild(wrapper);
|
|
912
|
-
|
|
913
|
-
const graph = new Graph();
|
|
914
|
-
|
|
915
|
-
const nodes = model.get("nodes") || [];
|
|
916
|
-
nodes.forEach((node) => {
|
|
917
|
-
graph.addNode(node.id, {
|
|
918
|
-
label: node.label || node.id,
|
|
919
|
-
x: node.x ?? Math.random() * 100,
|
|
920
|
-
y: node.y ?? Math.random() * 100,
|
|
921
|
-
size: node.size || 10,
|
|
922
|
-
color: node.color || "#6366f1",
|
|
923
|
-
});
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
const edges = model.get("edges") || [];
|
|
927
|
-
edges.forEach((edge, i) => {
|
|
928
|
-
graph.addEdge(edge.source, edge.target, {
|
|
929
|
-
label: edge.label || "",
|
|
930
|
-
size: edge.size || 2,
|
|
931
|
-
color: edge.color || "#94a3b8",
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
const renderer = new Sigma(graph, container, {
|
|
936
|
-
renderLabels: model.get("show_labels"),
|
|
937
|
-
renderEdgeLabels: model.get("show_edge_labels"),
|
|
938
|
-
defaultNodeColor: "#6366f1",
|
|
939
|
-
defaultEdgeColor: "#94a3b8",
|
|
940
|
-
labelColor: { color: "#333" },
|
|
941
|
-
labelSize: 12,
|
|
942
|
-
labelWeight: "500",
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
renderer.on("clickNode", ({ node }) => {
|
|
946
|
-
const nodeData = graph.getNodeAttributes(node);
|
|
947
|
-
model.set("selected_node", { id: node, ...nodeData });
|
|
948
|
-
model.save_changes();
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
renderer.on("clickEdge", ({ edge }) => {
|
|
952
|
-
const edgeData = graph.getEdgeAttributes(edge);
|
|
953
|
-
const [source, target] = graph.extremities(edge);
|
|
954
|
-
model.set("selected_edge", { source, target, ...edgeData });
|
|
955
|
-
model.save_changes();
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
renderer.on("clickStage", () => {
|
|
959
|
-
model.set("selected_node", null);
|
|
960
|
-
model.set("selected_edge", null);
|
|
961
|
-
model.save_changes();
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
model.on("change:nodes", () => {
|
|
965
|
-
graph.clear();
|
|
966
|
-
const newNodes = model.get("nodes") || [];
|
|
967
|
-
newNodes.forEach((node) => {
|
|
968
|
-
graph.addNode(node.id, {
|
|
969
|
-
label: node.label || node.id,
|
|
970
|
-
x: node.x ?? Math.random() * 100,
|
|
971
|
-
y: node.y ?? Math.random() * 100,
|
|
972
|
-
size: node.size || 10,
|
|
973
|
-
color: node.color || "#6366f1",
|
|
974
|
-
});
|
|
975
|
-
});
|
|
976
|
-
const newEdges = model.get("edges") || [];
|
|
977
|
-
newEdges.forEach((edge) => {
|
|
978
|
-
graph.addEdge(edge.source, edge.target, {
|
|
979
|
-
label: edge.label || "",
|
|
980
|
-
size: edge.size || 2,
|
|
981
|
-
color: edge.color || "#94a3b8",
|
|
982
|
-
});
|
|
983
|
-
});
|
|
984
|
-
renderer.refresh();
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
model.on("change:edges", () => {
|
|
988
|
-
graph.clearEdges();
|
|
989
|
-
const newEdges = model.get("edges") || [];
|
|
990
|
-
newEdges.forEach((edge) => {
|
|
991
|
-
graph.addEdge(edge.source, edge.target, {
|
|
992
|
-
label: edge.label || "",
|
|
993
|
-
size: edge.size || 2,
|
|
994
|
-
color: edge.color || "#94a3b8",
|
|
995
|
-
});
|
|
996
|
-
});
|
|
997
|
-
renderer.refresh();
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
return () => {
|
|
1001
|
-
renderer.kill();
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
export default { render };
|
|
1006
|
-
"""
|
|
1007
|
-
|
|
1008
|
-
_CSS = """
|
|
1009
|
-
/* === CSS VARIABLES === */
|
|
1010
|
-
.awg-wrapper {
|
|
1011
|
-
--awg-bg: #ffffff;
|
|
1012
|
-
--awg-bg-secondary: #f8f9fa;
|
|
1013
|
-
--awg-bg-tertiary: #f3f4f6;
|
|
1014
|
-
--awg-border: #e5e7eb;
|
|
1015
|
-
--awg-text: #111827;
|
|
1016
|
-
--awg-text-secondary: #6b7280;
|
|
1017
|
-
--awg-text-muted: #9ca3af;
|
|
1018
|
-
--awg-accent: #6366f1;
|
|
1019
|
-
--awg-accent-hover: #4f46e5;
|
|
1020
|
-
--awg-graph-bg: #fafafa;
|
|
1021
|
-
--awg-input-bg: #ffffff;
|
|
1022
|
-
--awg-input-border: #d1d5db;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
.awg-wrapper.awg-dark {
|
|
1026
|
-
--awg-bg: #1a1a2e;
|
|
1027
|
-
--awg-bg-secondary: #16213e;
|
|
1028
|
-
--awg-bg-tertiary: #0f3460;
|
|
1029
|
-
--awg-border: #2d3748;
|
|
1030
|
-
--awg-text: #e2e8f0;
|
|
1031
|
-
--awg-text-secondary: #a0aec0;
|
|
1032
|
-
--awg-text-muted: #718096;
|
|
1033
|
-
--awg-accent: #818cf8;
|
|
1034
|
-
--awg-accent-hover: #6366f1;
|
|
1035
|
-
--awg-graph-bg: #0f0f1a;
|
|
1036
|
-
--awg-input-bg: #1e1e32;
|
|
1037
|
-
--awg-input-border: #3d4a5c;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
/* === BASE === */
|
|
1041
|
-
.awg-wrapper {
|
|
1042
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
1043
|
-
display: flex;
|
|
1044
|
-
flex-direction: column;
|
|
1045
|
-
border: 1px solid var(--awg-border);
|
|
1046
|
-
border-radius: 8px;
|
|
1047
|
-
overflow: hidden;
|
|
1048
|
-
background: var(--awg-bg);
|
|
1049
|
-
color: var(--awg-text);
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
.awg-content {
|
|
1053
|
-
display: flex;
|
|
1054
|
-
position: relative;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/* === TOOLBAR === */
|
|
1058
|
-
.awg-toolbar {
|
|
1059
|
-
display: flex;
|
|
1060
|
-
align-items: center;
|
|
1061
|
-
gap: 8px;
|
|
1062
|
-
padding: 8px 12px;
|
|
1063
|
-
background: var(--awg-bg-secondary);
|
|
1064
|
-
border-bottom: 1px solid var(--awg-border);
|
|
1065
|
-
min-height: 44px;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
.awg-query-container {
|
|
1069
|
-
flex: 1;
|
|
1070
|
-
display: flex;
|
|
1071
|
-
align-items: center;
|
|
1072
|
-
gap: 8px;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
.awg-query-input {
|
|
1076
|
-
flex: 1;
|
|
1077
|
-
padding: 8px 12px;
|
|
1078
|
-
border: 1px solid var(--awg-input-border);
|
|
1079
|
-
border-radius: 6px;
|
|
1080
|
-
font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", monospace;
|
|
1081
|
-
font-size: 13px;
|
|
1082
|
-
background: var(--awg-input-bg);
|
|
1083
|
-
color: var(--awg-text);
|
|
1084
|
-
transition: border-color 0.15s, box-shadow 0.15s;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
.awg-query-input:focus {
|
|
1088
|
-
outline: none;
|
|
1089
|
-
border-color: var(--awg-accent);
|
|
1090
|
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
.awg-query-input::placeholder {
|
|
1094
|
-
color: var(--awg-text-muted);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
/* === BUTTONS === */
|
|
1098
|
-
.awg-btn {
|
|
1099
|
-
display: inline-flex;
|
|
1100
|
-
align-items: center;
|
|
1101
|
-
justify-content: center;
|
|
1102
|
-
min-width: 36px;
|
|
1103
|
-
height: 36px;
|
|
1104
|
-
padding: 0 10px;
|
|
1105
|
-
border: 1px solid var(--awg-input-border);
|
|
1106
|
-
border-radius: 6px;
|
|
1107
|
-
background: var(--awg-input-bg);
|
|
1108
|
-
color: var(--awg-text);
|
|
1109
|
-
cursor: pointer;
|
|
1110
|
-
transition: all 0.15s;
|
|
1111
|
-
position: relative;
|
|
1112
|
-
font-size: 13px;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
.awg-btn:hover {
|
|
1116
|
-
background: var(--awg-bg-tertiary);
|
|
1117
|
-
border-color: var(--awg-text-muted);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
.awg-btn:disabled {
|
|
1121
|
-
opacity: 0.5;
|
|
1122
|
-
cursor: not-allowed;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
.awg-btn-primary {
|
|
1126
|
-
background: var(--awg-accent);
|
|
1127
|
-
border-color: var(--awg-accent);
|
|
1128
|
-
color: #fff;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
.awg-btn-primary:hover {
|
|
1132
|
-
background: var(--awg-accent-hover);
|
|
1133
|
-
border-color: var(--awg-accent-hover);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
.awg-btn-primary:disabled {
|
|
1137
|
-
background: var(--awg-text-muted);
|
|
1138
|
-
border-color: var(--awg-text-muted);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
.awg-btn-full {
|
|
1142
|
-
width: 100%;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/* === STATUS DOTS === */
|
|
1146
|
-
.awg-status-dot {
|
|
1147
|
-
position: absolute;
|
|
1148
|
-
top: 4px;
|
|
1149
|
-
right: 4px;
|
|
1150
|
-
width: 8px;
|
|
1151
|
-
height: 8px;
|
|
1152
|
-
border-radius: 50%;
|
|
1153
|
-
border: 1.5px solid var(--awg-bg);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
.awg-status-dot-inline {
|
|
1157
|
-
display: inline-block;
|
|
1158
|
-
width: 8px;
|
|
1159
|
-
height: 8px;
|
|
1160
|
-
border-radius: 50%;
|
|
1161
|
-
margin-right: 6px;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
.awg-status-disconnected { background: #6b7280; }
|
|
1165
|
-
.awg-status-connecting { background: #f59e0b; animation: awg-pulse 1s infinite; }
|
|
1166
|
-
.awg-status-connected { background: #22c55e; }
|
|
1167
|
-
.awg-status-error { background: #ef4444; }
|
|
1168
|
-
|
|
1169
|
-
@keyframes awg-pulse {
|
|
1170
|
-
0%, 100% { opacity: 1; }
|
|
1171
|
-
50% { opacity: 0.4; }
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/* === SPINNER === */
|
|
1175
|
-
.awg-spinner {
|
|
1176
|
-
width: 14px;
|
|
1177
|
-
height: 14px;
|
|
1178
|
-
border: 2px solid rgba(255,255,255,0.3);
|
|
1179
|
-
border-top-color: #fff;
|
|
1180
|
-
border-radius: 50%;
|
|
1181
|
-
animation: awg-spin 0.8s linear infinite;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
@keyframes awg-spin {
|
|
1185
|
-
to { transform: rotate(360deg); }
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/* === GRAPH CONTAINER === */
|
|
1189
|
-
.awg-graph-container {
|
|
1190
|
-
flex: 1;
|
|
1191
|
-
background: var(--awg-graph-bg);
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
/* === SETTINGS PANEL === */
|
|
1195
|
-
.awg-settings-panel {
|
|
1196
|
-
width: 0;
|
|
1197
|
-
overflow: hidden;
|
|
1198
|
-
background: var(--awg-bg-secondary);
|
|
1199
|
-
border-left: 1px solid var(--awg-border);
|
|
1200
|
-
transition: width 0.2s ease;
|
|
1201
|
-
display: flex;
|
|
1202
|
-
flex-direction: column;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
.awg-settings-panel.awg-panel-open {
|
|
1206
|
-
width: 260px;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
.awg-panel-header {
|
|
1210
|
-
padding: 12px 16px;
|
|
1211
|
-
font-weight: 600;
|
|
1212
|
-
font-size: 13px;
|
|
1213
|
-
text-transform: uppercase;
|
|
1214
|
-
letter-spacing: 0.5px;
|
|
1215
|
-
color: var(--awg-text-secondary);
|
|
1216
|
-
border-bottom: 1px solid var(--awg-border);
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
.awg-panel-form {
|
|
1220
|
-
padding: 16px;
|
|
1221
|
-
display: flex;
|
|
1222
|
-
flex-direction: column;
|
|
1223
|
-
gap: 14px;
|
|
1224
|
-
flex: 1;
|
|
1225
|
-
overflow-y: auto;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
.awg-panel-actions {
|
|
1229
|
-
padding: 12px 16px;
|
|
1230
|
-
border-top: 1px solid var(--awg-border);
|
|
1231
|
-
display: flex;
|
|
1232
|
-
flex-direction: column;
|
|
1233
|
-
gap: 8px;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
.awg-connection-status {
|
|
1237
|
-
font-size: 12px;
|
|
1238
|
-
color: var(--awg-text-secondary);
|
|
1239
|
-
display: flex;
|
|
1240
|
-
align-items: center;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
.awg-panel-error {
|
|
1244
|
-
padding: 10px 16px;
|
|
1245
|
-
background: rgba(239, 68, 68, 0.1);
|
|
1246
|
-
color: #ef4444;
|
|
1247
|
-
font-size: 12px;
|
|
1248
|
-
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
/* === FORM ELEMENTS === */
|
|
1252
|
-
.awg-form-group {
|
|
1253
|
-
display: flex;
|
|
1254
|
-
flex-direction: column;
|
|
1255
|
-
gap: 5px;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
.awg-label {
|
|
1259
|
-
font-size: 11px;
|
|
1260
|
-
font-weight: 600;
|
|
1261
|
-
text-transform: uppercase;
|
|
1262
|
-
letter-spacing: 0.3px;
|
|
1263
|
-
color: var(--awg-text-secondary);
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
.awg-input, .awg-select {
|
|
1267
|
-
padding: 8px 10px;
|
|
1268
|
-
border: 1px solid var(--awg-input-border);
|
|
1269
|
-
border-radius: 5px;
|
|
1270
|
-
font-size: 13px;
|
|
1271
|
-
background: var(--awg-input-bg);
|
|
1272
|
-
color: var(--awg-text);
|
|
1273
|
-
transition: border-color 0.15s, box-shadow 0.15s;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
.awg-input:focus, .awg-select:focus {
|
|
1277
|
-
outline: none;
|
|
1278
|
-
border-color: var(--awg-accent);
|
|
1279
|
-
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
.awg-select {
|
|
1283
|
-
cursor: pointer;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
.awg-select option:disabled {
|
|
1287
|
-
color: var(--awg-text-muted);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/* === TOGGLE SWITCH === */
|
|
1291
|
-
.awg-toggle {
|
|
1292
|
-
display: flex;
|
|
1293
|
-
align-items: center;
|
|
1294
|
-
cursor: pointer;
|
|
1295
|
-
gap: 10px;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
.awg-toggle input {
|
|
1299
|
-
display: none;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
.awg-toggle-slider {
|
|
1303
|
-
width: 36px;
|
|
1304
|
-
height: 20px;
|
|
1305
|
-
background: var(--awg-input-border);
|
|
1306
|
-
border-radius: 10px;
|
|
1307
|
-
position: relative;
|
|
1308
|
-
transition: background 0.2s;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
.awg-toggle-slider::after {
|
|
1312
|
-
content: "";
|
|
1313
|
-
position: absolute;
|
|
1314
|
-
top: 2px;
|
|
1315
|
-
left: 2px;
|
|
1316
|
-
width: 16px;
|
|
1317
|
-
height: 16px;
|
|
1318
|
-
background: #fff;
|
|
1319
|
-
border-radius: 50%;
|
|
1320
|
-
transition: transform 0.2s;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
.awg-toggle input:checked + .awg-toggle-slider {
|
|
1324
|
-
background: var(--awg-accent);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
.awg-toggle input:checked + .awg-toggle-slider::after {
|
|
1328
|
-
transform: translateX(16px);
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
.awg-toggle-label {
|
|
1332
|
-
font-size: 13px;
|
|
1333
|
-
color: var(--awg-text);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
/* === SCHEMA PANEL (LEFT) === */
|
|
1337
|
-
.awg-schema-panel {
|
|
1338
|
-
width: 0;
|
|
1339
|
-
overflow: hidden;
|
|
1340
|
-
background: var(--awg-bg-secondary);
|
|
1341
|
-
border-right: 1px solid var(--awg-border);
|
|
1342
|
-
transition: width 0.2s ease;
|
|
1343
|
-
display: flex;
|
|
1344
|
-
flex-direction: column;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
.awg-schema-panel.awg-panel-open {
|
|
1348
|
-
width: 220px;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
.awg-schema-content {
|
|
1352
|
-
flex: 1;
|
|
1353
|
-
overflow-y: auto;
|
|
1354
|
-
padding: 8px 0;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
.awg-schema-empty {
|
|
1358
|
-
padding: 20px 16px;
|
|
1359
|
-
color: var(--awg-text-muted);
|
|
1360
|
-
font-size: 12px;
|
|
1361
|
-
text-align: center;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
.awg-schema-section {
|
|
1365
|
-
margin-bottom: 8px;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
.awg-schema-section-header {
|
|
1369
|
-
display: flex;
|
|
1370
|
-
align-items: center;
|
|
1371
|
-
gap: 8px;
|
|
1372
|
-
padding: 8px 16px;
|
|
1373
|
-
font-size: 11px;
|
|
1374
|
-
font-weight: 600;
|
|
1375
|
-
text-transform: uppercase;
|
|
1376
|
-
letter-spacing: 0.3px;
|
|
1377
|
-
color: var(--awg-text-secondary);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
.awg-schema-item {
|
|
1381
|
-
border-bottom: 1px solid var(--awg-border);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
.awg-schema-item:last-child {
|
|
1385
|
-
border-bottom: none;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
.awg-schema-item-header {
|
|
1389
|
-
display: flex;
|
|
1390
|
-
align-items: center;
|
|
1391
|
-
gap: 6px;
|
|
1392
|
-
padding: 8px 16px;
|
|
1393
|
-
cursor: pointer;
|
|
1394
|
-
transition: background 0.1s;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
.awg-schema-item-header:hover {
|
|
1398
|
-
background: var(--awg-bg-tertiary);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
.awg-schema-expand {
|
|
1402
|
-
color: var(--awg-text-muted);
|
|
1403
|
-
transition: transform 0.15s;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
.awg-schema-item-expanded .awg-schema-expand {
|
|
1407
|
-
transform: rotate(90deg);
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
.awg-schema-name {
|
|
1411
|
-
font-size: 13px;
|
|
1412
|
-
color: var(--awg-accent);
|
|
1413
|
-
cursor: pointer;
|
|
1414
|
-
flex: 1;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
.awg-schema-name:hover {
|
|
1418
|
-
text-decoration: underline;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
.awg-schema-props {
|
|
1422
|
-
display: none;
|
|
1423
|
-
padding: 4px 16px 8px 32px;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
.awg-schema-item-expanded .awg-schema-props {
|
|
1427
|
-
display: block;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
.awg-schema-prop {
|
|
1431
|
-
display: flex;
|
|
1432
|
-
align-items: center;
|
|
1433
|
-
gap: 6px;
|
|
1434
|
-
padding: 4px 8px;
|
|
1435
|
-
font-size: 12px;
|
|
1436
|
-
color: var(--awg-text-secondary);
|
|
1437
|
-
cursor: pointer;
|
|
1438
|
-
border-radius: 4px;
|
|
1439
|
-
transition: background 0.1s;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
.awg-schema-prop:hover {
|
|
1443
|
-
background: var(--awg-bg-tertiary);
|
|
1444
|
-
color: var(--awg-text);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
.awg-schema-prop-empty {
|
|
1448
|
-
color: var(--awg-text-muted);
|
|
1449
|
-
font-style: italic;
|
|
1450
|
-
cursor: default;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
.awg-schema-prop-empty:hover {
|
|
1454
|
-
background: transparent;
|
|
1455
|
-
color: var(--awg-text-muted);
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
/* === PROPERTIES PANEL (RIGHT) === */
|
|
1459
|
-
.awg-properties-panel {
|
|
1460
|
-
width: 0;
|
|
1461
|
-
overflow: hidden;
|
|
1462
|
-
background: var(--awg-bg-secondary);
|
|
1463
|
-
border-left: 1px solid var(--awg-border);
|
|
1464
|
-
transition: width 0.2s ease;
|
|
1465
|
-
display: flex;
|
|
1466
|
-
flex-direction: column;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
.awg-properties-panel.awg-panel-open {
|
|
1470
|
-
width: 260px;
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
.awg-properties-content {
|
|
1474
|
-
flex: 1;
|
|
1475
|
-
overflow-y: auto;
|
|
1476
|
-
padding: 8px 0;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
.awg-properties-empty {
|
|
1480
|
-
padding: 20px 16px;
|
|
1481
|
-
color: var(--awg-text-muted);
|
|
1482
|
-
font-size: 12px;
|
|
1483
|
-
text-align: center;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
.awg-properties-header {
|
|
1487
|
-
display: flex;
|
|
1488
|
-
align-items: center;
|
|
1489
|
-
gap: 8px;
|
|
1490
|
-
padding: 10px 16px;
|
|
1491
|
-
font-size: 12px;
|
|
1492
|
-
font-weight: 600;
|
|
1493
|
-
color: var(--awg-text-secondary);
|
|
1494
|
-
border-bottom: 1px solid var(--awg-border);
|
|
1495
|
-
background: var(--awg-bg-tertiary);
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
.awg-properties-type {
|
|
1499
|
-
text-transform: uppercase;
|
|
1500
|
-
letter-spacing: 0.3px;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
.awg-property-item {
|
|
1504
|
-
display: flex;
|
|
1505
|
-
flex-direction: column;
|
|
1506
|
-
padding: 8px 16px;
|
|
1507
|
-
border-bottom: 1px solid var(--awg-border);
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
.awg-property-item:last-child {
|
|
1511
|
-
border-bottom: none;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
.awg-property-key {
|
|
1515
|
-
font-size: 10px;
|
|
1516
|
-
font-weight: 600;
|
|
1517
|
-
text-transform: uppercase;
|
|
1518
|
-
letter-spacing: 0.3px;
|
|
1519
|
-
color: var(--awg-text-muted);
|
|
1520
|
-
margin-bottom: 2px;
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
.awg-property-value {
|
|
1524
|
-
font-size: 13px;
|
|
1525
|
-
color: var(--awg-text);
|
|
1526
|
-
word-break: break-all;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
.awg-property-truncate {
|
|
1530
|
-
overflow: hidden;
|
|
1531
|
-
text-overflow: ellipsis;
|
|
1532
|
-
white-space: nowrap;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
.awg-property-labels {
|
|
1536
|
-
display: flex;
|
|
1537
|
-
flex-wrap: wrap;
|
|
1538
|
-
gap: 4px;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
.awg-label-tag {
|
|
1542
|
-
display: inline-block;
|
|
1543
|
-
padding: 2px 8px;
|
|
1544
|
-
font-size: 11px;
|
|
1545
|
-
font-weight: 500;
|
|
1546
|
-
background: var(--awg-accent);
|
|
1547
|
-
color: #fff;
|
|
1548
|
-
border-radius: 10px;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
.awg-edge-tag {
|
|
1552
|
-
display: inline-block;
|
|
1553
|
-
padding: 2px 8px;
|
|
1554
|
-
font-size: 11px;
|
|
1555
|
-
font-weight: 500;
|
|
1556
|
-
background: var(--awg-bg-tertiary);
|
|
1557
|
-
color: var(--awg-text);
|
|
1558
|
-
border-radius: 4px;
|
|
1559
|
-
border: 1px solid var(--awg-border);
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
/* === SMALL BUTTONS === */
|
|
1563
|
-
.awg-btn-sm {
|
|
1564
|
-
min-width: 24px;
|
|
1565
|
-
height: 24px;
|
|
1566
|
-
padding: 0 4px;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
.awg-panel-header {
|
|
1570
|
-
display: flex;
|
|
1571
|
-
align-items: center;
|
|
1572
|
-
justify-content: space-between;
|
|
1573
|
-
}
|
|
1574
|
-
"""
|
|
1575
|
-
|
|
1576
18
|
|
|
1577
19
|
class Graph(anywidget.AnyWidget):
|
|
1578
|
-
"""Interactive graph visualization widget using Sigma.js.
|
|
20
|
+
"""Interactive graph visualization widget using Sigma.js.
|
|
21
|
+
|
|
22
|
+
Supports Neo4j (browser-side) and Grafeo (Python-side) backends.
|
|
23
|
+
|
|
24
|
+
Examples
|
|
25
|
+
--------
|
|
26
|
+
Basic usage with static data:
|
|
27
|
+
|
|
28
|
+
>>> graph = Graph(
|
|
29
|
+
... nodes=[{"id": "a", "label": "Alice"}, {"id": "b", "label": "Bob"}],
|
|
30
|
+
... edges=[{"source": "a", "target": "b", "label": "KNOWS"}],
|
|
31
|
+
... )
|
|
32
|
+
|
|
33
|
+
With Neo4j connection:
|
|
34
|
+
|
|
35
|
+
>>> graph = Graph(
|
|
36
|
+
... database_backend="neo4j",
|
|
37
|
+
... connection_uri="neo4j+s://demo.neo4jlabs.com",
|
|
38
|
+
... connection_username="neo4j",
|
|
39
|
+
... connection_password="password",
|
|
40
|
+
... )
|
|
41
|
+
|
|
42
|
+
With Grafeo backend:
|
|
43
|
+
|
|
44
|
+
>>> import grafeo
|
|
45
|
+
>>> db = grafeo.GrafeoDB()
|
|
46
|
+
>>> graph = Graph(database_backend="grafeo", grafeo_db=db)
|
|
47
|
+
"""
|
|
1579
48
|
|
|
1580
|
-
_esm =
|
|
1581
|
-
_css =
|
|
49
|
+
_esm = get_esm()
|
|
50
|
+
_css = get_css()
|
|
1582
51
|
|
|
1583
|
-
# Graph
|
|
52
|
+
# === Graph Data ===
|
|
1584
53
|
nodes = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
1585
54
|
edges = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
1586
55
|
|
|
1587
|
-
# Display
|
|
56
|
+
# === Display Settings ===
|
|
1588
57
|
width = traitlets.Int(default_value=800).tag(sync=True)
|
|
1589
58
|
height = traitlets.Int(default_value=600).tag(sync=True)
|
|
1590
59
|
background = traitlets.Unicode(default_value="#fafafa").tag(sync=True)
|
|
1591
60
|
show_labels = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1592
61
|
show_edge_labels = traitlets.Bool(default_value=False).tag(sync=True)
|
|
1593
62
|
|
|
1594
|
-
# Selection
|
|
63
|
+
# === Selection State ===
|
|
1595
64
|
selected_node = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
1596
65
|
selected_edge = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
1597
66
|
|
|
1598
|
-
# Toolbar
|
|
67
|
+
# === Toolbar Visibility ===
|
|
1599
68
|
show_toolbar = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1600
69
|
show_settings = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1601
70
|
show_query_input = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1602
71
|
|
|
1603
|
-
# Theme
|
|
72
|
+
# === Theme ===
|
|
1604
73
|
dark_mode = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1605
74
|
|
|
1606
|
-
# Database
|
|
75
|
+
# === Database Backend ===
|
|
1607
76
|
database_backend = traitlets.Unicode(default_value="neo4j").tag(sync=True)
|
|
1608
77
|
|
|
1609
|
-
# Neo4j
|
|
78
|
+
# === Neo4j Connection (browser-side) ===
|
|
1610
79
|
connection_uri = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1611
80
|
connection_username = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1612
81
|
connection_password = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1613
82
|
connection_database = traitlets.Unicode(default_value="neo4j").tag(sync=True)
|
|
1614
83
|
|
|
1615
|
-
# Query
|
|
84
|
+
# === Query State ===
|
|
1616
85
|
query = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1617
86
|
query_language = traitlets.Unicode(default_value="cypher").tag(sync=True)
|
|
1618
87
|
query_running = traitlets.Bool(default_value=False).tag(sync=True)
|
|
1619
88
|
query_error = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1620
89
|
|
|
1621
|
-
# Connection
|
|
90
|
+
# === Connection State ===
|
|
1622
91
|
connection_status = traitlets.Unicode(default_value="disconnected").tag(sync=True)
|
|
1623
92
|
|
|
1624
|
-
# Schema
|
|
93
|
+
# === Schema Data ===
|
|
1625
94
|
schema_node_types = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
1626
95
|
schema_edge_types = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
1627
96
|
|
|
1628
|
-
# Query
|
|
97
|
+
# === Query Execution Trigger (for Python backends) ===
|
|
1629
98
|
_execute_query = traitlets.Int(default_value=0).tag(sync=True)
|
|
1630
99
|
|
|
1631
100
|
def __init__(
|
|
@@ -1648,8 +117,50 @@ class Graph(anywidget.AnyWidget):
|
|
|
1648
117
|
connection_password: str = "",
|
|
1649
118
|
connection_database: str = "neo4j",
|
|
1650
119
|
grafeo_db: Any = None,
|
|
120
|
+
backend: DatabaseBackend | None = None,
|
|
1651
121
|
**kwargs: Any,
|
|
1652
122
|
) -> None:
|
|
123
|
+
"""Initialize the Graph widget.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
nodes : list[dict], optional
|
|
128
|
+
List of node dictionaries with 'id' and optional 'label', 'color', etc.
|
|
129
|
+
edges : list[dict], optional
|
|
130
|
+
List of edge dictionaries with 'source', 'target', and optional 'label'.
|
|
131
|
+
width : int
|
|
132
|
+
Widget width in pixels.
|
|
133
|
+
height : int
|
|
134
|
+
Widget height in pixels.
|
|
135
|
+
background : str
|
|
136
|
+
Background color for the graph area.
|
|
137
|
+
show_labels : bool
|
|
138
|
+
Whether to show node labels.
|
|
139
|
+
show_edge_labels : bool
|
|
140
|
+
Whether to show edge labels.
|
|
141
|
+
show_toolbar : bool
|
|
142
|
+
Whether to show the toolbar.
|
|
143
|
+
show_settings : bool
|
|
144
|
+
Whether to show the settings button.
|
|
145
|
+
show_query_input : bool
|
|
146
|
+
Whether to show the query input.
|
|
147
|
+
dark_mode : bool
|
|
148
|
+
Whether to use dark theme.
|
|
149
|
+
database_backend : str
|
|
150
|
+
Database backend: "neo4j" or "grafeo".
|
|
151
|
+
connection_uri : str
|
|
152
|
+
Neo4j connection URI.
|
|
153
|
+
connection_username : str
|
|
154
|
+
Neo4j username.
|
|
155
|
+
connection_password : str
|
|
156
|
+
Neo4j password.
|
|
157
|
+
connection_database : str
|
|
158
|
+
Neo4j database name.
|
|
159
|
+
grafeo_db : Any
|
|
160
|
+
Grafeo database instance for Python-side execution (legacy).
|
|
161
|
+
backend : DatabaseBackend | None
|
|
162
|
+
Generic database backend implementing the DatabaseBackend protocol.
|
|
163
|
+
"""
|
|
1653
164
|
super().__init__(
|
|
1654
165
|
nodes=nodes or [],
|
|
1655
166
|
edges=edges or [],
|
|
@@ -1671,33 +182,55 @@ class Graph(anywidget.AnyWidget):
|
|
|
1671
182
|
)
|
|
1672
183
|
self._node_click_callbacks: list[Callable] = []
|
|
1673
184
|
self._edge_click_callbacks: list[Callable] = []
|
|
1674
|
-
|
|
185
|
+
|
|
186
|
+
# Support both legacy grafeo_db and new generic backend
|
|
187
|
+
if backend is not None:
|
|
188
|
+
self._backend = backend
|
|
189
|
+
elif grafeo_db is not None:
|
|
190
|
+
self._backend = GrafeoBackend(grafeo_db)
|
|
191
|
+
else:
|
|
192
|
+
self._backend = None
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def backend(self) -> DatabaseBackend | None:
|
|
196
|
+
"""Get the current database backend."""
|
|
197
|
+
return self._backend
|
|
198
|
+
|
|
199
|
+
@backend.setter
|
|
200
|
+
def backend(self, value: DatabaseBackend | None) -> None:
|
|
201
|
+
"""Set the database backend."""
|
|
202
|
+
self._backend = value
|
|
1675
203
|
|
|
1676
204
|
@property
|
|
1677
205
|
def grafeo_db(self) -> Any:
|
|
1678
|
-
"""Get the Grafeo database instance."""
|
|
1679
|
-
|
|
206
|
+
"""Get the Grafeo database instance (legacy property)."""
|
|
207
|
+
if isinstance(self._backend, GrafeoBackend):
|
|
208
|
+
return self._backend.db
|
|
209
|
+
return None
|
|
1680
210
|
|
|
1681
211
|
@grafeo_db.setter
|
|
1682
212
|
def grafeo_db(self, value: Any) -> None:
|
|
1683
|
-
"""Set the Grafeo database instance."""
|
|
1684
|
-
self.
|
|
213
|
+
"""Set the Grafeo database instance (legacy property)."""
|
|
214
|
+
self._backend = GrafeoBackend(value) if value else None
|
|
1685
215
|
|
|
1686
216
|
@observe("_execute_query")
|
|
1687
217
|
def _on_execute_query(self, change: dict) -> None:
|
|
1688
218
|
"""Handle query execution trigger from JavaScript."""
|
|
1689
219
|
if change["new"] == 0:
|
|
1690
220
|
return # Skip initial value
|
|
1691
|
-
if self.
|
|
1692
|
-
self.
|
|
221
|
+
if self._backend is not None:
|
|
222
|
+
self._execute_backend_query()
|
|
223
|
+
|
|
224
|
+
def _execute_backend_query(self) -> None:
|
|
225
|
+
"""Execute query against the configured backend."""
|
|
226
|
+
if not self._backend:
|
|
227
|
+
self.query_error = "No database backend configured"
|
|
228
|
+
return
|
|
1693
229
|
|
|
1694
|
-
def _execute_grafeo_query(self) -> None:
|
|
1695
|
-
"""Execute query against Grafeo database."""
|
|
1696
230
|
try:
|
|
1697
231
|
self.query_running = True
|
|
1698
232
|
self.query_error = ""
|
|
1699
|
-
|
|
1700
|
-
nodes, edges = self._process_grafeo_result(result)
|
|
233
|
+
nodes, edges = self._backend.execute(self.query)
|
|
1701
234
|
self.nodes = nodes
|
|
1702
235
|
self.edges = edges
|
|
1703
236
|
except Exception as e:
|
|
@@ -1705,68 +238,216 @@ class Graph(anywidget.AnyWidget):
|
|
|
1705
238
|
finally:
|
|
1706
239
|
self.query_running = False
|
|
1707
240
|
|
|
1708
|
-
def _process_grafeo_result(self, result: Any) -> tuple[list[dict], list[dict]]:
|
|
1709
|
-
"""Process Grafeo query results into nodes and edges."""
|
|
1710
|
-
nodes: dict[str, dict] = {}
|
|
1711
|
-
edges: list[dict] = []
|
|
1712
|
-
|
|
1713
|
-
# Handle result as iterable of records
|
|
1714
|
-
records = list(result) if hasattr(result, "__iter__") else [result]
|
|
1715
|
-
|
|
1716
|
-
for record in records:
|
|
1717
|
-
# Try to extract items from record
|
|
1718
|
-
if hasattr(record, "items"):
|
|
1719
|
-
items = record.items() if callable(record.items) else record.items
|
|
1720
|
-
elif hasattr(record, "data"):
|
|
1721
|
-
items = record.data().items()
|
|
1722
|
-
elif isinstance(record, dict):
|
|
1723
|
-
items = record.items()
|
|
1724
|
-
else:
|
|
1725
|
-
continue
|
|
1726
|
-
|
|
1727
|
-
for key, value in items:
|
|
1728
|
-
if _is_node(value):
|
|
1729
|
-
node_id = _get_node_id(value)
|
|
1730
|
-
if node_id not in nodes:
|
|
1731
|
-
nodes[node_id] = _node_to_dict(value)
|
|
1732
|
-
elif _is_relationship(value):
|
|
1733
|
-
edges.append(_relationship_to_dict(value))
|
|
1734
|
-
|
|
1735
|
-
return list(nodes.values()), edges
|
|
1736
|
-
|
|
1737
241
|
@classmethod
|
|
1738
242
|
def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> Graph:
|
|
1739
|
-
"""Create a Graph from a dictionary with nodes and edges keys.
|
|
243
|
+
"""Create a Graph from a dictionary with 'nodes' and 'edges' keys.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
data : dict
|
|
248
|
+
Dictionary with 'nodes' and 'edges' lists.
|
|
249
|
+
**kwargs
|
|
250
|
+
Additional arguments passed to Graph constructor.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
Graph
|
|
255
|
+
New Graph instance.
|
|
256
|
+
"""
|
|
1740
257
|
return cls(nodes=data.get("nodes", []), edges=data.get("edges", []), **kwargs)
|
|
1741
258
|
|
|
1742
259
|
@classmethod
|
|
1743
260
|
def from_cypher(cls, result: Any, **kwargs: Any) -> Graph:
|
|
1744
|
-
"""Create a Graph from Cypher query results.
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
261
|
+
"""Create a Graph from Cypher query results.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
result : Any
|
|
266
|
+
Query result from Neo4j driver or similar.
|
|
267
|
+
**kwargs
|
|
268
|
+
Additional arguments passed to Graph constructor.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
Graph
|
|
273
|
+
New Graph instance with extracted nodes and edges.
|
|
274
|
+
|
|
275
|
+
Example
|
|
276
|
+
-------
|
|
277
|
+
>>> from neo4j import GraphDatabase
|
|
278
|
+
>>> driver = GraphDatabase.driver(uri, auth=auth)
|
|
279
|
+
>>> with driver.session() as session:
|
|
280
|
+
... result = session.run("MATCH (n)-[r]->(m) RETURN n, r, m")
|
|
281
|
+
... graph = Graph.from_cypher(result)
|
|
282
|
+
"""
|
|
283
|
+
from anywidget_graph.converters import CypherConverter
|
|
284
|
+
|
|
285
|
+
data = CypherConverter().convert(result)
|
|
286
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def from_gql(cls, result: Any, **kwargs: Any) -> Graph:
|
|
290
|
+
"""Create a Graph from GQL query results.
|
|
291
|
+
|
|
292
|
+
GQL (ISO Graph Query Language) uses a similar format to Cypher.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
result : Any
|
|
297
|
+
Query result from GQL-compatible database.
|
|
298
|
+
**kwargs
|
|
299
|
+
Additional arguments passed to Graph constructor.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
Graph
|
|
304
|
+
New Graph instance with extracted nodes and edges.
|
|
305
|
+
"""
|
|
306
|
+
from anywidget_graph.converters import GQLConverter
|
|
307
|
+
|
|
308
|
+
data = GQLConverter().convert(result)
|
|
309
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def from_sparql(
|
|
313
|
+
cls,
|
|
314
|
+
result: Any,
|
|
315
|
+
subject_var: str = "s",
|
|
316
|
+
predicate_var: str = "p",
|
|
317
|
+
object_var: str = "o",
|
|
318
|
+
**kwargs: Any,
|
|
319
|
+
) -> Graph:
|
|
320
|
+
"""Create a Graph from SPARQL query results.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
result : Any
|
|
325
|
+
SPARQL query result (e.g., from RDFLib or SPARQLWrapper).
|
|
326
|
+
subject_var : str
|
|
327
|
+
Variable name for subjects (default: "s").
|
|
328
|
+
predicate_var : str
|
|
329
|
+
Variable name for predicates (default: "p").
|
|
330
|
+
object_var : str
|
|
331
|
+
Variable name for objects (default: "o").
|
|
332
|
+
**kwargs
|
|
333
|
+
Additional arguments passed to Graph constructor.
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
Graph
|
|
338
|
+
New Graph instance with extracted nodes and edges.
|
|
339
|
+
|
|
340
|
+
Example
|
|
341
|
+
-------
|
|
342
|
+
>>> from rdflib import Graph as RDFGraph
|
|
343
|
+
>>> g = RDFGraph()
|
|
344
|
+
>>> g.parse("data.ttl")
|
|
345
|
+
>>> result = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
346
|
+
>>> graph = Graph.from_sparql(result)
|
|
347
|
+
"""
|
|
348
|
+
from anywidget_graph.converters import SPARQLConverter
|
|
349
|
+
|
|
350
|
+
converter = SPARQLConverter(
|
|
351
|
+
subject_var=subject_var,
|
|
352
|
+
predicate_var=predicate_var,
|
|
353
|
+
object_var=object_var,
|
|
354
|
+
)
|
|
355
|
+
data = converter.convert(result)
|
|
356
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def from_gremlin(cls, result: Any, **kwargs: Any) -> Graph:
|
|
360
|
+
"""Create a Graph from Gremlin/TinkerPop query results.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
result : Any
|
|
365
|
+
Gremlin traversal result.
|
|
366
|
+
**kwargs
|
|
367
|
+
Additional arguments passed to Graph constructor.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
Graph
|
|
372
|
+
New Graph instance with extracted nodes and edges.
|
|
373
|
+
|
|
374
|
+
Example
|
|
375
|
+
-------
|
|
376
|
+
>>> from gremlin_python.driver import client
|
|
377
|
+
>>> gremlin_client = client.Client('ws://localhost:8182/gremlin', 'g')
|
|
378
|
+
>>> result = gremlin_client.submit("g.V().limit(10)").all().result()
|
|
379
|
+
>>> graph = Graph.from_gremlin(result)
|
|
380
|
+
"""
|
|
381
|
+
from anywidget_graph.converters import GremlinConverter
|
|
382
|
+
|
|
383
|
+
data = GremlinConverter().convert(result)
|
|
384
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def from_graphql(
|
|
388
|
+
cls,
|
|
389
|
+
result: Any,
|
|
390
|
+
nodes_path: str | None = None,
|
|
391
|
+
edges_path: str | None = None,
|
|
392
|
+
id_field: str = "id",
|
|
393
|
+
label_field: str = "name",
|
|
394
|
+
**kwargs: Any,
|
|
395
|
+
) -> Graph:
|
|
396
|
+
"""Create a Graph from GraphQL JSON response.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
result : Any
|
|
401
|
+
GraphQL JSON response.
|
|
402
|
+
nodes_path : str | None
|
|
403
|
+
Dot-separated path to nodes list (e.g., "data.users").
|
|
404
|
+
edges_path : str | None
|
|
405
|
+
Dot-separated path to edges list.
|
|
406
|
+
id_field : str
|
|
407
|
+
Field name for node IDs (default: "id").
|
|
408
|
+
label_field : str
|
|
409
|
+
Field name for node labels (default: "name").
|
|
410
|
+
**kwargs
|
|
411
|
+
Additional arguments passed to Graph constructor.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
Graph
|
|
416
|
+
New Graph instance with extracted nodes and edges.
|
|
417
|
+
|
|
418
|
+
Example
|
|
419
|
+
-------
|
|
420
|
+
>>> import requests
|
|
421
|
+
>>> response = requests.post(url, json={"query": query})
|
|
422
|
+
>>> graph = Graph.from_graphql(
|
|
423
|
+
... response.json(),
|
|
424
|
+
... nodes_path="data.characters.results",
|
|
425
|
+
... )
|
|
426
|
+
"""
|
|
427
|
+
from anywidget_graph.converters import GraphQLConverter
|
|
428
|
+
|
|
429
|
+
converter = GraphQLConverter(
|
|
430
|
+
nodes_path=nodes_path,
|
|
431
|
+
edges_path=edges_path,
|
|
432
|
+
id_field=id_field,
|
|
433
|
+
label_field=label_field,
|
|
434
|
+
)
|
|
435
|
+
data = converter.convert(result)
|
|
436
|
+
return cls(nodes=data["nodes"], edges=data["edges"], **kwargs)
|
|
1767
437
|
|
|
1768
438
|
def on_node_click(self, callback: Callable[[str, dict], None]) -> Callable:
|
|
1769
|
-
"""Register a callback for node click events.
|
|
439
|
+
"""Register a callback for node click events.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
callback : Callable[[str, dict], None]
|
|
444
|
+
Function called with (node_id, node_attributes) when a node is clicked.
|
|
445
|
+
|
|
446
|
+
Returns
|
|
447
|
+
-------
|
|
448
|
+
Callable
|
|
449
|
+
The callback function (for decorator usage).
|
|
450
|
+
"""
|
|
1770
451
|
self._node_click_callbacks.append(callback)
|
|
1771
452
|
|
|
1772
453
|
def observer(change: dict) -> None:
|
|
@@ -1780,7 +461,18 @@ class Graph(anywidget.AnyWidget):
|
|
|
1780
461
|
return callback
|
|
1781
462
|
|
|
1782
463
|
def on_edge_click(self, callback: Callable[[dict], None]) -> Callable:
|
|
1783
|
-
"""Register a callback for edge click events.
|
|
464
|
+
"""Register a callback for edge click events.
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
callback : Callable[[dict], None]
|
|
469
|
+
Function called with edge data when an edge is clicked.
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
Callable
|
|
474
|
+
The callback function (for decorator usage).
|
|
475
|
+
"""
|
|
1784
476
|
self._edge_click_callbacks.append(callback)
|
|
1785
477
|
|
|
1786
478
|
def observer(change: dict) -> None:
|
|
@@ -1790,85 +482,3 @@ class Graph(anywidget.AnyWidget):
|
|
|
1790
482
|
|
|
1791
483
|
self.observe(observer, names=["selected_edge"])
|
|
1792
484
|
return callback
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
def _is_node(obj: Any) -> bool:
|
|
1796
|
-
if hasattr(obj, "labels") and hasattr(obj, "element_id"):
|
|
1797
|
-
return True
|
|
1798
|
-
if hasattr(obj, "labels") and hasattr(obj, "properties"):
|
|
1799
|
-
return True
|
|
1800
|
-
if isinstance(obj, dict) and "id" in obj:
|
|
1801
|
-
return True
|
|
1802
|
-
return False
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
def _is_relationship(obj: Any) -> bool:
|
|
1806
|
-
if hasattr(obj, "type") and hasattr(obj, "start_node"):
|
|
1807
|
-
return True
|
|
1808
|
-
if hasattr(obj, "type") and hasattr(obj, "source") and hasattr(obj, "target"):
|
|
1809
|
-
return True
|
|
1810
|
-
if isinstance(obj, dict) and "source" in obj and "target" in obj:
|
|
1811
|
-
return True
|
|
1812
|
-
return False
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
def _get_node_id(node: Any) -> str:
|
|
1816
|
-
if hasattr(node, "element_id"):
|
|
1817
|
-
return str(node.element_id)
|
|
1818
|
-
if hasattr(node, "id"):
|
|
1819
|
-
return str(node.id)
|
|
1820
|
-
if isinstance(node, dict):
|
|
1821
|
-
return str(node.get("id", id(node)))
|
|
1822
|
-
return str(id(node))
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
def _node_to_dict(node: Any) -> dict:
|
|
1826
|
-
result: dict[str, Any] = {"id": _get_node_id(node)}
|
|
1827
|
-
|
|
1828
|
-
if hasattr(node, "labels"):
|
|
1829
|
-
labels = list(node.labels) if hasattr(node.labels, "__iter__") else [node.labels]
|
|
1830
|
-
if labels:
|
|
1831
|
-
result["label"] = labels[0]
|
|
1832
|
-
result["labels"] = labels
|
|
1833
|
-
|
|
1834
|
-
if hasattr(node, "properties"):
|
|
1835
|
-
props = node.properties if isinstance(node.properties, dict) else dict(node.properties)
|
|
1836
|
-
result.update(props)
|
|
1837
|
-
elif hasattr(node, "items"):
|
|
1838
|
-
result.update(dict(node))
|
|
1839
|
-
elif isinstance(node, dict):
|
|
1840
|
-
result.update(node)
|
|
1841
|
-
|
|
1842
|
-
if "label" not in result and "name" in result:
|
|
1843
|
-
result["label"] = result["name"]
|
|
1844
|
-
|
|
1845
|
-
return result
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
def _relationship_to_dict(rel: Any) -> dict:
|
|
1849
|
-
result: dict[str, Any] = {}
|
|
1850
|
-
|
|
1851
|
-
if hasattr(rel, "start_node") and hasattr(rel, "end_node"):
|
|
1852
|
-
result["source"] = _get_node_id(rel.start_node)
|
|
1853
|
-
result["target"] = _get_node_id(rel.end_node)
|
|
1854
|
-
elif hasattr(rel, "source") and hasattr(rel, "target"):
|
|
1855
|
-
result["source"] = _get_node_id(rel.source)
|
|
1856
|
-
result["target"] = _get_node_id(rel.target)
|
|
1857
|
-
elif isinstance(rel, dict):
|
|
1858
|
-
result["source"] = str(rel.get("source", ""))
|
|
1859
|
-
result["target"] = str(rel.get("target", ""))
|
|
1860
|
-
|
|
1861
|
-
if hasattr(rel, "type"):
|
|
1862
|
-
result["label"] = str(rel.type)
|
|
1863
|
-
elif isinstance(rel, dict) and "type" in rel:
|
|
1864
|
-
result["label"] = str(rel["type"])
|
|
1865
|
-
elif isinstance(rel, dict) and "label" in rel:
|
|
1866
|
-
result["label"] = str(rel["label"])
|
|
1867
|
-
|
|
1868
|
-
if hasattr(rel, "properties"):
|
|
1869
|
-
props = rel.properties if isinstance(rel.properties, dict) else dict(rel.properties)
|
|
1870
|
-
result.update(props)
|
|
1871
|
-
elif hasattr(rel, "items") and not isinstance(rel, dict):
|
|
1872
|
-
result.update(dict(rel))
|
|
1873
|
-
|
|
1874
|
-
return result
|