solid_litequeen 0.10.3 → 0.11.0
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.
- checksums.yaml +4 -4
- data/app/assets/images/solid_litequeen/icons/workflow.svg +1 -0
- data/app/assets/images/solid_litequeen/icons/x.svg +1 -0
- data/app/controllers/solid_litequeen/databases_controller.rb +35 -0
- data/app/javascript/solid_litequeen/controllers/table_relations_controller.js +295 -0
- data/app/views/layouts/solid_litequeen/application.html.erb +2 -0
- data/app/views/solid_litequeen/databases/_table-relationships-dialog.html.erb +10 -0
- data/app/views/solid_litequeen/databases/show.html.erb +14 -1
- data/config/importmap.rb +2 -0
- data/lib/solid_litequeen/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20c5e5b3dda06319b2fc54f414d021f38b8bce98a343c1ab3a9085658255956f
|
4
|
+
data.tar.gz: aeef980f3988611b19ad8300944659c2ef619d5c71206aa4be56108a09919d46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06af085c8332f1d5a3fb8412c2108101b316ecfb4cd64f7eceb9a82720f0d700fa3bf4ca9c3248183e539b7736de04553405f5ab5634f22eca5778e5193d7445
|
7
|
+
data.tar.gz: d8a8255598db402cf2b7d9b4db1785ddc0f7b91956bf8dbe962aef69fa29d3e6954eef0a08396042924b4b2784b3934ba9cdd2bbac026dea7d2929991ca8b3ae
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-workflow"><rect width="8" height="8" x="3" y="3" rx="2"></rect><path d="M7 11v4a2 2 0 0 0 2 2h4"></path><rect width="8" height="8" x="13" y="13" rx="2"></rect></svg>
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
|
@@ -18,11 +18,46 @@ module SolidLitequeen
|
|
18
18
|
|
19
19
|
tables = DynamicDatabase.connection.tables
|
20
20
|
|
21
|
+
foreign_keys = []
|
21
22
|
|
22
23
|
@tables = tables.map do |table|
|
24
|
+
fk = DynamicDatabase.connection.foreign_keys(table)
|
25
|
+
foreign_keys.concat(fk) unless fk.empty?
|
26
|
+
|
23
27
|
row_count = DynamicDatabase.connection.select_value("SELECT COUNT(*) FROM #{table}").to_i
|
24
28
|
{ name: table, row_count: row_count }
|
25
29
|
end
|
30
|
+
|
31
|
+
tables = {}
|
32
|
+
relations = []
|
33
|
+
|
34
|
+
foreign_keys.each do |rel|
|
35
|
+
from_table = rel[:from_table]
|
36
|
+
to_table = rel[:to_table]
|
37
|
+
fk_field = rel.dig(:options).dig(:column)
|
38
|
+
pk_field = rel.dig(:options).dig(:primary_key)
|
39
|
+
|
40
|
+
# Initialize tables if not already present
|
41
|
+
tables[from_table] ||= { name: from_table, fields: [] }
|
42
|
+
tables[to_table] ||= { name: to_table, fields: [] }
|
43
|
+
|
44
|
+
# Add fields if not already included
|
45
|
+
tables[from_table][:fields] << fk_field unless tables[from_table][:fields].include?(fk_field)
|
46
|
+
tables[to_table][:fields] << pk_field unless tables[to_table][:fields].include?(pk_field)
|
47
|
+
|
48
|
+
# Build a simplified relation object
|
49
|
+
relations << {
|
50
|
+
from_table: from_table,
|
51
|
+
from_field: fk_field,
|
52
|
+
to_table: to_table,
|
53
|
+
to_field: pk_field
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
@table_relations = {
|
58
|
+
tables: tables.values,
|
59
|
+
relations: relations
|
60
|
+
}
|
26
61
|
end
|
27
62
|
|
28
63
|
def table_rows
|
@@ -0,0 +1,295 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
// Connects to data-controller="table-relations"
|
4
|
+
export default class extends Controller {
|
5
|
+
connect() {
|
6
|
+
const table_relations_data = JSON.parse(
|
7
|
+
table_relationships.dataset.relations,
|
8
|
+
);
|
9
|
+
this.tables = table_relations_data.tables;
|
10
|
+
this.relations = table_relations_data.relations;
|
11
|
+
|
12
|
+
// graph manages the data structure and state of the diagram
|
13
|
+
this.graph = new joint.dia.Graph();
|
14
|
+
|
15
|
+
// paper handles rendering and user interaction
|
16
|
+
this.paper = new joint.dia.Paper({
|
17
|
+
el: document.getElementById("paper"),
|
18
|
+
model: this.graph,
|
19
|
+
width: 1000,
|
20
|
+
height: 600,
|
21
|
+
gridSize: 10,
|
22
|
+
drawGrid: true,
|
23
|
+
background: {
|
24
|
+
color: "#f8f9fa",
|
25
|
+
},
|
26
|
+
interactive: (cellView) => {
|
27
|
+
// If this cell is embedded (a field node),
|
28
|
+
// return false so that it doesn't capture pointer events.
|
29
|
+
// this way it gets delegated to the parent
|
30
|
+
if (cellView.model.parent()) {
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
return true;
|
34
|
+
},
|
35
|
+
});
|
36
|
+
|
37
|
+
// so we can render stuff when we open the dialog to avoid render issues with wrong sizes
|
38
|
+
this.#observeDialogOpenStatus();
|
39
|
+
}
|
40
|
+
|
41
|
+
disconnect() {
|
42
|
+
console.log("disconnected...");
|
43
|
+
}
|
44
|
+
|
45
|
+
#buildTables() {
|
46
|
+
this.tables.forEach((table, index) => {
|
47
|
+
// Initial position - will be adjusted by layout algorithm
|
48
|
+
const position = { x: 100, y: 100 };
|
49
|
+
// Create table node for each table with its fields
|
50
|
+
const tableCells = this.#createTableNode(
|
51
|
+
table.name,
|
52
|
+
table.name,
|
53
|
+
table.fields,
|
54
|
+
position,
|
55
|
+
);
|
56
|
+
this.graph.addCells(tableCells);
|
57
|
+
});
|
58
|
+
}
|
59
|
+
|
60
|
+
#buildLinks() {
|
61
|
+
// biome-ignore lint/complexity/noForEach: <explanation>
|
62
|
+
this.relations.forEach((relation) => {
|
63
|
+
const sourceId = `${relation.from_table}-${relation.from_field}`;
|
64
|
+
const targetId = `${relation.to_table}-${relation.to_field}`;
|
65
|
+
|
66
|
+
const link = new joint.shapes.standard.Link({
|
67
|
+
source: { id: sourceId },
|
68
|
+
target: { id: targetId },
|
69
|
+
attrs: {
|
70
|
+
line: {
|
71
|
+
stroke: "#333",
|
72
|
+
strokeWidth: 2,
|
73
|
+
targetMarker: {
|
74
|
+
type: "path",
|
75
|
+
d: "M 10 -5 0 0 10 5 z",
|
76
|
+
},
|
77
|
+
},
|
78
|
+
},
|
79
|
+
router: { name: "manhattan" },
|
80
|
+
connector: { name: "rounded" },
|
81
|
+
});
|
82
|
+
|
83
|
+
this.graph.addCell(link);
|
84
|
+
});
|
85
|
+
}
|
86
|
+
|
87
|
+
#createTableNode(id, tableName, fields, position) {
|
88
|
+
// Calculate height based on number of fields
|
89
|
+
const headerHeight = 30;
|
90
|
+
const fieldHeight = 25;
|
91
|
+
const width = 270;
|
92
|
+
const height = headerHeight + fields.length * fieldHeight;
|
93
|
+
|
94
|
+
// Create the main table node
|
95
|
+
const tableNode = new joint.shapes.standard.HeaderedRectangle({
|
96
|
+
id: id,
|
97
|
+
position: position,
|
98
|
+
size: { width: width, height: height },
|
99
|
+
attrs: {
|
100
|
+
body: {
|
101
|
+
fill: "#f5f5f5",
|
102
|
+
strokeWidth: 2,
|
103
|
+
stroke: "#ccc",
|
104
|
+
},
|
105
|
+
header: {
|
106
|
+
fill: "#e0e0e0",
|
107
|
+
strokeWidth: 1,
|
108
|
+
stroke: "#ccc",
|
109
|
+
height: headerHeight,
|
110
|
+
},
|
111
|
+
headerText: {
|
112
|
+
text: tableName,
|
113
|
+
fill: "#000",
|
114
|
+
fontWeight: "bold",
|
115
|
+
fontSize: 16,
|
116
|
+
},
|
117
|
+
},
|
118
|
+
});
|
119
|
+
|
120
|
+
// Create field nodes
|
121
|
+
const fieldNodes = fields.map((field, index) => {
|
122
|
+
const fieldNode = new joint.shapes.standard.Rectangle({
|
123
|
+
id: `${id}-${field}`,
|
124
|
+
position: {
|
125
|
+
x: position.x,
|
126
|
+
y: position.y + headerHeight + index * fieldHeight,
|
127
|
+
},
|
128
|
+
size: { width: width, height: fieldHeight },
|
129
|
+
attrs: {
|
130
|
+
body: {
|
131
|
+
fill: "#ffffff",
|
132
|
+
strokeWidth: 1,
|
133
|
+
stroke: "#ddd",
|
134
|
+
},
|
135
|
+
label: {
|
136
|
+
text: field,
|
137
|
+
fill: "#333",
|
138
|
+
fontSize: 14,
|
139
|
+
},
|
140
|
+
},
|
141
|
+
ports: {
|
142
|
+
groups: {
|
143
|
+
left: {
|
144
|
+
position: "left",
|
145
|
+
attrs: {
|
146
|
+
circle: {
|
147
|
+
r: 4,
|
148
|
+
magnet: true,
|
149
|
+
fill: "transparent",
|
150
|
+
stroke: "transparent",
|
151
|
+
},
|
152
|
+
},
|
153
|
+
},
|
154
|
+
right: {
|
155
|
+
position: "right",
|
156
|
+
attrs: {
|
157
|
+
circle: {
|
158
|
+
r: 4,
|
159
|
+
magnet: true,
|
160
|
+
fill: "transparent",
|
161
|
+
stroke: "transparent",
|
162
|
+
},
|
163
|
+
},
|
164
|
+
},
|
165
|
+
},
|
166
|
+
},
|
167
|
+
});
|
168
|
+
|
169
|
+
// Add ports for connections on left and right sides
|
170
|
+
fieldNode.addPort({ group: "left" });
|
171
|
+
fieldNode.addPort({ group: "right" });
|
172
|
+
|
173
|
+
// Allow events to bubble to the parent by not stopping delegation.
|
174
|
+
fieldNode.set("stopDelegation", false);
|
175
|
+
return fieldNode;
|
176
|
+
});
|
177
|
+
|
178
|
+
// Embed header and fields in the table node
|
179
|
+
// biome-ignore lint/complexity/noForEach: <explanation>
|
180
|
+
fieldNodes.forEach((fieldNode) => {
|
181
|
+
tableNode.embed(fieldNode);
|
182
|
+
});
|
183
|
+
|
184
|
+
return [tableNode, ...fieldNodes];
|
185
|
+
}
|
186
|
+
|
187
|
+
#applyLayout() {
|
188
|
+
// Create a new directed graph
|
189
|
+
const g = new dagre.graphlib.Graph();
|
190
|
+
|
191
|
+
// Set an object for the graph label
|
192
|
+
g.setGraph({
|
193
|
+
rankdir: "LR",
|
194
|
+
nodesep: 80,
|
195
|
+
edgesep: 50,
|
196
|
+
ranksep: 150,
|
197
|
+
marginx: 50,
|
198
|
+
marginy: 50,
|
199
|
+
});
|
200
|
+
|
201
|
+
// Default to assigning a new object as a label for each new edge.
|
202
|
+
g.setDefaultEdgeLabel(() => ({}));
|
203
|
+
|
204
|
+
// Get all the root elements (tables, not fields)
|
205
|
+
const elements = this.graph.getElements().filter((el) => !el.parent());
|
206
|
+
|
207
|
+
// Add nodes to the graph
|
208
|
+
// biome-ignore lint/complexity/noForEach: <explanation>
|
209
|
+
elements.forEach((element) => {
|
210
|
+
g.setNode(element.id, {
|
211
|
+
width: element.get("size").width,
|
212
|
+
height: element.get("size").height,
|
213
|
+
});
|
214
|
+
});
|
215
|
+
|
216
|
+
// Add edges to the graph
|
217
|
+
// biome-ignore lint/complexity/noForEach: <explanation>
|
218
|
+
this.graph.getLinks().forEach((link) => {
|
219
|
+
const source = this.graph.getCell(link.get("source").id);
|
220
|
+
const target = this.graph.getCell(link.get("target").id);
|
221
|
+
|
222
|
+
// Only add edges between parent elements
|
223
|
+
if (source && target) {
|
224
|
+
const sourceParent = source.parent()
|
225
|
+
? this.graph.getCell(source.parent())
|
226
|
+
: source;
|
227
|
+
const targetParent = target.parent()
|
228
|
+
? this.graph.getCell(target.parent())
|
229
|
+
: target;
|
230
|
+
|
231
|
+
if (sourceParent.id !== targetParent.id) {
|
232
|
+
g.setEdge(sourceParent.id, targetParent.id);
|
233
|
+
}
|
234
|
+
}
|
235
|
+
});
|
236
|
+
|
237
|
+
// Run the layout
|
238
|
+
dagre.layout(g);
|
239
|
+
|
240
|
+
// Apply the layout to the JointJS graph
|
241
|
+
// biome-ignore lint/complexity/noForEach: <explanation>
|
242
|
+
g.nodes().forEach((nodeId) => {
|
243
|
+
const element = this.graph.getCell(nodeId);
|
244
|
+
if (element) {
|
245
|
+
const node = g.node(nodeId);
|
246
|
+
|
247
|
+
// Position the element at the center of the node position
|
248
|
+
element.position(node.x - node.width / 2, node.y - node.height / 2);
|
249
|
+
|
250
|
+
// Adjust the position of embedded elements (fields)
|
251
|
+
const embeds = element.getEmbeddedCells();
|
252
|
+
if (embeds.length > 0) {
|
253
|
+
const position = element.position();
|
254
|
+
const headerHeight = 30;
|
255
|
+
const fieldHeight = 25;
|
256
|
+
|
257
|
+
embeds.forEach((embed, index) => {
|
258
|
+
embed.position(
|
259
|
+
position.x,
|
260
|
+
position.y + headerHeight + index * fieldHeight,
|
261
|
+
);
|
262
|
+
});
|
263
|
+
}
|
264
|
+
}
|
265
|
+
});
|
266
|
+
|
267
|
+
// Fit the content to the paper
|
268
|
+
this.paper.fitToContent({
|
269
|
+
padding: 50,
|
270
|
+
allowNewOrigin: "any",
|
271
|
+
});
|
272
|
+
}
|
273
|
+
|
274
|
+
#observeDialogOpenStatus() {
|
275
|
+
const observer = new MutationObserver((mutations) => {
|
276
|
+
for (const mutation of mutations) {
|
277
|
+
if (
|
278
|
+
mutation.type === "attributes" &&
|
279
|
+
mutation.attributeName === "open"
|
280
|
+
) {
|
281
|
+
if (this.element.hasAttribute("open")) {
|
282
|
+
// The dialog has been opened – run your code
|
283
|
+
setTimeout(() => {
|
284
|
+
this.#buildTables();
|
285
|
+
this.#buildLinks();
|
286
|
+
this.#applyLayout();
|
287
|
+
}, 50);
|
288
|
+
}
|
289
|
+
}
|
290
|
+
}
|
291
|
+
});
|
292
|
+
|
293
|
+
observer.observe(this.element, { attributes: true });
|
294
|
+
}
|
295
|
+
}
|
@@ -10,6 +10,8 @@
|
|
10
10
|
|
11
11
|
<%= stylesheet_link_tag "solid_litequeen/application", media: "all", "data-turbo-track": "reload" %>
|
12
12
|
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/@joint/core@4.0.1/dist/joint.js"></script>
|
14
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
|
13
15
|
<style type="text/tailwindcss">
|
14
16
|
@plugins "form";
|
15
17
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<dialog id="table_relationships" data-controller="table-relations" data-relations="<%= @table_relations.to_json %>" class="w-[1200px] h-full m-auto overscroll-y-contain">
|
2
|
+
<form method="submit" class="flex flex-row-reverse">
|
3
|
+
<button formmethod="dialog" class="cursor-pointer mr-4 mt-2 outline-none">
|
4
|
+
<%= image_tag "solid_litequeen/icons/x.svg", class: "size-5" %>
|
5
|
+
</button>
|
6
|
+
</form>
|
7
|
+
<h1 class="text-black text-center mt-4">Table Relationships</h1>
|
8
|
+
|
9
|
+
<div id="paper" class="m-auto my-10 w-full h-[600px] border rounded"></div>
|
10
|
+
</dialog>
|
@@ -7,7 +7,20 @@
|
|
7
7
|
</h1>
|
8
8
|
|
9
9
|
<div class="mb-6">
|
10
|
-
<h2 class="text-xl font-semibold text-gray-800">
|
10
|
+
<h2 class="text-xl font-semibold text-gray-800">
|
11
|
+
<span>
|
12
|
+
Tables
|
13
|
+
</span>
|
14
|
+
<% if @table_relations[:tables].any? %>
|
15
|
+
|
16
|
+
<%= render "table-relationships-dialog" %>
|
17
|
+
|
18
|
+
<button title="Relationships" onclick="table_relationships.showModal()" class="hover:cursor-pointer outline-none">
|
19
|
+
<%= image_tag "solid_litequeen/icons/workflow.svg", class: "size-5 -mb-1" %>
|
20
|
+
</button>
|
21
|
+
|
22
|
+
<% end %>
|
23
|
+
</h2>
|
11
24
|
<p class="text-gray-600"><%= pluralize(@tables.count, "table") %> found</p>
|
12
25
|
</div>
|
13
26
|
|
data/config/importmap.rb
CHANGED
@@ -5,5 +5,7 @@ pin "application", to: "solid_litequeen/application.js", preload: true
|
|
5
5
|
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
6
6
|
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
7
7
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
8
|
+
pin " @joint/core", to: "join.js", preload: false
|
9
|
+
pin " @dagrejs/dagre", to: "dagre.min.js", preload: false
|
8
10
|
pin_all_from SolidLitequeen::Engine.root.join("app/javascript/solid_litequeen/controllers"), under: "controllers", to: "solid_litequeen/controllers"
|
9
11
|
# pin_all_from SolidLitequeen::Engine.root.join("app/javascript/solid_litequeen/helpers"), under: "helpers", to: "solid_litequeen/helpers"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_litequeen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vik Borges
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -166,6 +166,8 @@ files:
|
|
166
166
|
- app/assets/images/solid_litequeen/icons/chevron-right.svg
|
167
167
|
- app/assets/images/solid_litequeen/icons/database.svg
|
168
168
|
- app/assets/images/solid_litequeen/icons/info.svg
|
169
|
+
- app/assets/images/solid_litequeen/icons/workflow.svg
|
170
|
+
- app/assets/images/solid_litequeen/icons/x.svg
|
169
171
|
- app/assets/stylesheets/solid_litequeen/application.css
|
170
172
|
- app/controllers/solid_litequeen/application_controller.rb
|
171
173
|
- app/controllers/solid_litequeen/databases_controller.rb
|
@@ -175,11 +177,13 @@ files:
|
|
175
177
|
- app/javascript/solid_litequeen/controllers/application.js
|
176
178
|
- app/javascript/solid_litequeen/controllers/index.js
|
177
179
|
- app/javascript/solid_litequeen/controllers/table_controller.js
|
180
|
+
- app/javascript/solid_litequeen/controllers/table_relations_controller.js
|
178
181
|
- app/jobs/solid_litequeen/application_job.rb
|
179
182
|
- app/mailers/solid_litequeen/application_mailer.rb
|
180
183
|
- app/models/solid_litequeen/application_record.rb
|
181
184
|
- app/views/layouts/solid_litequeen/application.html.erb
|
182
185
|
- app/views/solid_litequeen/_database-selector.html.erb
|
186
|
+
- app/views/solid_litequeen/databases/_table-relationships-dialog.html.erb
|
183
187
|
- app/views/solid_litequeen/databases/index.html.erb
|
184
188
|
- app/views/solid_litequeen/databases/show.html.erb
|
185
189
|
- app/views/solid_litequeen/databases/table_rows.html.erb
|