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.
- package/changelog.md +16 -0
- package/frontend/index.html +1 -1
- package/frontend/js/api.js +67 -0
- package/frontend/js/app.js +1554 -999
- package/frontend/js/components/modal.js +208 -3
- package/frontend/js/components/queryEditor.js +9 -4
- package/frontend/js/components/queryHistoryDetail.js +34 -12
- package/frontend/js/components/queryHistoryPanel.js +8 -11
- package/frontend/js/components/rowEditorPanel.js +26 -5
- package/frontend/js/components/sidebar.js +49 -5
- package/frontend/js/components/structureGraph.js +614 -632
- package/frontend/js/components/tableDesignerSqlPreview.js +24 -25
- package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
- package/frontend/js/router.js +81 -71
- package/frontend/js/store.js +3140 -2179
- package/frontend/js/views/charts.js +2 -2
- package/frontend/js/views/data.js +28 -14
- package/frontend/js/views/editor.js +172 -177
- package/frontend/js/views/mediaTagging.js +931 -0
- package/frontend/styles/base.css +11 -1
- package/frontend/styles/components.css +54 -6
- package/frontend/styles/views.css +910 -0
- package/package.json +1 -1
- package/server/routes/charts.js +4 -1
- package/server/routes/data.js +14 -0
- package/server/routes/mediaTagging.js +166 -0
- package/server/routes/sql.js +1 -0
- package/server/server.js +4 -0
- package/server/services/sqlite/dataBrowserService.js +25 -0
- package/server/services/sqlite/exportService.js +31 -1
- package/server/services/sqlite/mediaTaggingService.js +1689 -0
- package/server/services/storage/appStateStore.js +325 -3
- package/server/services/storage/queryHistoryUtils.js +165 -2
- package/shortkeys.md +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { showToast } from
|
|
2
|
-
import { escapeHtml, formatNumber } from
|
|
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
|
-
|
|
10
|
+
return `table:${tableName}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function getSchemaSignature(schema) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
45
|
+
const columns = new Set();
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
+
return columns;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function getVisibleColumns(tableData) {
|
|
59
|
-
|
|
59
|
+
return (tableData?.columns ?? []).filter(column => column.visible !== false);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function getRelationshipCount(tableData) {
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
if (foreignKeyColumns.has(column.name)) {
|
|
87
|
+
flags.push('<span class="structure-graph__flag is-link">FK</span>');
|
|
88
|
+
}
|
|
92
89
|
|
|
93
|
-
|
|
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
|
-
|
|
92
|
+
return flags.join('');
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
export function renderDdlSection(ddl, emptyLabel =
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
114
|
+
hasDdl ? ddlText : emptyLabel,
|
|
122
115
|
)}</pre>
|
|
123
116
|
</section>
|
|
124
117
|
`;
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
export function clearInspector() {
|
|
128
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
if (!tableData || tableData.type !== 'table') {
|
|
134
|
+
return clearInspector();
|
|
135
|
+
}
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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(
|
|
162
|
-
${createSummaryCard(
|
|
163
|
-
${createSummaryCard(
|
|
164
|
-
${createSummaryCard(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
240
|
+
return [...nodes, ...edges];
|
|
248
241
|
}
|
|
249
242
|
|
|
250
243
|
export function getElkLayoutOptions(overrides = {}) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
378
|
+
if (!cy) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
388
381
|
|
|
389
|
-
|
|
382
|
+
cy.elements().removeClass('selected related hovered dimmed');
|
|
390
383
|
}
|
|
391
384
|
|
|
392
385
|
export function highlightConnectedElements(cy, element) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
386
|
+
if (!cy || !element) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
396
389
|
|
|
397
|
-
|
|
398
|
-
|
|
390
|
+
resetHighlights(cy);
|
|
391
|
+
const allElements = cy.elements();
|
|
399
392
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
405
|
+
const source = element.source();
|
|
406
|
+
const target = element.target();
|
|
407
|
+
const highlighted = source.union(target).union(element);
|
|
415
408
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
447
|
-
|
|
439
|
+
currentGraph.cleanup.forEach(cleanup => cleanup());
|
|
440
|
+
currentGraph.cleanup = [];
|
|
448
441
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
442
|
+
if (currentGraph.resizeObserver) {
|
|
443
|
+
currentGraph.resizeObserver.disconnect();
|
|
444
|
+
}
|
|
452
445
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
446
|
+
if (currentGraph.resizeHandler) {
|
|
447
|
+
window.removeEventListener('resize', currentGraph.resizeHandler);
|
|
448
|
+
}
|
|
456
449
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
450
|
+
if (currentGraph.cy) {
|
|
451
|
+
currentGraph.cy.destroy();
|
|
452
|
+
}
|
|
460
453
|
|
|
461
|
-
|
|
454
|
+
currentGraph = null;
|
|
462
455
|
}
|
|
463
456
|
|
|
464
457
|
export function resetPersistedStructureGraphState() {
|
|
465
|
-
|
|
458
|
+
persistedGraphState = null;
|
|
466
459
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
460
|
+
if (currentGraph) {
|
|
461
|
+
currentGraph.persistStateOnDestroy = false;
|
|
462
|
+
}
|
|
470
463
|
}
|
|
471
464
|
|
|
472
465
|
function syncEntryHighlights(selectedNames = [], relatedNames = []) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
482
|
+
if (!currentGraph) {
|
|
483
|
+
return clearInspector();
|
|
484
|
+
}
|
|
494
485
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
486
|
+
return currentGraph.initialSelectedTableName
|
|
487
|
+
? clearInspector()
|
|
488
|
+
: currentGraph.initialInspectorMarkup || clearInspector();
|
|
498
489
|
}
|
|
499
490
|
|
|
500
491
|
function updateOpenDataButton() {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
492
|
+
if (!currentGraph?.openDataButton) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
504
495
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
512
|
-
?.
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
505
|
+
if (!ddl.trim()) {
|
|
506
|
+
showToast('No DDL available to copy.', 'alert');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
520
509
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
519
|
+
if (!currentGraph?.root) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
533
522
|
|
|
534
|
-
|
|
535
|
-
|
|
523
|
+
currentGraph.root.classList.toggle('is-inspector-hidden', currentGraph.inspectorHidden);
|
|
524
|
+
currentGraph.cy?.resize();
|
|
536
525
|
}
|
|
537
526
|
|
|
538
527
|
function updateInspectorToggleButton() {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
528
|
+
if (!currentGraph?.inspectorToggleButton) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
542
531
|
|
|
543
|
-
|
|
544
|
-
|
|
532
|
+
currentGraph.inspectorToggleButton.classList.toggle('is-active', currentGraph.inspectorHidden);
|
|
533
|
+
currentGraph.inspectorToggleButton.innerHTML = `
|
|
545
534
|
<span class="material-symbols-outlined text-sm">${
|
|
546
|
-
|
|
535
|
+
currentGraph.inspectorHidden ? 'right_panel_open' : 'right_panel_close'
|
|
547
536
|
}</span>
|
|
548
|
-
${currentGraph.inspectorHidden ?
|
|
537
|
+
${currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector'}
|
|
549
538
|
`;
|
|
550
539
|
}
|
|
551
540
|
|
|
552
541
|
function clearSelection() {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
542
|
+
if (!currentGraph) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
556
545
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
584
|
+
if (!currentGraph) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
598
587
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
getTableId(currentGraph.selectedTableName)
|
|
602
|
-
);
|
|
588
|
+
if (currentGraph.selectedTableName) {
|
|
589
|
+
const selectedNode = currentGraph.cy.getElementById(getTableId(currentGraph.selectedTableName));
|
|
603
590
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
591
|
+
if (selectedNode.nonempty()) {
|
|
592
|
+
highlightConnectedElements(currentGraph.cy, selectedNode);
|
|
593
|
+
syncEntryHighlights([currentGraph.selectedTableName]);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
608
596
|
}
|
|
609
|
-
}
|
|
610
597
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
598
|
+
resetHighlights(currentGraph.cy);
|
|
599
|
+
syncEntryHighlights();
|
|
600
|
+
updateOpenDataButton();
|
|
614
601
|
}
|
|
615
602
|
|
|
616
603
|
function runLayout(cy, onStop) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
615
|
+
layout.run();
|
|
629
616
|
}
|
|
630
617
|
|
|
631
618
|
function restorePersistedViewport(cy, persistedState) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
619
|
+
if (!persistedState?.nodePositions) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
635
622
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
623
|
+
cy.batch(() => {
|
|
624
|
+
cy.nodes().forEach(node => {
|
|
625
|
+
const nextPosition = persistedState.nodePositions[node.id()];
|
|
639
626
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
627
|
+
if (!nextPosition) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
643
630
|
|
|
644
|
-
|
|
631
|
+
node.position(nextPosition);
|
|
632
|
+
});
|
|
645
633
|
});
|
|
646
|
-
});
|
|
647
634
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
635
|
+
if (typeof persistedState.zoom === 'number') {
|
|
636
|
+
cy.zoom(persistedState.zoom);
|
|
637
|
+
}
|
|
651
638
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
639
|
+
if (persistedState.pan) {
|
|
640
|
+
cy.pan(persistedState.pan);
|
|
641
|
+
}
|
|
655
642
|
|
|
656
|
-
|
|
643
|
+
return true;
|
|
657
644
|
}
|
|
658
645
|
|
|
659
646
|
export function setupToolbar(cy) {
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
722
|
-
|
|
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
|
-
|
|
728
|
-
|
|
710
|
+
mountVersion += 1;
|
|
711
|
+
destroyCurrentGraph();
|
|
729
712
|
}
|
|
730
713
|
|
|
731
714
|
export async function mountStructureGraph(snapshot) {
|
|
732
|
-
|
|
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
|
-
|
|
717
|
+
if (!root) {
|
|
718
|
+
teardownStructureGraph();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
819
721
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
}
|