sqlite-hub 0.8.7 → 0.9.1

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