sqlite-hub 2.0.1 → 2.2.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/README.md +21 -9
- package/docs/MCP.md +8 -0
- package/docs/changelog.md +16 -0
- package/docs/guidelines/DESIGN.md +64 -0
- package/docs/todo.md +2 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/logs_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +18 -0
- package/frontend/js/app.js +567 -15
- package/frontend/js/components/badges.js +16 -0
- package/frontend/js/components/connectionCard.js +35 -0
- package/frontend/js/components/modal.js +268 -247
- package/frontend/js/components/queryHistoryDetail.js +25 -45
- package/frontend/js/components/rowEditorPanel.js +7 -60
- package/frontend/js/components/structureGraph.js +292 -13
- package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
- package/frontend/js/components/tableDesignerEditor.js +51 -16
- package/frontend/js/components/tableDesignerSidebar.js +9 -5
- package/frontend/js/store.js +1847 -190
- package/frontend/js/utils/connectionRegistry.js +76 -0
- package/frontend/js/utils/markdownDocuments.js +21 -4
- package/frontend/js/utils/tableDesigner.js +431 -62
- package/frontend/js/views/backups.js +2 -5
- package/frontend/js/views/charts.js +20 -10
- package/frontend/js/views/connections.js +101 -6
- package/frontend/js/views/data.js +52 -9
- package/frontend/js/views/documents.js +153 -59
- package/frontend/js/views/structure.js +28 -5
- package/frontend/js/views/tableAdvisor.js +14 -6
- package/frontend/js/views/tableDesigner.js +44 -9
- package/frontend/styles/base.css +9 -0
- package/frontend/styles/components.css +676 -79
- package/frontend/styles/structure-graph.css +49 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +12 -0
- package/package.json +1 -1
- package/server/routes/backups.js +37 -1
- package/server/routes/charts.js +28 -0
- package/server/routes/connections.js +12 -0
- package/server/routes/data.js +30 -2
- package/server/routes/documents.js +61 -5
- package/server/routes/settings.js +32 -0
- package/server/routes/tableDesigner.js +35 -2
- package/server/server.js +3 -3
- package/server/services/mcpToolService.js +30 -0
- package/server/services/sqlite/connectionManager.js +25 -2
- package/server/services/sqlite/dataBrowserService.js +21 -0
- package/server/services/sqlite/introspection.js +39 -1
- package/server/services/sqlite/overviewService.js +1 -0
- package/server/services/sqlite/structureService.js +66 -11
- package/server/services/sqlite/tableAdvisor.js +6 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
- package/server/services/sqlite/tableDesigner/sql.js +31 -2
- package/server/services/sqlite/tableDesigner/validation.js +69 -9
- package/server/services/sqlite/tableDesignerService.js +110 -5
- package/server/services/storage/appStateStore.js +590 -10
- package/server/utils/fileValidation.js +5 -0
- package/server/utils/userActionLog.js +49 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
|
@@ -23,42 +23,14 @@ function canOpenQueryHistoryInCharts(item) {
|
|
|
23
23
|
return Boolean(item?.chartsEligible);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function getQueryTypeTone(queryType) {
|
|
27
|
-
if (queryType === "select" || queryType === "update") {
|
|
28
|
-
return "success";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (queryType === "pragma") {
|
|
32
|
-
return "primary";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return "muted";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getExecutionSourceTone(source) {
|
|
39
|
-
if (source === "api") {
|
|
40
|
-
return "primary";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (source === "cli") {
|
|
44
|
-
return "warning";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (source === "mcp") {
|
|
48
|
-
return "muted";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return "success";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
26
|
function renderRunItem(run) {
|
|
55
27
|
const executedBy = String(run.executedBy ?? "user").trim().toLowerCase() || "user";
|
|
56
28
|
|
|
57
29
|
return `
|
|
58
30
|
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
59
31
|
<div class="flex flex-wrap items-center gap-2">
|
|
60
|
-
${renderStatusBadge(run.status,
|
|
61
|
-
${renderStatusBadge(executedBy,
|
|
32
|
+
${renderStatusBadge(run.status, "muted")}
|
|
33
|
+
${renderStatusBadge(executedBy, "muted")}
|
|
62
34
|
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
63
35
|
${escapeHtml(formatCompactDateTime(run.executedAt))}
|
|
64
36
|
</span>
|
|
@@ -100,9 +72,14 @@ export function renderQueryHistoryDetail({
|
|
|
100
72
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
101
73
|
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
102
74
|
<div class="flex items-center justify-between gap-3">
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
75
|
+
<div class="min-w-0">
|
|
76
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
|
|
77
|
+
SQL Editor // Query History
|
|
78
|
+
</div>
|
|
79
|
+
<h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
80
|
+
Loading
|
|
81
|
+
</h2>
|
|
82
|
+
</div>
|
|
106
83
|
<button
|
|
107
84
|
class="query-history-icon-button"
|
|
108
85
|
data-action="clear-query-history-selection"
|
|
@@ -124,9 +101,14 @@ export function renderQueryHistoryDetail({
|
|
|
124
101
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
125
102
|
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
126
103
|
<div class="flex items-center justify-between gap-3">
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
104
|
+
<div class="min-w-0">
|
|
105
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
|
|
106
|
+
SQL Editor // Query History
|
|
107
|
+
</div>
|
|
108
|
+
<h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
109
|
+
Error
|
|
110
|
+
</h2>
|
|
111
|
+
</div>
|
|
130
112
|
<button
|
|
131
113
|
class="query-history-icon-button"
|
|
132
114
|
data-action="clear-query-history-selection"
|
|
@@ -153,12 +135,10 @@ export function renderQueryHistoryDetail({
|
|
|
153
135
|
].join("")
|
|
154
136
|
: "";
|
|
155
137
|
const statusMarkup = [
|
|
156
|
-
renderStatusBadge(item.queryType,
|
|
157
|
-
item.isSaved ? renderStatusBadge("saved", "
|
|
158
|
-
item.isDestructive ? renderStatusBadge("destructive", "
|
|
159
|
-
item.lastRun
|
|
160
|
-
? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success")
|
|
161
|
-
: "",
|
|
138
|
+
renderStatusBadge(item.queryType, "muted"),
|
|
139
|
+
item.isSaved ? renderStatusBadge("saved", "muted") : "",
|
|
140
|
+
item.isDestructive ? renderStatusBadge("destructive", "muted") : "",
|
|
141
|
+
item.lastRun ? renderStatusBadge(item.lastRun.status, "muted") : "",
|
|
162
142
|
].join("");
|
|
163
143
|
const metaMarkup = [
|
|
164
144
|
renderDetailMetaItem("Last Used", formatDateTime(item.lastUsedAt)),
|
|
@@ -176,9 +156,9 @@ export function renderQueryHistoryDetail({
|
|
|
176
156
|
return [
|
|
177
157
|
'<section class="flex h-full min-h-0 flex-col bg-surface-low">',
|
|
178
158
|
'<div class="border-b border-outline-variant/10 px-5 py-4">',
|
|
179
|
-
'<div class="flex items-center justify-between gap-3"><div>',
|
|
180
|
-
'<div class="text-[10px]
|
|
181
|
-
'<h2 class="mt-1 font-body text-lg font-black uppercase tracking-tight text-on-surface">',
|
|
159
|
+
'<div class="flex items-center justify-between gap-3"><div class="min-w-0">',
|
|
160
|
+
'<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">SQL Editor // Query History</div>',
|
|
161
|
+
'<h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">',
|
|
182
162
|
escapeHtml(item.displayTitle),
|
|
183
163
|
"</h2></div>",
|
|
184
164
|
'<button class="query-history-icon-button" data-action="clear-query-history-selection" type="button"><span class="material-symbols-outlined text-[18px]">close</span></button>',
|
|
@@ -215,7 +215,7 @@ function renderTextCharacterCountBadge(value) {
|
|
|
215
215
|
|
|
216
216
|
return `
|
|
217
217
|
<span
|
|
218
|
-
class="
|
|
218
|
+
class="status-badge status-badge--muted"
|
|
219
219
|
data-row-editor-char-count
|
|
220
220
|
${count === null ? "hidden" : ""}
|
|
221
221
|
>
|
|
@@ -337,22 +337,10 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
337
337
|
`;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
function getValueStateBadgeClassName(state) {
|
|
341
|
-
if (state === "null") {
|
|
342
|
-
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (state === "empty") {
|
|
346
|
-
return "border-outline-variant/35 bg-surface-container-high text-on-surface-variant";
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
350
|
-
}
|
|
351
|
-
|
|
352
340
|
function renderValueStateBadge(state) {
|
|
353
341
|
return `
|
|
354
342
|
<span
|
|
355
|
-
class="
|
|
343
|
+
class="status-badge status-badge--muted"
|
|
356
344
|
data-row-editor-value-state
|
|
357
345
|
data-value-state="${escapeHtml(state)}"
|
|
358
346
|
>${escapeHtml(getRowEditorValueStateLabel(state))}</span>
|
|
@@ -447,44 +435,11 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
447
435
|
`;
|
|
448
436
|
}
|
|
449
437
|
|
|
450
|
-
function getFieldBadgeClassName(tone) {
|
|
451
|
-
if (tone === "primary-key") {
|
|
452
|
-
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (tone === "foreign-key") {
|
|
456
|
-
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (tone === "url") {
|
|
460
|
-
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (tone === "email") {
|
|
464
|
-
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (tone === "check") {
|
|
468
|
-
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (tone === "filepath") {
|
|
472
|
-
return "border-outline-variant/30 bg-surface-container-high text-on-surface";
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (tone === "char-count") {
|
|
476
|
-
return "border-outline-variant/25 bg-surface-container text-on-surface-variant";
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
480
|
-
}
|
|
481
|
-
|
|
482
438
|
function renderFieldBadge(badge) {
|
|
483
439
|
const label = typeof badge === "object" ? badge.label : badge;
|
|
484
|
-
const tone = typeof badge === "object" ? badge.tone : "";
|
|
485
440
|
|
|
486
441
|
return `
|
|
487
|
-
<span class="
|
|
442
|
+
<span class="status-badge status-badge--muted">
|
|
488
443
|
${escapeHtml(label)}
|
|
489
444
|
</span>
|
|
490
445
|
`;
|
|
@@ -578,27 +533,19 @@ export function renderRowEditorPanel({
|
|
|
578
533
|
]
|
|
579
534
|
.filter(Boolean)
|
|
580
535
|
.join("");
|
|
536
|
+
const eyebrow = [sectionLabel, subtitle].filter(Boolean).join(" // ");
|
|
581
537
|
|
|
582
538
|
return `
|
|
583
539
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
584
540
|
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
585
541
|
<div class="flex items-start justify-between gap-4">
|
|
586
542
|
<div class="min-w-0 flex-1">
|
|
587
|
-
<div class="text-[10px]
|
|
588
|
-
${escapeHtml(
|
|
543
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
|
|
544
|
+
${escapeHtml(eyebrow)}
|
|
589
545
|
</div>
|
|
590
|
-
<h2 class="mt-
|
|
546
|
+
<h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
591
547
|
${escapeHtml(title)}
|
|
592
548
|
</h2>
|
|
593
|
-
${
|
|
594
|
-
subtitle
|
|
595
|
-
? `
|
|
596
|
-
<div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
597
|
-
${escapeHtml(subtitle)}
|
|
598
|
-
</div>
|
|
599
|
-
`
|
|
600
|
-
: ""
|
|
601
|
-
}
|
|
602
549
|
</div>
|
|
603
550
|
<button
|
|
604
551
|
aria-label="Close panel"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { showToast } from '../store.js';
|
|
1
|
+
import { clearStructureSelection, showToast } from '../store.js';
|
|
2
2
|
import { replaceChildrenFromRenderedMarkup } from '../utils/dom.js';
|
|
3
3
|
import { escapeHtml, formatNumber } from '../utils/format.js';
|
|
4
4
|
|
|
@@ -56,10 +56,68 @@ function getTableId(tableName) {
|
|
|
56
56
|
return `table:${tableName}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function isShadowRelationship(edgeData) {
|
|
60
|
+
return edgeData?.relationshipKind === 'shadow';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveShadowOwnerTableName(shadowTable, tables, tableMap) {
|
|
64
|
+
if (!shadowTable?.isShadow) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const explicitOwnerName = String(shadowTable.shadowOwnerTable ?? '');
|
|
69
|
+
const explicitOwner = tableMap.get(explicitOwnerName);
|
|
70
|
+
|
|
71
|
+
if (explicitOwner?.isVirtual) {
|
|
72
|
+
return explicitOwnerName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
tables
|
|
77
|
+
.filter(table => table.isVirtual && shadowTable.name.startsWith(`${table.name}_`))
|
|
78
|
+
.sort((left, right) => right.name.length - left.name.length)[0]?.name ?? ''
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function unquoteSqlIdentifier(identifier) {
|
|
83
|
+
const value = String(identifier ?? '').trim();
|
|
84
|
+
|
|
85
|
+
if (value.length >= 2 && value[0] === '"' && value.at(-1) === '"') {
|
|
86
|
+
return value.slice(1, -1).replaceAll('""', '"');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (value.length >= 2 && value[0] === '`' && value.at(-1) === '`') {
|
|
90
|
+
return value.slice(1, -1).replaceAll('``', '`');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value.length >= 2 && value[0] === '[' && value.at(-1) === ']') {
|
|
94
|
+
return value.slice(1, -1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractVirtualTableModule(ddl = '') {
|
|
101
|
+
const match = String(ddl ?? '').match(
|
|
102
|
+
/\bUSING\s+("[^"]+(?:""[^"]*)*"|`[^`]+(?:``[^`]*)*`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_]*)/i,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return match ? unquoteSqlIdentifier(match[1]) : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveVirtualTableModule(table) {
|
|
109
|
+
if (!table?.isVirtual) {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return String(table.virtualModule ?? '').trim() || extractVirtualTableModule(table.ddl);
|
|
114
|
+
}
|
|
115
|
+
|
|
59
116
|
function getSchemaSignature(schema) {
|
|
60
117
|
return JSON.stringify(
|
|
61
118
|
(schema?.tables ?? []).map(table => ({
|
|
62
119
|
name: table.name,
|
|
120
|
+
virtualModule: table.virtualModule ?? null,
|
|
63
121
|
columns: (table.columns ?? []).map(column => column.name),
|
|
64
122
|
foreignKeys: (table.foreignKeys ?? []).map(foreignKey => ({
|
|
65
123
|
referencedTable: foreignKey.referencedTable,
|
|
@@ -237,6 +295,18 @@ function createColumnFlags(column, foreignKeyColumns) {
|
|
|
237
295
|
return flags.join('');
|
|
238
296
|
}
|
|
239
297
|
|
|
298
|
+
function renderTableKindFlag(tableData) {
|
|
299
|
+
if (tableData?.isVirtual) {
|
|
300
|
+
return '<span class="structure-graph__flag is-virtual" title="Virtual table">Virtual</span>';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (tableData?.isShadow) {
|
|
304
|
+
return '<span class="structure-graph__flag is-shadow" title="Shadow table">Shadow</span>';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
|
|
240
310
|
function quoteSqlIdentifier(identifier) {
|
|
241
311
|
return `"${String(identifier ?? '').replaceAll('"', '""')}"`;
|
|
242
312
|
}
|
|
@@ -298,6 +368,47 @@ function buildJoinSql(edgeData) {
|
|
|
298
368
|
}
|
|
299
369
|
|
|
300
370
|
function renderJoinInspector(edgeData) {
|
|
371
|
+
if (isShadowRelationship(edgeData)) {
|
|
372
|
+
const sourceTable = edgeData?.sourceTable || '';
|
|
373
|
+
const targetTable = edgeData?.targetTable || '';
|
|
374
|
+
|
|
375
|
+
return `
|
|
376
|
+
<div class="structure-graph__panel">
|
|
377
|
+
<div class="space-y-3">
|
|
378
|
+
<div class="structure-graph__panel-heading">
|
|
379
|
+
<div class="structure-graph__eyebrow">Shadow Link</div>
|
|
380
|
+
<button
|
|
381
|
+
class="query-history-icon-button"
|
|
382
|
+
data-structure-graph-action="toggle-inspector"
|
|
383
|
+
type="button"
|
|
384
|
+
aria-label="Hide inspector"
|
|
385
|
+
>
|
|
386
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="structure-graph__title">${escapeHtml(sourceTable)} → ${escapeHtml(targetTable)}</div>
|
|
390
|
+
<div class="structure-graph__subtitle">Shadow table relationship</div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<section class="structure-graph__section">
|
|
394
|
+
<div class="structure-graph__section-title">Relationship</div>
|
|
395
|
+
<div class="structure-graph__join-flow">
|
|
396
|
+
<div class="structure-graph__join-table">${escapeHtml(sourceTable)}</div>
|
|
397
|
+
<span class="material-symbols-outlined structure-graph__join-arrow" aria-hidden="true">arrow_forward</span>
|
|
398
|
+
<div class="structure-graph__join-table">${escapeHtml(targetTable)}</div>
|
|
399
|
+
</div>
|
|
400
|
+
<div class="structure-graph__join-condition-list">
|
|
401
|
+
<div class="structure-graph__join-condition">
|
|
402
|
+
<span>Virtual table</span>
|
|
403
|
+
<span aria-hidden="true">→</span>
|
|
404
|
+
<span>Shadow table</span>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</section>
|
|
408
|
+
</div>
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
|
|
301
412
|
const sourceTable = edgeData?.sourceTable || '';
|
|
302
413
|
const targetTable = edgeData?.targetTable || '';
|
|
303
414
|
const conditions = getJoinConditions(edgeData);
|
|
@@ -429,7 +540,10 @@ export function renderInspector(tableData) {
|
|
|
429
540
|
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
430
541
|
</button>
|
|
431
542
|
</div>
|
|
432
|
-
<div class="
|
|
543
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
|
544
|
+
<div class="structure-graph__title min-w-0">${escapeHtml(tableData.name)}</div>
|
|
545
|
+
${renderTableKindFlag(tableData)}
|
|
546
|
+
</div>
|
|
433
547
|
</div>
|
|
434
548
|
|
|
435
549
|
<div class="structure-graph__summary">
|
|
@@ -474,19 +588,51 @@ export function renderInspector(tableData) {
|
|
|
474
588
|
export function buildGraphElements(schema) {
|
|
475
589
|
const tables = schema?.tables ?? [];
|
|
476
590
|
const tableMap = new Map(tables.map(table => [table.name, table]));
|
|
477
|
-
const nodes = tables.map(table =>
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
table,
|
|
486
|
-
|
|
487
|
-
|
|
591
|
+
const nodes = tables.map(table => {
|
|
592
|
+
const isVirtual = Boolean(table.isVirtual);
|
|
593
|
+
const isShadow = Boolean(table.isShadow);
|
|
594
|
+
const virtualModule = resolveVirtualTableModule(table);
|
|
595
|
+
const moduleWidth = virtualModule ? virtualModule.length * 8 + 72 : 0;
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
group: 'nodes',
|
|
599
|
+
classes: [isVirtual ? 'virtual-table' : '', isShadow ? 'shadow-table' : ''].filter(Boolean).join(' '),
|
|
600
|
+
data: {
|
|
601
|
+
id: getTableId(table.name),
|
|
602
|
+
label: table.name,
|
|
603
|
+
tableName: table.name,
|
|
604
|
+
tableKind: table.tableKind ?? 'table',
|
|
605
|
+
isVirtual,
|
|
606
|
+
isShadow,
|
|
607
|
+
virtualModule,
|
|
608
|
+
width: Math.max(isVirtual ? 208 : 184, table.name.length * 9 + 64, moduleWidth),
|
|
609
|
+
height: isVirtual ? (virtualModule ? 82 : 70) : 56,
|
|
610
|
+
table,
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
});
|
|
488
614
|
const edges = [];
|
|
489
615
|
|
|
616
|
+
tables.forEach(table => {
|
|
617
|
+
const shadowOwnerTableName = resolveShadowOwnerTableName(table, tables, tableMap);
|
|
618
|
+
|
|
619
|
+
if (!shadowOwnerTableName || !tableMap.has(shadowOwnerTableName)) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
edges.push({
|
|
624
|
+
group: 'edges',
|
|
625
|
+
classes: 'shadow-link',
|
|
626
|
+
data: {
|
|
627
|
+
source: getTableId(shadowOwnerTableName),
|
|
628
|
+
target: getTableId(table.name),
|
|
629
|
+
sourceTable: shadowOwnerTableName,
|
|
630
|
+
targetTable: table.name,
|
|
631
|
+
relationshipKind: 'shadow',
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
490
636
|
tables.forEach(table => {
|
|
491
637
|
(table.foreignKeys ?? []).forEach((foreignKey, foreignKeyIndex) => {
|
|
492
638
|
if (!tableMap.has(foreignKey.referencedTable)) {
|
|
@@ -582,6 +728,27 @@ export function createCytoscapeInstance(container, elements) {
|
|
|
582
728
|
'transition-duration': '140ms',
|
|
583
729
|
},
|
|
584
730
|
},
|
|
731
|
+
{
|
|
732
|
+
selector: 'node.virtual-table',
|
|
733
|
+
style: {
|
|
734
|
+
'background-color': '#3d1238',
|
|
735
|
+
'border-color': '#ff4fa3',
|
|
736
|
+
'border-width': 2.4,
|
|
737
|
+
color: '#ffe8f4',
|
|
738
|
+
'font-size': 12.5,
|
|
739
|
+
'text-wrap': 'wrap',
|
|
740
|
+
'text-max-width': 174,
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
selector: 'node.shadow-table',
|
|
745
|
+
style: {
|
|
746
|
+
'background-color': '#221f1a',
|
|
747
|
+
'border-color': '#8f8a75',
|
|
748
|
+
'border-style': 'dashed',
|
|
749
|
+
color: '#f3ecd0',
|
|
750
|
+
},
|
|
751
|
+
},
|
|
585
752
|
{
|
|
586
753
|
selector: 'node.selected',
|
|
587
754
|
style: {
|
|
@@ -643,6 +810,22 @@ export function createCytoscapeInstance(container, elements) {
|
|
|
643
810
|
'transition-duration': '120ms',
|
|
644
811
|
},
|
|
645
812
|
},
|
|
813
|
+
{
|
|
814
|
+
selector: 'edge.shadow-link',
|
|
815
|
+
style: {
|
|
816
|
+
width: 1.8,
|
|
817
|
+
label: '',
|
|
818
|
+
'line-color': '#8f8a75',
|
|
819
|
+
'line-style': 'dashed',
|
|
820
|
+
'target-arrow-color': '#8f8a75',
|
|
821
|
+
'target-arrow-shape': 'vee',
|
|
822
|
+
'arrow-scale': 0.9,
|
|
823
|
+
'text-background-opacity': 0,
|
|
824
|
+
'text-border-opacity': 0,
|
|
825
|
+
'text-outline-width': 0,
|
|
826
|
+
'z-index': 0,
|
|
827
|
+
},
|
|
828
|
+
},
|
|
646
829
|
{
|
|
647
830
|
selector: 'edge.related',
|
|
648
831
|
style: {
|
|
@@ -714,6 +897,21 @@ function showEdgeReadout(edge, renderedPosition) {
|
|
|
714
897
|
return;
|
|
715
898
|
}
|
|
716
899
|
|
|
900
|
+
if (isShadowRelationship(edge.data())) {
|
|
901
|
+
const sourceTable = edge.data('sourceTable') || edge.source().data('tableName') || '?';
|
|
902
|
+
const targetTable = edge.data('targetTable') || edge.target().data('tableName') || '?';
|
|
903
|
+
|
|
904
|
+
currentGraph.edgeReadout.innerHTML = `
|
|
905
|
+
<div class="structure-graph__edge-readout-meta">Shadow table</div>
|
|
906
|
+
<div class="structure-graph__edge-readout-path">
|
|
907
|
+
${escapeHtml(sourceTable)} <span aria-hidden="true">→</span> ${escapeHtml(targetTable)}
|
|
908
|
+
</div>
|
|
909
|
+
`;
|
|
910
|
+
currentGraph.edgeReadout.removeAttribute('hidden');
|
|
911
|
+
positionEdgeReadout(renderedPosition);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
717
915
|
const sourceTable = edge.data('sourceTable') || edge.source().data('tableName') || '?';
|
|
718
916
|
const targetTable = edge.data('targetTable') || edge.target().data('tableName') || '?';
|
|
719
917
|
const sourceColumn = edge.data('sourceColumn') || '?';
|
|
@@ -741,6 +939,65 @@ function hideEdgeReadout() {
|
|
|
741
939
|
currentGraph.edgeReadout.setAttribute('hidden', 'hidden');
|
|
742
940
|
}
|
|
743
941
|
|
|
942
|
+
function clearVirtualModuleBadges(graph = currentGraph) {
|
|
943
|
+
if (!graph?.virtualModuleBadges) {
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
graph.virtualModuleBadges.forEach(badge => badge.remove());
|
|
948
|
+
graph.virtualModuleBadges.clear();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function syncVirtualModuleBadges(graph = currentGraph) {
|
|
952
|
+
if (!graph?.cy || !graph.canvasShell || !graph.virtualModuleBadges) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const activeNodeIds = new Set();
|
|
957
|
+
|
|
958
|
+
graph.cy.nodes('.virtual-table').forEach(node => {
|
|
959
|
+
const moduleName = String(node.data('virtualModule') ?? '').trim();
|
|
960
|
+
|
|
961
|
+
if (!moduleName) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const nodeId = node.id();
|
|
966
|
+
activeNodeIds.add(nodeId);
|
|
967
|
+
|
|
968
|
+
let badge = graph.virtualModuleBadges.get(nodeId);
|
|
969
|
+
|
|
970
|
+
if (!badge) {
|
|
971
|
+
badge = document.createElement('div');
|
|
972
|
+
badge.className = 'structure-graph__virtual-module-badge';
|
|
973
|
+
badge.dataset.structureVirtualModuleBadge = node.data('tableName') || '';
|
|
974
|
+
graph.canvasShell.appendChild(badge);
|
|
975
|
+
graph.virtualModuleBadges.set(nodeId, badge);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
badge.textContent = moduleName;
|
|
979
|
+
badge.classList.toggle('is-dimmed', node.hasClass('dimmed'));
|
|
980
|
+
badge.classList.toggle('is-related', node.hasClass('related'));
|
|
981
|
+
badge.classList.toggle('is-selected', node.hasClass('selected'));
|
|
982
|
+
|
|
983
|
+
const position = node.renderedPosition();
|
|
984
|
+
const renderedHeight = Number(node.renderedHeight?.() ?? 0);
|
|
985
|
+
const yOffset = Math.max(12, Math.min(22, renderedHeight * 0.24));
|
|
986
|
+
const scale = Math.max(0.72, Math.min(1, graph.cy.zoom()));
|
|
987
|
+
|
|
988
|
+
badge.style.left = `${position.x}px`;
|
|
989
|
+
badge.style.top = `${position.y + yOffset}px`;
|
|
990
|
+
badge.style.transform = `translate(-50%, -50%) scale(${scale})`;
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
graph.virtualModuleBadges.forEach((badge, nodeId) => {
|
|
994
|
+
if (!activeNodeIds.has(nodeId)) {
|
|
995
|
+
badge.remove();
|
|
996
|
+
graph.virtualModuleBadges.delete(nodeId);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
744
1001
|
function createGraphStateSnapshot(graph = currentGraph) {
|
|
745
1002
|
if (!graph?.cy) {
|
|
746
1003
|
return null;
|
|
@@ -858,6 +1115,8 @@ function destroyCurrentGraph() {
|
|
|
858
1115
|
currentGraph.resizeObserver.disconnect();
|
|
859
1116
|
}
|
|
860
1117
|
|
|
1118
|
+
clearVirtualModuleBadges(currentGraph);
|
|
1119
|
+
|
|
861
1120
|
if (currentGraph.resizeHandler) {
|
|
862
1121
|
window.removeEventListener('resize', currentGraph.resizeHandler);
|
|
863
1122
|
}
|
|
@@ -987,6 +1246,8 @@ function clearSelection() {
|
|
|
987
1246
|
syncEntryHighlights();
|
|
988
1247
|
replaceChildrenFromRenderedMarkup(currentGraph.inspector, getDefaultInspectorMarkup());
|
|
989
1248
|
updateOpenDataButton();
|
|
1249
|
+
syncVirtualModuleBadges();
|
|
1250
|
+
clearStructureSelection();
|
|
990
1251
|
}
|
|
991
1252
|
|
|
992
1253
|
function applyTableSelection(node, { focus = true } = {}) {
|
|
@@ -1002,6 +1263,7 @@ function applyTableSelection(node, { focus = true } = {}) {
|
|
|
1002
1263
|
highlightConnectedElements(currentGraph.cy, node);
|
|
1003
1264
|
syncEntryHighlights([tableData.name]);
|
|
1004
1265
|
updateOpenDataButton();
|
|
1266
|
+
syncVirtualModuleBadges();
|
|
1005
1267
|
|
|
1006
1268
|
if (focus) {
|
|
1007
1269
|
currentGraph.cy.animate(
|
|
@@ -1033,6 +1295,7 @@ function applyEdgeSelection(edge) {
|
|
|
1033
1295
|
highlightConnectedElements(currentGraph.cy, edge);
|
|
1034
1296
|
syncEntryHighlights([], [edge.data('sourceTable'), edge.data('targetTable')]);
|
|
1035
1297
|
updateOpenDataButton();
|
|
1298
|
+
syncVirtualModuleBadges();
|
|
1036
1299
|
return edge;
|
|
1037
1300
|
}
|
|
1038
1301
|
|
|
@@ -1202,6 +1465,7 @@ function runLayout(cy, onStop, { randomize = false } = {}) {
|
|
|
1202
1465
|
|
|
1203
1466
|
applyReadableLayoutPositions(cy, { variant: currentGraph?.layoutVariant ?? 0 });
|
|
1204
1467
|
cy.fit(cy.elements(), 90);
|
|
1468
|
+
syncVirtualModuleBadges();
|
|
1205
1469
|
|
|
1206
1470
|
if (typeof onStop === 'function') {
|
|
1207
1471
|
onStop();
|
|
@@ -1235,6 +1499,8 @@ function restorePersistedViewport(cy, persistedState) {
|
|
|
1235
1499
|
cy.pan(persistedState.pan);
|
|
1236
1500
|
}
|
|
1237
1501
|
|
|
1502
|
+
syncVirtualModuleBadges();
|
|
1503
|
+
|
|
1238
1504
|
return true;
|
|
1239
1505
|
}
|
|
1240
1506
|
|
|
@@ -1375,6 +1641,7 @@ export async function mountStructureGraph(snapshot) {
|
|
|
1375
1641
|
persistStateOnDestroy: true,
|
|
1376
1642
|
selectedEdgeId: null,
|
|
1377
1643
|
selectedTableName: null,
|
|
1644
|
+
virtualModuleBadges: new Map(),
|
|
1378
1645
|
};
|
|
1379
1646
|
|
|
1380
1647
|
currentGraph.cleanup.push(setupToolbar(cy));
|
|
@@ -1407,14 +1674,20 @@ export async function mountStructureGraph(snapshot) {
|
|
|
1407
1674
|
});
|
|
1408
1675
|
|
|
1409
1676
|
cy.on('dragfree', 'node', () => {
|
|
1677
|
+
syncVirtualModuleBadges();
|
|
1410
1678
|
scheduleGraphStatePersist(0);
|
|
1411
1679
|
});
|
|
1412
1680
|
|
|
1681
|
+
cy.on('position drag', 'node', () => {
|
|
1682
|
+
syncVirtualModuleBadges();
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1413
1685
|
cy.on('mouseover', 'edge', event => {
|
|
1414
1686
|
const edge = event.target;
|
|
1415
1687
|
highlightConnectedElements(cy, edge);
|
|
1416
1688
|
syncEntryHighlights([], [edge.data('sourceTable'), edge.data('targetTable')]);
|
|
1417
1689
|
showEdgeReadout(edge, event.renderedPosition);
|
|
1690
|
+
syncVirtualModuleBadges();
|
|
1418
1691
|
});
|
|
1419
1692
|
|
|
1420
1693
|
cy.on('mousemove', 'edge', event => {
|
|
@@ -1424,10 +1697,12 @@ export async function mountStructureGraph(snapshot) {
|
|
|
1424
1697
|
cy.on('mouseout', 'edge', () => {
|
|
1425
1698
|
hideEdgeReadout();
|
|
1426
1699
|
restoreGraphState();
|
|
1700
|
+
syncVirtualModuleBadges();
|
|
1427
1701
|
});
|
|
1428
1702
|
|
|
1429
1703
|
cy.on('pan zoom', () => {
|
|
1430
1704
|
hideEdgeReadout();
|
|
1705
|
+
syncVirtualModuleBadges();
|
|
1431
1706
|
scheduleGraphStatePersist(300);
|
|
1432
1707
|
});
|
|
1433
1708
|
|
|
@@ -1465,14 +1740,18 @@ export async function mountStructureGraph(snapshot) {
|
|
|
1465
1740
|
});
|
|
1466
1741
|
}
|
|
1467
1742
|
|
|
1743
|
+
syncVirtualModuleBadges();
|
|
1744
|
+
|
|
1468
1745
|
if (typeof ResizeObserver === 'function') {
|
|
1469
1746
|
currentGraph.resizeObserver = new ResizeObserver(() => {
|
|
1470
1747
|
cy.resize();
|
|
1748
|
+
syncVirtualModuleBadges();
|
|
1471
1749
|
});
|
|
1472
1750
|
currentGraph.resizeObserver.observe(canvas);
|
|
1473
1751
|
} else {
|
|
1474
1752
|
currentGraph.resizeHandler = () => {
|
|
1475
1753
|
cy.resize();
|
|
1754
|
+
syncVirtualModuleBadges();
|
|
1476
1755
|
};
|
|
1477
1756
|
window.addEventListener('resize', currentGraph.resizeHandler);
|
|
1478
1757
|
}
|