anywidget-graph 0.1.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 +417 -226
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/METADATA +4 -3
- anywidget_graph-0.2.1.dist-info/RECORD +28 -0
- anywidget_graph-0.1.0.dist-info/RECORD +0 -6
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Properties panel component for displaying selected node/edge details.
|
|
3
|
+
*/
|
|
4
|
+
import { ICONS } from "./icons.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create the properties panel.
|
|
8
|
+
*/
|
|
9
|
+
export function createPropertiesPanel(model) {
|
|
10
|
+
const panel = document.createElement("div");
|
|
11
|
+
panel.className = "awg-properties-panel";
|
|
12
|
+
|
|
13
|
+
// Header
|
|
14
|
+
const header = document.createElement("div");
|
|
15
|
+
header.className = "awg-panel-header";
|
|
16
|
+
header.innerHTML = "<span>Properties</span>";
|
|
17
|
+
panel.appendChild(header);
|
|
18
|
+
|
|
19
|
+
// Content
|
|
20
|
+
const content = document.createElement("div");
|
|
21
|
+
content.className = "awg-properties-content";
|
|
22
|
+
|
|
23
|
+
function renderProperties() {
|
|
24
|
+
content.innerHTML = "";
|
|
25
|
+
|
|
26
|
+
const selectedNode = model.get("selected_node");
|
|
27
|
+
const selectedEdge = model.get("selected_edge");
|
|
28
|
+
|
|
29
|
+
if (!selectedNode && !selectedEdge) {
|
|
30
|
+
const empty = document.createElement("div");
|
|
31
|
+
empty.className = "awg-properties-empty";
|
|
32
|
+
empty.textContent = "Click a node or edge to view properties";
|
|
33
|
+
content.appendChild(empty);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (selectedNode) {
|
|
38
|
+
renderNodeProperties(content, selectedNode);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (selectedEdge) {
|
|
42
|
+
renderEdgeProperties(content, selectedEdge);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
model.on("change:selected_node", renderProperties);
|
|
47
|
+
model.on("change:selected_edge", renderProperties);
|
|
48
|
+
renderProperties();
|
|
49
|
+
|
|
50
|
+
panel.appendChild(content);
|
|
51
|
+
return panel;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render node properties.
|
|
56
|
+
*/
|
|
57
|
+
function renderNodeProperties(content, node) {
|
|
58
|
+
// Node header
|
|
59
|
+
const nodeHeader = document.createElement("div");
|
|
60
|
+
nodeHeader.className = "awg-properties-header";
|
|
61
|
+
nodeHeader.innerHTML = `${ICONS.node} <span class="awg-properties-type">Node</span>`;
|
|
62
|
+
content.appendChild(nodeHeader);
|
|
63
|
+
|
|
64
|
+
// Node ID
|
|
65
|
+
const idItem = document.createElement("div");
|
|
66
|
+
idItem.className = "awg-property-item";
|
|
67
|
+
idItem.innerHTML = `<span class="awg-property-key">id</span><span class="awg-property-value">${node.id || "N/A"}</span>`;
|
|
68
|
+
content.appendChild(idItem);
|
|
69
|
+
|
|
70
|
+
// Labels
|
|
71
|
+
if (node.labels && node.labels.length > 0) {
|
|
72
|
+
const labelsItem = document.createElement("div");
|
|
73
|
+
labelsItem.className = "awg-property-item";
|
|
74
|
+
labelsItem.innerHTML = `<span class="awg-property-key">labels</span><span class="awg-property-value awg-property-labels">${node.labels.map((l) => `<span class="awg-label-tag">${l}</span>`).join("")}</span>`;
|
|
75
|
+
content.appendChild(labelsItem);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Other properties
|
|
79
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
80
|
+
if (["id", "labels", "label", "x", "y", "size", "color"].includes(key)) return;
|
|
81
|
+
const item = document.createElement("div");
|
|
82
|
+
item.className = "awg-property-item";
|
|
83
|
+
const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
84
|
+
item.innerHTML = `<span class="awg-property-key">${key}</span><span class="awg-property-value">${displayValue}</span>`;
|
|
85
|
+
content.appendChild(item);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render edge properties.
|
|
91
|
+
*/
|
|
92
|
+
function renderEdgeProperties(content, edge) {
|
|
93
|
+
// Edge header
|
|
94
|
+
const edgeHeader = document.createElement("div");
|
|
95
|
+
edgeHeader.className = "awg-properties-header";
|
|
96
|
+
edgeHeader.innerHTML = `${ICONS.edge} <span class="awg-properties-type">Relationship</span>`;
|
|
97
|
+
content.appendChild(edgeHeader);
|
|
98
|
+
|
|
99
|
+
// Edge type/label
|
|
100
|
+
if (edge.label) {
|
|
101
|
+
const typeItem = document.createElement("div");
|
|
102
|
+
typeItem.className = "awg-property-item";
|
|
103
|
+
typeItem.innerHTML = `<span class="awg-property-key">type</span><span class="awg-property-value"><span class="awg-edge-tag">${edge.label}</span></span>`;
|
|
104
|
+
content.appendChild(typeItem);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Source/Target
|
|
108
|
+
const sourceItem = document.createElement("div");
|
|
109
|
+
sourceItem.className = "awg-property-item";
|
|
110
|
+
sourceItem.innerHTML = `<span class="awg-property-key">source</span><span class="awg-property-value awg-property-truncate">${edge.source}</span>`;
|
|
111
|
+
content.appendChild(sourceItem);
|
|
112
|
+
|
|
113
|
+
const targetItem = document.createElement("div");
|
|
114
|
+
targetItem.className = "awg-property-item";
|
|
115
|
+
targetItem.innerHTML = `<span class="awg-property-key">target</span><span class="awg-property-value awg-property-truncate">${edge.target}</span>`;
|
|
116
|
+
content.appendChild(targetItem);
|
|
117
|
+
|
|
118
|
+
// Other properties
|
|
119
|
+
Object.entries(edge).forEach(([key, value]) => {
|
|
120
|
+
if (["source", "target", "label", "size", "color"].includes(key)) return;
|
|
121
|
+
const item = document.createElement("div");
|
|
122
|
+
item.className = "awg-property-item";
|
|
123
|
+
const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
124
|
+
item.innerHTML = `<span class="awg-property-key">${key}</span><span class="awg-property-value">${displayValue}</span>`;
|
|
125
|
+
content.appendChild(item);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Toggle properties panel visibility.
|
|
131
|
+
*/
|
|
132
|
+
export function togglePropertiesPanel(wrapper) {
|
|
133
|
+
const panel = wrapper.querySelector(".awg-properties-panel");
|
|
134
|
+
if (panel) {
|
|
135
|
+
panel.classList.toggle("awg-panel-open");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema browser sidebar component.
|
|
3
|
+
*/
|
|
4
|
+
import { ICONS } from "./icons.js";
|
|
5
|
+
import { fetchSchema } from "./neo4j.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create the schema sidebar panel.
|
|
9
|
+
*/
|
|
10
|
+
export function createSchemaPanel(model, onExecuteQuery) {
|
|
11
|
+
const panel = document.createElement("div");
|
|
12
|
+
panel.className = "awg-schema-panel";
|
|
13
|
+
|
|
14
|
+
// Header
|
|
15
|
+
const header = document.createElement("div");
|
|
16
|
+
header.className = "awg-panel-header";
|
|
17
|
+
header.innerHTML = `<span>Schema</span>`;
|
|
18
|
+
|
|
19
|
+
const refreshBtn = document.createElement("button");
|
|
20
|
+
refreshBtn.className = "awg-btn awg-btn-icon awg-btn-sm";
|
|
21
|
+
refreshBtn.innerHTML = ICONS.refresh;
|
|
22
|
+
refreshBtn.title = "Refresh schema";
|
|
23
|
+
refreshBtn.addEventListener("click", () => fetchSchema(model));
|
|
24
|
+
header.appendChild(refreshBtn);
|
|
25
|
+
|
|
26
|
+
panel.appendChild(header);
|
|
27
|
+
|
|
28
|
+
// Content
|
|
29
|
+
const content = document.createElement("div");
|
|
30
|
+
content.className = "awg-schema-content";
|
|
31
|
+
|
|
32
|
+
function renderSchema() {
|
|
33
|
+
content.innerHTML = "";
|
|
34
|
+
|
|
35
|
+
const nodeTypes = model.get("schema_node_types") || [];
|
|
36
|
+
const edgeTypes = model.get("schema_edge_types") || [];
|
|
37
|
+
|
|
38
|
+
if (nodeTypes.length === 0 && edgeTypes.length === 0) {
|
|
39
|
+
const empty = document.createElement("div");
|
|
40
|
+
empty.className = "awg-schema-empty";
|
|
41
|
+
empty.textContent = "Connect to load schema";
|
|
42
|
+
content.appendChild(empty);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Node types section
|
|
47
|
+
if (nodeTypes.length > 0) {
|
|
48
|
+
const nodeSection = document.createElement("div");
|
|
49
|
+
nodeSection.className = "awg-schema-section";
|
|
50
|
+
|
|
51
|
+
const nodeHeader = document.createElement("div");
|
|
52
|
+
nodeHeader.className = "awg-schema-section-header";
|
|
53
|
+
nodeHeader.innerHTML = `${ICONS.node} <span>Node Labels</span>`;
|
|
54
|
+
nodeSection.appendChild(nodeHeader);
|
|
55
|
+
|
|
56
|
+
nodeTypes.forEach(({ label, properties }) => {
|
|
57
|
+
const item = createSchemaItem(label, properties, "node", model, onExecuteQuery);
|
|
58
|
+
nodeSection.appendChild(item);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
content.appendChild(nodeSection);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Edge types section
|
|
65
|
+
if (edgeTypes.length > 0) {
|
|
66
|
+
const edgeSection = document.createElement("div");
|
|
67
|
+
edgeSection.className = "awg-schema-section";
|
|
68
|
+
|
|
69
|
+
const edgeHeader = document.createElement("div");
|
|
70
|
+
edgeHeader.className = "awg-schema-section-header";
|
|
71
|
+
edgeHeader.innerHTML = `${ICONS.edge} <span>Relationships</span>`;
|
|
72
|
+
edgeSection.appendChild(edgeHeader);
|
|
73
|
+
|
|
74
|
+
edgeTypes.forEach(({ type, properties }) => {
|
|
75
|
+
const item = createSchemaItem(type, properties, "edge", model, onExecuteQuery);
|
|
76
|
+
edgeSection.appendChild(item);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
content.appendChild(edgeSection);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
model.on("change:schema_node_types", renderSchema);
|
|
84
|
+
model.on("change:schema_edge_types", renderSchema);
|
|
85
|
+
renderSchema();
|
|
86
|
+
|
|
87
|
+
panel.appendChild(content);
|
|
88
|
+
return panel;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a schema item (node label or relationship type).
|
|
93
|
+
*/
|
|
94
|
+
function createSchemaItem(name, properties, type, model, onExecuteQuery) {
|
|
95
|
+
const item = document.createElement("div");
|
|
96
|
+
item.className = "awg-schema-item";
|
|
97
|
+
|
|
98
|
+
const itemHeader = document.createElement("div");
|
|
99
|
+
itemHeader.className = "awg-schema-item-header";
|
|
100
|
+
|
|
101
|
+
const expandBtn = document.createElement("span");
|
|
102
|
+
expandBtn.className = "awg-schema-expand";
|
|
103
|
+
expandBtn.innerHTML = ICONS.chevronRight;
|
|
104
|
+
|
|
105
|
+
const nameSpan = document.createElement("span");
|
|
106
|
+
nameSpan.className = "awg-schema-name";
|
|
107
|
+
nameSpan.textContent = name;
|
|
108
|
+
|
|
109
|
+
itemHeader.appendChild(expandBtn);
|
|
110
|
+
itemHeader.appendChild(nameSpan);
|
|
111
|
+
|
|
112
|
+
// Click on name to query
|
|
113
|
+
nameSpan.addEventListener("click", (e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
let query;
|
|
116
|
+
if (type === "node") {
|
|
117
|
+
query = `MATCH (n:\`${name}\`) RETURN n LIMIT 25`;
|
|
118
|
+
} else {
|
|
119
|
+
query = `MATCH (a)-[r:\`${name}\`]->(b) RETURN a, r, b LIMIT 25`;
|
|
120
|
+
}
|
|
121
|
+
model.set("query", query);
|
|
122
|
+
model.save_changes();
|
|
123
|
+
onExecuteQuery();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Properties list (collapsed by default)
|
|
127
|
+
const propsList = document.createElement("div");
|
|
128
|
+
propsList.className = "awg-schema-props";
|
|
129
|
+
|
|
130
|
+
if (properties && properties.length > 0) {
|
|
131
|
+
properties.forEach((prop) => {
|
|
132
|
+
const propItem = document.createElement("div");
|
|
133
|
+
propItem.className = "awg-schema-prop";
|
|
134
|
+
propItem.innerHTML = `${ICONS.property} <span>${prop}</span>`;
|
|
135
|
+
|
|
136
|
+
// Click on property to query with that property
|
|
137
|
+
propItem.addEventListener("click", (e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
let query;
|
|
140
|
+
if (type === "node") {
|
|
141
|
+
query = `MATCH (n:\`${name}\`) RETURN n.\`${prop}\` AS ${prop}, n LIMIT 25`;
|
|
142
|
+
} else {
|
|
143
|
+
query = `MATCH (a)-[r:\`${name}\`]->(b) RETURN r.\`${prop}\` AS ${prop}, a, r, b LIMIT 25`;
|
|
144
|
+
}
|
|
145
|
+
model.set("query", query);
|
|
146
|
+
model.save_changes();
|
|
147
|
+
onExecuteQuery();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
propsList.appendChild(propItem);
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
const noProp = document.createElement("div");
|
|
154
|
+
noProp.className = "awg-schema-prop awg-schema-prop-empty";
|
|
155
|
+
noProp.textContent = "No properties";
|
|
156
|
+
propsList.appendChild(noProp);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
item.appendChild(itemHeader);
|
|
160
|
+
item.appendChild(propsList);
|
|
161
|
+
|
|
162
|
+
// Toggle expand/collapse
|
|
163
|
+
itemHeader.addEventListener("click", () => {
|
|
164
|
+
item.classList.toggle("awg-schema-item-expanded");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return item;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Toggle schema panel visibility.
|
|
172
|
+
*/
|
|
173
|
+
export function toggleSchemaPanel(wrapper) {
|
|
174
|
+
const panel = wrapper.querySelector(".awg-schema-panel");
|
|
175
|
+
if (panel) {
|
|
176
|
+
panel.classList.toggle("awg-panel-open");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings panel component.
|
|
3
|
+
*/
|
|
4
|
+
import { ICONS } from "./icons.js";
|
|
5
|
+
import * as neo4jBackend from "./neo4j.js";
|
|
6
|
+
|
|
7
|
+
// Configuration constants
|
|
8
|
+
const BACKENDS = [
|
|
9
|
+
{ id: "neo4j", name: "Neo4j" },
|
|
10
|
+
{ id: "grafeo", name: "Grafeo" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const QUERY_LANGUAGES = [
|
|
14
|
+
{ id: "cypher", name: "Cypher", enabled: true },
|
|
15
|
+
{ id: "gql", name: "GQL", enabled: false },
|
|
16
|
+
{ id: "sparql", name: "SPARQL", enabled: false },
|
|
17
|
+
{ id: "gremlin", name: "Gremlin", enabled: false },
|
|
18
|
+
{ id: "graphql", name: "GraphQL", enabled: false },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create the settings panel.
|
|
23
|
+
*/
|
|
24
|
+
export function createSettingsPanel(model) {
|
|
25
|
+
const panel = document.createElement("div");
|
|
26
|
+
panel.className = "awg-settings-panel";
|
|
27
|
+
|
|
28
|
+
// Header
|
|
29
|
+
const header = document.createElement("div");
|
|
30
|
+
header.className = "awg-panel-header";
|
|
31
|
+
header.innerHTML = "<span>Settings</span>";
|
|
32
|
+
panel.appendChild(header);
|
|
33
|
+
|
|
34
|
+
// Form
|
|
35
|
+
const form = document.createElement("div");
|
|
36
|
+
form.className = "awg-panel-form";
|
|
37
|
+
|
|
38
|
+
// Dark mode toggle
|
|
39
|
+
form.appendChild(
|
|
40
|
+
createFormGroup("Theme", () => {
|
|
41
|
+
const toggle = document.createElement("label");
|
|
42
|
+
toggle.className = "awg-toggle";
|
|
43
|
+
const checkbox = document.createElement("input");
|
|
44
|
+
checkbox.type = "checkbox";
|
|
45
|
+
checkbox.checked = model.get("dark_mode");
|
|
46
|
+
checkbox.addEventListener("change", (e) => {
|
|
47
|
+
model.set("dark_mode", e.target.checked);
|
|
48
|
+
model.save_changes();
|
|
49
|
+
});
|
|
50
|
+
model.on("change:dark_mode", () => {
|
|
51
|
+
checkbox.checked = model.get("dark_mode");
|
|
52
|
+
});
|
|
53
|
+
const slider = document.createElement("span");
|
|
54
|
+
slider.className = "awg-toggle-slider";
|
|
55
|
+
const label = document.createElement("span");
|
|
56
|
+
label.className = "awg-toggle-label";
|
|
57
|
+
label.textContent = "Dark mode";
|
|
58
|
+
toggle.appendChild(checkbox);
|
|
59
|
+
toggle.appendChild(slider);
|
|
60
|
+
toggle.appendChild(label);
|
|
61
|
+
return toggle;
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Backend selector
|
|
66
|
+
form.appendChild(
|
|
67
|
+
createFormGroup("Backend", () => {
|
|
68
|
+
const select = document.createElement("select");
|
|
69
|
+
select.className = "awg-select";
|
|
70
|
+
BACKENDS.forEach((b) => {
|
|
71
|
+
const opt = document.createElement("option");
|
|
72
|
+
opt.value = b.id;
|
|
73
|
+
opt.textContent = b.name;
|
|
74
|
+
opt.selected = model.get("database_backend") === b.id;
|
|
75
|
+
select.appendChild(opt);
|
|
76
|
+
});
|
|
77
|
+
select.addEventListener("change", (e) => {
|
|
78
|
+
model.set("database_backend", e.target.value);
|
|
79
|
+
model.save_changes();
|
|
80
|
+
updateFormVisibility();
|
|
81
|
+
});
|
|
82
|
+
return select;
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Neo4j fields container
|
|
87
|
+
const neo4jFields = document.createElement("div");
|
|
88
|
+
neo4jFields.className = "awg-neo4j-fields";
|
|
89
|
+
|
|
90
|
+
// URI
|
|
91
|
+
neo4jFields.appendChild(
|
|
92
|
+
createFormGroup("URI", () => {
|
|
93
|
+
const input = document.createElement("input");
|
|
94
|
+
input.type = "text";
|
|
95
|
+
input.className = "awg-input";
|
|
96
|
+
input.placeholder = "neo4j+s://localhost:7687";
|
|
97
|
+
input.value = model.get("connection_uri") || "";
|
|
98
|
+
input.addEventListener("input", (e) => {
|
|
99
|
+
model.set("connection_uri", e.target.value);
|
|
100
|
+
model.save_changes();
|
|
101
|
+
});
|
|
102
|
+
return input;
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Username
|
|
107
|
+
neo4jFields.appendChild(
|
|
108
|
+
createFormGroup("Username", () => {
|
|
109
|
+
const input = document.createElement("input");
|
|
110
|
+
input.type = "text";
|
|
111
|
+
input.className = "awg-input";
|
|
112
|
+
input.placeholder = "neo4j";
|
|
113
|
+
input.value = model.get("connection_username") || "";
|
|
114
|
+
input.addEventListener("input", (e) => {
|
|
115
|
+
model.set("connection_username", e.target.value);
|
|
116
|
+
model.save_changes();
|
|
117
|
+
});
|
|
118
|
+
return input;
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Password
|
|
123
|
+
neo4jFields.appendChild(
|
|
124
|
+
createFormGroup("Password", () => {
|
|
125
|
+
const input = document.createElement("input");
|
|
126
|
+
input.type = "password";
|
|
127
|
+
input.className = "awg-input";
|
|
128
|
+
input.value = model.get("connection_password") || "";
|
|
129
|
+
input.addEventListener("input", (e) => {
|
|
130
|
+
model.set("connection_password", e.target.value);
|
|
131
|
+
model.save_changes();
|
|
132
|
+
});
|
|
133
|
+
return input;
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
form.appendChild(neo4jFields);
|
|
138
|
+
|
|
139
|
+
// Database name
|
|
140
|
+
form.appendChild(
|
|
141
|
+
createFormGroup("Database", () => {
|
|
142
|
+
const input = document.createElement("input");
|
|
143
|
+
input.type = "text";
|
|
144
|
+
input.className = "awg-input";
|
|
145
|
+
input.placeholder = "neo4j";
|
|
146
|
+
input.value = model.get("connection_database") || "neo4j";
|
|
147
|
+
input.addEventListener("input", (e) => {
|
|
148
|
+
model.set("connection_database", e.target.value);
|
|
149
|
+
model.save_changes();
|
|
150
|
+
});
|
|
151
|
+
return input;
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Query language
|
|
156
|
+
form.appendChild(
|
|
157
|
+
createFormGroup("Language", () => {
|
|
158
|
+
const select = document.createElement("select");
|
|
159
|
+
select.className = "awg-select";
|
|
160
|
+
QUERY_LANGUAGES.forEach((lang) => {
|
|
161
|
+
const opt = document.createElement("option");
|
|
162
|
+
opt.value = lang.id;
|
|
163
|
+
opt.textContent = lang.name;
|
|
164
|
+
opt.disabled = !lang.enabled;
|
|
165
|
+
opt.selected = model.get("query_language") === lang.id;
|
|
166
|
+
select.appendChild(opt);
|
|
167
|
+
});
|
|
168
|
+
select.addEventListener("change", (e) => {
|
|
169
|
+
model.set("query_language", e.target.value);
|
|
170
|
+
model.save_changes();
|
|
171
|
+
});
|
|
172
|
+
return select;
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
panel.appendChild(form);
|
|
177
|
+
|
|
178
|
+
// Connect button
|
|
179
|
+
const actions = document.createElement("div");
|
|
180
|
+
actions.className = "awg-panel-actions";
|
|
181
|
+
|
|
182
|
+
const connectBtn = document.createElement("button");
|
|
183
|
+
connectBtn.className = "awg-btn awg-btn-primary awg-btn-full";
|
|
184
|
+
|
|
185
|
+
const statusIndicator = document.createElement("div");
|
|
186
|
+
statusIndicator.className = "awg-connection-status";
|
|
187
|
+
|
|
188
|
+
function updateConnectButton() {
|
|
189
|
+
const status = model.get("connection_status");
|
|
190
|
+
const backend = model.get("database_backend");
|
|
191
|
+
|
|
192
|
+
if (backend === "grafeo") {
|
|
193
|
+
connectBtn.textContent = "Python Backend";
|
|
194
|
+
connectBtn.disabled = true;
|
|
195
|
+
statusIndicator.innerHTML =
|
|
196
|
+
'<span class="awg-status-dot-inline awg-status-connected"></span> Grafeo';
|
|
197
|
+
} else if (status === "connected") {
|
|
198
|
+
connectBtn.textContent = "Disconnect";
|
|
199
|
+
connectBtn.disabled = false;
|
|
200
|
+
statusIndicator.innerHTML =
|
|
201
|
+
'<span class="awg-status-dot-inline awg-status-connected"></span> Connected';
|
|
202
|
+
} else if (status === "connecting") {
|
|
203
|
+
connectBtn.textContent = "Connecting...";
|
|
204
|
+
connectBtn.disabled = true;
|
|
205
|
+
statusIndicator.innerHTML =
|
|
206
|
+
'<span class="awg-status-dot-inline awg-status-connecting"></span> Connecting';
|
|
207
|
+
} else if (status === "error") {
|
|
208
|
+
connectBtn.textContent = "Retry";
|
|
209
|
+
connectBtn.disabled = false;
|
|
210
|
+
statusIndicator.innerHTML =
|
|
211
|
+
'<span class="awg-status-dot-inline awg-status-error"></span> Error';
|
|
212
|
+
} else {
|
|
213
|
+
connectBtn.textContent = "Connect";
|
|
214
|
+
connectBtn.disabled = false;
|
|
215
|
+
statusIndicator.innerHTML =
|
|
216
|
+
'<span class="awg-status-dot-inline awg-status-disconnected"></span> Disconnected';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateFormVisibility() {
|
|
221
|
+
const backend = model.get("database_backend");
|
|
222
|
+
neo4jFields.style.display = backend === "neo4j" ? "block" : "none";
|
|
223
|
+
updateConnectButton();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
connectBtn.addEventListener("click", async () => {
|
|
227
|
+
const backend = model.get("database_backend");
|
|
228
|
+
if (backend !== "neo4j") return;
|
|
229
|
+
|
|
230
|
+
const status = model.get("connection_status");
|
|
231
|
+
if (status === "connected") {
|
|
232
|
+
await neo4jBackend.disconnect(model);
|
|
233
|
+
} else {
|
|
234
|
+
await neo4jBackend.connect(
|
|
235
|
+
model.get("connection_uri"),
|
|
236
|
+
model.get("connection_username"),
|
|
237
|
+
model.get("connection_password"),
|
|
238
|
+
model
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
model.on("change:connection_status", updateConnectButton);
|
|
244
|
+
model.on("change:database_backend", updateFormVisibility);
|
|
245
|
+
|
|
246
|
+
actions.appendChild(statusIndicator);
|
|
247
|
+
actions.appendChild(connectBtn);
|
|
248
|
+
panel.appendChild(actions);
|
|
249
|
+
|
|
250
|
+
// Error display
|
|
251
|
+
const errorDiv = document.createElement("div");
|
|
252
|
+
errorDiv.className = "awg-panel-error";
|
|
253
|
+
function updateError() {
|
|
254
|
+
const err = model.get("query_error");
|
|
255
|
+
errorDiv.textContent = err || "";
|
|
256
|
+
errorDiv.style.display = err ? "block" : "none";
|
|
257
|
+
}
|
|
258
|
+
model.on("change:query_error", updateError);
|
|
259
|
+
updateError();
|
|
260
|
+
panel.appendChild(errorDiv);
|
|
261
|
+
|
|
262
|
+
// Initialize
|
|
263
|
+
updateFormVisibility();
|
|
264
|
+
|
|
265
|
+
return panel;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create a form group with label and input.
|
|
270
|
+
*/
|
|
271
|
+
function createFormGroup(label, inputFn) {
|
|
272
|
+
const group = document.createElement("div");
|
|
273
|
+
group.className = "awg-form-group";
|
|
274
|
+
|
|
275
|
+
const labelEl = document.createElement("label");
|
|
276
|
+
labelEl.className = "awg-label";
|
|
277
|
+
labelEl.textContent = label;
|
|
278
|
+
group.appendChild(labelEl);
|
|
279
|
+
|
|
280
|
+
group.appendChild(inputFn());
|
|
281
|
+
return group;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Toggle settings panel visibility.
|
|
286
|
+
*/
|
|
287
|
+
export function toggleSettingsPanel(wrapper) {
|
|
288
|
+
const panel = wrapper.querySelector(".awg-settings-panel");
|
|
289
|
+
if (panel) {
|
|
290
|
+
panel.classList.toggle("awg-panel-open");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Update the status dot appearance.
|
|
296
|
+
*/
|
|
297
|
+
export function updateStatusDot(dot, status) {
|
|
298
|
+
dot.className = "awg-status-dot awg-status-" + status;
|
|
299
|
+
}
|