sqlite-hub 0.7.0 → 0.9.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.
Files changed (45) hide show
  1. package/changelog.md +25 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/api.js +67 -0
  5. package/frontend/js/app.js +1401 -921
  6. package/frontend/js/components/connectionCard.js +3 -4
  7. package/frontend/js/components/emptyState.js +4 -4
  8. package/frontend/js/components/modal.js +239 -30
  9. package/frontend/js/components/queryEditor.js +11 -8
  10. package/frontend/js/components/queryHistoryDetail.js +27 -8
  11. package/frontend/js/components/queryHistoryPanel.js +6 -5
  12. package/frontend/js/components/rowEditorPanel.js +84 -58
  13. package/frontend/js/components/sidebar.js +76 -33
  14. package/frontend/js/components/structureGraph.js +629 -715
  15. package/frontend/js/components/tableDesignerEditor.js +9 -8
  16. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  17. package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
  18. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  19. package/frontend/js/router.js +81 -71
  20. package/frontend/js/store.js +3095 -2165
  21. package/frontend/js/views/charts.js +68 -40
  22. package/frontend/js/views/connections.js +5 -17
  23. package/frontend/js/views/data.js +40 -27
  24. package/frontend/js/views/editor.js +172 -177
  25. package/frontend/js/views/mediaTagging.js +861 -0
  26. package/frontend/js/views/overview.js +149 -10
  27. package/frontend/js/views/settings.js +2 -2
  28. package/frontend/js/views/structure.js +74 -70
  29. package/frontend/js/views/tableDesigner.js +7 -2
  30. package/frontend/styles/base.css +73 -1
  31. package/frontend/styles/components.css +105 -105
  32. package/frontend/styles/structure-graph.css +19 -82
  33. package/frontend/styles/tokens.css +2 -0
  34. package/frontend/styles/views.css +823 -30
  35. package/package.json +1 -1
  36. package/server/routes/charts.js +4 -1
  37. package/server/routes/data.js +14 -0
  38. package/server/routes/mediaTagging.js +166 -0
  39. package/server/routes/sql.js +1 -0
  40. package/server/server.js +4 -0
  41. package/server/services/sqlite/dataBrowserService.js +25 -0
  42. package/server/services/sqlite/exportService.js +31 -1
  43. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  44. package/server/services/sqlite/overviewService.js +68 -0
  45. package/server/services/storage/appStateStore.js +321 -2
@@ -1,4 +1,5 @@
1
- import { escapeHtml, formatNumber } from "../utils/format.js";
1
+ import { showToast } from '../store.js';
2
+ import { escapeHtml, formatNumber } from '../utils/format.js';
2
3
 
3
4
  let cytoscapeFactory = null;
4
5
  let currentGraph = null;
@@ -6,67 +7,64 @@ let mountVersion = 0;
6
7
  let persistedGraphState = null;
7
8
 
8
9
  function getTableId(tableName) {
9
- return `table:${tableName}`;
10
+ return `table:${tableName}`;
10
11
  }
11
12
 
12
13
  function getSchemaSignature(schema) {
13
- return JSON.stringify(
14
- (schema?.tables ?? []).map((table) => ({
15
- name: table.name,
16
- columns: (table.columns ?? []).map((column) => column.name),
17
- foreignKeys: (table.foreignKeys ?? []).map((foreignKey) => ({
18
- referencedTable: foreignKey.referencedTable,
19
- mappings: (foreignKey.mappings ?? []).map(
20
- (mapping) => `${mapping.from || "?"}->${mapping.to || "rowid"}`
21
- ),
22
- })),
23
- }))
24
- );
14
+ return JSON.stringify(
15
+ (schema?.tables ?? []).map(table => ({
16
+ name: table.name,
17
+ columns: (table.columns ?? []).map(column => column.name),
18
+ foreignKeys: (table.foreignKeys ?? []).map(foreignKey => ({
19
+ referencedTable: foreignKey.referencedTable,
20
+ mappings: (foreignKey.mappings ?? []).map(
21
+ mapping => `${mapping.from || '?'}->${mapping.to || 'rowid'}`,
22
+ ),
23
+ })),
24
+ })),
25
+ );
25
26
  }
26
27
 
27
28
  function getPersistedGraphState(schema) {
28
- const schemaSignature = getSchemaSignature(schema);
29
+ const schemaSignature = getSchemaSignature(schema);
30
+
31
+ if (persistedGraphState?.schemaSignature !== schemaSignature) {
32
+ return {
33
+ schemaSignature,
34
+ state: null,
35
+ };
36
+ }
29
37
 
30
- if (persistedGraphState?.schemaSignature !== schemaSignature) {
31
38
  return {
32
- schemaSignature,
33
- state: null,
39
+ schemaSignature,
40
+ state: persistedGraphState,
34
41
  };
35
- }
36
-
37
- return {
38
- schemaSignature,
39
- state: persistedGraphState,
40
- };
41
42
  }
42
43
 
