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.
- package/changelog.md +25 -0
- package/docs/DESIGN_GUIDELINES.md +36 -0
- package/frontend/index.html +79 -0
- package/frontend/js/api.js +67 -0
- package/frontend/js/app.js +1401 -921
- package/frontend/js/components/connectionCard.js +3 -4
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/modal.js +239 -30
- package/frontend/js/components/queryEditor.js +11 -8
- package/frontend/js/components/queryHistoryDetail.js +27 -8
- package/frontend/js/components/queryHistoryPanel.js +6 -5
- package/frontend/js/components/rowEditorPanel.js +84 -58
- package/frontend/js/components/sidebar.js +76 -33
- package/frontend/js/components/structureGraph.js +629 -715
- package/frontend/js/components/tableDesignerEditor.js +9 -8
- package/frontend/js/components/tableDesignerSidebar.js +11 -10
- package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
- package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
- package/frontend/js/router.js +81 -71
- package/frontend/js/store.js +3095 -2165
- package/frontend/js/views/charts.js +68 -40
- package/frontend/js/views/connections.js +5 -17
- package/frontend/js/views/data.js +40 -27
- package/frontend/js/views/editor.js +172 -177
- package/frontend/js/views/mediaTagging.js +861 -0
- package/frontend/js/views/overview.js +149 -10
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +74 -70
- package/frontend/js/views/tableDesigner.js +7 -2
- package/frontend/styles/base.css +73 -1
- package/frontend/styles/components.css +105 -105
- package/frontend/styles/structure-graph.css +19 -82
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +823 -30
- 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/sqlite/overviewService.js +68 -0
- package/server/services/storage/appStateStore.js +321 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
10
|
+
return `table:${tableName}`;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function getSchemaSignature(schema) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
45
|
+
const columns = new Set();
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
+
return columns;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function getVisibleColumns(tableData) {
|
|
58
|
-
|
|
59
|
+
return (tableData?.columns ?? []).filter(column => column.visible !== false);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function getRelationshipCount(tableData) {
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
if (!tableData || tableData.type !== 'table') {
|
|
134
|
+
return clearInspector();
|
|
135
|
+
}
|
|
117
136
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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(
|
|
136
|
-
${createSummaryCard(
|
|
137
|
-
${createSummaryCard(
|
|
138
|
-
${createSummaryCard(
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
240
|
+
return [...nodes, ...edges];
|
|
227
241
|
}
|
|
228
242
|
|
|
229
243
|
export function getElkLayoutOptions(overrides = {}) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
378
|
+
if (!cy) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
367
381
|
|
|
368
|
-
|
|
382
|
+
cy.elements().removeClass('selected related hovered dimmed');
|
|
369
383
|
}
|
|
370
384
|
|
|
371
385
|
export function highlightConnectedElements(cy, element) {
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
(node) => String(node.data("tableName")).toLowerCase() === normalizedQuery
|
|
411
|
-
);
|
|
390
|
+
resetHighlights(cy);
|
|
391
|
+
const allElements = cy.elements();
|
|
412
392
|
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
405
|
+
const source = element.source();
|
|
406
|
+
const target = element.target();
|
|
407
|
+
const highlighted = source.union(target).union(element);
|
|
424
408
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
)
|
|
429
|
-
);
|
|
409
|
+
allElements.difference(highlighted).addClass('dimmed');
|
|
410
|
+
source.addClass('related');
|
|
411
|
+
target.addClass('related');
|
|
412
|
+
element.addClass('hovered');
|
|
430
413
|
}
|
|
431
414
|
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
482
|
-
|
|
439
|
+
currentGraph.cleanup.forEach(cleanup => cleanup());
|
|
440
|
+
currentGraph.cleanup = [];
|
|
483
441
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
442
|
+
if (currentGraph.resizeObserver) {
|
|
443
|
+
currentGraph.resizeObserver.disconnect();
|
|
444
|
+
}
|
|
487
445
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
446
|
+
if (currentGraph.resizeHandler) {
|
|
447
|
+
window.removeEventListener('resize', currentGraph.resizeHandler);
|
|
448
|
+
}
|
|
491
449
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
450
|
+
if (currentGraph.cy) {
|
|
451
|
+
currentGraph.cy.destroy();
|
|
452
|
+
}
|
|
495
453
|
|
|
496
|
-
|
|
454
|
+
currentGraph = null;
|
|
497
455
|
}
|
|
498
456
|
|
|
499
457
|
export function resetPersistedStructureGraphState() {
|
|
500
|
-
|
|
458
|
+
persistedGraphState = null;
|
|
501
459
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
460
|
+
if (currentGraph) {
|
|
461
|
+
currentGraph.persistStateOnDestroy = false;
|
|
462
|
+
}
|
|
505
463
|
}
|
|
506
464
|
|
|
507
465
|
function syncEntryHighlights(selectedNames = [], relatedNames = []) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
482
|
+
if (!currentGraph) {
|
|
483
|
+
return clearInspector();
|
|
484
|
+
}
|
|
529
485
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
486
|
+
return currentGraph.initialSelectedTableName
|
|
487
|
+
? clearInspector()
|
|
488
|
+
: currentGraph.initialInspectorMarkup || clearInspector();
|
|
533
489
|
}
|
|
534
490
|
|
|
535
491
|
function updateOpenDataButton() {
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
519
|
+
if (!currentGraph?.root) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
549
522
|
|
|
550
|
-
|
|
551
|
-
|
|
523
|
+
currentGraph.root.classList.toggle('is-inspector-hidden', currentGraph.inspectorHidden);
|
|
524
|
+
currentGraph.cy?.resize();
|
|
552
525
|
}
|
|
553
526
|
|
|
554
527
|
function updateInspectorToggleButton() {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
528
|
+
if (!currentGraph?.inspectorToggleButton) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
558
531
|
|
|
559
|
-
|
|
560
|
-
|
|
532
|
+
currentGraph.inspectorToggleButton.classList.toggle('is-active', currentGraph.inspectorHidden);
|
|
533
|
+
currentGraph.inspectorToggleButton.innerHTML = `
|
|
561
534
|
<span class="material-symbols-outlined text-sm">${
|
|
562
|
-
|
|
535
|
+
currentGraph.inspectorHidden ? 'right_panel_open' : 'right_panel_close'
|
|
563
536
|
}</span>
|
|
564
|
-
${currentGraph.inspectorHidden ?
|
|
537
|
+
${currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector'}
|
|
565
538
|
`;
|
|
566
539
|
}
|
|
567
540
|
|
|
568
|
-
function clearSelection(
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
584
|
+
if (!currentGraph) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
619
587
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
getTableId(currentGraph.selectedTableName)
|
|
623
|
-
);
|
|
588
|
+
if (currentGraph.selectedTableName) {
|
|
589
|
+
const selectedNode = currentGraph.cy.getElementById(getTableId(currentGraph.selectedTableName));
|
|
624
590
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
591
|
+
if (selectedNode.nonempty()) {
|
|
592
|
+
highlightConnectedElements(currentGraph.cy, selectedNode);
|
|
593
|
+
syncEntryHighlights([currentGraph.selectedTableName]);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
629
596
|
}
|
|
630
|
-
}
|
|
631
597
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
598
|
+
resetHighlights(currentGraph.cy);
|
|
599
|
+
syncEntryHighlights();
|
|
600
|
+
updateOpenDataButton();
|
|
635
601
|
}
|
|
636
602
|
|
|
637
603
|
function runLayout(cy, onStop) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
615
|
+
layout.run();
|
|
650
616
|
}
|
|
651
617
|
|
|
652
618
|
function restorePersistedViewport(cy, persistedState) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
619
|
+
if (!persistedState?.nodePositions) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
656
622
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
623
|
+
cy.batch(() => {
|
|
624
|
+
cy.nodes().forEach(node => {
|
|
625
|
+
const nextPosition = persistedState.nodePositions[node.id()];
|
|
660
626
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
627
|
+
if (!nextPosition) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
664
630
|
|
|
665
|
-
|
|
631
|
+
node.position(nextPosition);
|
|
632
|
+
});
|
|
666
633
|
});
|
|
667
|
-
});
|
|
668
634
|
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
719
645
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
646
|
+
export function setupToolbar(cy) {
|
|
647
|
+
if (!currentGraph) {
|
|
648
|
+
return () => {};
|
|
723
649
|
}
|
|
724
650
|
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
655
|
+
currentGraph.openDataButton = openDataButton;
|
|
656
|
+
currentGraph.inspectorToggleButton = inspectorToggleButton;
|
|
657
|
+
updateOpenDataButton();
|
|
658
|
+
updateInspectorToggleButton();
|
|
659
|
+
syncInspectorLayout();
|
|
733
660
|
|
|
734
|
-
|
|
735
|
-
|
|
661
|
+
const onToolbarClick = async event => {
|
|
662
|
+
const button = event.target.closest('[data-structure-graph-action]');
|
|
736
663
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
664
|
+
if (!button) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
740
667
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
765
|
-
case "toggle-inspector":
|
|
766
|
-
currentGraph.inspectorHidden = !currentGraph.inspectorHidden;
|
|
767
|
-
updateInspectorToggleButton();
|
|
768
|
-
syncInspectorLayout();
|
|
769
|
-
break;
|
|
770
|
-
default:
|
|
771
|
-
}
|
|
772
|
-
};
|
|
700
|
+
};
|
|
773
701
|
|
|
774
|
-
|
|
775
|
-
root.addEventListener("click", onToolbarClick);
|
|
702
|
+
root.addEventListener('click', onToolbarClick);
|
|
776
703
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
root.removeEventListener("click", onToolbarClick);
|
|
781
|
-
};
|
|
704
|
+
return () => {
|
|
705
|
+
root.removeEventListener('click', onToolbarClick);
|
|
706
|
+
};
|
|
782
707
|
}
|
|
783
708
|
|
|
784
709
|
export function teardownStructureGraph() {
|
|
785
|
-
|
|
786
|
-
|
|
710
|
+
mountVersion += 1;
|
|
711
|
+
destroyCurrentGraph();
|
|
787
712
|
}
|
|
788
713
|
|
|
789
714
|
export async function mountStructureGraph(snapshot) {
|
|
790
|
-
|
|
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
|
-
|
|
717
|
+
if (!root) {
|
|
718
|
+
teardownStructureGraph();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
879
721
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
722
|
+
const version = ++mountVersion;
|
|
723
|
+
destroyCurrentGraph();
|
|
883
724
|
|
|
884
|
-
|
|
885
|
-
currentGraph.searchRoot.classList.toggle("is-miss", Boolean(cachedState?.searchMiss));
|
|
886
|
-
}
|
|
725
|
+
const [{ getCytoscape }] = await Promise.all([import('../lib/cytoscapeRuntime.js')]);
|
|
887
726
|
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
}
|