sqlite-hub 1.0.0 → 1.1.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/README.md +47 -33
- package/bin/sqlite-hub.js +127 -47
- package/frontend/js/api.js +6 -0
- package/frontend/js/app.js +229 -44
- package/frontend/js/components/modal.js +17 -1
- package/frontend/js/components/queryChartRenderer.js +28 -3
- package/frontend/js/components/queryEditor.js +24 -26
- package/frontend/js/components/queryHistoryDetail.js +1 -1
- package/frontend/js/components/queryHistoryHeader.js +48 -0
- package/frontend/js/components/queryHistoryList.js +132 -0
- package/frontend/js/components/queryHistoryPanel.js +72 -136
- package/frontend/js/components/structureGraph.js +700 -89
- package/frontend/js/components/tableDesignerEditor.js +26 -47
- package/frontend/js/components/tableDesignerSidebar.js +7 -31
- package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
- package/frontend/js/store.js +97 -8
- package/frontend/js/utils/exportFilenames.js +3 -1
- package/frontend/js/views/charts.js +326 -174
- package/frontend/js/views/connections.js +59 -64
- package/frontend/js/views/data.js +48 -54
- package/frontend/js/views/documents.js +62 -23
- package/frontend/js/views/editor.js +0 -2
- package/frontend/js/views/mediaTagging.js +6 -5
- package/frontend/js/views/settings.js +78 -0
- package/frontend/js/views/structure.js +48 -32
- package/frontend/js/views/tableDesigner.js +45 -1
- package/frontend/styles/components.css +259 -54
- package/frontend/styles/structure-graph.css +150 -37
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +75 -38
- package/package.json +3 -2
- package/server/routes/export.js +89 -22
- package/server/routes/externalApi.js +84 -2
- package/server/routes/settings.js +37 -28
- package/server/services/appInfoService.js +215 -0
- package/server/services/databaseCommandService.js +50 -6
- package/server/services/sqlite/dataBrowserService.js +11 -3
- package/server/services/sqlite/exportService.js +307 -22
- package/tests/api-token-auth.test.js +110 -1
- package/tests/cli-args.test.js +16 -3
- package/tests/database-command-service.test.js +40 -3
- package/tests/export-blob.test.js +54 -1
- package/tests/export-filenames.test.js +4 -0
- package/tests/settings-api-tokens-route.test.js +22 -1
- package/tests/settings-metadata.test.js +99 -1
- package/tests/settings-view.test.js +28 -0
|
@@ -7,6 +7,24 @@ let currentGraph = null;
|
|
|
7
7
|
let mountVersion = 0;
|
|
8
8
|
let persistedGraphState = null;
|
|
9
9
|
const STRUCTURE_INSPECTOR_VISIBLE_STORAGE_KEY = 'sqlite_hub_structure_inspector_visible';
|
|
10
|
+
const STRUCTURE_GRAPH_STATE_STORAGE_PREFIX = 'sqlite_hub_structure_graph_state';
|
|
11
|
+
const STRUCTURE_GRAPH_STATE_STORAGE_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
function getHash(input) {
|
|
14
|
+
let hash = 2166136261;
|
|
15
|
+
const text = String(input ?? '');
|
|
16
|
+
|
|
17
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
18
|
+
hash ^= text.charCodeAt(index);
|
|
19
|
+
hash = Math.imul(hash, 16777619);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (hash >>> 0).toString(36);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getGraphStateStorageKey(schemaSignature) {
|
|
26
|
+
return `${STRUCTURE_GRAPH_STATE_STORAGE_PREFIX}:${getHash(schemaSignature)}`;
|
|
27
|
+
}
|
|
10
28
|
|
|
11
29
|
function readStoredInspectorHidden() {
|
|
12
30
|
try {
|
|
@@ -53,19 +71,118 @@ function getSchemaSignature(schema) {
|
|
|
53
71
|
);
|
|
54
72
|
}
|
|
55
73
|
|
|
74
|
+
function normalizeGraphNodePositions(nodePositions) {
|
|
75
|
+
if (!nodePositions || typeof nodePositions !== 'object') {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const normalized = {};
|
|
80
|
+
|
|
81
|
+
Object.entries(nodePositions).forEach(([nodeId, position]) => {
|
|
82
|
+
if (!position || typeof position !== 'object') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const x = Number(position.x);
|
|
87
|
+
const y = Number(position.y);
|
|
88
|
+
|
|
89
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
normalized[nodeId] = { x, y };
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return Object.keys(normalized).length ? normalized : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeGraphStatePayload(payload, schemaSignature) {
|
|
100
|
+
if (
|
|
101
|
+
!payload ||
|
|
102
|
+
typeof payload !== 'object' ||
|
|
103
|
+
payload.version !== STRUCTURE_GRAPH_STATE_STORAGE_VERSION ||
|
|
104
|
+
payload.schemaSignature !== schemaSignature
|
|
105
|
+
) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nodePositions = normalizeGraphNodePositions(payload.nodePositions);
|
|
110
|
+
|
|
111
|
+
if (!nodePositions) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const zoom = Number(payload.zoom);
|
|
116
|
+
const panX = Number(payload.pan?.x);
|
|
117
|
+
const panY = Number(payload.pan?.y);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
schemaSignature,
|
|
121
|
+
nodePositions,
|
|
122
|
+
pan: Number.isFinite(panX) && Number.isFinite(panY) ? { x: panX, y: panY } : null,
|
|
123
|
+
zoom: Number.isFinite(zoom) ? zoom : null,
|
|
124
|
+
inspectorHidden: Boolean(payload.inspectorHidden),
|
|
125
|
+
selectedTableName: typeof payload.selectedTableName === 'string' ? payload.selectedTableName : null,
|
|
126
|
+
layoutVariant: Number.isFinite(Number(payload.layoutVariant)) ? Number(payload.layoutVariant) : 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readStoredGraphState(schemaSignature) {
|
|
131
|
+
try {
|
|
132
|
+
const rawValue = globalThis.localStorage?.getItem(getGraphStateStorageKey(schemaSignature));
|
|
133
|
+
|
|
134
|
+
if (!rawValue) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return normalizeGraphStatePayload(JSON.parse(rawValue), schemaSignature);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function writeStoredGraphState(graphState) {
|
|
145
|
+
if (!graphState?.schemaSignature || !graphState.nodePositions) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
globalThis.localStorage?.setItem(
|
|
151
|
+
getGraphStateStorageKey(graphState.schemaSignature),
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
version: STRUCTURE_GRAPH_STATE_STORAGE_VERSION,
|
|
154
|
+
schemaSignature: graphState.schemaSignature,
|
|
155
|
+
nodePositions: graphState.nodePositions,
|
|
156
|
+
pan: graphState.pan ?? null,
|
|
157
|
+
zoom: graphState.zoom ?? null,
|
|
158
|
+
inspectorHidden: Boolean(graphState.inspectorHidden),
|
|
159
|
+
selectedTableName: graphState.selectedTableName ?? null,
|
|
160
|
+
layoutVariant: Number.isFinite(Number(graphState.layoutVariant)) ? Number(graphState.layoutVariant) : 0,
|
|
161
|
+
savedAt: new Date().toISOString(),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
56
170
|
function getPersistedGraphState(schema) {
|
|
57
171
|
const schemaSignature = getSchemaSignature(schema);
|
|
58
172
|
|
|
59
|
-
if (persistedGraphState?.schemaSignature
|
|
173
|
+
if (persistedGraphState?.schemaSignature === schemaSignature) {
|
|
60
174
|
return {
|
|
61
175
|
schemaSignature,
|
|
62
|
-
state:
|
|
176
|
+
state: persistedGraphState,
|
|
63
177
|
};
|
|
64
178
|
}
|
|
65
179
|
|
|
180
|
+
const storedState = readStoredGraphState(schemaSignature);
|
|
181
|
+
persistedGraphState = storedState;
|
|
182
|
+
|
|
66
183
|
return {
|
|
67
184
|
schemaSignature,
|
|
68
|
-
state:
|
|
185
|
+
state: storedState,
|
|
69
186
|
};
|
|
70
187
|
}
|
|
71
188
|
|
|
@@ -120,6 +237,136 @@ function createColumnFlags(column, foreignKeyColumns) {
|
|
|
120
237
|
return flags.join('');
|
|
121
238
|
}
|
|
122
239
|
|
|
240
|
+
function quoteSqlIdentifier(identifier) {
|
|
241
|
+
return `"${String(identifier ?? '').replaceAll('"', '""')}"`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function qualifySqlIdentifier(tableName, columnName) {
|
|
245
|
+
return `${quoteSqlIdentifier(tableName)}.${quoteSqlIdentifier(columnName)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getJoinConditions(edgeData) {
|
|
249
|
+
const conditions = Array.isArray(edgeData?.joinConditions) ? edgeData.joinConditions : [];
|
|
250
|
+
|
|
251
|
+
if (conditions.length) {
|
|
252
|
+
return conditions
|
|
253
|
+
.map(condition => ({
|
|
254
|
+
sourceTable: condition.sourceTable || edgeData.sourceTable || '',
|
|
255
|
+
targetTable: condition.targetTable || edgeData.targetTable || '',
|
|
256
|
+
sourceColumn: condition.sourceColumn || '',
|
|
257
|
+
targetColumn: condition.targetColumn || 'rowid',
|
|
258
|
+
}))
|
|
259
|
+
.filter(condition => condition.sourceTable && condition.targetTable && condition.sourceColumn);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
sourceTable: edgeData?.sourceTable || '',
|
|
265
|
+
targetTable: edgeData?.targetTable || '',
|
|
266
|
+
sourceColumn: edgeData?.sourceColumn || '',
|
|
267
|
+
targetColumn: edgeData?.targetColumn || 'rowid',
|
|
268
|
+
},
|
|
269
|
+
].filter(condition => condition.sourceTable && condition.targetTable && condition.sourceColumn);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildJoinSql(edgeData) {
|
|
273
|
+
const sourceTable = edgeData?.sourceTable || '';
|
|
274
|
+
const targetTable = edgeData?.targetTable || '';
|
|
275
|
+
const conditions = getJoinConditions(edgeData);
|
|
276
|
+
|
|
277
|
+
if (!sourceTable || !targetTable || !conditions.length) {
|
|
278
|
+
return '-- Join preview is unavailable for this relationship.';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const joinLines = conditions.map((condition, index) => {
|
|
282
|
+
const prefix = index === 0 ? ' ON ' : ' AND ';
|
|
283
|
+
|
|
284
|
+
return [
|
|
285
|
+
prefix,
|
|
286
|
+
qualifySqlIdentifier(condition.sourceTable, condition.sourceColumn),
|
|
287
|
+
' = ',
|
|
288
|
+
qualifySqlIdentifier(condition.targetTable, condition.targetColumn),
|
|
289
|
+
].join('');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return [
|
|
293
|
+
'SELECT *',
|
|
294
|
+
`FROM ${quoteSqlIdentifier(sourceTable)}`,
|
|
295
|
+
`JOIN ${quoteSqlIdentifier(targetTable)}`,
|
|
296
|
+
`${joinLines.join('\n')};`,
|
|
297
|
+
].join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderJoinInspector(edgeData) {
|
|
301
|
+
const sourceTable = edgeData?.sourceTable || '';
|
|
302
|
+
const targetTable = edgeData?.targetTable || '';
|
|
303
|
+
const conditions = getJoinConditions(edgeData);
|
|
304
|
+
const joinSql = buildJoinSql(edgeData);
|
|
305
|
+
|
|
306
|
+
return `
|
|
307
|
+
<div class="structure-graph__panel">
|
|
308
|
+
<div class="space-y-3">
|
|
309
|
+
<div class="structure-graph__panel-heading">
|
|
310
|
+
<div class="structure-graph__eyebrow">Join Preview</div>
|
|
311
|
+
<button
|
|
312
|
+
class="query-history-icon-button"
|
|
313
|
+
data-structure-graph-action="toggle-inspector"
|
|
314
|
+
type="button"
|
|
315
|
+
aria-label="Hide inspector"
|
|
316
|
+
>
|
|
317
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="structure-graph__title">${escapeHtml(sourceTable)} → ${escapeHtml(targetTable)}</div>
|
|
321
|
+
<div class="structure-graph__subtitle">Foreign key relationship</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<section class="structure-graph__section">
|
|
325
|
+
<div class="structure-graph__section-title">Relationship</div>
|
|
326
|
+
<div class="structure-graph__join-flow">
|
|
327
|
+
<div class="structure-graph__join-table">${escapeHtml(sourceTable)}</div>
|
|
328
|
+
<span class="material-symbols-outlined structure-graph__join-arrow" aria-hidden="true">arrow_forward</span>
|
|
329
|
+
<div class="structure-graph__join-table">${escapeHtml(targetTable)}</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="structure-graph__join-condition-list">
|
|
332
|
+
${
|
|
333
|
+
conditions.length
|
|
334
|
+
? conditions
|
|
335
|
+
.map(
|
|
336
|
+
condition => `
|
|
337
|
+
<div class="structure-graph__join-condition">
|
|
338
|
+
<span>${escapeHtml(formatQualifiedEdgeColumn(condition.sourceTable, condition.sourceColumn))}</span>
|
|
339
|
+
<span aria-hidden="true">=</span>
|
|
340
|
+
<span>${escapeHtml(formatQualifiedEdgeColumn(condition.targetTable, condition.targetColumn))}</span>
|
|
341
|
+
</div>
|
|
342
|
+
`,
|
|
343
|
+
)
|
|
344
|
+
.join('')
|
|
345
|
+
: '<div class="text-sm text-on-surface-variant/45">No join columns found for this relationship.</div>'
|
|
346
|
+
}
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
|
|
350
|
+
<section class="structure-graph__section">
|
|
351
|
+
<div class="structure-graph__section-header">
|
|
352
|
+
<div class="structure-graph__section-title">SQL</div>
|
|
353
|
+
<button
|
|
354
|
+
class="standard-button"
|
|
355
|
+
data-structure-graph-action="copy-join-sql"
|
|
356
|
+
type="button"
|
|
357
|
+
>
|
|
358
|
+
<span class="material-symbols-outlined text-sm">content_copy</span>
|
|
359
|
+
Copy join
|
|
360
|
+
</button>
|
|
361
|
+
</div>
|
|
362
|
+
<pre class="structure-graph__ddl structure-graph__join-sql custom-scrollbar" data-structure-graph-join-sql>${escapeHtml(
|
|
363
|
+
joinSql,
|
|
364
|
+
)}</pre>
|
|
365
|
+
</section>
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
|
|
123
370
|
export function renderDdlSection(ddl, emptyLabel = 'No DDL available.') {
|
|
124
371
|
const ddlText = typeof ddl === 'string' ? ddl : '';
|
|
125
372
|
const hasDdl = Boolean(ddlText.trim());
|
|
@@ -171,11 +418,18 @@ export function renderInspector(tableData) {
|
|
|
171
418
|
return `
|
|
172
419
|
<div class="structure-graph__panel">
|
|
173
420
|
<div class="space-y-3">
|
|
174
|
-
<div class="structure-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
421
|
+
<div class="structure-graph__panel-heading">
|
|
422
|
+
<div class="structure-graph__eyebrow">Table Inspector</div>
|
|
423
|
+
<button
|
|
424
|
+
class="query-history-icon-button"
|
|
425
|
+
data-structure-graph-action="toggle-inspector"
|
|
426
|
+
type="button"
|
|
427
|
+
aria-label="Hide inspector"
|
|
428
|
+
>
|
|
429
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
430
|
+
</button>
|
|
178
431
|
</div>
|
|
432
|
+
<div class="structure-graph__title">${escapeHtml(tableData.name)}</div>
|
|
179
433
|
</div>
|
|
180
434
|
|
|
181
435
|
<div class="structure-graph__summary">
|
|
@@ -239,6 +493,13 @@ export function buildGraphElements(schema) {
|
|
|
239
493
|
return;
|
|
240
494
|
}
|
|
241
495
|
|
|
496
|
+
const joinConditions = (foreignKey.mappings ?? []).map(mapping => ({
|
|
497
|
+
sourceTable: table.name,
|
|
498
|
+
targetTable: foreignKey.referencedTable,
|
|
499
|
+
sourceColumn: mapping.from || '',
|
|
500
|
+
targetColumn: mapping.to || 'rowid',
|
|
501
|
+
}));
|
|
502
|
+
|
|
242
503
|
(foreignKey.mappings ?? []).forEach((mapping, mappingIndex) => {
|
|
243
504
|
edges.push({
|
|
244
505
|
group: 'edges',
|
|
@@ -259,6 +520,7 @@ export function buildGraphElements(schema) {
|
|
|
259
520
|
targetTable: foreignKey.referencedTable,
|
|
260
521
|
sourceColumn: mapping.from || '',
|
|
261
522
|
targetColumn: mapping.to || 'rowid',
|
|
523
|
+
joinConditions,
|
|
262
524
|
},
|
|
263
525
|
});
|
|
264
526
|
});
|
|
@@ -268,22 +530,14 @@ export function buildGraphElements(schema) {
|
|
|
268
530
|
return [...nodes, ...edges];
|
|
269
531
|
}
|
|
270
532
|
|
|
271
|
-
|
|
533
|
+
function getReadableLayoutGeometry(variant = 0) {
|
|
272
534
|
return {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
algorithm: 'layered',
|
|
280
|
-
'elk.direction': 'RIGHT',
|
|
281
|
-
'elk.edgeRouting': 'ORTHOGONAL',
|
|
282
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': 60,
|
|
283
|
-
'elk.spacing.nodeNode': 40,
|
|
284
|
-
'elk.padding': 30,
|
|
285
|
-
},
|
|
286
|
-
...overrides,
|
|
535
|
+
connectedBaseY: 150,
|
|
536
|
+
isolatedY: -170,
|
|
537
|
+
isolatedSpacing: 380,
|
|
538
|
+
rankSpacing: 440,
|
|
539
|
+
rowSpacing: 230,
|
|
540
|
+
variant,
|
|
287
541
|
};
|
|
288
542
|
}
|
|
289
543
|
|
|
@@ -310,92 +564,112 @@ export function createCytoscapeInstance(container, elements) {
|
|
|
310
564
|
width: 'data(width)',
|
|
311
565
|
height: 'data(height)',
|
|
312
566
|
label: 'data(label)',
|
|
313
|
-
'background-color': '#
|
|
314
|
-
'border-color': '#
|
|
315
|
-
'border-width': 1.
|
|
316
|
-
color: '#
|
|
567
|
+
'background-color': '#302b12',
|
|
568
|
+
'border-color': '#c0a31d',
|
|
569
|
+
'border-width': 1.8,
|
|
570
|
+
color: '#fff8dc',
|
|
317
571
|
'font-family': 'Space Grotesk, Inter, sans-serif',
|
|
318
|
-
'font-size': 13,
|
|
572
|
+
'font-size': 13.5,
|
|
319
573
|
'font-weight': 700,
|
|
320
574
|
'text-wrap': 'ellipsis',
|
|
321
|
-
'text-max-width':
|
|
575
|
+
'text-max-width': 170,
|
|
322
576
|
'text-halign': 'center',
|
|
323
577
|
'text-valign': 'center',
|
|
578
|
+
'text-outline-color': '#11100b',
|
|
579
|
+
'text-outline-width': 1.5,
|
|
324
580
|
'overlay-opacity': 0,
|
|
325
|
-
'transition-property': 'background-color, border-color, opacity, color',
|
|
581
|
+
'transition-property': 'background-color, border-color, opacity, color, width',
|
|
326
582
|
'transition-duration': '140ms',
|
|
327
583
|
},
|
|
328
584
|
},
|
|
329
585
|
{
|
|
330
586
|
selector: 'node.selected',
|
|
331
587
|
style: {
|
|
332
|
-
'background-color': '#
|
|
333
|
-
'border-color': '#
|
|
334
|
-
'border-width': 2
|
|
335
|
-
color: '#
|
|
588
|
+
'background-color': '#5a4e00',
|
|
589
|
+
'border-color': '#ffea00',
|
|
590
|
+
'border-width': 3.2,
|
|
591
|
+
color: '#fffbd3',
|
|
336
592
|
},
|
|
337
593
|
},
|
|
338
594
|
{
|
|
339
595
|
selector: 'node.related',
|
|
340
596
|
style: {
|
|
341
|
-
'background-color': '#
|
|
342
|
-
'border-color': '#
|
|
343
|
-
'border-width': 2.
|
|
344
|
-
color: '#
|
|
597
|
+
'background-color': '#06454a',
|
|
598
|
+
'border-color': '#1ef6ff',
|
|
599
|
+
'border-width': 2.8,
|
|
600
|
+
color: '#edfeff',
|
|
345
601
|
},
|
|
346
602
|
},
|
|
347
603
|
{
|
|
348
604
|
selector: 'node.dimmed',
|
|
349
605
|
style: {
|
|
350
|
-
opacity: 0.
|
|
606
|
+
opacity: 0.46,
|
|
351
607
|
},
|
|
352
608
|
},
|
|
353
609
|
{
|
|
354
610
|
selector: 'edge',
|
|
355
611
|
style: {
|
|
356
|
-
width:
|
|
612
|
+
width: 2.2,
|
|
357
613
|
label: 'data(label)',
|
|
358
|
-
color: '#
|
|
614
|
+
color: '#fff5b8',
|
|
359
615
|
'font-family': 'Roboto Mono, monospace',
|
|
360
|
-
'font-size':
|
|
361
|
-
'
|
|
362
|
-
'text-background-
|
|
363
|
-
'text-background-
|
|
364
|
-
'
|
|
365
|
-
'
|
|
616
|
+
'font-size': 9.5,
|
|
617
|
+
'font-weight': 700,
|
|
618
|
+
'text-background-color': '#050706',
|
|
619
|
+
'text-background-opacity': 0.98,
|
|
620
|
+
'text-background-padding': 5,
|
|
621
|
+
'text-background-shape': 'roundrectangle',
|
|
622
|
+
'text-border-color': '#3d3510',
|
|
623
|
+
'text-border-opacity': 0.9,
|
|
624
|
+
'text-border-width': 1,
|
|
625
|
+
'text-outline-color': '#050706',
|
|
626
|
+
'text-outline-width': 1.8,
|
|
627
|
+
'text-wrap': 'wrap',
|
|
628
|
+
'text-max-width': 170,
|
|
629
|
+
'text-margin-y': -7,
|
|
630
|
+
'line-color': '#c8a81d',
|
|
631
|
+
'target-arrow-color': '#c8a81d',
|
|
632
|
+
'arrow-scale': 1.18,
|
|
366
633
|
'target-arrow-shape': 'triangle',
|
|
367
|
-
'curve-style': '
|
|
368
|
-
'
|
|
369
|
-
'
|
|
634
|
+
'curve-style': 'bezier',
|
|
635
|
+
'control-point-step-size': 82,
|
|
636
|
+
'loop-direction': '45deg',
|
|
637
|
+
'loop-sweep': '90deg',
|
|
370
638
|
'source-endpoint': 'outside-to-node',
|
|
371
639
|
'target-endpoint': 'outside-to-node',
|
|
372
640
|
'overlay-opacity': 0,
|
|
373
|
-
'
|
|
641
|
+
'z-index': 1,
|
|
642
|
+
'transition-property': 'line-color, target-arrow-color, opacity, color, width',
|
|
374
643
|
'transition-duration': '120ms',
|
|
375
644
|
},
|
|
376
645
|
},
|
|
377
646
|
{
|
|
378
647
|
selector: 'edge.related',
|
|
379
648
|
style: {
|
|
380
|
-
'line-color': '#
|
|
381
|
-
'target-arrow-color': '#
|
|
382
|
-
color: '#
|
|
383
|
-
|
|
649
|
+
'line-color': '#1ef6ff',
|
|
650
|
+
'target-arrow-color': '#1ef6ff',
|
|
651
|
+
color: '#edfeff',
|
|
652
|
+
'text-border-color': '#1ef6ff',
|
|
653
|
+
width: 3.1,
|
|
654
|
+
'z-index': 24,
|
|
384
655
|
},
|
|
385
656
|
},
|
|
386
657
|
{
|
|
387
658
|
selector: 'edge.hovered',
|
|
388
659
|
style: {
|
|
389
|
-
'line-color': '#
|
|
390
|
-
'target-arrow-color': '#
|
|
391
|
-
|
|
392
|
-
|
|
660
|
+
'line-color': '#ffea00',
|
|
661
|
+
'target-arrow-color': '#ffea00',
|
|
662
|
+
'arrow-scale': 1.35,
|
|
663
|
+
color: '#fffbd3',
|
|
664
|
+
'text-border-color': '#ffea00',
|
|
665
|
+
width: 3.6,
|
|
666
|
+
'z-index': 32,
|
|
393
667
|
},
|
|
394
668
|
},
|
|
395
669
|
{
|
|
396
670
|
selector: 'edge.dimmed',
|
|
397
671
|
style: {
|
|
398
|
-
opacity: 0.
|
|
672
|
+
opacity: 0.36,
|
|
399
673
|
},
|
|
400
674
|
},
|
|
401
675
|
],
|
|
@@ -410,6 +684,129 @@ export function resetHighlights(cy) {
|
|
|
410
684
|
cy.elements().removeClass('selected related hovered dimmed');
|
|
411
685
|
}
|
|
412
686
|
|
|
687
|
+
function formatQualifiedEdgeColumn(tableName, columnName) {
|
|
688
|
+
const safeTableName = String(tableName || '?');
|
|
689
|
+
const safeColumnName = String(columnName || 'rowid');
|
|
690
|
+
|
|
691
|
+
return `${safeTableName}.${safeColumnName}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function positionEdgeReadout(renderedPosition) {
|
|
695
|
+
if (!currentGraph?.edgeReadout || !currentGraph.canvasShell || !renderedPosition) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const readout = currentGraph.edgeReadout;
|
|
700
|
+
const shell = currentGraph.canvasShell;
|
|
701
|
+
const width = readout.offsetWidth || 280;
|
|
702
|
+
const height = readout.offsetHeight || 64;
|
|
703
|
+
const maxLeft = Math.max(12, shell.clientWidth - width - 12);
|
|
704
|
+
const maxTop = Math.max(12, shell.clientHeight - height - 12);
|
|
705
|
+
const left = Math.min(Math.max(renderedPosition.x + 16, 12), maxLeft);
|
|
706
|
+
const top = Math.min(Math.max(renderedPosition.y - height / 2, 12), maxTop);
|
|
707
|
+
|
|
708
|
+
readout.style.left = `${left}px`;
|
|
709
|
+
readout.style.top = `${top}px`;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function showEdgeReadout(edge, renderedPosition) {
|
|
713
|
+
if (!currentGraph?.edgeReadout || !edge) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const sourceTable = edge.data('sourceTable') || edge.source().data('tableName') || '?';
|
|
718
|
+
const targetTable = edge.data('targetTable') || edge.target().data('tableName') || '?';
|
|
719
|
+
const sourceColumn = edge.data('sourceColumn') || '?';
|
|
720
|
+
const targetColumn = edge.data('targetColumn') || 'rowid';
|
|
721
|
+
const sourceLabel = formatQualifiedEdgeColumn(sourceTable, sourceColumn);
|
|
722
|
+
const targetLabel = formatQualifiedEdgeColumn(targetTable, targetColumn);
|
|
723
|
+
|
|
724
|
+
currentGraph.edgeReadout.innerHTML = `
|
|
725
|
+
<div class="structure-graph__edge-readout-meta">
|
|
726
|
+
${escapeHtml(sourceTable)} <span aria-hidden="true">→</span> ${escapeHtml(targetTable)}
|
|
727
|
+
</div>
|
|
728
|
+
<div class="structure-graph__edge-readout-path">
|
|
729
|
+
${escapeHtml(sourceLabel)} <span aria-hidden="true">→</span> ${escapeHtml(targetLabel)}
|
|
730
|
+
</div>
|
|
731
|
+
`;
|
|
732
|
+
currentGraph.edgeReadout.removeAttribute('hidden');
|
|
733
|
+
positionEdgeReadout(renderedPosition);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function hideEdgeReadout() {
|
|
737
|
+
if (!currentGraph?.edgeReadout) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
currentGraph.edgeReadout.setAttribute('hidden', 'hidden');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function createGraphStateSnapshot(graph = currentGraph) {
|
|
745
|
+
if (!graph?.cy) {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const nodePositions = {};
|
|
750
|
+
|
|
751
|
+
graph.cy.nodes().forEach(node => {
|
|
752
|
+
const x = node.position('x');
|
|
753
|
+
const y = node.position('y');
|
|
754
|
+
|
|
755
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
756
|
+
nodePositions[node.id()] = { x, y };
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
if (!Object.keys(nodePositions).length) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
schemaSignature: getSchemaSignature(graph.schema),
|
|
766
|
+
nodePositions,
|
|
767
|
+
pan: graph.cy.pan(),
|
|
768
|
+
zoom: graph.cy.zoom(),
|
|
769
|
+
inspectorHidden: graph.inspectorHidden,
|
|
770
|
+
selectedTableName: graph.selectedTableName,
|
|
771
|
+
layoutVariant: graph.layoutVariant ?? 0,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function persistCurrentGraphState({ persistToStorage = true } = {}) {
|
|
776
|
+
const graphState = createGraphStateSnapshot();
|
|
777
|
+
|
|
778
|
+
if (!graphState) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
persistedGraphState = graphState;
|
|
783
|
+
|
|
784
|
+
if (persistToStorage) {
|
|
785
|
+
writeStoredGraphState(graphState);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function scheduleGraphStatePersist(delay = 180) {
|
|
792
|
+
if (!currentGraph || currentGraph.persistStateOnDestroy === false) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (currentGraph.persistTimer) {
|
|
797
|
+
window.clearTimeout(currentGraph.persistTimer);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
currentGraph.persistTimer = window.setTimeout(() => {
|
|
801
|
+
if (!currentGraph || currentGraph.persistStateOnDestroy === false) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
currentGraph.persistTimer = null;
|
|
806
|
+
persistCurrentGraphState();
|
|
807
|
+
}, delay);
|
|
808
|
+
}
|
|
809
|
+
|
|
413
810
|
export function highlightConnectedElements(cy, element) {
|
|
414
811
|
if (!cy || !element) {
|
|
415
812
|
return;
|
|
@@ -446,27 +843,17 @@ function destroyCurrentGraph() {
|
|
|
446
843
|
}
|
|
447
844
|
|
|
448
845
|
if (currentGraph.cy && currentGraph.persistStateOnDestroy !== false) {
|
|
449
|
-
|
|
450
|
-
currentGraph.cy.nodes().forEach(node => {
|
|
451
|
-
nodePositions[node.id()] = {
|
|
452
|
-
x: node.position('x'),
|
|
453
|
-
y: node.position('y'),
|
|
454
|
-
};
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
persistedGraphState = {
|
|
458
|
-
schemaSignature: getSchemaSignature(currentGraph.schema),
|
|
459
|
-
nodePositions,
|
|
460
|
-
pan: currentGraph.cy.pan(),
|
|
461
|
-
zoom: currentGraph.cy.zoom(),
|
|
462
|
-
inspectorHidden: currentGraph.inspectorHidden,
|
|
463
|
-
selectedTableName: currentGraph.selectedTableName,
|
|
464
|
-
};
|
|
846
|
+
persistCurrentGraphState();
|
|
465
847
|
}
|
|
466
848
|
|
|
467
849
|
currentGraph.cleanup.forEach(cleanup => cleanup());
|
|
468
850
|
currentGraph.cleanup = [];
|
|
469
851
|
|
|
852
|
+
if (currentGraph.persistTimer) {
|
|
853
|
+
window.clearTimeout(currentGraph.persistTimer);
|
|
854
|
+
currentGraph.persistTimer = null;
|
|
855
|
+
}
|
|
856
|
+
|
|
470
857
|
if (currentGraph.resizeObserver) {
|
|
471
858
|
currentGraph.resizeObserver.disconnect();
|
|
472
859
|
}
|
|
@@ -483,6 +870,10 @@ function destroyCurrentGraph() {
|
|
|
483
870
|
}
|
|
484
871
|
|
|
485
872
|
export function resetPersistedStructureGraphState() {
|
|
873
|
+
if (currentGraph?.persistStateOnDestroy !== false) {
|
|
874
|
+
persistCurrentGraphState();
|
|
875
|
+
}
|
|
876
|
+
|
|
486
877
|
persistedGraphState = null;
|
|
487
878
|
|
|
488
879
|
if (currentGraph) {
|
|
@@ -543,6 +934,23 @@ async function copyInspectorDdl(button) {
|
|
|
543
934
|
}
|
|
544
935
|
}
|
|
545
936
|
|
|
937
|
+
async function copyJoinSql(button) {
|
|
938
|
+
const sqlNode = button?.closest('.structure-graph__section')?.querySelector('[data-structure-graph-join-sql]');
|
|
939
|
+
const sql = sqlNode?.textContent ?? '';
|
|
940
|
+
|
|
941
|
+
if (!sql.trim()) {
|
|
942
|
+
showToast('No join SQL available to copy.', 'alert');
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
try {
|
|
947
|
+
await navigator.clipboard.writeText(sql);
|
|
948
|
+
showToast('Join SQL copied.', 'success');
|
|
949
|
+
} catch (error) {
|
|
950
|
+
showToast('Clipboard access failed.', 'alert');
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
546
954
|
function syncInspectorLayout() {
|
|
547
955
|
if (!currentGraph?.root) {
|
|
548
956
|
return;
|
|
@@ -558,6 +966,7 @@ function updateInspectorToggleButton() {
|
|
|
558
966
|
}
|
|
559
967
|
|
|
560
968
|
currentGraph.inspectorToggleButton.classList.toggle('is-active', currentGraph.inspectorHidden);
|
|
969
|
+
currentGraph.inspectorToggleButton.setAttribute('aria-pressed', currentGraph.inspectorHidden ? 'true' : 'false');
|
|
561
970
|
const icon = document.createElement('span');
|
|
562
971
|
const label = currentGraph.inspectorHidden ? 'Show Inspector' : 'Hide Inspector';
|
|
563
972
|
|
|
@@ -571,7 +980,9 @@ function clearSelection() {
|
|
|
571
980
|
return;
|
|
572
981
|
}
|
|
573
982
|
|
|
983
|
+
hideEdgeReadout();
|
|
574
984
|
currentGraph.selectedTableName = null;
|
|
985
|
+
currentGraph.selectedEdgeId = null;
|
|
575
986
|
resetHighlights(currentGraph.cy);
|
|
576
987
|
syncEntryHighlights();
|
|
577
988
|
replaceChildrenFromRenderedMarkup(currentGraph.inspector, getDefaultInspectorMarkup());
|
|
@@ -583,7 +994,9 @@ function applyTableSelection(node, { focus = true } = {}) {
|
|
|
583
994
|
return null;
|
|
584
995
|
}
|
|
585
996
|
|
|
997
|
+
hideEdgeReadout();
|
|
586
998
|
const tableData = node.data('table');
|
|
999
|
+
currentGraph.selectedEdgeId = null;
|
|
587
1000
|
currentGraph.selectedTableName = tableData.name;
|
|
588
1001
|
replaceChildrenFromRenderedMarkup(currentGraph.inspector, renderInspector(tableData));
|
|
589
1002
|
highlightConnectedElements(currentGraph.cy, node);
|
|
@@ -608,6 +1021,21 @@ function applyTableSelection(node, { focus = true } = {}) {
|
|
|
608
1021
|
return node;
|
|
609
1022
|
}
|
|
610
1023
|
|
|
1024
|
+
function applyEdgeSelection(edge) {
|
|
1025
|
+
if (!currentGraph || !edge) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
hideEdgeReadout();
|
|
1030
|
+
currentGraph.selectedTableName = null;
|
|
1031
|
+
currentGraph.selectedEdgeId = edge.id();
|
|
1032
|
+
replaceChildrenFromRenderedMarkup(currentGraph.inspector, renderJoinInspector(edge.data()));
|
|
1033
|
+
highlightConnectedElements(currentGraph.cy, edge);
|
|
1034
|
+
syncEntryHighlights([], [edge.data('sourceTable'), edge.data('targetTable')]);
|
|
1035
|
+
updateOpenDataButton();
|
|
1036
|
+
return edge;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
611
1039
|
function restoreGraphState() {
|
|
612
1040
|
if (!currentGraph) {
|
|
613
1041
|
return;
|
|
@@ -623,24 +1051,163 @@ function restoreGraphState() {
|
|
|
623
1051
|
}
|
|
624
1052
|
}
|
|
625
1053
|
|
|
1054
|
+
if (currentGraph.selectedEdgeId) {
|
|
1055
|
+
const selectedEdge = currentGraph.cy.getElementById(currentGraph.selectedEdgeId);
|
|
1056
|
+
|
|
1057
|
+
if (selectedEdge.nonempty()) {
|
|
1058
|
+
highlightConnectedElements(currentGraph.cy, selectedEdge);
|
|
1059
|
+
syncEntryHighlights([], [selectedEdge.data('sourceTable'), selectedEdge.data('targetTable')]);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
626
1064
|
resetHighlights(currentGraph.cy);
|
|
627
1065
|
syncEntryHighlights();
|
|
628
1066
|
updateOpenDataButton();
|
|
629
1067
|
}
|
|
630
1068
|
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1069
|
+
function getSortedGraphNodes(nodes) {
|
|
1070
|
+
return nodes
|
|
1071
|
+
.toArray()
|
|
1072
|
+
.sort((left, right) => String(left.data('tableName')).localeCompare(String(right.data('tableName'))));
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function getIncomingAverageY(node) {
|
|
1076
|
+
const incomingY = [];
|
|
1077
|
+
|
|
1078
|
+
node.incomers('edge').forEach(edge => {
|
|
1079
|
+
const y = edge.source().position('y');
|
|
1080
|
+
|
|
1081
|
+
if (Number.isFinite(y)) {
|
|
1082
|
+
incomingY.push(y);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
642
1085
|
|
|
643
|
-
|
|
1086
|
+
if (!incomingY.length) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return incomingY.reduce((sum, value) => sum + value, 0) / incomingY.length;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function getLayoutVariantOffset(variant, rank, index, axis) {
|
|
1094
|
+
if (!variant) {
|
|
1095
|
+
return 0;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const factor = axis === 'x' ? 28 : 46;
|
|
1099
|
+
const wave = axis === 'x' ? Math.cos : Math.sin;
|
|
1100
|
+
|
|
1101
|
+
return wave((variant + 1) * (rank + 1.7) * (index + 2.3)) * factor;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function applyReadableLayoutPositions(cy, { variant = 0 } = {}) {
|
|
1105
|
+
const nodes = getSortedGraphNodes(cy.nodes());
|
|
1106
|
+
|
|
1107
|
+
if (!nodes.length) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const geometry = getReadableLayoutGeometry(variant);
|
|
1112
|
+
const rankById = new Map(nodes.map(node => [node.id(), 0]));
|
|
1113
|
+
|
|
1114
|
+
for (let iteration = 0; iteration < nodes.length; iteration += 1) {
|
|
1115
|
+
let changed = false;
|
|
1116
|
+
|
|
1117
|
+
cy.edges().forEach(edge => {
|
|
1118
|
+
const source = edge.source();
|
|
1119
|
+
const target = edge.target();
|
|
1120
|
+
|
|
1121
|
+
if (source.id() === target.id()) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const sourceRank = rankById.get(source.id()) ?? 0;
|
|
1126
|
+
const targetRank = rankById.get(target.id()) ?? 0;
|
|
1127
|
+
|
|
1128
|
+
if (targetRank < sourceRank + 1) {
|
|
1129
|
+
rankById.set(target.id(), sourceRank + 1);
|
|
1130
|
+
changed = true;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
if (!changed) {
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const connectedNodes = nodes.filter(node => node.connectedEdges().nonempty());
|
|
1140
|
+
const isolatedNodes = nodes.filter(node => node.connectedEdges().empty());
|
|
1141
|
+
const nodesByRank = new Map();
|
|
1142
|
+
|
|
1143
|
+
connectedNodes.forEach(node => {
|
|
1144
|
+
const rank = rankById.get(node.id()) ?? 0;
|
|
1145
|
+
const group = nodesByRank.get(rank) ?? [];
|
|
1146
|
+
|
|
1147
|
+
group.push(node);
|
|
1148
|
+
nodesByRank.set(rank, group);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
const ranks = [...nodesByRank.keys()].sort((left, right) => left - right);
|
|
1152
|
+
const maxRank = ranks.at(-1) ?? 0;
|
|
1153
|
+
const connectedBaseX = -(maxRank * geometry.rankSpacing) / 2;
|
|
1154
|
+
|
|
1155
|
+
ranks.forEach(rank => {
|
|
1156
|
+
const group = (nodesByRank.get(rank) ?? []).sort((left, right) => {
|
|
1157
|
+
const leftAverageY = getIncomingAverageY(left);
|
|
1158
|
+
const rightAverageY = getIncomingAverageY(right);
|
|
1159
|
+
|
|
1160
|
+
if (leftAverageY !== null && rightAverageY !== null && leftAverageY !== rightAverageY) {
|
|
1161
|
+
return leftAverageY - rightAverageY;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const degreeDelta = right.degree(false) - left.degree(false);
|
|
1165
|
+
|
|
1166
|
+
if (degreeDelta !== 0) {
|
|
1167
|
+
return degreeDelta;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return String(left.data('tableName')).localeCompare(String(right.data('tableName')));
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
group.forEach((node, index) => {
|
|
1174
|
+
node.position({
|
|
1175
|
+
x:
|
|
1176
|
+
connectedBaseX +
|
|
1177
|
+
rank * geometry.rankSpacing +
|
|
1178
|
+
getLayoutVariantOffset(geometry.variant, rank, index, 'x'),
|
|
1179
|
+
y:
|
|
1180
|
+
geometry.connectedBaseY +
|
|
1181
|
+
(index - (group.length - 1) / 2) * geometry.rowSpacing +
|
|
1182
|
+
(rank % 2 === 0 ? 0 : 58) +
|
|
1183
|
+
getLayoutVariantOffset(geometry.variant, rank, index, 'y'),
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
isolatedNodes.forEach((node, index) => {
|
|
1189
|
+
node.position({
|
|
1190
|
+
x:
|
|
1191
|
+
(index - (isolatedNodes.length - 1) / 2) * geometry.isolatedSpacing +
|
|
1192
|
+
getLayoutVariantOffset(geometry.variant, -1, index, 'x'),
|
|
1193
|
+
y: geometry.isolatedY + getLayoutVariantOffset(geometry.variant, -1, index, 'y'),
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function runLayout(cy, onStop, { randomize = false } = {}) {
|
|
1199
|
+
if (currentGraph) {
|
|
1200
|
+
currentGraph.layoutVariant = randomize ? (currentGraph.layoutVariant ?? 0) + 1 : 0;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
applyReadableLayoutPositions(cy, { variant: currentGraph?.layoutVariant ?? 0 });
|
|
1204
|
+
cy.fit(cy.elements(), 90);
|
|
1205
|
+
|
|
1206
|
+
if (typeof onStop === 'function') {
|
|
1207
|
+
onStop();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
persistCurrentGraphState();
|
|
644
1211
|
}
|
|
645
1212
|
|
|
646
1213
|
function restorePersistedViewport(cy, persistedState) {
|
|
@@ -696,16 +1263,18 @@ export function setupToolbar(cy) {
|
|
|
696
1263
|
switch (button.dataset.structureGraphAction) {
|
|
697
1264
|
case 'fit':
|
|
698
1265
|
cy.fit(cy.elements(), 60);
|
|
1266
|
+
scheduleGraphStatePersist();
|
|
699
1267
|
break;
|
|
700
1268
|
case 'relayout':
|
|
1269
|
+
hideEdgeReadout();
|
|
701
1270
|
runLayout(cy, () => {
|
|
702
1271
|
if (currentGraph?.selectedTableName) {
|
|
703
1272
|
const selectedNode = cy.getElementById(getTableId(currentGraph.selectedTableName));
|
|
704
1273
|
if (selectedNode.nonempty()) {
|
|
705
|
-
applyTableSelection(selectedNode, { focus:
|
|
1274
|
+
applyTableSelection(selectedNode, { focus: false });
|
|
706
1275
|
}
|
|
707
1276
|
}
|
|
708
|
-
});
|
|
1277
|
+
}, { randomize: true });
|
|
709
1278
|
break;
|
|
710
1279
|
case 'clear':
|
|
711
1280
|
clearSelection();
|
|
@@ -720,10 +1289,14 @@ export function setupToolbar(cy) {
|
|
|
720
1289
|
storeInspectorHidden(currentGraph.inspectorHidden);
|
|
721
1290
|
updateInspectorToggleButton();
|
|
722
1291
|
syncInspectorLayout();
|
|
1292
|
+
scheduleGraphStatePersist();
|
|
723
1293
|
break;
|
|
724
1294
|
case 'copy-ddl':
|
|
725
1295
|
await copyInspectorDdl(button);
|
|
726
1296
|
break;
|
|
1297
|
+
case 'copy-join-sql':
|
|
1298
|
+
await copyJoinSql(button);
|
|
1299
|
+
break;
|
|
727
1300
|
default:
|
|
728
1301
|
}
|
|
729
1302
|
};
|
|
@@ -762,6 +1335,8 @@ export async function mountStructureGraph(snapshot) {
|
|
|
762
1335
|
const canvas = root.querySelector('[data-structure-graph-canvas]');
|
|
763
1336
|
const inspector = root.querySelector('[data-structure-graph-inspector]');
|
|
764
1337
|
const empty = root.querySelector('[data-structure-graph-empty]');
|
|
1338
|
+
const canvasShell = canvas?.closest('.structure-graph__canvas-shell');
|
|
1339
|
+
const edgeReadout = root.querySelector('[data-structure-graph-edge-readout]');
|
|
765
1340
|
const schema = snapshot.structure.data?.graph ?? { tables: [] };
|
|
766
1341
|
const { schemaSignature, state: cachedState } = getPersistedGraphState(schema);
|
|
767
1342
|
|
|
@@ -782,6 +1357,8 @@ export async function mountStructureGraph(snapshot) {
|
|
|
782
1357
|
currentGraph = {
|
|
783
1358
|
root,
|
|
784
1359
|
canvas,
|
|
1360
|
+
canvasShell: canvasShell instanceof HTMLElement ? canvasShell : null,
|
|
1361
|
+
edgeReadout: edgeReadout instanceof HTMLElement ? edgeReadout : null,
|
|
785
1362
|
inspector,
|
|
786
1363
|
initialInspectorMarkup: inspector.innerHTML,
|
|
787
1364
|
initialSelectedTableName: null,
|
|
@@ -794,14 +1371,26 @@ export async function mountStructureGraph(snapshot) {
|
|
|
794
1371
|
openDataButton: null,
|
|
795
1372
|
inspectorToggleButton: null,
|
|
796
1373
|
inspectorHidden: cachedState?.inspectorHidden ?? readStoredInspectorHidden(),
|
|
1374
|
+
layoutVariant: cachedState?.layoutVariant ?? 0,
|
|
797
1375
|
persistStateOnDestroy: true,
|
|
1376
|
+
selectedEdgeId: null,
|
|
798
1377
|
selectedTableName: null,
|
|
799
1378
|
};
|
|
800
1379
|
|
|
801
1380
|
currentGraph.cleanup.push(setupToolbar(cy));
|
|
802
1381
|
|
|
1382
|
+
const persistBeforeUnload = () => {
|
|
1383
|
+
persistCurrentGraphState();
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
window.addEventListener('beforeunload', persistBeforeUnload);
|
|
1387
|
+
currentGraph.cleanup.push(() => {
|
|
1388
|
+
window.removeEventListener('beforeunload', persistBeforeUnload);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
803
1391
|
cy.on('tap', 'node', event => {
|
|
804
1392
|
applyTableSelection(event.target, { focus: false });
|
|
1393
|
+
scheduleGraphStatePersist();
|
|
805
1394
|
});
|
|
806
1395
|
|
|
807
1396
|
cy.on('tap', event => {
|
|
@@ -810,16 +1399,38 @@ export async function mountStructureGraph(snapshot) {
|
|
|
810
1399
|
}
|
|
811
1400
|
});
|
|
812
1401
|
|
|
1402
|
+
cy.on('tap', 'edge', event => {
|
|
1403
|
+
applyEdgeSelection(event.target);
|
|
1404
|
+
showEdgeReadout(event.target, event.renderedPosition);
|
|
1405
|
+
updateOpenDataButton();
|
|
1406
|
+
scheduleGraphStatePersist();
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
cy.on('dragfree', 'node', () => {
|
|
1410
|
+
scheduleGraphStatePersist(0);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
813
1413
|
cy.on('mouseover', 'edge', event => {
|
|
814
1414
|
const edge = event.target;
|
|
815
1415
|
highlightConnectedElements(cy, edge);
|
|
816
1416
|
syncEntryHighlights([], [edge.data('sourceTable'), edge.data('targetTable')]);
|
|
1417
|
+
showEdgeReadout(edge, event.renderedPosition);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
cy.on('mousemove', 'edge', event => {
|
|
1421
|
+
showEdgeReadout(event.target, event.renderedPosition);
|
|
817
1422
|
});
|
|
818
1423
|
|
|
819
1424
|
cy.on('mouseout', 'edge', () => {
|
|
1425
|
+
hideEdgeReadout();
|
|
820
1426
|
restoreGraphState();
|
|
821
1427
|
});
|
|
822
1428
|
|
|
1429
|
+
cy.on('pan zoom', () => {
|
|
1430
|
+
hideEdgeReadout();
|
|
1431
|
+
scheduleGraphStatePersist(300);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
823
1434
|
const selectedTableName = snapshot.structure.data?.grouped?.tables?.some(
|
|
824
1435
|
table => table.name === snapshot.structure.selectedName,
|
|
825
1436
|
)
|