sqlite-hub 2.0.1 → 2.1.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.
Files changed (85) hide show
  1. package/README.md +14 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +10 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/structure.js +28 -5
  61. package/frontend/js/views/tableAdvisor.js +14 -6
  62. package/frontend/js/views/tableDesigner.js +44 -9
  63. package/frontend/styles/components.css +676 -79
  64. package/frontend/styles/structure-graph.css +49 -0
  65. package/frontend/styles/tailwind.generated.css +1 -1
  66. package/package.json +1 -1
  67. package/server/routes/connections.js +12 -0
  68. package/server/routes/data.js +1 -1
  69. package/server/routes/tableDesigner.js +15 -1
  70. package/server/services/mcpToolService.js +28 -0
  71. package/server/services/sqlite/connectionManager.js +23 -2
  72. package/server/services/sqlite/dataBrowserService.js +21 -0
  73. package/server/services/sqlite/introspection.js +39 -1
  74. package/server/services/sqlite/structureService.js +66 -11
  75. package/server/services/sqlite/tableAdvisor.js +6 -0
  76. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  77. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  78. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  79. package/server/services/sqlite/tableDesignerService.js +110 -5
  80. package/server/services/storage/appStateStore.js +303 -4
  81. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  82. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  83. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  84. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  85. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -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="structure-graph__title">${escapeHtml(tableData.name)}</div>
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
- group: 'nodes',
479
- data: {
480
- id: getTableId(table.name),
481
- label: table.name,
482
- tableName: table.name,
483
- width: Math.max(184, table.name.length * 9 + 64),
484
- height: 56,
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
  }