43
44
  function collectForeignKeyColumns(tableData) {
44
- const columns = new Set();
45
+ const columns = new Set();
45
46
 
46
- (tableData?.foreignKeys ?? []).forEach((foreignKey) => {
47
- (foreignKey.mappings ?? []).forEach((mapping) => {
48
- if (mapping.from) {
49
- columns.add(mapping.from);
50
- }
47
+ (tableData?.foreignKeys ?? []).forEach(foreignKey => {
48
+ (foreignKey.mappings ?? []).forEach(mapping => {
49
+ if (mapping.from) {
50
+ columns.add(mapping.from);
51
+ }
52
+ });
51
53
  });
52
- });
53
54
 
54
- return columns;
55
+ return columns;
55
56
  }
56
57
 
57
58
  function getVisibleColumns(tableData) {
58
- return (tableData?.columns ?? []).filter((column) => column.visible !== false);
59
+ return (tableData?.columns ?? []).filter(column => column.visible !== false);
59
60
  }
60
61
 
61
62
  function getRelationshipCount(tableData) {
62
- return (tableData?.foreignKeys ?? []).reduce(
63
- (count, foreignKey) => count + (foreignKey.mappings?.length ?? 0),
64
- 0
65
- );
63
+ return (tableData?.foreignKeys ?? []).reduce((count, foreignKey) => count + (foreignKey.mappings?.length ?? 0), 0);
66
64
  }
67
65
 
68
66
  function createSummaryCard(label, value) {
69
- return `
67
+ return `
70
68
  <div class="structure-graph__summary-card">
71
69
  <div class="structure-graph__summary-label">${escapeHtml(label)}</div>
72
70
  <div class="structure-graph__summary-value">${escapeHtml(String(value))}</div>
@@ -75,31 +73,52 @@ function createSummaryCard(label, value) {
75
73
  }
76
74
 
77
75
  function createColumnFlags(column, foreignKeyColumns) {
78
- const flags = [];
76
+ const flags = [];
77
+
78
+ if (column.primaryKeyPosition > 0) {
79
+ flags.push(
80
+ `<span class="structure-graph__flag is-key">PK${
81
+ column.primaryKeyPosition > 1 ? ` ${escapeHtml(String(column.primaryKeyPosition))}` : ''
82
+ }</span>`,
83
+ );
84
+ }
79
85
 
80
- if (column.primaryKeyPosition > 0) {
81
- flags.push(
82
- `<span class="structure-graph__flag is-key">PK${
83
- column.primaryKeyPosition > 1 ? ` ${escapeHtml(String(column.primaryKeyPosition))}` : ""
84
- }</span>`
85
- );
86
- }
86
+ if (foreignKeyColumns.has(column.name)) {
87
+ flags.push('<span class="structure-graph__flag is-link">FK</span>');
88
+ }
87
89
 
88
- if (foreignKeyColumns.has(column.name)) {
89
- flags.push('<span class="structure-graph__flag is-link">FK</span>');
90
- }
90
+ flags.push(`<span class="structure-graph__flag is-nullable">${column.notNull ? 'NOT NULL' : 'NULLABLE'}</span>`);
91
+
92
+ return flags.join('');
93
+ }
91
94
 
92
- flags.push(
93
- `<span class="structure-graph__flag is-nullable">${
94
- column.notNull ? "NOT NULL" : "NULLABLE"
95
- }</span>`
96
- );
95
+ export function renderDdlSection(ddl, emptyLabel = 'No DDL available.') {
96
+ const ddlText = typeof ddl === 'string' ? ddl : '';
97
+ const hasDdl = Boolean(ddlText.trim());
97
98
 
98
- return flags.join("");
99
+ return `
100
+ <section class="structure-graph__section">
101
+ <div class="structure-graph__section-header">
102
+ <div class="structure-graph__section-title">DDL</div>
103
+ <button
104
+ class="standard-button"
105
+ data-structure-graph-action="copy-ddl"
106
+ ${hasDdl ? '' : 'disabled'}
107
+ type="button"
108
+ >
109
+ <span class="material-symbols-outlined text-sm">content_copy</span>
110
+ Copy to clipboard
111
+ </button>
112
+ </div>
113
+ <pre class="structure-graph__ddl custom-scrollbar" data-structure-graph-ddl>${escapeHtml(
114
+ hasDdl ? ddlText : emptyLabel,
115
+ )}</pre>
116
+ </section>
117
+ `;
99
118
  }
100
119
 
101
120
  export function clearInspector() {
102
- return `
121
+ return `
103
122
  <div class="structure-graph__panel is-empty">
104
123
  <span class="material-symbols-outlined structure-graph__empty-icon">hub</span>
105
124
  <div class="structure-graph__title">Graph Ready</div>
@@ -111,17 +130,17 @@ export function clearInspector() {
111
130
  }
112
131
 
113
132
  export function renderInspector(tableData) {
114
- if (!tableData || tableData.type !== "table") {
115
- return clearInspector();
116
- }
133
+ if (!tableData || tableData.type !== 'table') {
134
+ return clearInspector();
135
+ }
117
136
 
118
- const visibleColumns = getVisibleColumns(tableData);
119
- const foreignKeyColumns = collectForeignKeyColumns(tableData);
120
- const primaryKeyCount = visibleColumns.filter((column) => column.primaryKeyPosition > 0).length;
121
- const foreignKeyCount = getRelationshipCount(tableData);
122
- const nullableCount = visibleColumns.filter((column) => !column.notNull).length;
137
+ const visibleColumns = getVisibleColumns(tableData);
138
+ const foreignKeyColumns = collectForeignKeyColumns(tableData);
139
+ const primaryKeyCount = visibleColumns.filter(column => column.primaryKeyPosition > 0).length;
140
+ const foreignKeyCount = getRelationshipCount(tableData);
141
+ const nullableCount = visibleColumns.filter(column => !column.notNull).length;
123
142
 
124
- return `
143
+ return `
125
144
  <div class="structure-graph__panel">
126
145
  <div class="space-y-3">
127
146
  <div class="structure-graph__eyebrow">Table Inspector</div>
@@ -132,794 +151,689 @@ export function renderInspector(tableData) {
132
151
  </div>
133
152
 
134
153
  <div class="structure-graph__summary">
135
- ${createSummaryCard("Columns", formatNumber(visibleColumns.length))}
136
- ${createSummaryCard("PK Columns", formatNumber(primaryKeyCount))}
137
- ${createSummaryCard("FK Links", formatNumber(foreignKeyCount))}
138
- ${createSummaryCard("Nullable", formatNumber(nullableCount))}
154
+ ${createSummaryCard('Columns', formatNumber(visibleColumns.length))}
155
+ ${createSummaryCard('PK Columns', formatNumber(primaryKeyCount))}
156
+ ${createSummaryCard('FK Links', formatNumber(foreignKeyCount))}
157
+ ${createSummaryCard('Nullable', formatNumber(nullableCount))}
139
158
  </div>
140
159
 
141
160
  <section class="structure-graph__section">
142
161
  <div class="structure-graph__section-title">Columns</div>
143
162
  <div class="structure-graph__column-list">
144
163
  ${
145
- visibleColumns.length
146
- ? visibleColumns
147
- .map(
148
- (column) => `
164
+ visibleColumns.length
165
+ ? visibleColumns
166
+ .map(
167
+ column => `
149
168
  <div class="structure-graph__column-row">
150
169
  <div class="min-w-0 space-y-1">
151
170
  <div class="structure-graph__column-name">${escapeHtml(column.name)}</div>
152
171
  <div class="structure-graph__column-type">${escapeHtml(
153
- column.declaredType || column.affinity || "BLOB"
172
+ column.declaredType || column.affinity || 'BLOB',
154
173
  )}</div>
155
174
  </div>
156
175
  <div class="structure-graph__column-flags">
157
176
  ${createColumnFlags(column, foreignKeyColumns)}
158
177
  </div>
159
178
  </div>
160
- `
161
- )
162
- .join("")
163
- : '<div class="text-sm text-on-surface-variant/45">No visible columns found.</div>'
179
+ `,
180
+ )
181
+ .join('')
182
+ : '<div class="text-sm text-on-surface-variant/45">No visible columns found.</div>'
164
183
  }
165
184
  </div>
166
185
  </section>
167
186
 
168
- <section class="structure-graph__section">
169
- <div class="structure-graph__section-title">DDL</div>
170
- <pre class="structure-graph__ddl custom-scrollbar">${escapeHtml(
171
- tableData.ddl || "No DDL available."
172
- )}</pre>
173
- </section>
187
+ ${renderDdlSection(tableData.ddl)}
174
188
  </div>
175
189
  `;
176
190
  }
177
191
 
178
192
  export function buildGraphElements(schema) {
179
- const tables = schema?.tables ?? [];
180
- const tableMap = new Map(tables.map((table) => [table.name, table]));
181
- const nodes = tables.map((table) => ({
182
- group: "nodes",
183
- data: {
184
- id: getTableId(table.name),
185
- label: table.name,
186
- tableName: table.name,
187
- width: Math.max(184, table.name.length * 9 + 64),
188
- height: 56,
189
- table,
190
- },
191
- }));
192
- const edges = [];
193
-
194
- tables.forEach((table) => {
195
- (table.foreignKeys ?? []).forEach((foreignKey, foreignKeyIndex) => {
196
- if (!tableMap.has(foreignKey.referencedTable)) {
197
- return;
198
- }
199
-
200
- (foreignKey.mappings ?? []).forEach((mapping, mappingIndex) => {
201
- edges.push({
202
- group: "edges",
203
- data: {
204
- id: [
205
- "edge",
206
- table.name,
207
- foreignKey.referencedTable,
208
- mapping.from || `source_${mappingIndex}`,
209
- mapping.to || `target_${mappingIndex}`,
210
- foreignKeyIndex,
211
- mappingIndex,
212
- ].join(":"),
213
- source: getTableId(table.name),
214
- target: getTableId(foreignKey.referencedTable),
215
- label: `${mapping.from || "?"} → ${mapping.to || "rowid"}`,
216
- sourceTable: table.name,
217
- targetTable: foreignKey.referencedTable,
218
- sourceColumn: mapping.from || "",
219
- targetColumn: mapping.to || "rowid",
220
- },
193
+ const tables = schema?.tables ?? [];
194
+ const tableMap = new Map(tables.map(table => [table.name, table]));
195
+ const nodes = tables.map(table => ({
196
+ group: 'nodes',
197
+ data: {
198
+ id: getTableId(table.name),
199
+ label: table.name,
200
+ tableName: table.name,
201
+ width: Math.max(184, table.name.length * 9 + 64),
202
+ height: 56,
203
+ table,
204
+ },
205
+ }));
206
+ const edges = [];
207
+
208
+ tables.forEach(table => {
209
+ (table.foreignKeys ?? []).forEach((foreignKey, foreignKeyIndex) => {
210
+ if (!tableMap.has(foreignKey.referencedTable)) {
211
+ return;
212
+ }
213
+
214
+ (foreignKey.mappings ?? []).forEach((mapping, mappingIndex) => {
215
+ edges.push({
216
+ group: 'edges',
217
+ data: {
218
+ id: [
219
+ 'edge',
220
+ table.name,
221
+ foreignKey.referencedTable,
222
+ mapping.from || `source_${mappingIndex}`,
223
+ mapping.to || `target_${mappingIndex}`,
224
+ foreignKeyIndex,
225
+ mappingIndex,
226
+ ].join(':'),
227
+ source: getTableId(table.name),
228
+ target: getTableId(foreignKey.referencedTable),
229
+ label: `${mapping.from || '?'} → ${mapping.to || 'rowid'}`,
230
+ sourceTable: table.name,
231
+ targetTable: foreignKey.referencedTable,
232
+ sourceColumn: mapping.from || '',
233
+ targetColumn: mapping.to || 'rowid',
234
+ },
235
+ });
236
+ });
221
237
  });
222
- });
223
238
  });
224
- });
225
239
 
226
- return [...nodes, ...edges];
240
+ return [...nodes, ...edges];
227
241
  }
228
242
 
229
243
  export function getElkLayoutOptions(overrides = {}) {
230
- return {
231
- name: "elk",
232
- fit: false,
233
- padding: 30,
234
- animate: false,
235
- nodeDimensionsIncludeLabels: false,
236
- elk: {
237
- algorithm: "layered",
238
- "elk.direction": "RIGHT",
239
- "elk.edgeRouting": "ORTHOGONAL",
240
- "elk.layered.spacing.nodeNodeBetweenLayers": 60,
241
- "elk.spacing.nodeNode": 40,
242
- "elk.padding": 30,
243
- },
244
- ...overrides,
245
- };
244
+ return {
245
+ name: 'elk',
246
+ fit: false,
247
+ padding: 30,
248
+ animate: false,
249
+ nodeDimensionsIncludeLabels: false,
250
+ elk: {
251
+ algorithm: 'layered',
252
+ 'elk.direction': 'RIGHT',
253
+ 'elk.edgeRouting': 'ORTHOGONAL',
254
+ 'elk.layered.spacing.nodeNodeBetweenLayers': 60,
255
+ 'elk.spacing.nodeNode': 40,
256
+ 'elk.padding': 30,
257
+ },
258
+ ...overrides,
259
+ };
246
260
  }
247
261
 
248
262
  export function createCytoscapeInstance(container, elements) {
249
- if (!cytoscapeFactory) {
250
- throw new Error("Cytoscape runtime has not been initialized.");
251
- }
252
-
253
- return cytoscapeFactory({
254
- container,
255
- elements,
256
- layout: { name: "preset" },
257
- minZoom: 0.2,
258
- maxZoom: 2.5,
259
- wheelSensitivity: 0.18,
260
- motionBlur: false,
261
- textureOnViewport: true,
262
- boxSelectionEnabled: false,
263
- style: [
264
- {
265
- selector: "node",
266
- style: {
267
- shape: "roundrectangle",
268
- width: "data(width)",
269
- height: "data(height)",
270
- label: "data(label)",
271
- "background-color": "#262421",
272
- "border-color": "#8a7b34",
273
- "border-width": 1.4,
274
- color: "#f4efe8",
275
- "font-family": "Space Grotesk, Inter, sans-serif",
276
- "font-size": 13,
277
- "font-weight": 700,
278
- "text-wrap": "ellipsis",
279
- "text-max-width": 150,
280
- "text-halign": "center",
281
- "text-valign": "center",
282
- "overlay-opacity": 0,
283
- "transition-property": "background-color, border-color, opacity, color",
284
- "transition-duration": "140ms",
285
- },
286
- },
287
- {
288
- selector: "node.selected",
289
- style: {
290
- "background-color": "#3b340b",
291
- "border-color": "#fce300",
292
- "border-width": 2.8,
293
- color: "#fff6ae",
294
- },
295
- },
296
- {
297
- selector: "node.related",
298
- style: {
299
- "background-color": "#0d2d30",
300
- "border-color": "#2dfaff",
301
- "border-width": 2.2,
302
- color: "#fbfffe",
303
- },
304
- },
305
- {
306
- selector: "node.dimmed",
307
- style: {
308
- opacity: 0.24,
309
- },
310
- },
311
- {
312
- selector: "edge",
313
- style: {
314
- width: 1.6,
315
- label: "data(label)",
316
- color: "#e7dfbd",
317
- "font-family": "Roboto Mono, monospace",
318
- "font-size": 8,
319
- "text-background-color": "#171715",
320
- "text-background-opacity": 0.94,
321
- "text-background-padding": 4,
322
- "line-color": "#8a7b34",
323
- "target-arrow-color": "#8a7b34",
324
- "target-arrow-shape": "triangle",
325
- "curve-style": "taxi",
326
- "taxi-direction": "rightward",
327
- "taxi-turn": 20,
328
- "source-endpoint": "outside-to-node",
329
- "target-endpoint": "outside-to-node",
330
- "overlay-opacity": 0,
331
- "transition-property": "line-color, target-arrow-color, opacity, color",
332
- "transition-duration": "120ms",
333
- },
334
- },
335
- {
336
- selector: "edge.related",
337
- style: {
338
- "line-color": "#2dfaff",
339
- "target-arrow-color": "#2dfaff",
340
- color: "#fbfffe",
341
- width: 2.3,
342
- },
343
- },
344
- {
345
- selector: "edge.hovered",
346
- style: {
347
- "line-color": "#fce300",
348
- "target-arrow-color": "#fce300",
349
- color: "#fff6ae",
350
- width: 2.8,
351
- },
352
- },
353
- {
354
- selector: "edge.dimmed",
355
- style: {
356
- opacity: 0.16,
357
- },
358
- },
359
- ],
360
- });
263
+ if (!cytoscapeFactory) {
264
+ throw new Error('Cytoscape runtime has not been initialized.');
265
+ }
266
+
267
+ return cytoscapeFactory({
268
+ container,
269
+ elements,
270
+ layout: { name: 'preset' },
271
+ minZoom: 0.2,
272
+ maxZoom: 2.5,
273
+ wheelSensitivity: 0.18,
274
+ motionBlur: false,
275
+ textureOnViewport: true,
276
+ boxSelectionEnabled: false,
277
+ style: [
278
+ {
279
+ selector: 'node',
280
+ style: {
281
+ shape: 'roundrectangle',
282
+ width: 'data(width)',
283
+ height: 'data(height)',
284
+ label: 'data(label)',
285
+ 'background-color': '#262421',
286
+ 'border-color': '#8a7b34',
287
+ 'border-width': 1.4,
288
+ color: '#f4efe8',
289
+ 'font-family': 'Space Grotesk, Inter, sans-serif',
290
+ 'font-size': 13,
291
+ 'font-weight': 700,
292
+ 'text-wrap': 'ellipsis',
293
+ 'text-max-width': 150,
294
+ 'text-halign': 'center',
295
+ 'text-valign': 'center',
296
+ 'overlay-opacity': 0,
297
+ 'transition-property': 'background-color, border-color, opacity, color',
298
+ 'transition-duration': '140ms',
299
+ },
300
+ },
301
+ {
302
+ selector: 'node.selected',
303
+ style: {
304
+ 'background-color': '#3b340b',
305
+ 'border-color': '#fce300',
306
+ 'border-width': 2.8,
307
+ color: '#fff6ae',
308
+ },
309
+ },
310
+ {
311
+ selector: 'node.related',
312
+ style: {
313
+ 'background-color': '#0d2d30',
314
+ 'border-color': '#2dfaff',
315
+ 'border-width': 2.2,
316
+ color: '#fbfffe',
317
+ },
318
+ },
319
+ {
320
+ selector: 'node.dimmed',
321
+ style: {
322
+ opacity: 0.24,
323
+ },
324
+ },
325
+ {
326
+ selector: 'edge',
327
+ style: {
328
+ width: 1.6,
329
+ label: 'data(label)',
330
+ color: '#e7dfbd',
331
+ 'font-family': 'Roboto Mono, monospace',
332
+ 'font-size': 8,
333
+ 'text-background-color': '#171715',
334
+ 'text-background-opacity': 0.94,
335
+ 'text-background-padding': 4,
336
+ 'line-color': '#8a7b34',
337
+ 'target-arrow-color': '#8a7b34',
338
+ 'target-arrow-shape': 'triangle',
339
+ 'curve-style': 'taxi',
340
+ 'taxi-direction': 'rightward',
341
+ 'taxi-turn': 20,
342
+ 'source-endpoint': 'outside-to-node',
343
+ 'target-endpoint': 'outside-to-node',
344
+ 'overlay-opacity': 0,
345
+ 'transition-property': 'line-color, target-arrow-color, opacity, color',
346
+ 'transition-duration': '120ms',
347
+ },
348
+ },
349
+ {
350
+ selector: 'edge.related',
351
+ style: {
352
+ 'line-color': '#2dfaff',
353
+ 'target-arrow-color': '#2dfaff',
354
+ color: '#fbfffe',
355
+ width: 2.3,
356
+ },
357
+ },
358
+ {
359
+ selector: 'edge.hovered',
360
+ style: {
361
+ 'line-color': '#fce300',
362
+ 'target-arrow-color': '#fce300',
363
+ color: '#fff6ae',
364
+ width: 2.8,
365
+ },
366
+ },
367
+ {
368
+ selector: 'edge.dimmed',
369
+ style: {
370
+ opacity: 0.16,
371
+ },
372
+ },
373
+ ],
374
+ });
361
375
  }
362
376
 
363
377
  export function resetHighlights(cy) {
364
- if (!cy) {
365
- return;
366
- }
378
+ if (!cy) {
379
+ return;
380
+ }
367
381
 
368
- cy.elements().removeClass("selected related hovered dimmed");
382
+ cy.elements().removeClass('selected related hovered dimmed');
369
383
  }
370
384
 
371
385
  export function highlightConnectedElements(cy, element) {
372
- if (!cy || !element) {
373
- return;
374
- }
375
-
376
- resetHighlights(cy);
377
- const allElements = cy.elements();
378
-
379
- if (element.isNode()) {
380
- const connectedEdges = element.connectedEdges();
381
- const relatedNodes = connectedEdges.connectedNodes().union(element);
382
- const highlighted = relatedNodes.union(connectedEdges);
383
-
384
- allElements.difference(highlighted).addClass("dimmed");
385
- element.addClass("selected");
386
- relatedNodes.difference(element).addClass("related");
387
- connectedEdges.addClass("related");
388
- return;
389
- }
390
-
391
- const source = element.source();
392
- const target = element.target();
393
- const highlighted = source.union(target).union(element);
394
-
395
- allElements.difference(highlighted).addClass("dimmed");
396
- source.addClass("related");
397
- target.addClass("related");
398
- element.addClass("hovered");
399
- }
400
-
401
- function getBestMatchingNode(cy, name) {
402
- const normalizedQuery = String(name ?? "").trim().toLowerCase();
403
-
404
- if (!normalizedQuery) {
405
- return null;
406
- }
386
+ if (!cy || !element) {
387
+ return;
388
+ }
407
389
 
408
- const nodes = cy.nodes().toArray();
409
- const exactMatch = nodes.find(
410
- (node) => String(node.data("tableName")).toLowerCase() === normalizedQuery
411
- );
390
+ resetHighlights(cy);
391
+ const allElements = cy.elements();
412
392
 
413
- if (exactMatch) {
414
- return exactMatch;
415
- }
393
+ if (element.isNode()) {
394
+ const connectedEdges = element.connectedEdges();
395
+ const relatedNodes = connectedEdges.connectedNodes().union(element);
396
+ const highlighted = relatedNodes.union(connectedEdges);
416
397
 
417
- const startsWithMatch = nodes.find((node) =>
418
- String(node.data("tableName")).toLowerCase().startsWith(normalizedQuery)
419
- );
398
+ allElements.difference(highlighted).addClass('dimmed');
399
+ element.addClass('selected');
400
+ relatedNodes.difference(element).addClass('related');
401
+ connectedEdges.addClass('related');
402
+ return;
403
+ }
420
404
 
421
- if (startsWithMatch) {
422
- return startsWithMatch;
423
- }
405
+ const source = element.source();
406
+ const target = element.target();
407
+ const highlighted = source.union(target).union(element);
424
408
 
425
- return (
426
- nodes.find((node) =>
427
- String(node.data("tableName")).toLowerCase().includes(normalizedQuery)
428
- ) ?? null
429
- );
409
+ allElements.difference(highlighted).addClass('dimmed');
410
+ source.addClass('related');
411
+ target.addClass('related');
412
+ element.addClass('hovered');
430
413
  }
431
414
 
432
- export function focusTableByName(cy, name) {
433
- const node = getBestMatchingNode(cy, name);
434
-
435
- if (!node) {
436
- return null;
437
- }
438
-
439
- cy.animate(
440
- {
441
- fit: {
442
- eles: node.closedNeighborhood(),
443
- padding: 120,
444
- },
445
- duration: 240,
446
- },
447
- {
448
- queue: false,
415
+ function destroyCurrentGraph() {
416
+ if (!currentGraph) {
417
+ return;
449
418
  }
450
- );
451
-
452
- return node;
453
- }
454
419
 
455
- function destroyCurrentGraph() {
456
- if (!currentGraph) {
457
- return;
458
- }
459
-
460
- if (currentGraph.cy && currentGraph.persistStateOnDestroy !== false) {
461
- const nodePositions = {};
462
- currentGraph.cy.nodes().forEach((node) => {
463
- nodePositions[node.id()] = {
464
- x: node.position("x"),
465
- y: node.position("y"),
466
- };
467
- });
420
+ if (currentGraph.cy && currentGraph.persistStateOnDestroy !== false) {
421
+ const nodePositions = {};
422
+ currentGraph.cy.nodes().forEach(node => {
423
+ nodePositions[node.id()] = {
424
+ x: node.position('x'),
425
+ y: node.position('y'),
426
+ };
427
+ });
468
428
 
469
- persistedGraphState = {
470
- schemaSignature: getSchemaSignature(currentGraph.schema),
471
- nodePositions,
472
- pan: currentGraph.cy.pan(),
473
- zoom: currentGraph.cy.zoom(),
474
- inspectorHidden: currentGraph.inspectorHidden,
475
- selectedTableName: currentGraph.selectedTableName,
476
- searchValue: currentGraph.searchInput?.value ?? "",
477
- searchMiss: currentGraph.searchRoot?.classList.contains("is-miss") ?? false,
478
- };
479
- }
429
+ persistedGraphState = {
430
+ schemaSignature: getSchemaSignature(currentGraph.schema),
431
+ nodePositions,
432
+ pan: currentGraph.cy.pan(),
433
+ zoom: currentGraph.cy.zoom(),
434
+ inspectorHidden: currentGraph.inspectorHidden,
435
+ selectedTableName: currentGraph.selectedTableName,
436
+ };
437
+ }
480
438
 
481
- currentGraph.cleanup.forEach((cleanup) => cleanup());
482
- currentGraph.cleanup = [];
439
+ currentGraph.cleanup.forEach(cleanup => cleanup());
440
+ currentGraph.cleanup = [];
483
441
 
484
- if (currentGraph.resizeObserver) {
485
- currentGraph.resizeObserver.disconnect();
486
- }
442
+ if (currentGraph.resizeObserver) {
443
+ currentGraph.resizeObserver.disconnect();
444
+ }
487
445
 
488
- if (currentGraph.resizeHandler) {
489
- window.removeEventListener("resize", currentGraph.resizeHandler);
490
- }
446
+ if (currentGraph.resizeHandler) {
447
+ window.removeEventListener('resize', currentGraph.resizeHandler);
448
+ }
491
449
 
492
- if (currentGraph.cy) {
493
- currentGraph.cy.destroy();
494
- }
450
+ if (currentGraph.cy) {
451
+ currentGraph.cy.destroy();
452
+ }
495
453
 
496
- currentGraph = null;
454
+ currentGraph = null;
497
455
  }
498
456
 
499
457
  export function resetPersistedStructureGraphState() {
500
- persistedGraphState = null;
458
+ persistedGraphState = null;
501
459
 
502
- if (currentGraph) {
503
- currentGraph.persistStateOnDestroy = false;
504
- }
460
+ if (currentGraph) {
461
+ currentGraph.persistStateOnDestroy = false;
462
+ }
505
463
  }
506
464
 
507
465
  function syncEntryHighlights(selectedNames = [], relatedNames = []) {
508
- if (!currentGraph?.root) {
509
- return;
510
- }
511
-
512
- const selectedSet = new Set(selectedNames);
513
- const relatedSet = new Set(relatedNames);
514
- const scope = currentGraph.root.closest(".view-frame") ?? document;
515
-
516
- scope
517
- .querySelectorAll("[data-structure-entry-name]")
518
- .forEach((node) => {
519
- const tableName = node.dataset.structureEntryName ?? "";
520
- node.classList.toggle("structure-entry--graph-active", selectedSet.has(tableName));
521
- node.classList.toggle("structure-entry--graph-related", relatedSet.has(tableName));
466
+ if (!currentGraph?.root) {
467
+ return;
468
+ }
469
+
470
+ const selectedSet = new Set(selectedNames);
471
+ const relatedSet = new Set(relatedNames);
472
+ const scope = currentGraph.root.closest('.view-frame') ?? document;
473
+
474
+ scope.querySelectorAll('[data-structure-entry-name]').forEach(node => {
475
+ const tableName = node.dataset.structureEntryName ?? '';
476
+ node.classList.toggle('structure-entry--graph-active', selectedSet.has(tableName));
477
+ node.classList.toggle('structure-entry--graph-related', relatedSet.has(tableName));
522
478
  });
523
479
  }
524
480
 
525
481
  function getDefaultInspectorMarkup() {
526
- if (!currentGraph) {
527
- return clearInspector();
528
- }
482
+ if (!currentGraph) {
483
+ return clearInspector();
484
+ }
529
485
 
530
- return currentGraph.initialSelectedTableName
531
- ? clearInspector()
532
- : currentGraph.initialInspectorMarkup || clearInspector();
486
+ return currentGraph.initialSelectedTableName
487
+ ? clearInspector()
488
+ : currentGraph.initialInspectorMarkup || clearInspector();
533
489
  }
534
490
 
535
491
  function updateOpenDataButton() {
536
- if (!currentGraph?.openDataButton) {
537
- return;
538
- }
492
+ if (!currentGraph?.openDataButton) {
493
+ return;
494
+ }
495
+
496
+ const isEnabled = Boolean(currentGraph.selectedTableName);
497
+ currentGraph.openDataButton.disabled = !isEnabled;
498
+ currentGraph.openDataButton.classList.toggle('is-disabled', !isEnabled);
499
+ }
500
+
501
+ async function copyInspectorDdl(button) {
502
+ const ddlNode = button?.closest('.structure-graph__section')?.querySelector('[data-structure-graph-ddl]');
503
+ const ddl = ddlNode?.textContent ?? '';
504
+
505
+ if (!ddl.trim()) {
506
+ showToast('No DDL available to copy.', 'alert');
507
+ return;
508
+ }
539
509
 
540
- const isEnabled = Boolean(currentGraph.selectedTableName);
541
- currentGraph.openDataButton.disabled = !isEnabled;
542
- currentGraph.openDataButton.classList.toggle("is-disabled", !isEnabled);
510
+ try {
511
+ await navigator.clipboard.writeText(ddl);
512
+ showToast('DDL copied.', 'success');
513
+ } catch (error) {
514
+ showToast('Clipboard access failed.', 'alert');
515
+ }
543
516
  }
544
517
 
545
518
  function syncInspectorLayout() {
546
- if (!currentGraph?.root) {
547
- return;
548
- }
519
+ if (!currentGraph?.root) {
520
+ return;
521
+ }
549
522
 
550
- currentGraph.root.classList.toggle("is-inspector-hidden", currentGraph.inspectorHidden);
551
- currentGraph.cy?.resize();
523
+ currentGraph.root.classList.toggle('is-inspector-hidden', currentGraph.inspectorHidden);
524
+ currentGraph.cy?.resize();
552
525
  }
553
526
 
554
527
  function updateInspectorToggleButton() {
555
- if (!currentGraph?.inspectorToggleButton) {
556
- return;
557
- }
528
+ if (!currentGraph?.inspectorToggleButton) {
529
+ return;
530
+ }
558
531
 
559
- currentGraph.inspectorToggleButton.classList.toggle("is-active", currentGraph.inspectorHidden);
560
- currentGraph.inspectorToggleButton.innerHTML = `
532
+ currentGraph.inspectorToggleButton.classList.toggle('is-active', currentGraph.inspectorHidden);
533
+ currentGraph.inspectorToggleButton.innerHTML = `
561
534
  <span class="material-symbols-outlined text-sm">${
562
- currentGraph.inspectorHidden ? "right_panel_open" : "right_panel_close"
535
+ currentGraph.inspectorHidden ? 'right_panel_open' : 'right_panel_close'
563
536
  }</span>
564
- ${currentGraph.inspectorHidden ? "Show Inspector" : "Hide Inspector"}
537
+ ${currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector'}
565
538
  `;
566
539
  }
567
540
 
568
- function clearSelection({ preserveSearch = true } = {}) {
569
- if (!currentGraph) {
570
- return;
571
- }
572
-
573
- currentGraph.selectedTableName = null;
574
- if (!preserveSearch && currentGraph.searchInput) {
575
- currentGraph.searchInput.value = "";
576
- currentGraph.searchRoot.classList.remove("is-miss");
577
- }
541
+ function clearSelection() {
542
+ if (!currentGraph) {
543
+ return;
544
+ }
578
545
 
579
- resetHighlights(currentGraph.cy);
580
- syncEntryHighlights();
581
- currentGraph.inspector.innerHTML = getDefaultInspectorMarkup();
582
- updateOpenDataButton();
546
+ currentGraph.selectedTableName = null;
547
+ resetHighlights(currentGraph.cy);
548
+ syncEntryHighlights();
549
+ currentGraph.inspector.innerHTML = getDefaultInspectorMarkup();
550
+ updateOpenDataButton();
583
551
  }
584
552
 
585
553
  function applyTableSelection(node, { focus = true } = {}) {
586
- if (!currentGraph || !node) {
587
- return null;
588
- }
589
-
590
- const tableData = node.data("table");
591
- currentGraph.selectedTableName = tableData.name;
592
- currentGraph.inspector.innerHTML = renderInspector(tableData);
593
- highlightConnectedElements(currentGraph.cy, node);
594
- syncEntryHighlights([tableData.name]);
595
- updateOpenDataButton();
596
-
597
- if (focus) {
598
- currentGraph.cy.animate(
599
- {
600
- fit: {
601
- eles: node.closedNeighborhood(),
602
- padding: 120,
603
- },
604
- duration: 240,
605
- },
606
- {
607
- queue: false,
608
- }
609
- );
610
- }
554
+ if (!currentGraph || !node) {
555
+ return null;
556
+ }
611
557
 
612
- return node;
558
+ const tableData = node.data('table');
559
+ currentGraph.selectedTableName = tableData.name;
560
+ currentGraph.inspector.innerHTML = renderInspector(tableData);
561
+ highlightConnectedElements(currentGraph.cy, node);
562
+ syncEntryHighlights([tableData.name]);
563
+ updateOpenDataButton();
564
+
565
+ if (focus) {
566
+ currentGraph.cy.animate(
567
+ {
568
+ fit: {
569
+ eles: node.closedNeighborhood(),
570
+ padding: 120,
571
+ },
572
+ duration: 240,
573
+ },
574
+ {
575
+ queue: false,
576
+ },
577
+ );
578
+ }
579
+
580
+ return node;
613
581
  }
614
582
 
615
583
  function restoreGraphState() {
616
- if (!currentGraph) {
617
- return;
618
- }
584
+ if (!currentGraph) {
585
+ return;
586
+ }
619
587
 
620
- if (currentGraph.selectedTableName) {
621
- const selectedNode = currentGraph.cy.getElementById(
622
- getTableId(currentGraph.selectedTableName)
623
- );
588
+ if (currentGraph.selectedTableName) {
589
+ const selectedNode = currentGraph.cy.getElementById(getTableId(currentGraph.selectedTableName));
624
590
 
625
- if (selectedNode.nonempty()) {
626
- highlightConnectedElements(currentGraph.cy, selectedNode);
627
- syncEntryHighlights([currentGraph.selectedTableName]);
628
- return;
591
+ if (selectedNode.nonempty()) {
592
+ highlightConnectedElements(currentGraph.cy, selectedNode);
593
+ syncEntryHighlights([currentGraph.selectedTableName]);
594
+ return;
595
+ }
629
596
  }
630
- }
631
597
 
632
- resetHighlights(currentGraph.cy);
633
- syncEntryHighlights();
634
- updateOpenDataButton();
598
+ resetHighlights(currentGraph.cy);
599
+ syncEntryHighlights();
600
+ updateOpenDataButton();
635
601
  }
636
602
 
637
603
  function runLayout(cy, onStop) {
638
- const layout = cy.layout(
639
- getElkLayoutOptions({
640
- stop: () => {
641
- cy.fit(cy.elements(), 60);
642
- if (typeof onStop === "function") {
643
- onStop();
644
- }
645
- },
646
- })
647
- );
604
+ const layout = cy.layout(
605
+ getElkLayoutOptions({
606
+ stop: () => {
607
+ cy.fit(cy.elements(), 60);
608
+ if (typeof onStop === 'function') {
609
+ onStop();
610
+ }
611
+ },
612
+ }),
613
+ );
648
614
 
649
- layout.run();
615
+ layout.run();
650
616
  }
651
617
 
652
618
  function restorePersistedViewport(cy, persistedState) {
653
- if (!persistedState?.nodePositions) {
654
- return false;
655
- }
619
+ if (!persistedState?.nodePositions) {
620
+ return false;
621
+ }
656
622
 
657
- cy.batch(() => {
658
- cy.nodes().forEach((node) => {
659
- const nextPosition = persistedState.nodePositions[node.id()];
623
+ cy.batch(() => {
624
+ cy.nodes().forEach(node => {
625
+ const nextPosition = persistedState.nodePositions[node.id()];
660
626
 
661
- if (!nextPosition) {
662
- return;
663
- }
627
+ if (!nextPosition) {
628
+ return;
629
+ }
664
630
 
665
- node.position(nextPosition);
631
+ node.position(nextPosition);
632
+ });
666
633
  });
667
- });
668
634
 
669
- if (typeof persistedState.zoom === "number") {
670
- cy.zoom(persistedState.zoom);
671
- }
672
-
673
- if (persistedState.pan) {
674
- cy.pan(persistedState.pan);
675
- }
676
-
677
- return true;
678
- }
679
-
680
- export function setupToolbar(cy, schema) {
681
- if (!currentGraph) {
682
- return () => {};
683
- }
684
-
685
- const { root } = currentGraph;
686
- const searchRoot = root.querySelector("[data-structure-graph-search]");
687
- const searchInput = root.querySelector("[data-structure-graph-search-input]");
688
- const openDataButton = root.querySelector('[data-structure-graph-action="open-data"]');
689
- const inspectorToggleButton = root.querySelector(
690
- '[data-structure-graph-action="toggle-inspector"]'
691
- );
692
- let searchTimer = null;
693
-
694
- currentGraph.searchRoot = searchRoot;
695
- currentGraph.searchInput = searchInput;
696
- currentGraph.openDataButton = openDataButton;
697
- currentGraph.inspectorToggleButton = inspectorToggleButton;
698
- updateOpenDataButton();
699
- updateInspectorToggleButton();
700
- syncInspectorLayout();
701
-
702
- const handleSearch = () => {
703
- if (!searchInput || !searchRoot) {
704
- return;
635
+ if (typeof persistedState.zoom === 'number') {
636
+ cy.zoom(persistedState.zoom);
705
637
  }
706
638
 
707
- const value = searchInput.value.trim();
708
-
709
- if (!value) {
710
- searchRoot.classList.remove("is-miss");
711
- restoreGraphState();
712
- if (!currentGraph.selectedTableName) {
713
- currentGraph.inspector.innerHTML = getDefaultInspectorMarkup();
714
- }
715
- return;
639
+ if (persistedState.pan) {
640
+ cy.pan(persistedState.pan);
716
641
  }
717
642
 
718
- const matchedNode = focusTableByName(cy, value);
643
+ return true;
644
+ }
719
645
 
720
- if (!matchedNode) {
721
- searchRoot.classList.add("is-miss");
722
- return;
646
+ export function setupToolbar(cy) {
647
+ if (!currentGraph) {
648
+ return () => {};
723
649
  }
724
650
 
725
- searchRoot.classList.remove("is-miss");
726
- applyTableSelection(matchedNode, { focus: true });
727
- };
651
+ const { root } = currentGraph;
652
+ const openDataButton = root.querySelector('[data-structure-graph-action="open-data"]');
653
+ const inspectorToggleButton = root.querySelector('[data-structure-graph-action="toggle-inspector"]');
728
654
 
729
- const onSearchInput = () => {
730
- window.clearTimeout(searchTimer);
731
- searchTimer = window.setTimeout(handleSearch, 140);
732
- };
655
+ currentGraph.openDataButton = openDataButton;
656
+ currentGraph.inspectorToggleButton = inspectorToggleButton;
657
+ updateOpenDataButton();
658
+ updateInspectorToggleButton();
659
+ syncInspectorLayout();
733
660
 
734
- const onToolbarClick = (event) => {
735
- const button = event.target.closest("[data-structure-graph-action]");
661
+ const onToolbarClick = async event => {
662
+ const button = event.target.closest('[data-structure-graph-action]');
736
663
 
737
- if (!button) {
738
- return;
739
- }
664
+ if (!button) {
665
+ return;
666
+ }
740
667
 
741
- switch (button.dataset.structureGraphAction) {
742
- case "fit":
743
- cy.fit(cy.elements(), 60);
744
- break;
745
- case "relayout":
746
- runLayout(cy, () => {
747
- if (currentGraph?.selectedTableName) {
748
- const selectedNode = cy.getElementById(
749
- getTableId(currentGraph.selectedTableName)
750
- );
751
- if (selectedNode.nonempty()) {
752
- applyTableSelection(selectedNode, { focus: true });
753
- }
754
- }
755
- });
756
- break;
757
- case "clear":
758
- clearSelection({ preserveSearch: false });
759
- break;
760
- case "open-data":
761
- if (currentGraph?.selectedTableName) {
762
- window.location.hash = `#/data/${encodeURIComponent(currentGraph.selectedTableName)}`;
668
+ switch (button.dataset.structureGraphAction) {
669
+ case 'fit':
670
+ cy.fit(cy.elements(), 60);
671
+ break;
672
+ case 'relayout':
673
+ runLayout(cy, () => {
674
+ if (currentGraph?.selectedTableName) {
675
+ const selectedNode = cy.getElementById(getTableId(currentGraph.selectedTableName));
676
+ if (selectedNode.nonempty()) {
677
+ applyTableSelection(selectedNode, { focus: true });
678
+ }
679
+ }
680
+ });
681
+ break;
682
+ case 'clear':
683
+ clearSelection();
684
+ break;
685
+ case 'open-data':
686
+ if (currentGraph?.selectedTableName) {
687
+ window.location.hash = `#/data/${encodeURIComponent(currentGraph.selectedTableName)}`;
688
+ }
689
+ break;
690
+ case 'toggle-inspector':
691
+ currentGraph.inspectorHidden = !currentGraph.inspectorHidden;
692
+ updateInspectorToggleButton();
693
+ syncInspectorLayout();
694
+ break;
695
+ case 'copy-ddl':
696
+ await copyInspectorDdl(button);
697
+ break;
698
+ default:
763
699
  }
764
- break;
765
- case "toggle-inspector":
766
- currentGraph.inspectorHidden = !currentGraph.inspectorHidden;
767
- updateInspectorToggleButton();
768
- syncInspectorLayout();
769
- break;
770
- default:
771
- }
772
- };
700
+ };
773
701
 
774
- searchInput?.addEventListener("input", onSearchInput);
775
- root.addEventListener("click", onToolbarClick);
702
+ root.addEventListener('click', onToolbarClick);
776
703
 
777
- return () => {
778
- window.clearTimeout(searchTimer);
779
- searchInput?.removeEventListener("input", onSearchInput);
780
- root.removeEventListener("click", onToolbarClick);
781
- };
704
+ return () => {
705
+ root.removeEventListener('click', onToolbarClick);
706
+ };
782
707
  }
783
708
 
784
709
  export function teardownStructureGraph() {
785
- mountVersion += 1;
786
- destroyCurrentGraph();
710
+ mountVersion += 1;
711
+ destroyCurrentGraph();
787
712
  }
788
713
 
789
714
  export async function mountStructureGraph(snapshot) {
790
- const root = document.querySelector("[data-structure-graph-root]");
791
-
792
- if (!root) {
793
- teardownStructureGraph();
794
- return;
795
- }
796
-
797
- const version = ++mountVersion;
798
- destroyCurrentGraph();
799
-
800
- const [{ getCytoscape }] = await Promise.all([import("../lib/cytoscapeRuntime.js")]);
801
-
802
- if (version !== mountVersion || !root.isConnected) {
803
- return;
804
- }
805
-
806
- cytoscapeFactory = getCytoscape();
807
-
808
- const canvas = root.querySelector("[data-structure-graph-canvas]");
809
- const inspector = root.querySelector("[data-structure-graph-inspector]");
810
- const empty = root.querySelector("[data-structure-graph-empty]");
811
- const schema = snapshot.structure.data?.graph ?? { tables: [] };
812
- const { schemaSignature, state: cachedState } = getPersistedGraphState(schema);
813
-
814
- if (!(canvas instanceof HTMLElement) || !(inspector instanceof HTMLElement)) {
815
- return;
816
- }
817
-
818
- if (!schema.tables?.length) {
819
- empty?.removeAttribute("hidden");
820
- inspector.innerHTML = clearInspector();
821
- return;
822
- }
823
-
824
- empty?.setAttribute("hidden", "hidden");
825
-
826
- const cy = createCytoscapeInstance(canvas, buildGraphElements(schema));
827
-
828
- currentGraph = {
829
- root,
830
- canvas,
831
- inspector,
832
- initialInspectorMarkup: inspector.innerHTML,
833
- initialSelectedTableName: null,
834
- schemaSignature,
835
- schema,
836
- cy,
837
- cleanup: [],
838
- resizeObserver: null,
839
- resizeHandler: null,
840
- searchRoot: null,
841
- searchInput: null,
842
- openDataButton: null,
843
- inspectorToggleButton: null,
844
- inspectorHidden: cachedState?.inspectorHidden ?? false,
845
- persistStateOnDestroy: true,
846
- selectedTableName: null,
847
- };
848
-
849
- currentGraph.cleanup.push(setupToolbar(cy, schema));
850
-
851
- cy.on("tap", "node", (event) => {
852
- applyTableSelection(event.target, { focus: false });
853
- });
854
-
855
- cy.on("tap", (event) => {
856
- if (event.target === cy) {
857
- clearSelection();
858
- }
859
- });
860
-
861
- cy.on("mouseover", "edge", (event) => {
862
- const edge = event.target;
863
- highlightConnectedElements(cy, edge);
864
- syncEntryHighlights([], [edge.data("sourceTable"), edge.data("targetTable")]);
865
- });
866
-
867
- cy.on("mouseout", "edge", () => {
868
- restoreGraphState();
869
- });
870
-
871
- const selectedTableName =
872
- snapshot.structure.data?.grouped?.tables?.some(
873
- (table) => table.name === snapshot.structure.selectedName
874
- )
875
- ? snapshot.structure.selectedName
876
- : null;
715
+ const root = document.querySelector('[data-structure-graph-root]');
877
716
 
878
- currentGraph.initialSelectedTableName = selectedTableName;
717
+ if (!root) {
718
+ teardownStructureGraph();
719
+ return;
720
+ }
879
721
 
880
- if (currentGraph.searchInput && cachedState?.searchValue) {
881
- currentGraph.searchInput.value = cachedState.searchValue;
882
- }
722
+ const version = ++mountVersion;
723
+ destroyCurrentGraph();
883
724
 
884
- if (currentGraph.searchRoot) {
885
- currentGraph.searchRoot.classList.toggle("is-miss", Boolean(cachedState?.searchMiss));
886
- }
725
+ const [{ getCytoscape }] = await Promise.all([import('../lib/cytoscapeRuntime.js')]);
887
726
 
888
- if (restorePersistedViewport(cy, cachedState)) {
889
- if (selectedTableName) {
890
- const selectedNode = cy.getElementById(getTableId(selectedTableName));
891
- if (selectedNode.nonempty()) {
892
- applyTableSelection(selectedNode, { focus: false });
893
- } else {
894
- restoreGraphState();
895
- }
896
- } else {
897
- restoreGraphState();
727
+ if (version !== mountVersion || !root.isConnected) {
728
+ return;
898
729
  }
899
- } else {
900
- runLayout(cy, () => {
901
- if (!currentGraph || version !== mountVersion) {
730
+
731
+ cytoscapeFactory = getCytoscape();
732
+
733
+ const canvas = root.querySelector('[data-structure-graph-canvas]');
734
+ const inspector = root.querySelector('[data-structure-graph-inspector]');
735
+ const empty = root.querySelector('[data-structure-graph-empty]');
736
+ const schema = snapshot.structure.data?.graph ?? { tables: [] };
737
+ const { schemaSignature, state: cachedState } = getPersistedGraphState(schema);
738
+
739
+ if (!(canvas instanceof HTMLElement) || !(inspector instanceof HTMLElement)) {
902
740
  return;
903
- }
741
+ }
904
742
 
905
- if (selectedTableName) {
906
- const selectedNode = cy.getElementById(getTableId(selectedTableName));
907
- if (selectedNode.nonempty()) {
908
- applyTableSelection(selectedNode, { focus: false });
743
+ if (!schema.tables?.length) {
744
+ empty?.removeAttribute('hidden');
745
+ inspector.innerHTML = clearInspector();
746
+ return;
747
+ }
748
+
749
+ empty?.setAttribute('hidden', 'hidden');
750
+
751
+ const cy = createCytoscapeInstance(canvas, buildGraphElements(schema));
752
+
753
+ currentGraph = {
754
+ root,
755
+ canvas,
756
+ inspector,
757
+ initialInspectorMarkup: inspector.innerHTML,
758
+ initialSelectedTableName: null,
759
+ schemaSignature,
760
+ schema,
761
+ cy,
762
+ cleanup: [],
763
+ resizeObserver: null,
764
+ resizeHandler: null,
765
+ openDataButton: null,
766
+ inspectorToggleButton: null,
767
+ inspectorHidden: cachedState?.inspectorHidden ?? false,
768
+ persistStateOnDestroy: true,
769
+ selectedTableName: null,
770
+ };
771
+
772
+ currentGraph.cleanup.push(setupToolbar(cy));
773
+
774
+ cy.on('tap', 'node', event => {
775
+ applyTableSelection(event.target, { focus: false });
776
+ });
777
+
778
+ cy.on('tap', event => {
779
+ if (event.target === cy) {
780
+ clearSelection();
909
781
  }
910
- }
911
782
  });
912
- }
913
783
 
914
- if (typeof ResizeObserver === "function") {
915
- currentGraph.resizeObserver = new ResizeObserver(() => {
916
- cy.resize();
784
+ cy.on('mouseover', 'edge', event => {
785
+ const edge = event.target;
786
+ highlightConnectedElements(cy, edge);
787
+ syncEntryHighlights([], [edge.data('sourceTable'), edge.data('targetTable')]);
917
788
  });
918
- currentGraph.resizeObserver.observe(canvas);
919
- } else {
920
- currentGraph.resizeHandler = () => {
921
- cy.resize();
922
- };
923
- window.addEventListener("resize", currentGraph.resizeHandler);
924
- }
789
+
790
+ cy.on('mouseout', 'edge', () => {
791
+ restoreGraphState();
792
+ });
793
+
794
+ const selectedTableName = snapshot.structure.data?.grouped?.tables?.some(
795
+ table => table.name === snapshot.structure.selectedName,
796
+ )
797
+ ? snapshot.structure.selectedName
798
+ : null;
799
+
800
+ currentGraph.initialSelectedTableName = selectedTableName;
801
+
802
+ if (restorePersistedViewport(cy, cachedState)) {
803
+ if (selectedTableName) {
804
+ const selectedNode = cy.getElementById(getTableId(selectedTableName));
805
+ if (selectedNode.nonempty()) {
806
+ applyTableSelection(selectedNode, { focus: false });
807
+ } else {
808
+ restoreGraphState();
809
+ }
810
+ } else {
811
+ restoreGraphState();
812
+ }
813
+ } else {
814
+ runLayout(cy, () => {
815
+ if (!currentGraph || version !== mountVersion) {
816
+ return;
817
+ }
818
+
819
+ if (selectedTableName) {
820
+ const selectedNode = cy.getElementById(getTableId(selectedTableName));
821
+ if (selectedNode.nonempty()) {
822
+ applyTableSelection(selectedNode, { focus: false });
823
+ }
824
+ }
825
+ });
826
+ }
827
+
828
+ if (typeof ResizeObserver === 'function') {
829
+ currentGraph.resizeObserver = new ResizeObserver(() => {
830
+ cy.resize();
831
+ });
832
+ currentGraph.resizeObserver.observe(canvas);
833
+ } else {
834
+ currentGraph.resizeHandler = () => {
835
+ cy.resize();
836
+ };
837
+ window.addEventListener('resize', currentGraph.resizeHandler);
838
+ }
925
839
  }