anywidget-graph 0.1.0__py3-none-any.whl → 0.2.0__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/widget.py
CHANGED
|
@@ -6,22 +6,909 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
import anywidget
|
|
8
8
|
import traitlets
|
|
9
|
+
from traitlets import observe
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from collections.abc import Callable
|
|
12
13
|
|
|
13
|
-
_ESM = """
|
|
14
|
+
_ESM = r"""
|
|
14
15
|
import Graph from "https://esm.sh/graphology@0.25.4";
|
|
15
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";
|
|
16
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 ===
|
|
17
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
|
|
18
894
|
const container = document.createElement("div");
|
|
895
|
+
container.className = "awg-graph-container";
|
|
19
896
|
container.style.width = model.get("width") + "px";
|
|
20
897
|
container.style.height = model.get("height") + "px";
|
|
21
|
-
container
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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);
|
|
25
912
|
|
|
26
913
|
const graph = new Graph();
|
|
27
914
|
|
|
@@ -119,8 +1006,570 @@ export default { render };
|
|
|
119
1006
|
"""
|
|
120
1007
|
|
|
121
1008
|
_CSS = """
|
|
122
|
-
|
|
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 {
|
|
123
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;
|
|
124
1573
|
}
|
|
125
1574
|
"""
|
|
126
1575
|
|
|
@@ -131,27 +1580,159 @@ class Graph(anywidget.AnyWidget):
|
|
|
131
1580
|
_esm = _ESM
|
|
132
1581
|
_css = _CSS
|
|
133
1582
|
|
|
1583
|
+
# Graph data
|
|
134
1584
|
nodes = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
135
1585
|
edges = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
136
1586
|
|
|
1587
|
+
# Display settings
|
|
137
1588
|
width = traitlets.Int(default_value=800).tag(sync=True)
|
|
138
1589
|
height = traitlets.Int(default_value=600).tag(sync=True)
|
|
139
1590
|
background = traitlets.Unicode(default_value="#fafafa").tag(sync=True)
|
|
140
1591
|
show_labels = traitlets.Bool(default_value=True).tag(sync=True)
|
|
141
1592
|
show_edge_labels = traitlets.Bool(default_value=False).tag(sync=True)
|
|
142
1593
|
|
|
1594
|
+
# Selection state
|
|
143
1595
|
selected_node = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
144
1596
|
selected_edge = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
145
1597
|
|
|
1598
|
+
# Toolbar visibility
|
|
1599
|
+
show_toolbar = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1600
|
+
show_settings = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1601
|
+
show_query_input = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1602
|
+
|
|
1603
|
+
# Theme
|
|
1604
|
+
dark_mode = traitlets.Bool(default_value=True).tag(sync=True)
|
|
1605
|
+
|
|
1606
|
+
# Database backend
|
|
1607
|
+
database_backend = traitlets.Unicode(default_value="neo4j").tag(sync=True)
|
|
1608
|
+
|
|
1609
|
+
# Neo4j connection (browser-side)
|
|
1610
|
+
connection_uri = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1611
|
+
connection_username = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1612
|
+
connection_password = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1613
|
+
connection_database = traitlets.Unicode(default_value="neo4j").tag(sync=True)
|
|
1614
|
+
|
|
1615
|
+
# Query state
|
|
1616
|
+
query = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1617
|
+
query_language = traitlets.Unicode(default_value="cypher").tag(sync=True)
|
|
1618
|
+
query_running = traitlets.Bool(default_value=False).tag(sync=True)
|
|
1619
|
+
query_error = traitlets.Unicode(default_value="").tag(sync=True)
|
|
1620
|
+
|
|
1621
|
+
# Connection state
|
|
1622
|
+
connection_status = traitlets.Unicode(default_value="disconnected").tag(sync=True)
|
|
1623
|
+
|
|
1624
|
+
# Schema data
|
|
1625
|
+
schema_node_types = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
1626
|
+
schema_edge_types = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
1627
|
+
|
|
1628
|
+
# Query execution trigger (for Python backends)
|
|
1629
|
+
_execute_query = traitlets.Int(default_value=0).tag(sync=True)
|
|
1630
|
+
|
|
146
1631
|
def __init__(
|
|
147
1632
|
self,
|
|
148
1633
|
nodes: list[dict[str, Any]] | None = None,
|
|
149
1634
|
edges: list[dict[str, Any]] | None = None,
|
|
1635
|
+
*,
|
|
1636
|
+
width: int = 800,
|
|
1637
|
+
height: int = 600,
|
|
1638
|
+
background: str = "#fafafa",
|
|
1639
|
+
show_labels: bool = True,
|
|
1640
|
+
show_edge_labels: bool = False,
|
|
1641
|
+
show_toolbar: bool = True,
|
|
1642
|
+
show_settings: bool = True,
|
|
1643
|
+
show_query_input: bool = True,
|
|
1644
|
+
dark_mode: bool = True,
|
|
1645
|
+
database_backend: str = "neo4j",
|
|
1646
|
+
connection_uri: str = "",
|
|
1647
|
+
connection_username: str = "",
|
|
1648
|
+
connection_password: str = "",
|
|
1649
|
+
connection_database: str = "neo4j",
|
|
1650
|
+
grafeo_db: Any = None,
|
|
150
1651
|
**kwargs: Any,
|
|
151
1652
|
) -> None:
|
|
152
|
-
super().__init__(
|
|
1653
|
+
super().__init__(
|
|
1654
|
+
nodes=nodes or [],
|
|
1655
|
+
edges=edges or [],
|
|
1656
|
+
width=width,
|
|
1657
|
+
height=height,
|
|
1658
|
+
background=background,
|
|
1659
|
+
show_labels=show_labels,
|
|
1660
|
+
show_edge_labels=show_edge_labels,
|
|
1661
|
+
show_toolbar=show_toolbar,
|
|
1662
|
+
show_settings=show_settings,
|
|
1663
|
+
show_query_input=show_query_input,
|
|
1664
|
+
dark_mode=dark_mode,
|
|
1665
|
+
database_backend=database_backend,
|
|
1666
|
+
connection_uri=connection_uri,
|
|
1667
|
+
connection_username=connection_username,
|
|
1668
|
+
connection_password=connection_password,
|
|
1669
|
+
connection_database=connection_database,
|
|
1670
|
+
**kwargs,
|
|
1671
|
+
)
|
|
153
1672
|
self._node_click_callbacks: list[Callable] = []
|
|
154
1673
|
self._edge_click_callbacks: list[Callable] = []
|
|
1674
|
+
self._grafeo_db = grafeo_db
|
|
1675
|
+
|
|
1676
|
+
@property
|
|
1677
|
+
def grafeo_db(self) -> Any:
|
|
1678
|
+
"""Get the Grafeo database instance."""
|
|
1679
|
+
return self._grafeo_db
|
|
1680
|
+
|
|
1681
|
+
@grafeo_db.setter
|
|
1682
|
+
def grafeo_db(self, value: Any) -> None:
|
|
1683
|
+
"""Set the Grafeo database instance."""
|
|
1684
|
+
self._grafeo_db = value
|
|
1685
|
+
|
|
1686
|
+
@observe("_execute_query")
|
|
1687
|
+
def _on_execute_query(self, change: dict) -> None:
|
|
1688
|
+
"""Handle query execution trigger from JavaScript."""
|
|
1689
|
+
if change["new"] == 0:
|
|
1690
|
+
return # Skip initial value
|
|
1691
|
+
if self.database_backend == "grafeo" and self._grafeo_db is not None:
|
|
1692
|
+
self._execute_grafeo_query()
|
|
1693
|
+
|
|
1694
|
+
def _execute_grafeo_query(self) -> None:
|
|
1695
|
+
"""Execute query against Grafeo database."""
|
|
1696
|
+
try:
|
|
1697
|
+
self.query_running = True
|
|
1698
|
+
self.query_error = ""
|
|
1699
|
+
result = self._grafeo_db.execute(self.query)
|
|
1700
|
+
nodes, edges = self._process_grafeo_result(result)
|
|
1701
|
+
self.nodes = nodes
|
|
1702
|
+
self.edges = edges
|
|
1703
|
+
except Exception as e:
|
|
1704
|
+
self.query_error = str(e)
|
|
1705
|
+
finally:
|
|
1706
|
+
self.query_running = False
|
|
1707
|
+
|
|
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
|
|
155
1736
|
|
|
156
1737
|
@classmethod
|
|
157
1738
|
def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> Graph:
|