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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8bea5f5d1e8b1ca9a1c3cc11443592734bad832f263f61c70bace53c74881f42
4
- data.tar.gz: f4e91d3189ca8e38c3acce297f1da104d99adff11b5ede2b32a02ab3a49f2f0f
3
+ metadata.gz: 20c5e5b3dda06319b2fc54f414d021f38b8bce98a343c1ab3a9085658255956f
4
+ data.tar.gz: aeef980f3988611b19ad8300944659c2ef619d5c71206aa4be56108a09919d46
5
5
  SHA512:
6
- metadata.gz: 2b67cf3712f2f5931fbc43e7ba3d53cc77324d8f231de9d24041068dc5704d000ba53e977914355d8a09b168a88ef9cdff8ec44eee0ec01a36a2e927fb9e9277
7
- data.tar.gz: 80cff9bb4a94b9ef65eaa3be3cfc72819b03a820f61bfbcac334607897d8ca6c724fe823e5303df2f5bb6d9d13f9e64978a1d0951d0da8c5b077eab10d2d4d8c
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">Tables</h2>
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"
@@ -1,3 +1,3 @@
1
1
  module SolidLitequeen
2
- VERSION = "0.10.3"
2
+ VERSION = "0.11.0"
3
3
  end
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.10.3
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 00:00:00.000000000 Z
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